Source: lib/offline/storage.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.offline.Storage');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.Player');
  20. goog.require('shaka.log');
  21. goog.require('shaka.media.DrmEngine');
  22. goog.require('shaka.media.ManifestParser');
  23. goog.require('shaka.net.NetworkingEngine');
  24. goog.require('shaka.offline.DownloadManager');
  25. goog.require('shaka.offline.ManifestConverter');
  26. goog.require('shaka.offline.OfflineUri');
  27. goog.require('shaka.offline.StorageCellPath');
  28. goog.require('shaka.offline.StorageMuxer');
  29. goog.require('shaka.offline.StoredContentUtils');
  30. goog.require('shaka.offline.StreamBandwidthEstimator');
  31. goog.require('shaka.util.ConfigUtils');
  32. goog.require('shaka.util.Error');
  33. goog.require('shaka.util.Functional');
  34. goog.require('shaka.util.IDestroyable');
  35. goog.require('shaka.util.LanguageUtils');
  36. goog.require('shaka.util.ManifestParserUtils');
  37. goog.require('shaka.util.MapUtils');
  38. goog.require('shaka.util.StreamUtils');
  39. /**
  40. * This manages persistent offline data including storage, listing, and deleting
  41. * stored manifests. Playback of offline manifests are done through the Player
  42. * using a special URI (see shaka.offline.OfflineUri).
  43. *
  44. * First, check support() to see if offline is supported by the platform.
  45. * Second, configure() the storage object with callbacks to your application.
  46. * Third, call store(), remove(), or list() as needed.
  47. * When done, call destroy().
  48. *
  49. * @param {shaka.Player} player
  50. * The player instance to pull configuration data from.
  51. *
  52. * @struct
  53. * @constructor
  54. * @implements {shaka.util.IDestroyable}
  55. * @export
  56. */
  57. shaka.offline.Storage = function(player) {
  58. // It is an easy mistake to make to pass a Player proxy from CastProxy.
  59. // Rather than throw a vague exception later, throw an explicit and clear one
  60. // now.
  61. if (!player || player.constructor != shaka.Player) {
  62. throw new shaka.util.Error(
  63. shaka.util.Error.Severity.CRITICAL,
  64. shaka.util.Error.Category.STORAGE,
  65. shaka.util.Error.Code.LOCAL_PLAYER_INSTANCE_REQUIRED);
  66. }
  67. /** @private {shaka.Player} */
  68. this.player_ = player;
  69. /** @private {?shakaExtern.OfflineConfiguration} */
  70. this.config_ = this.defaultConfig_();
  71. /** @private {boolean} */
  72. this.storeInProgress_ = false;
  73. /** @private {Array.<shakaExtern.Track>} */
  74. this.firstPeriodTracks_ = null;
  75. /**
  76. * A list of segment ids for all the segments that were added during the
  77. * current store. If the store fails or is aborted, these need to be
  78. * removed from storage.
  79. * @private {!Array.<number>}
  80. */
  81. this.segmentsFromStore_ = [];
  82. };
  83. /**
  84. * Gets whether offline storage is supported. Returns true if offline storage
  85. * is supported for clear content. Support for offline storage of encrypted
  86. * content will not be determined until storage is attempted.
  87. *
  88. * @return {boolean}
  89. * @export
  90. */
  91. shaka.offline.Storage.support = function() {
  92. return shaka.offline.StorageMuxer.support();
  93. };
  94. /**
  95. * @override
  96. * @export
  97. */
  98. shaka.offline.Storage.prototype.destroy = function() {
  99. this.config_ = null;
  100. this.player_ = null;
  101. // TODO: Need to wait for whatever current store, remove, or list async
  102. // operations that may be in progress to stop/fail before we
  103. // resolve this promise.
  104. return Promise.resolve();
  105. };
  106. /**
  107. * Sets configuration values for Storage. This is not associated with
  108. * Player.configure and will not change Player.
  109. *
  110. * There are two important callbacks configured here: one for download progress,
  111. * and one to decide which tracks to store.
  112. *
  113. * The default track selection callback will store the largest SD video track.
  114. * Provide your own callback to choose the tracks you want to store.
  115. *
  116. * @param {!Object} config This should follow the form of
  117. * {@link shakaExtern.OfflineConfiguration}, but you may omit any field you do
  118. * not wish to change.
  119. * @export
  120. */
  121. shaka.offline.Storage.prototype.configure = function(config) {
  122. goog.asserts.assert(this.config_, 'Storage must not be destroyed');
  123. shaka.util.ConfigUtils.mergeConfigObjects(
  124. this.config_, config, this.defaultConfig_(), {}, '');
  125. };
  126. /**
  127. * Stores the given manifest. If the content is encrypted, and encrypted
  128. * content cannot be stored on this platform, the Promise will be rejected with
  129. * error code 6001, REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE.
  130. *
  131. * @param {string} uri The URI of the manifest to store.
  132. * @param {!Object=} opt_appMetadata An arbitrary object from the application
  133. * that will be stored along-side the offline content. Use this for any
  134. * application-specific metadata you need associated with the stored content.
  135. * For details on the data types that can be stored here, please refer to
  136. * {@link https://goo.gl/h62coS}
  137. * @param {!shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
  138. * @return {!Promise.<shakaExtern.StoredContent>} A Promise to a structure
  139. * representing what was stored. The "offlineUri" member is the URI that
  140. * should be given to Player.load() to play this piece of content offline.
  141. * The "appMetadata" member is the appMetadata argument you passed to store().
  142. * @export
  143. */
  144. shaka.offline.Storage.prototype.store = async function(
  145. uri, opt_appMetadata, opt_manifestParserFactory) {
  146. // TODO: Create a way for a download to be canceled while being downloaded.
  147. this.requireSupport_();
  148. if (this.storeInProgress_) {
  149. return Promise.reject(new shaka.util.Error(
  150. shaka.util.Error.Severity.CRITICAL,
  151. shaka.util.Error.Category.STORAGE,
  152. shaka.util.Error.Code.STORE_ALREADY_IN_PROGRESS));
  153. }
  154. this.storeInProgress_ = true;
  155. /** @type {!Object} */
  156. let appMetadata = opt_appMetadata || {};
  157. let error = null;
  158. let onError = (e) => {
  159. // To avoid hiding a previously thrown error, throw the older error.
  160. error = error || e;
  161. };
  162. let data = await this.loadInternal(
  163. uri, onError, opt_manifestParserFactory);
  164. let canDownload = !data.manifest.presentationTimeline.isLive() &&
  165. !data.manifest.presentationTimeline.isInProgress();
  166. if (!canDownload) {
  167. throw new shaka.util.Error(
  168. shaka.util.Error.Severity.CRITICAL,
  169. shaka.util.Error.Category.STORAGE,
  170. shaka.util.Error.Code.CANNOT_STORE_LIVE_OFFLINE,
  171. uri);
  172. }
  173. this.checkDestroyed_();
  174. if (error) { throw error; }
  175. /** @type {!shaka.offline.StorageMuxer} */
  176. const muxer = new shaka.offline.StorageMuxer();
  177. return shaka.util.IDestroyable.with([muxer, data.drmEngine], async () => {
  178. try {
  179. await muxer.init();
  180. this.checkDestroyed_();
  181. // Re-filter now that DrmEngine is initialized.
  182. this.filterAllPeriods_(data.drmEngine, data.manifest.periods);
  183. // Get the cell that we are saving the manifest to. Once we get a cell
  184. // we will only reference the cell and not the muxer so that the manifest
  185. // and segments will all be saved to the same cell.
  186. let active = await muxer.getActive();
  187. this.checkDestroyed_();
  188. try {
  189. let manifestDB = await this.downloadManifest_(
  190. active.cell,
  191. data.drmEngine,
  192. data.manifest,
  193. uri,
  194. appMetadata || {});
  195. this.checkDestroyed_();
  196. let ids = await active.cell.addManifests([manifestDB]);
  197. this.checkDestroyed_();
  198. let offlineUri = shaka.offline.OfflineUri.manifest(
  199. active.path.mechanism, active.path.cell, ids[0]);
  200. return shaka.offline.StoredContentUtils.fromManifestDB(
  201. offlineUri, manifestDB);
  202. } catch (e) {
  203. // We need to remove all the segments that did get into storage as
  204. // the manifest won't be playable.
  205. let segmentsToRemove = this.segmentsFromStore_;
  206. let noop = () => {};
  207. await active.cell.removeSegments(segmentsToRemove, noop);
  208. // If we already had an error, ignore this error to avoid hiding
  209. // the original error.
  210. throw error || e;
  211. }
  212. } finally {
  213. this.storeInProgress_ = false;
  214. this.firstPeriodTracks_ = null;
  215. this.segmentsFromStore_ = [];
  216. }
  217. });
  218. };
  219. /**
  220. * Create a download manager and download the manifest.
  221. *
  222. * @param {shakaExtern.StorageCell} storage
  223. * @param {!shaka.media.DrmEngine} drm
  224. * @param {shakaExtern.Manifest} manifest
  225. * @param {string} uri
  226. * @param {!Object} metadata
  227. * @return {!Promise.<shakaExtern.ManifestDB>}
  228. * @private
  229. */
  230. shaka.offline.Storage.prototype.downloadManifest_ = function(
  231. storage, drm, manifest, uri, metadata) {
  232. const noSize = 0;
  233. let pendingContent = shaka.offline.StoredContentUtils.fromManifest(
  234. uri, manifest, noSize, metadata);
  235. /** @type {!shaka.offline.DownloadManager} */
  236. let downloader = new shaka.offline.DownloadManager((progress, size) => {
  237. // Update the size of the stored content before issuing a progress update.
  238. pendingContent.size = size;
  239. this.config_.progressCallback(pendingContent, progress);
  240. });
  241. /** @type {shakaExtern.ManifestDB} */
  242. let manifestDB;
  243. return shaka.util.IDestroyable.with([downloader], () => {
  244. manifestDB = this.createOfflineManifest_(
  245. downloader, storage, drm, manifest, uri, metadata);
  246. return downloader.download(this.getNetEngine_());
  247. }).then(() => {
  248. // Update the size before saving it.
  249. manifestDB.size = pendingContent.size;
  250. return manifestDB;
  251. });
  252. };
  253. /**
  254. * Removes the given stored content. This will also attempt to release the
  255. * licenses, if any.
  256. *
  257. * @param {string} contentUri
  258. * @return {!Promise}
  259. * @export
  260. */
  261. shaka.offline.Storage.prototype.remove = function(contentUri) {
  262. this.requireSupport_();
  263. let nullableUri = shaka.offline.OfflineUri.parse(contentUri);
  264. if (nullableUri == null || !nullableUri.isManifest()) {
  265. return Promise.reject(new shaka.util.Error(
  266. shaka.util.Error.Severity.CRITICAL,
  267. shaka.util.Error.Category.STORAGE,
  268. shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
  269. contentUri));
  270. }
  271. let uri = /** @type {!shaka.offline.OfflineUri} */ (nullableUri);
  272. let muxer = new shaka.offline.StorageMuxer();
  273. return shaka.util.IDestroyable.with([muxer], async () => {
  274. await muxer.init();
  275. let cell = await muxer.getCell(uri.mechanism(), uri.cell());
  276. let manifests = await cell.getManifests([uri.key()]);
  277. let manifest = manifests[0];
  278. await Promise.all([
  279. this.removeFromDRM_(uri, manifest),
  280. this.removeFromStorage_(cell, uri, manifest)
  281. ]);
  282. });
  283. };
  284. /**
  285. * @param {!shaka.offline.OfflineUri} uri
  286. * @param {shakaExtern.ManifestDB} manifestDB
  287. * @return {!Promise}
  288. * @private
  289. */
  290. shaka.offline.Storage.prototype.removeFromDRM_ = function(uri, manifestDB) {
  291. let netEngine = this.getNetEngine_();
  292. let error;
  293. let onError = (e) => {
  294. // Ignore errors if the session was already removed.
  295. if (e.code != shaka.util.Error.Code.OFFLINE_SESSION_REMOVED) {
  296. error = e;
  297. }
  298. };
  299. let drmEngine = new shaka.media.DrmEngine({
  300. netEngine: netEngine,
  301. onError: onError,
  302. onKeyStatus: () => {},
  303. onExpirationUpdated: () => {},
  304. onEvent: () => {}
  305. });
  306. drmEngine.configure(this.player_.getConfiguration().drm);
  307. let converter = new shaka.offline.ManifestConverter(
  308. uri.mechanism(), uri.cell());
  309. let manifest = converter.fromManifestDB(manifestDB);
  310. return shaka.util.IDestroyable.with([drmEngine], async () => {
  311. await drmEngine.init(manifest, this.config_.usePersistentLicense);
  312. await drmEngine.removeSessions(manifestDB.sessionIds);
  313. }).then(() => { if (error) { throw error; } });
  314. };
  315. /**
  316. * @param {shakaExtern.StorageCell} storage
  317. * @param {!shaka.offline.OfflineUri} uri
  318. * @param {shakaExtern.ManifestDB} manifest
  319. * @return {!Promise}
  320. * @private
  321. */
  322. shaka.offline.Storage.prototype.removeFromStorage_ = function(
  323. storage, uri, manifest) {
  324. /** @type {!Array.<number>} */
  325. let segmentIds = shaka.offline.Storage.getAllSegmentIds_(manifest);
  326. // Count(segments) + Count(manifests)
  327. let toRemove = segmentIds.length + 1;
  328. let removed = 0;
  329. let pendingContent = shaka.offline.StoredContentUtils.fromManifestDB(
  330. uri, manifest);
  331. let onRemove = (key) => {
  332. removed += 1;
  333. this.config_.progressCallback(pendingContent, removed / toRemove);
  334. };
  335. return Promise.all([
  336. storage.removeSegments(segmentIds, onRemove),
  337. storage.removeManifests([uri.key()], onRemove)
  338. ]);
  339. };
  340. /**
  341. * Lists all the stored content available.
  342. *
  343. * @return {!Promise.<!Array.<shakaExtern.StoredContent>>} A Promise to an
  344. * array of structures representing all stored content. The "offlineUri"
  345. * member of the structure is the URI that should be given to Player.load()
  346. * to play this piece of content offline. The "appMetadata" member is the
  347. * appMetadata argument you passed to store().
  348. * @export
  349. */
  350. shaka.offline.Storage.prototype.list = function() {
  351. this.requireSupport_();
  352. /** @type {!Array.<shakaExtern.StoredContent>} */
  353. let result = [];
  354. /**
  355. * @param {!shaka.offline.StorageCellPath} path
  356. * @param {shakaExtern.StorageCell} cell
  357. */
  358. async function onCell(path, cell) {
  359. let manifests = await cell.getAllManifests();
  360. shaka.util.MapUtils.forEach(manifests, (key, manifest) => {
  361. let uri = shaka.offline.OfflineUri.manifest(
  362. path.mechanism, path.cell, key);
  363. let content = shaka.offline.StoredContentUtils.fromManifestDB(
  364. uri, manifest);
  365. result.push(content);
  366. });
  367. }
  368. // Go over each storage cell and call |onCell| to create our list of
  369. // stored content.
  370. let muxer = new shaka.offline.StorageMuxer();
  371. return shaka.util.IDestroyable.with([muxer], async () => {
  372. await muxer.init();
  373. let p = Promise.resolve();
  374. muxer.forEachCell((path, cell) => {
  375. p = p.then(() => onCell(path, cell));
  376. });
  377. await p;
  378. }).then(() => result);
  379. };
  380. /**
  381. * Loads the given manifest, parses it, and constructs the DrmEngine. This
  382. * stops the manifest parser. This may be replaced by tests.
  383. *
  384. * @param {string} manifestUri
  385. * @param {function(*)} onError
  386. * @param {!shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
  387. * @return {!Promise.<{
  388. * manifest: shakaExtern.Manifest,
  389. * drmEngine: !shaka.media.DrmEngine
  390. * }>}
  391. */
  392. shaka.offline.Storage.prototype.loadInternal = function(
  393. manifestUri, onError, opt_manifestParserFactory) {
  394. let netEngine = this.getNetEngine_();
  395. let config = this.player_.getConfiguration();
  396. /** @type {shakaExtern.Manifest} */
  397. let manifest;
  398. /** @type {!shaka.media.DrmEngine} */
  399. let drmEngine;
  400. /** @type {shakaExtern.ManifestParser} */
  401. let manifestParser;
  402. let onKeyStatusChange = function() {};
  403. return shaka.media.ManifestParser
  404. .getFactory(
  405. manifestUri, netEngine, config.manifest.retryParameters,
  406. opt_manifestParserFactory)
  407. .then(function(factory) {
  408. this.checkDestroyed_();
  409. drmEngine = new shaka.media.DrmEngine({
  410. netEngine: netEngine,
  411. onError: onError,
  412. onKeyStatus: onKeyStatusChange,
  413. onExpirationUpdated: () => {},
  414. onEvent: () => {}
  415. });
  416. drmEngine.configure(config.drm);
  417. let playerInterface = {
  418. networkingEngine: netEngine,
  419. filterAllPeriods: (periods) => {
  420. this.filterAllPeriods_(drmEngine, periods);
  421. },
  422. filterNewPeriod: (period) => {
  423. this.filterPeriod_(drmEngine, period);
  424. },
  425. onTimelineRegionAdded: function() {},
  426. onEvent: function() {},
  427. onError: onError
  428. };
  429. manifestParser = new factory();
  430. manifestParser.configure(config.manifest);
  431. return manifestParser.start(manifestUri, playerInterface);
  432. }.bind(this))
  433. .then(function(data) {
  434. this.checkDestroyed_();
  435. manifest = data;
  436. return drmEngine.init(manifest, this.config_.usePersistentLicense);
  437. }.bind(this))
  438. .then(function() {
  439. this.checkDestroyed_();
  440. return this.createSegmentIndex_(manifest);
  441. }.bind(this))
  442. .then(function() {
  443. this.checkDestroyed_();
  444. return drmEngine.createOrLoad();
  445. }.bind(this))
  446. .then(function() {
  447. this.checkDestroyed_();
  448. return manifestParser.stop();
  449. }.bind(this))
  450. .then(function() {
  451. this.checkDestroyed_();
  452. return {manifest: manifest, drmEngine: drmEngine};
  453. }.bind(this))
  454. .catch(function(error) {
  455. if (manifestParser) {
  456. return manifestParser.stop().then(function() { throw error; });
  457. } else {
  458. throw error;
  459. }
  460. });
  461. };
  462. /**
  463. * The default track selection function.
  464. *
  465. * @param {string} preferredAudioLanguage
  466. * @param {!Array.<shakaExtern.Track>} tracks
  467. * @return {!Array.<shakaExtern.Track>}
  468. */
  469. shaka.offline.Storage.defaultTrackSelect =
  470. function(preferredAudioLanguage, tracks) {
  471. const LanguageUtils = shaka.util.LanguageUtils;
  472. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  473. let selectedTracks = [];
  474. // Select variants with best language match.
  475. let audioLangPref = LanguageUtils.normalize(preferredAudioLanguage);
  476. let matchTypes = [
  477. LanguageUtils.MatchType.EXACT,
  478. LanguageUtils.MatchType.BASE_LANGUAGE_OKAY,
  479. LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY
  480. ];
  481. let allVariantTracks =
  482. tracks.filter(function(track) { return track.type == 'variant'; });
  483. // For each match type, get the tracks that match the audio preference for
  484. // that match type.
  485. let tracksByMatchType = matchTypes.map(function(match) {
  486. return allVariantTracks.filter(function(track) {
  487. let lang = LanguageUtils.normalize(track.language);
  488. return LanguageUtils.match(match, audioLangPref, lang);
  489. });
  490. });
  491. // Find the best match type that has any matches.
  492. let variantTracks;
  493. for (let i = 0; i < tracksByMatchType.length; i++) {
  494. if (tracksByMatchType[i].length) {
  495. variantTracks = tracksByMatchType[i];
  496. break;
  497. }
  498. }
  499. // Fall back to "primary" audio tracks, if present.
  500. if (!variantTracks) {
  501. let primaryTracks = allVariantTracks.filter(function(track) {
  502. return track.primary;
  503. });
  504. if (primaryTracks.length) {
  505. variantTracks = primaryTracks;
  506. }
  507. }
  508. // Otherwise, there is no good way to choose the language, so we don't choose
  509. // a language at all.
  510. if (!variantTracks) {
  511. variantTracks = allVariantTracks;
  512. // Issue a warning, but only if the content has multiple languages.
  513. // Otherwise, this warning would just be noise.
  514. let languages = allVariantTracks
  515. .map(function(track) { return track.language; })
  516. .filter(shaka.util.Functional.isNotDuplicate);
  517. if (languages.length > 1) {
  518. shaka.log.warning('Could not choose a good audio track based on ' +
  519. 'language preferences or primary tracks. An ' +
  520. 'arbitrary language will be stored!');
  521. }
  522. }
  523. // From previously selected variants, choose the SD ones (height <= 480).
  524. let tracksByHeight = variantTracks.filter(function(track) {
  525. return track.height && track.height <= 480;
  526. });
  527. // If variants don't have video or no video with height <= 480 was
  528. // found, proceed with the previously selected tracks.
  529. if (tracksByHeight.length) {
  530. // Sort by resolution, then select all variants which match the height
  531. // of the highest SD res. There may be multiple audio bitrates for the
  532. // same video resolution.
  533. tracksByHeight.sort(function(a, b) { return b.height - a.height; });
  534. variantTracks = tracksByHeight.filter(function(track) {
  535. return track.height == tracksByHeight[0].height;
  536. });
  537. }
  538. // Now sort by bandwidth.
  539. variantTracks.sort(function(a, b) { return a.bandwidth - b.bandwidth; });
  540. // If there are multiple matches at different audio bitrates, select the
  541. // middle bandwidth one.
  542. if (variantTracks.length) {
  543. selectedTracks.push(variantTracks[Math.floor(variantTracks.length / 2)]);
  544. }
  545. // Since this default callback is used primarily by our own demo app and by
  546. // app developers who haven't thought about which tracks they want, we should
  547. // select all text tracks, regardless of language. This makes for a better
  548. // demo for us, and does not rely on user preferences for the unconfigured
  549. // app.
  550. selectedTracks.push.apply(selectedTracks, tracks.filter(function(track) {
  551. return track.type == ContentType.TEXT;
  552. }));
  553. return selectedTracks;
  554. };
  555. /**
  556. * @return {shakaExtern.OfflineConfiguration}
  557. * @private
  558. */
  559. shaka.offline.Storage.prototype.defaultConfig_ = function() {
  560. let selectionCallback = (tracks) => {
  561. goog.asserts.assert(
  562. this.player_,
  563. 'The player should be non-null when selecting tracks');
  564. let config = this.player_.getConfiguration();
  565. return shaka.offline.Storage.defaultTrackSelect(
  566. config.preferredAudioLanguage, tracks);
  567. };
  568. let progressCallback = (content, percent) => {
  569. // Reference arguments to keep closure from removing them.
  570. // If the arguments are removed, it breaks our function length check
  571. // in mergeConfigObjects_().
  572. // NOTE: Chrome App Content Security Policy prohibits usage of new
  573. // Function().
  574. if (content || percent) return null;
  575. };
  576. return {
  577. trackSelectionCallback: selectionCallback,
  578. progressCallback: progressCallback,
  579. usePersistentLicense: true
  580. };
  581. };
  582. /**
  583. * @param {!shaka.media.DrmEngine} drmEngine
  584. * @param {!Array.<shakaExtern.Period>} periods
  585. * @private
  586. */
  587. shaka.offline.Storage.prototype.filterAllPeriods_ = function(
  588. drmEngine, periods) {
  589. periods.forEach((period) => this.filterPeriod_(drmEngine, period));
  590. };
  591. /**
  592. * @param {!shaka.media.DrmEngine} drmEngine
  593. * @param {shakaExtern.Period} period
  594. * @private
  595. */
  596. shaka.offline.Storage.prototype.filterPeriod_ = function(drmEngine, period) {
  597. const StreamUtils = shaka.util.StreamUtils;
  598. const maxHwRes = {width: Infinity, height: Infinity};
  599. /** @type {?shakaExtern.Variant} */
  600. let variant = null;
  601. if (this.firstPeriodTracks_) {
  602. let variantTrack = this.firstPeriodTracks_.filter(function(track) {
  603. return track.type == 'variant';
  604. })[0];
  605. if (variantTrack) {
  606. variant = StreamUtils.findVariantForTrack(period, variantTrack);
  607. }
  608. }
  609. /** @type {?shakaExtern.Stream} */
  610. let activeAudio = null;
  611. /** @type {?shakaExtern.Stream} */
  612. let activeVideo = null;
  613. if (variant) {
  614. // Use the first variant as the container of "active streams". This
  615. // is then used to filter out the streams that are not compatible with it.
  616. // This ensures that in multi-Period content, all Periods have streams
  617. // with compatible MIME types.
  618. if (variant.audio) activeAudio = variant.audio;
  619. if (variant.video) activeVideo = variant.video;
  620. }
  621. StreamUtils.filterNewPeriod(
  622. drmEngine, activeAudio, activeVideo, period);
  623. StreamUtils.applyRestrictions(
  624. period, this.player_.getConfiguration().restrictions, maxHwRes);
  625. };
  626. /**
  627. * Calls createSegmentIndex for all streams in the manifest.
  628. *
  629. * @param {shakaExtern.Manifest} manifest
  630. * @return {!Promise}
  631. * @private
  632. */
  633. shaka.offline.Storage.prototype.createSegmentIndex_ = function(manifest) {
  634. const Functional = shaka.util.Functional;
  635. let streams = manifest.periods
  636. .map(function(period) { return period.variants; })
  637. .reduce(Functional.collapseArrays, [])
  638. .map(function(variant) {
  639. let variantStreams = [];
  640. if (variant.audio) variantStreams.push(variant.audio);
  641. if (variant.video) variantStreams.push(variant.video);
  642. return variantStreams;
  643. })
  644. .reduce(Functional.collapseArrays, [])
  645. .filter(Functional.isNotDuplicate);
  646. let textStreams = manifest.periods
  647. .map(function(period) { return period.textStreams; })
  648. .reduce(Functional.collapseArrays, []);
  649. streams.push.apply(streams, textStreams);
  650. return Promise.all(
  651. streams.map(function(stream) { return stream.createSegmentIndex(); }));
  652. };
  653. /**
  654. * Creates an offline 'manifest' for the real manifest. This does not store the
  655. * segments yet, only adds them to the download manager through createPeriod_.
  656. *
  657. * @param {!shaka.offline.DownloadManager} downloader
  658. * @param {shakaExtern.StorageCell} storage
  659. * @param {!shaka.media.DrmEngine} drmEngine
  660. * @param {shakaExtern.Manifest} manifest
  661. * @param {string} originalManifestUri
  662. * @param {!Object} metadata
  663. * @return {shakaExtern.ManifestDB}
  664. * @private
  665. */
  666. shaka.offline.Storage.prototype.createOfflineManifest_ = function(
  667. downloader, storage, drmEngine, manifest, originalManifestUri, metadata) {
  668. let estimator = new shaka.offline.StreamBandwidthEstimator();
  669. let periods = manifest.periods.map((period) => {
  670. return this.createPeriod_(
  671. downloader, storage, estimator, drmEngine, manifest, period);
  672. });
  673. let drmInfo = drmEngine.getDrmInfo();
  674. let sessions = drmEngine.getSessionIds();
  675. if (drmInfo && this.config_.usePersistentLicense) {
  676. if (!sessions.length) {
  677. throw new shaka.util.Error(
  678. shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE,
  679. shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE, originalManifestUri);
  680. }
  681. // Don't store init data, since we have stored sessions.
  682. drmInfo.initData = [];
  683. }
  684. return {
  685. originalManifestUri: originalManifestUri,
  686. duration: manifest.presentationTimeline.getDuration(),
  687. size: 0,
  688. expiration: drmEngine.getExpiration(),
  689. periods: periods,
  690. sessionIds: this.config_.usePersistentLicense ? sessions : [],
  691. drmInfo: drmInfo,
  692. appMetadata: metadata
  693. };
  694. };
  695. /**
  696. * Converts a manifest Period to a database Period. This will use the current
  697. * configuration to get the tracks to use, then it will search each segment
  698. * index and add all the segments to the download manager through createStream_.
  699. *
  700. * @param {!shaka.offline.DownloadManager} downloader
  701. * @param {shakaExtern.StorageCell} storage
  702. * @param {shaka.offline.StreamBandwidthEstimator} estimator
  703. * @param {!shaka.media.DrmEngine} drmEngine
  704. * @param {shakaExtern.Manifest} manifest
  705. * @param {shakaExtern.Period} period
  706. * @return {shakaExtern.PeriodDB}
  707. * @private
  708. */
  709. shaka.offline.Storage.prototype.createPeriod_ = function(
  710. downloader, storage, estimator, drmEngine, manifest, period) {
  711. const StreamUtils = shaka.util.StreamUtils;
  712. let variantTracks = StreamUtils.getVariantTracks(period, null, null);
  713. let textTracks = StreamUtils.getTextTracks(period, null);
  714. let allTracks = variantTracks.concat(textTracks);
  715. let chosenTracks = this.config_.trackSelectionCallback(allTracks);
  716. if (this.firstPeriodTracks_ == null) {
  717. this.firstPeriodTracks_ = chosenTracks;
  718. // Now that the first tracks are chosen, filter again. This ensures all
  719. // Periods have compatible content types.
  720. this.filterAllPeriods_(drmEngine, manifest.periods);
  721. }
  722. // Check for any similar tracks.
  723. if (shaka.offline.Storage.lookForSimilarTracks_(chosenTracks)) {
  724. shaka.log.warning(
  725. 'Multiple tracks of the same type/kind/language given.');
  726. }
  727. // Pass all variants and text streams to the estimator so that we can
  728. // get the best estimate for each stream later.
  729. manifest.periods.forEach((period) => {
  730. period.variants.forEach((variant) => { estimator.addVariant(variant); });
  731. period.textStreams.forEach((text) => { estimator.addText(text); });
  732. });
  733. // Need a way to look up which streams should be downloaded. Use a map so
  734. // that we can easily lookup if a stream should be downloaded just by
  735. // checking if its id is in the map.
  736. let idMap = {};
  737. chosenTracks.forEach((track) => {
  738. if (track.type == 'variant' && track.audioId != null) {
  739. idMap[track.audioId] = true;
  740. }
  741. if (track.type == 'variant' && track.videoId != null) {
  742. idMap[track.videoId] = true;
  743. }
  744. if (track.type == 'text') {
  745. idMap[track.id] = true;
  746. }
  747. });
  748. // Find the streams we want to download and create a stream db instance
  749. // for each of them.
  750. let streamDBs = {};
  751. shaka.offline.Storage.getStreamSet_(manifest)
  752. .filter((stream) => !!idMap[stream.id])
  753. .forEach((stream) => {
  754. streamDBs[stream.id] = this.createStream_(
  755. downloader, storage, estimator, manifest, period, stream);
  756. });
  757. // Connect streams and variants together.
  758. chosenTracks.forEach((track) => {
  759. if (track.type == 'variant' && track.audioId != null) {
  760. streamDBs[track.audioId].variantIds.push(track.id);
  761. }
  762. if (track.type == 'variant' && track.videoId != null) {
  763. streamDBs[track.videoId].variantIds.push(track.id);
  764. }
  765. });
  766. return {
  767. startTime: period.startTime,
  768. streams: shaka.util.MapUtils.values(streamDBs)
  769. };
  770. };
  771. /**
  772. * Converts a manifest stream to a database stream. This will search the
  773. * segment index and add all the segments to the download manager.
  774. *
  775. * @param {!shaka.offline.DownloadManager} downloader
  776. * @param {shakaExtern.StorageCell} storage
  777. * @param {shaka.offline.StreamBandwidthEstimator} estimator
  778. * @param {shakaExtern.Manifest} manifest
  779. * @param {shakaExtern.Period} period
  780. * @param {shakaExtern.Stream} stream
  781. * @return {shakaExtern.StreamDB}
  782. * @private
  783. */
  784. shaka.offline.Storage.prototype.createStream_ = function(
  785. downloader, storage, estimator, manifest, period, stream) {
  786. /** @type {shakaExtern.StreamDB} */
  787. let streamDb = {
  788. id: stream.id,
  789. primary: stream.primary,
  790. presentationTimeOffset: stream.presentationTimeOffset || 0,
  791. contentType: stream.type,
  792. mimeType: stream.mimeType,
  793. codecs: stream.codecs,
  794. frameRate: stream.frameRate,
  795. kind: stream.kind,
  796. language: stream.language,
  797. label: stream.label,
  798. width: stream.width || null,
  799. height: stream.height || null,
  800. initSegmentKey: null,
  801. encrypted: stream.encrypted,
  802. keyId: stream.keyId,
  803. segments: [],
  804. variantIds: []
  805. };
  806. /** @type {number} */
  807. let startTime =
  808. manifest.presentationTimeline.getSegmentAvailabilityStart();
  809. // Download each stream in parallel.
  810. let downloadGroup = stream.id;
  811. shaka.offline.Storage.forEachSegment_(stream, startTime, (segment) => {
  812. downloader.queue(
  813. downloadGroup,
  814. this.createRequest_(segment),
  815. estimator.getSegmentEstimate(stream.id, segment),
  816. (data) => {
  817. return storage.addSegments([{data: data}]).then((ids) => {
  818. this.segmentsFromStore_.push(ids[0]);
  819. streamDb.segments.push({
  820. startTime: segment.startTime,
  821. endTime: segment.endTime,
  822. dataKey: ids[0]
  823. });
  824. });
  825. });
  826. });
  827. let initSegment = stream.initSegmentReference;
  828. if (initSegment) {
  829. downloader.queue(
  830. downloadGroup,
  831. this.createRequest_(initSegment),
  832. estimator.getInitSegmentEstimate(stream.id),
  833. (data) => {
  834. return storage.addSegments([{data: data}]).then((ids) => {
  835. this.segmentsFromStore_.push(ids[0]);
  836. streamDb.initSegmentKey = ids[0];
  837. });
  838. });
  839. }
  840. return streamDb;
  841. };
  842. /**
  843. * @param {shakaExtern.Stream} stream
  844. * @param {number} startTime
  845. * @param {function(!shaka.media.SegmentReference)} callback
  846. * @private
  847. */
  848. shaka.offline.Storage.forEachSegment_ = function(stream, startTime, callback) {
  849. /** @type {?number} */
  850. let i = stream.findSegmentPosition(startTime);
  851. /** @type {?shaka.media.SegmentReference} */
  852. let ref = i == null ? null : stream.getSegmentReference(i);
  853. while (ref) {
  854. callback(ref);
  855. ref = stream.getSegmentReference(++i);
  856. }
  857. };
  858. /**
  859. * Throws an error if the object is destroyed.
  860. * @private
  861. */
  862. shaka.offline.Storage.prototype.checkDestroyed_ = function() {
  863. if (!this.player_) {
  864. throw new shaka.util.Error(
  865. shaka.util.Error.Severity.CRITICAL,
  866. shaka.util.Error.Category.STORAGE,
  867. shaka.util.Error.Code.OPERATION_ABORTED);
  868. }
  869. };
  870. /**
  871. * Used by functions that need storage support to ensure that the current
  872. * platform has storage support before continuing. This should only be
  873. * needed to be used at the start of public methods.
  874. *
  875. * @private
  876. */
  877. shaka.offline.Storage.prototype.requireSupport_ = function() {
  878. if (!shaka.offline.Storage.support()) {
  879. throw new shaka.util.Error(
  880. shaka.util.Error.Severity.CRITICAL,
  881. shaka.util.Error.Category.STORAGE,
  882. shaka.util.Error.Code.STORAGE_NOT_SUPPORTED);
  883. }
  884. };
  885. /**
  886. * @return {!shaka.net.NetworkingEngine}
  887. * @private
  888. */
  889. shaka.offline.Storage.prototype.getNetEngine_ = function() {
  890. let net = this.player_.getNetworkingEngine();
  891. goog.asserts.assert(net, 'Player must not be destroyed');
  892. return net;
  893. };
  894. /**
  895. * @param {!shaka.media.SegmentReference|
  896. * !shaka.media.InitSegmentReference} segment
  897. * @return {shakaExtern.Request}
  898. * @private
  899. */
  900. shaka.offline.Storage.prototype.createRequest_ = function(segment) {
  901. let retryParams = this.player_.getConfiguration().streaming.retryParameters;
  902. let request = shaka.net.NetworkingEngine.makeRequest(
  903. segment.getUris(), retryParams);
  904. if (segment.startByte != 0 || segment.endByte != null) {
  905. let end = segment.endByte == null ? '' : segment.endByte;
  906. request.headers['Range'] = 'bytes=' + segment.startByte + '-' + end;
  907. }
  908. return request;
  909. };
  910. /**
  911. * @param {shakaExtern.ManifestDB} manifest
  912. * @return {!Array.<number>}
  913. * @private
  914. */
  915. shaka.offline.Storage.getAllSegmentIds_ = function(manifest) {
  916. /** @type {!Array.<number>} */
  917. let ids = [];
  918. // Get every segment for every stream in the manifest.
  919. manifest.periods.forEach(function(period) {
  920. period.streams.forEach(function(stream) {
  921. if (stream.initSegmentKey != null) {
  922. ids.push(stream.initSegmentKey);
  923. }
  924. stream.segments.forEach(function(segment) {
  925. ids.push(segment.dataKey);
  926. });
  927. });
  928. });
  929. return ids;
  930. };
  931. /**
  932. * Delete the on-disk storage and all the content it contains. This should not
  933. * be done in normal circumstances. Only do it when storage is rendered
  934. * unusable, such as by a version mismatch. No business logic will be run, and
  935. * licenses will not be released.
  936. *
  937. * @return {!Promise}
  938. * @export
  939. */
  940. shaka.offline.Storage.deleteAll = async function() {
  941. /** @type {!shaka.offline.StorageMuxer} */
  942. const muxer = new shaka.offline.StorageMuxer();
  943. try {
  944. // Wipe all content from all storage mechanisms.
  945. await muxer.erase();
  946. } finally {
  947. // Destroy the muxer, whether or not erase() succeeded.
  948. await muxer.destroy();
  949. }
  950. };
  951. /**
  952. * Look to see if there are any tracks that are "too" similar to each other.
  953. *
  954. * @param {!Array.<shakaExtern.Track>} tracks
  955. * @return {boolean}
  956. * @private
  957. */
  958. shaka.offline.Storage.lookForSimilarTracks_ = function(tracks) {
  959. return tracks.some((t0) => {
  960. return tracks.some((t1) => {
  961. return t0 != t1 &&
  962. t0.type == t1.type &&
  963. t0.kind == t1.kind &&
  964. t0.language == t1.language;
  965. });
  966. });
  967. };
  968. /**
  969. * Get a collection of streams that are in the manifest. This collection will
  970. * only have one instance of each stream (similar to a set).
  971. *
  972. * @param {shakaExtern.Manifest} manifest
  973. * @return {!Array.<shakaExtern.Stream>}
  974. * @private
  975. */
  976. shaka.offline.Storage.getStreamSet_ = function(manifest) {
  977. // Use a map so that we don't store duplicates. Since a stream's id should
  978. // be unique within the manifest, we can use that as the key.
  979. let map = {};
  980. manifest.periods.forEach((period) => {
  981. period.textStreams.forEach((text) => { map[text.id] = text; });
  982. period.variants.forEach((variant) => {
  983. if (variant.audio) { map[variant.audio.id] = variant.audio; }
  984. if (variant.video) { map[variant.video.id] = variant.video; }
  985. });
  986. });
  987. return shaka.util.MapUtils.values(map);
  988. };
  989. shaka.Player.registerSupportPlugin('offline', shaka.offline.Storage.support);