Source: lib/offline/storage.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.offline.Storage');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Player');
  9. goog.require('shaka.log');
  10. goog.require('shaka.media.DrmEngine');
  11. goog.require('shaka.media.ManifestParser');
  12. goog.require('shaka.net.NetworkingEngine');
  13. goog.require('shaka.net.NetworkingUtils');
  14. goog.require('shaka.offline.DownloadInfo');
  15. goog.require('shaka.offline.DownloadManager');
  16. goog.require('shaka.offline.OfflineUri');
  17. goog.require('shaka.offline.SessionDeleter');
  18. goog.require('shaka.offline.StorageMuxer');
  19. goog.require('shaka.offline.StoredContentUtils');
  20. goog.require('shaka.offline.StreamBandwidthEstimator');
  21. goog.require('shaka.util.AbortableOperation');
  22. goog.require('shaka.util.ArrayUtils');
  23. goog.require('shaka.util.ConfigUtils');
  24. goog.require('shaka.util.Destroyer');
  25. goog.require('shaka.util.Error');
  26. goog.require('shaka.util.IDestroyable');
  27. goog.require('shaka.util.Iterables');
  28. goog.require('shaka.util.MimeUtils');
  29. goog.require('shaka.util.Platform');
  30. goog.require('shaka.util.PlayerConfiguration');
  31. goog.require('shaka.util.StreamUtils');
  32. goog.requireType('shaka.media.SegmentReference');
  33. goog.requireType('shaka.offline.StorageCellHandle');
  34. /**
  35. * @summary
  36. * This manages persistent offline data including storage, listing, and deleting
  37. * stored manifests. Playback of offline manifests are done through the Player
  38. * using a special URI (see shaka.offline.OfflineUri).
  39. *
  40. * First, check support() to see if offline is supported by the platform.
  41. * Second, configure() the storage object with callbacks to your application.
  42. * Third, call store(), remove(), or list() as needed.
  43. * When done, call destroy().
  44. *
  45. * @implements {shaka.util.IDestroyable}
  46. * @export
  47. */
  48. shaka.offline.Storage = class {
  49. /**
  50. * @param {!shaka.Player=} player
  51. * A player instance to share a networking engine and configuration with.
  52. * When initializing with a player, storage is only valid as long as
  53. * |destroy| has not been called on the player instance. When omitted,
  54. * storage will manage its own networking engine and configuration.
  55. */
  56. constructor(player) {
  57. // It is an easy mistake to make to pass a Player proxy from CastProxy.
  58. // Rather than throw a vague exception later, throw an explicit and clear
  59. // one now.
  60. //
  61. // TODO(vaage): After we decide whether or not we want to support
  62. // initializing storage with a player proxy, we should either remove
  63. // this error or rename the error.
  64. if (player && player.constructor != shaka.Player) {
  65. throw new shaka.util.Error(
  66. shaka.util.Error.Severity.CRITICAL,
  67. shaka.util.Error.Category.STORAGE,
  68. shaka.util.Error.Code.LOCAL_PLAYER_INSTANCE_REQUIRED);
  69. }
  70. /** @private {?shaka.extern.PlayerConfiguration} */
  71. this.config_ = null;
  72. /** @private {shaka.net.NetworkingEngine} */
  73. this.networkingEngine_ = null;
  74. // Initialize |config_| and |networkingEngine_| based on whether or not
  75. // we were given a player instance.
  76. if (player) {
  77. this.config_ = player.getSharedConfiguration();
  78. this.networkingEngine_ = player.getNetworkingEngine();
  79. goog.asserts.assert(
  80. this.networkingEngine_,
  81. 'Storage should not be initialized with a player that had ' +
  82. '|destroy| called on it.');
  83. } else {
  84. this.config_ = shaka.util.PlayerConfiguration.createDefault();
  85. this.networkingEngine_ = new shaka.net.NetworkingEngine();
  86. }
  87. /**
  88. * A list of open operations that are being performed by this instance of
  89. * |shaka.offline.Storage|.
  90. *
  91. * @private {!Array.<!Promise>}
  92. */
  93. this.openOperations_ = [];
  94. /**
  95. * A list of open download managers that are being used to download things.
  96. *
  97. * @private {!Array.<!shaka.offline.DownloadManager>}
  98. */
  99. this.openDownloadManagers_ = [];
  100. /**
  101. * Storage should only destroy the networking engine if it was initialized
  102. * without a player instance. Store this as a flag here to avoid including
  103. * the player object in the destoyer's closure.
  104. *
  105. * @type {boolean}
  106. */
  107. const destroyNetworkingEngine = !player;
  108. /** @private {!shaka.util.Destroyer} */
  109. this.destroyer_ = new shaka.util.Destroyer(async () => {
  110. // Cancel all in-progress store operations.
  111. await Promise.all(this.openDownloadManagers_.map((dl) => dl.abortAll()));
  112. // Wait for all remaining open operations to end. Wrap each operations so
  113. // that a single rejected promise won't cause |Promise.all| to return
  114. // early or to return a rejected Promise.
  115. const noop = () => {};
  116. const awaits = [];
  117. for (const op of this.openOperations_) {
  118. awaits.push(op.then(noop, noop));
  119. }
  120. await Promise.all(awaits);
  121. // Wait until after all the operations have finished before we destroy
  122. // the networking engine to avoid any unexpected errors.
  123. if (destroyNetworkingEngine) {
  124. await this.networkingEngine_.destroy();
  125. }
  126. // Drop all references to internal objects to help with GC.
  127. this.config_ = null;
  128. this.networkingEngine_ = null;
  129. });
  130. }
  131. /**
  132. * Gets whether offline storage is supported. Returns true if offline storage
  133. * is supported for clear content. Support for offline storage of encrypted
  134. * content will not be determined until storage is attempted.
  135. *
  136. * @return {boolean}
  137. * @export
  138. */
  139. static support() {
  140. // Our Storage system is useless without MediaSource. MediaSource allows us
  141. // to pull data from anywhere (including our Storage system) and feed it to
  142. // the video element.
  143. if (!shaka.util.Platform.supportsMediaSource()) {
  144. return false;
  145. }
  146. return shaka.offline.StorageMuxer.support();
  147. }
  148. /**
  149. * @override
  150. * @export
  151. */
  152. destroy() {
  153. return this.destroyer_.destroy();
  154. }
  155. /**
  156. * Sets configuration values for Storage. This is associated with
  157. * Player.configure and will change the player instance given at
  158. * initialization.
  159. *
  160. * @param {string|!Object} config This should either be a field name or an
  161. * object following the form of {@link shaka.extern.PlayerConfiguration},
  162. * where you may omit any field you do not wish to change.
  163. * @param {*=} value This should be provided if the previous parameter
  164. * was a string field name.
  165. * @return {boolean}
  166. * @export
  167. */
  168. configure(config, value) {
  169. goog.asserts.assert(typeof(config) == 'object' || arguments.length == 2,
  170. 'String configs should have values!');
  171. // ('fieldName', value) format
  172. if (arguments.length == 2 && typeof(config) == 'string') {
  173. config = shaka.util.ConfigUtils.convertToConfigObject(config, value);
  174. }
  175. goog.asserts.assert(typeof(config) == 'object', 'Should be an object!');
  176. goog.asserts.assert(
  177. this.config_, 'Cannot reconfigure storage after calling destroy.');
  178. return shaka.util.PlayerConfiguration.mergeConfigObjects(
  179. /* destination= */ this.config_, /* updates= */ config );
  180. }
  181. /**
  182. * Return a copy of the current configuration. Modifications of the returned
  183. * value will not affect the Storage instance's active configuration. You
  184. * must call storage.configure() to make changes.
  185. *
  186. * @return {shaka.extern.PlayerConfiguration}
  187. * @export
  188. */
  189. getConfiguration() {
  190. goog.asserts.assert(this.config_, 'Config must not be null!');
  191. const ret = shaka.util.PlayerConfiguration.createDefault();
  192. shaka.util.PlayerConfiguration.mergeConfigObjects(
  193. ret, this.config_, shaka.util.PlayerConfiguration.createDefault());
  194. return ret;
  195. }
  196. /**
  197. * Return the networking engine that storage is using. If storage was
  198. * initialized with a player instance, then the networking engine returned
  199. * will be the same as |player.getNetworkingEngine()|.
  200. *
  201. * The returned value will only be null if |destroy| was called before
  202. * |getNetworkingEngine|.
  203. *
  204. * @return {shaka.net.NetworkingEngine}
  205. * @export
  206. */
  207. getNetworkingEngine() {
  208. return this.networkingEngine_;
  209. }
  210. /**
  211. * Stores the given manifest. If the content is encrypted, and encrypted
  212. * content cannot be stored on this platform, the Promise will be rejected
  213. * with error code 6001, REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE.
  214. * Multiple assets can be downloaded at the same time, but note that since
  215. * the storage instance has a single networking engine, multiple storage
  216. * objects will be necessary if some assets require unique network filters.
  217. * This snapshots the storage config at the time of the call, so it will not
  218. * honor any changes to config mid-store operation.
  219. *
  220. * @param {string} uri The URI of the manifest to store.
  221. * @param {!Object=} appMetadata An arbitrary object from the application
  222. * that will be stored along-side the offline content. Use this for any
  223. * application-specific metadata you need associated with the stored
  224. * content. For details on the data types that can be stored here, please
  225. * refer to {@link https://bit.ly/StructClone}
  226. * @param {string=} mimeType
  227. * The mime type for the content |manifestUri| points to.
  228. * @return {!shaka.extern.IAbortableOperation.<shaka.extern.StoredContent>}
  229. * An AbortableOperation that resolves with a structure representing what
  230. * was stored. The "offlineUri" member is the URI that should be given to
  231. * Player.load() to play this piece of content offline. The "appMetadata"
  232. * member is the appMetadata argument you passed to store().
  233. * If you want to cancel this download, call the "abort" method on
  234. * AbortableOperation.
  235. * @export
  236. */
  237. store(uri, appMetadata, mimeType) {
  238. goog.asserts.assert(
  239. this.networkingEngine_,
  240. 'Cannot call |store| after calling |destroy|.');
  241. // Get a copy of the current config.
  242. const config = this.getConfiguration();
  243. const getParser = async () => {
  244. goog.asserts.assert(
  245. this.networkingEngine_, 'Should not call |store| after |destroy|');
  246. if (!mimeType) {
  247. mimeType = await shaka.net.NetworkingUtils.getMimeType(
  248. uri, this.networkingEngine_, config.manifest.retryParameters);
  249. }
  250. const factory = shaka.media.ManifestParser.getFactory(
  251. uri,
  252. mimeType || null);
  253. return factory();
  254. };
  255. /** @type {!shaka.offline.DownloadManager} */
  256. const downloader =
  257. new shaka.offline.DownloadManager(this.networkingEngine_);
  258. this.openDownloadManagers_.push(downloader);
  259. const storeOp = this.store_(
  260. uri, appMetadata || {}, getParser, config, downloader);
  261. const abortableStoreOp = new shaka.util.AbortableOperation(storeOp, () => {
  262. return downloader.abortAll();
  263. });
  264. abortableStoreOp.finally(() => {
  265. shaka.util.ArrayUtils.remove(this.openDownloadManagers_, downloader);
  266. });
  267. return this.startAbortableOperation_(abortableStoreOp);
  268. }
  269. /**
  270. * See |shaka.offline.Storage.store| for details.
  271. *
  272. * @param {string} uri
  273. * @param {!Object} appMetadata
  274. * @param {function():!Promise.<shaka.extern.ManifestParser>} getParser
  275. * @param {shaka.extern.PlayerConfiguration} config
  276. * @param {!shaka.offline.DownloadManager} downloader
  277. * @return {!Promise.<shaka.extern.StoredContent>}
  278. * @private
  279. */
  280. async store_(uri, appMetadata, getParser, config, downloader) {
  281. this.requireSupport_();
  282. // Since we will need to use |parser|, |drmEngine|, |activeHandle|, and
  283. // |muxer| in the catch/finally blocks, we need to define them out here.
  284. // Since they may not get initialized when we enter the catch/finally block,
  285. // we need to assume that they may be null/undefined when we get there.
  286. /** @type {?shaka.extern.ManifestParser} */
  287. let parser = null;
  288. /** @type {?shaka.media.DrmEngine} */
  289. let drmEngine = null;
  290. /** @type {shaka.offline.StorageMuxer} */
  291. const muxer = new shaka.offline.StorageMuxer();
  292. /** @type {?shaka.offline.StorageCellHandle} */
  293. let activeHandle = null;
  294. /** @type {?number} */
  295. let manifestId = null;
  296. // This will be used to store any errors from drm engine. Whenever drm
  297. // engine is passed to another function to do work, we should check if this
  298. // was set.
  299. let drmError = null;
  300. try {
  301. parser = await getParser();
  302. const manifest = await this.parseManifest(uri, parser, config);
  303. // Check if we were asked to destroy ourselves while we were "away"
  304. // downloading the manifest.
  305. this.ensureNotDestroyed_();
  306. // Check if we can even download this type of manifest before trying to
  307. // create the drm engine.
  308. const canDownload = !manifest.presentationTimeline.isLive() &&
  309. !manifest.presentationTimeline.isInProgress();
  310. if (!canDownload) {
  311. throw new shaka.util.Error(
  312. shaka.util.Error.Severity.CRITICAL,
  313. shaka.util.Error.Category.STORAGE,
  314. shaka.util.Error.Code.CANNOT_STORE_LIVE_OFFLINE,
  315. uri);
  316. }
  317. // Create the DRM engine, and load the keys in the manifest.
  318. drmEngine = await this.createDrmEngine(
  319. manifest,
  320. (e) => { drmError = drmError || e; },
  321. config);
  322. // We could have been asked to destroy ourselves while we were "away"
  323. // creating the drm engine.
  324. this.ensureNotDestroyed_();
  325. if (drmError) {
  326. throw drmError;
  327. }
  328. await this.filterManifest_(manifest, drmEngine, config);
  329. await muxer.init();
  330. this.ensureNotDestroyed_();
  331. // Get the cell that we are saving the manifest to. Once we get a cell
  332. // we will only reference the cell and not the muxer so that the manifest
  333. // and segments will all be saved to the same cell.
  334. activeHandle = await muxer.getActive();
  335. this.ensureNotDestroyed_();
  336. goog.asserts.assert(drmEngine, 'drmEngine should be non-null here.');
  337. const {manifestDB, toDownload} = this.makeManifestDB_(
  338. drmEngine, manifest, uri, appMetadata, config, downloader);
  339. // Store the empty manifest, before downloading the segments.
  340. const ids = await activeHandle.cell.addManifests([manifestDB]);
  341. this.ensureNotDestroyed_();
  342. manifestId = ids[0];
  343. goog.asserts.assert(drmEngine, 'drmEngine should be non-null here.');
  344. this.ensureNotDestroyed_();
  345. if (drmError) {
  346. throw drmError;
  347. }
  348. await this.downloadSegments_(toDownload, manifestId, manifestDB,
  349. downloader, config, activeHandle.cell, manifest, drmEngine);
  350. this.ensureNotDestroyed_();
  351. this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
  352. await activeHandle.cell.updateManifest(manifestId, manifestDB);
  353. this.ensureNotDestroyed_();
  354. const offlineUri = shaka.offline.OfflineUri.manifest(
  355. activeHandle.path.mechanism, activeHandle.path.cell, manifestId);
  356. return shaka.offline.StoredContentUtils.fromManifestDB(
  357. offlineUri, manifestDB);
  358. } catch (e) {
  359. if (manifestId != null) {
  360. await shaka.offline.Storage.cleanStoredManifest(manifestId);
  361. }
  362. // If we already had an error, ignore this error to avoid hiding
  363. // the original error.
  364. throw drmError || e;
  365. } finally {
  366. await muxer.destroy();
  367. if (parser) {
  368. await parser.stop();
  369. }
  370. if (drmEngine) {
  371. await drmEngine.destroy();
  372. }
  373. }
  374. }
  375. /**
  376. * Download and then store the contents of each segment.
  377. * The promise this returns will wait for local downloads.
  378. *
  379. * @param {!Array.<!shaka.offline.DownloadInfo>} toDownload
  380. * @param {number} manifestId
  381. * @param {shaka.extern.ManifestDB} manifestDB
  382. * @param {!shaka.offline.DownloadManager} downloader
  383. * @param {shaka.extern.PlayerConfiguration} config
  384. * @param {shaka.extern.StorageCell} storage
  385. * @param {shaka.extern.Manifest} manifest
  386. * @param {!shaka.media.DrmEngine} drmEngine
  387. * @return {!Promise}
  388. * @private
  389. */
  390. async downloadSegments_(
  391. toDownload, manifestId, manifestDB, downloader, config, storage,
  392. manifest, drmEngine) {
  393. let pendingManifestUpdates = {};
  394. let pendingDataSize = 0;
  395. /**
  396. * @param {!Array.<!shaka.offline.DownloadInfo>} toDownload
  397. * @param {boolean} updateDRM
  398. */
  399. const download = async (toDownload, updateDRM) => {
  400. for (const download of toDownload) {
  401. const request = download.makeSegmentRequest(config);
  402. const estimateId = download.estimateId;
  403. const isInitSegment = download.isInitSegment;
  404. const onDownloaded = async (data) => {
  405. // Store the data.
  406. const dataKeys = await storage.addSegments([{data}]);
  407. this.ensureNotDestroyed_();
  408. // Store the necessary update to the manifest, to be processed later.
  409. const ref = /** @type {!shaka.media.SegmentReference} */ (
  410. download.ref);
  411. const id = shaka.offline.DownloadInfo.idForSegmentRef(ref);
  412. pendingManifestUpdates[id] = dataKeys[0];
  413. pendingDataSize += data.byteLength;
  414. };
  415. downloader.queue(download.groupId,
  416. request, estimateId, isInitSegment, onDownloaded);
  417. }
  418. await downloader.waitToFinish();
  419. if (updateDRM) {
  420. // Re-store the manifest, to attach session IDs.
  421. // These were (maybe) discovered inside the downloader; we can only add
  422. // them now, at the end, since the manifestDB is in flux during the
  423. // process of downloading and storing, and assignSegmentsToManifest
  424. // does not know about the DRM engine.
  425. this.ensureNotDestroyed_();
  426. this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
  427. await storage.updateManifest(manifestId, manifestDB);
  428. }
  429. };
  430. const usingBgFetch = false; // TODO: Get.
  431. try {
  432. if (this.getManifestIsEncrypted_(manifest) && usingBgFetch &&
  433. !this.getManifestIncludesInitData_(manifest)) {
  434. // Background fetch can't make DRM sessions, so if we have to get the
  435. // init data from the init segments, download those first before
  436. // anything else.
  437. await download(toDownload.filter((info) => info.isInitSegment), true);
  438. this.ensureNotDestroyed_();
  439. toDownload = toDownload.filter((info) => !info.isInitSegment);
  440. // Copy these and reset them now, before calling await.
  441. const manifestUpdates = pendingManifestUpdates;
  442. const dataSize = pendingDataSize;
  443. pendingManifestUpdates = {};
  444. pendingDataSize = 0;
  445. await shaka.offline.Storage.assignSegmentsToManifest(
  446. storage, manifestId, manifestDB, manifestUpdates, dataSize,
  447. () => this.ensureNotDestroyed_());
  448. this.ensureNotDestroyed_();
  449. }
  450. if (!usingBgFetch) {
  451. await download(toDownload, false);
  452. this.ensureNotDestroyed_();
  453. // Copy these and reset them now, before calling await.
  454. const manifestUpdates = pendingManifestUpdates;
  455. const dataSize = pendingDataSize;
  456. pendingManifestUpdates = {};
  457. pendingDataSize = 0;
  458. await shaka.offline.Storage.assignSegmentsToManifest(
  459. storage, manifestId, manifestDB, manifestUpdates, dataSize,
  460. () => this.ensureNotDestroyed_());
  461. this.ensureNotDestroyed_();
  462. goog.asserts.assert(
  463. !manifestDB.isIncomplete, 'The manifest should be complete by now');
  464. } else {
  465. // TODO: Send the request to the service worker. Don't await the result.
  466. }
  467. } catch (error) {
  468. const dataKeys = Object.values(pendingManifestUpdates);
  469. // Remove these pending segments that are not yet linked to the manifest.
  470. await storage.removeSegments(dataKeys, (key) => {});
  471. throw error;
  472. }
  473. }
  474. /**
  475. * Removes all of the contents for a given manifest, statelessly.
  476. *
  477. * @param {number} manifestId
  478. * @return {!Promise}
  479. */
  480. static async cleanStoredManifest(manifestId) {
  481. const muxer = new shaka.offline.StorageMuxer();
  482. await muxer.init();
  483. const activeHandle = await muxer.getActive();
  484. const uri = shaka.offline.OfflineUri.manifest(
  485. activeHandle.path.mechanism,
  486. activeHandle.path.cell,
  487. manifestId);
  488. await muxer.destroy();
  489. const storage = new shaka.offline.Storage();
  490. await storage.remove(uri.toString());
  491. }
  492. /**
  493. * Updates the given manifest, assigns database keys to segments, then stores
  494. * the updated manifest.
  495. *
  496. * It is up to the caller to ensure that this method is not called
  497. * concurrently on the same manifest.
  498. *
  499. * @param {shaka.extern.StorageCell} storage
  500. * @param {number} manifestId
  501. * @param {!shaka.extern.ManifestDB} manifestDB
  502. * @param {!Object.<string, number>} manifestUpdates
  503. * @param {number} dataSizeUpdate
  504. * @param {function()} throwIfAbortedFn A function that should throw if the
  505. * download has been aborted.
  506. * @return {!Promise}
  507. */
  508. static async assignSegmentsToManifest(
  509. storage, manifestId, manifestDB, manifestUpdates, dataSizeUpdate,
  510. throwIfAbortedFn) {
  511. let manifestUpdated = false;
  512. try {
  513. // Assign the stored data to the manifest.
  514. let complete = true;
  515. for (const stream of manifestDB.streams) {
  516. for (const segment of stream.segments) {
  517. let dataKey = segment.pendingSegmentRefId ?
  518. manifestUpdates[segment.pendingSegmentRefId] : null;
  519. if (dataKey != null) {
  520. segment.dataKey = dataKey;
  521. // Now that the segment has been associated with the appropriate
  522. // dataKey, the pendingSegmentRefId is no longer necessary.
  523. segment.pendingSegmentRefId = undefined;
  524. }
  525. dataKey = segment.pendingInitSegmentRefId ?
  526. manifestUpdates[segment.pendingInitSegmentRefId] : null;
  527. if (dataKey != null) {
  528. segment.initSegmentKey = dataKey;
  529. // Now that the init segment has been associated with the
  530. // appropriate initSegmentKey, the pendingInitSegmentRefId is no
  531. // longer necessary.
  532. segment.pendingInitSegmentRefId = undefined;
  533. }
  534. if (segment.pendingSegmentRefId) {
  535. complete = false;
  536. }
  537. if (segment.pendingInitSegmentRefId) {
  538. complete = false;
  539. }
  540. }
  541. }
  542. // Update the size of the manifest.
  543. manifestDB.size += dataSizeUpdate;
  544. // Mark the manifest as complete, if all segments are downloaded.
  545. if (complete) {
  546. manifestDB.isIncomplete = false;
  547. }
  548. // Update the manifest.
  549. await storage.updateManifest(manifestId, manifestDB);
  550. manifestUpdated = true;
  551. throwIfAbortedFn();
  552. } catch (e) {
  553. await shaka.offline.Storage.cleanStoredManifest(manifestId);
  554. if (!manifestUpdated) {
  555. const dataKeys = Object.values(manifestUpdates);
  556. // The cleanStoredManifest method will not "see" any segments that have
  557. // been downloaded but not assigned to the manifest yet. So un-store
  558. // them separately.
  559. await storage.removeSegments(dataKeys, (key) => {});
  560. }
  561. throw e;
  562. }
  563. }
  564. /**
  565. * Filter |manifest| such that it will only contain the variants and text
  566. * streams that we want to store and can actually play.
  567. *
  568. * @param {shaka.extern.Manifest} manifest
  569. * @param {!shaka.media.DrmEngine} drmEngine
  570. * @param {shaka.extern.PlayerConfiguration} config
  571. * @return {!Promise}
  572. * @private
  573. */
  574. async filterManifest_(manifest, drmEngine, config) {
  575. // Filter the manifest based on the restrictions given in the player
  576. // configuration.
  577. const maxHwRes = {width: Infinity, height: Infinity};
  578. shaka.util.StreamUtils.filterByRestrictions(
  579. manifest, config.restrictions, maxHwRes);
  580. // Filter the manifest based on what we know MediaCapabilities will be able
  581. // to play later (no point storing something we can't play).
  582. await shaka.util.StreamUtils.filterManifestByMediaCapabilities(
  583. drmEngine, manifest, config.offline.usePersistentLicense,
  584. config.drm.preferredKeySystems);
  585. // Gather all tracks.
  586. const allTracks = [];
  587. // Choose the codec that has the lowest average bandwidth.
  588. const preferredDecodingAttributes = config.preferredDecodingAttributes;
  589. const preferredVideoCodecs = config.preferredVideoCodecs;
  590. const preferredAudioCodecs = config.preferredAudioCodecs;
  591. shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
  592. manifest, preferredVideoCodecs, preferredAudioCodecs,
  593. preferredDecodingAttributes);
  594. for (const variant of manifest.variants) {
  595. goog.asserts.assert(
  596. shaka.util.StreamUtils.isPlayable(variant),
  597. 'We should have already filtered by "is playable"');
  598. allTracks.push(shaka.util.StreamUtils.variantToTrack(variant));
  599. }
  600. for (const text of manifest.textStreams) {
  601. allTracks.push(shaka.util.StreamUtils.textStreamToTrack(text));
  602. }
  603. for (const image of manifest.imageStreams) {
  604. allTracks.push(shaka.util.StreamUtils.imageStreamToTrack(image));
  605. }
  606. // Let the application choose which tracks to store.
  607. const chosenTracks =
  608. await config.offline.trackSelectionCallback(allTracks);
  609. const duration = manifest.presentationTimeline.getDuration();
  610. let sizeEstimate = 0;
  611. for (const track of chosenTracks) {
  612. const trackSize = track.bandwidth * duration / 8;
  613. sizeEstimate += trackSize;
  614. }
  615. try {
  616. const allowedDownload =
  617. await config.offline.downloadSizeCallback(sizeEstimate);
  618. if (!allowedDownload) {
  619. throw new shaka.util.Error(
  620. shaka.util.Error.Severity.CRITICAL,
  621. shaka.util.Error.Category.STORAGE,
  622. shaka.util.Error.Code.STORAGE_LIMIT_REACHED);
  623. }
  624. } catch (e) {
  625. // It is necessary to be able to catch the STORAGE_LIMIT_REACHED error
  626. if (e instanceof shaka.util.Error) {
  627. throw e;
  628. }
  629. shaka.log.warning(
  630. 'downloadSizeCallback has produced an unexpected error', e);
  631. throw new shaka.util.Error(
  632. shaka.util.Error.Severity.CRITICAL,
  633. shaka.util.Error.Category.STORAGE,
  634. shaka.util.Error.Code.DOWNLOAD_SIZE_CALLBACK_ERROR);
  635. }
  636. /** @type {!Set.<number>} */
  637. const variantIds = new Set();
  638. /** @type {!Set.<number>} */
  639. const textIds = new Set();
  640. /** @type {!Set.<number>} */
  641. const imageIds = new Set();
  642. // Collect the IDs of the chosen tracks.
  643. for (const track of chosenTracks) {
  644. if (track.type == 'variant') {
  645. variantIds.add(track.id);
  646. }
  647. if (track.type == 'text') {
  648. textIds.add(track.id);
  649. }
  650. if (track.type == 'image') {
  651. imageIds.add(track.id);
  652. }
  653. }
  654. // Filter the manifest to keep only what the app chose.
  655. manifest.variants =
  656. manifest.variants.filter((variant) => variantIds.has(variant.id));
  657. manifest.textStreams =
  658. manifest.textStreams.filter((stream) => textIds.has(stream.id));
  659. manifest.imageStreams =
  660. manifest.imageStreams.filter((stream) => imageIds.has(stream.id));
  661. // Check the post-filtered manifest for characteristics that may indicate
  662. // issues with how the app selected tracks.
  663. shaka.offline.Storage.validateManifest_(manifest);
  664. }
  665. /**
  666. * Create a download manager and download the manifest.
  667. * This also sets up download infos for each segment to be downloaded.
  668. *
  669. * @param {!shaka.media.DrmEngine} drmEngine
  670. * @param {shaka.extern.Manifest} manifest
  671. * @param {string} uri
  672. * @param {!Object} metadata
  673. * @param {shaka.extern.PlayerConfiguration} config
  674. * @param {!shaka.offline.DownloadManager} downloader
  675. * @return {{
  676. * manifestDB: shaka.extern.ManifestDB,
  677. * toDownload: !Array.<!shaka.offline.DownloadInfo>
  678. * }}
  679. * @private
  680. */
  681. makeManifestDB_(drmEngine, manifest, uri, metadata, config, downloader) {
  682. const pendingContent = shaka.offline.StoredContentUtils.fromManifest(
  683. uri, manifest, /* size= */ 0, metadata);
  684. // In https://github.com/shaka-project/shaka-player/issues/2652, we found
  685. // that this callback would be removed by the compiler if we reference the
  686. // config in the onProgress closure below. Reading it into a local
  687. // variable first seems to work around this apparent compiler bug.
  688. const progressCallback = config.offline.progressCallback;
  689. const onProgress = (progress, size) => {
  690. // Update the size of the stored content before issuing a progress
  691. // update.
  692. pendingContent.size = size;
  693. progressCallback(pendingContent, progress);
  694. };
  695. const onInitData = (initData, systemId) => {
  696. if (needsInitData && config.offline.usePersistentLicense &&
  697. currentSystemId == systemId) {
  698. drmEngine.newInitData('cenc', initData);
  699. }
  700. };
  701. downloader.setCallbacks(onProgress, onInitData);
  702. const needsInitData = this.getManifestIsEncrypted_(manifest) &&
  703. !this.getManifestIncludesInitData_(manifest);
  704. let currentSystemId = null;
  705. if (needsInitData) {
  706. const drmInfo = drmEngine.getDrmInfo();
  707. currentSystemId =
  708. shaka.offline.Storage.defaultSystemIds_.get(drmInfo.keySystem);
  709. }
  710. // Make the estimator, which is used to make the download registries.
  711. const estimator = new shaka.offline.StreamBandwidthEstimator();
  712. for (const stream of manifest.textStreams) {
  713. estimator.addText(stream);
  714. }
  715. for (const stream of manifest.imageStreams) {
  716. estimator.addImage(stream);
  717. }
  718. for (const variant of manifest.variants) {
  719. estimator.addVariant(variant);
  720. }
  721. const {streams, toDownload} = this.createStreams_(
  722. downloader, estimator, drmEngine, manifest, config);
  723. const drmInfo = drmEngine.getDrmInfo();
  724. const usePersistentLicense = config.offline.usePersistentLicense;
  725. if (drmInfo && usePersistentLicense) {
  726. // Don't store init data, since we have stored sessions.
  727. drmInfo.initData = [];
  728. }
  729. const manifestDB = {
  730. creationTime: Date.now(),
  731. originalManifestUri: uri,
  732. duration: manifest.presentationTimeline.getDuration(),
  733. size: 0,
  734. expiration: drmEngine.getExpiration(),
  735. streams,
  736. sessionIds: usePersistentLicense ? drmEngine.getSessionIds() : [],
  737. drmInfo,
  738. appMetadata: metadata,
  739. isIncomplete: true,
  740. sequenceMode: manifest.sequenceMode,
  741. type: manifest.type,
  742. };
  743. return {manifestDB, toDownload};
  744. }
  745. /**
  746. * @param {shaka.extern.Manifest} manifest
  747. * @return {boolean}
  748. * @private
  749. */
  750. getManifestIsEncrypted_(manifest) {
  751. return manifest.variants.some((variant) => {
  752. const videoEncrypted = variant.video && variant.video.encrypted;
  753. const audioEncrypted = variant.audio && variant.audio.encrypted;
  754. return videoEncrypted || audioEncrypted;
  755. });
  756. }
  757. /**
  758. * @param {shaka.extern.Manifest} manifest
  759. * @return {boolean}
  760. * @private
  761. */
  762. getManifestIncludesInitData_(manifest) {
  763. return manifest.variants.some((variant) => {
  764. const videoDrmInfos = variant.video ? variant.video.drmInfos : [];
  765. const audioDrmInfos = variant.audio ? variant.audio.drmInfos : [];
  766. const drmInfos = videoDrmInfos.concat(audioDrmInfos);
  767. return drmInfos.some((drmInfos) => {
  768. return drmInfos.initData && drmInfos.initData.length;
  769. });
  770. });
  771. }
  772. /**
  773. * @param {shaka.extern.Manifest} manifest
  774. * @param {shaka.extern.ManifestDB} manifestDB
  775. * @param {!shaka.media.DrmEngine} drmEngine
  776. * @param {shaka.extern.PlayerConfiguration} config
  777. * @private
  778. */
  779. setManifestDrmFields_(manifest, manifestDB, drmEngine, config) {
  780. manifestDB.expiration = drmEngine.getExpiration();
  781. const sessions = drmEngine.getSessionIds();
  782. manifestDB.sessionIds = config.offline.usePersistentLicense ?
  783. sessions : [];
  784. if (this.getManifestIsEncrypted_(manifest) &&
  785. config.offline.usePersistentLicense && !sessions.length) {
  786. throw new shaka.util.Error(
  787. shaka.util.Error.Severity.CRITICAL,
  788. shaka.util.Error.Category.STORAGE,
  789. shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE);
  790. }
  791. }
  792. /**
  793. * Removes the given stored content. This will also attempt to release the
  794. * licenses, if any.
  795. *
  796. * @param {string} contentUri
  797. * @return {!Promise}
  798. * @export
  799. */
  800. remove(contentUri) {
  801. return this.startOperation_(this.remove_(contentUri));
  802. }
  803. /**
  804. * See |shaka.offline.Storage.remove| for details.
  805. *
  806. * @param {string} contentUri
  807. * @return {!Promise}
  808. * @private
  809. */
  810. async remove_(contentUri) {
  811. this.requireSupport_();
  812. const nullableUri = shaka.offline.OfflineUri.parse(contentUri);
  813. if (nullableUri == null || !nullableUri.isManifest()) {
  814. throw new shaka.util.Error(
  815. shaka.util.Error.Severity.CRITICAL,
  816. shaka.util.Error.Category.STORAGE,
  817. shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
  818. contentUri);
  819. }
  820. /** @type {!shaka.offline.OfflineUri} */
  821. const uri = nullableUri;
  822. /** @type {!shaka.offline.StorageMuxer} */
  823. const muxer = new shaka.offline.StorageMuxer();
  824. try {
  825. await muxer.init();
  826. const cell = await muxer.getCell(uri.mechanism(), uri.cell());
  827. const manifests = await cell.getManifests([uri.key()]);
  828. const manifest = manifests[0];
  829. await Promise.all([
  830. this.removeFromDRM_(uri, manifest, muxer),
  831. this.removeFromStorage_(cell, uri, manifest),
  832. ]);
  833. } finally {
  834. await muxer.destroy();
  835. }
  836. }
  837. /**
  838. * @param {shaka.extern.ManifestDB} manifestDb
  839. * @param {boolean} isVideo
  840. * @return {!Array.<MediaKeySystemMediaCapability>}
  841. * @private
  842. */
  843. static getCapabilities_(manifestDb, isVideo) {
  844. const MimeUtils = shaka.util.MimeUtils;
  845. const ret = [];
  846. for (const stream of manifestDb.streams) {
  847. if (isVideo && stream.type == 'video') {
  848. ret.push({
  849. contentType: MimeUtils.getFullType(stream.mimeType, stream.codecs),
  850. robustness: manifestDb.drmInfo.videoRobustness,
  851. });
  852. } else if (!isVideo && stream.type == 'audio') {
  853. ret.push({
  854. contentType: MimeUtils.getFullType(stream.mimeType, stream.codecs),
  855. robustness: manifestDb.drmInfo.audioRobustness,
  856. });
  857. }
  858. }
  859. return ret;
  860. }
  861. /**
  862. * @param {!shaka.offline.OfflineUri} uri
  863. * @param {shaka.extern.ManifestDB} manifestDb
  864. * @param {!shaka.offline.StorageMuxer} muxer
  865. * @return {!Promise}
  866. * @private
  867. */
  868. async removeFromDRM_(uri, manifestDb, muxer) {
  869. goog.asserts.assert(this.networkingEngine_, 'Cannot be destroyed');
  870. await shaka.offline.Storage.deleteLicenseFor_(
  871. this.networkingEngine_, this.config_.drm, muxer, manifestDb);
  872. }
  873. /**
  874. * @param {shaka.extern.StorageCell} storage
  875. * @param {!shaka.offline.OfflineUri} uri
  876. * @param {shaka.extern.ManifestDB} manifest
  877. * @return {!Promise}
  878. * @private
  879. */
  880. removeFromStorage_(storage, uri, manifest) {
  881. /** @type {!Array.<number>} */
  882. const segmentIds = shaka.offline.Storage.getAllSegmentIds_(manifest);
  883. // Count(segments) + Count(manifests)
  884. const toRemove = segmentIds.length + 1;
  885. let removed = 0;
  886. const pendingContent = shaka.offline.StoredContentUtils.fromManifestDB(
  887. uri, manifest);
  888. const onRemove = (key) => {
  889. removed += 1;
  890. this.config_.offline.progressCallback(pendingContent, removed / toRemove);
  891. };
  892. return Promise.all([
  893. storage.removeSegments(segmentIds, onRemove),
  894. storage.removeManifests([uri.key()], onRemove),
  895. ]);
  896. }
  897. /**
  898. * Removes any EME sessions that were not successfully removed before. This
  899. * returns whether all the sessions were successfully removed.
  900. *
  901. * @return {!Promise.<boolean>}
  902. * @export
  903. */
  904. removeEmeSessions() {
  905. return this.startOperation_(this.removeEmeSessions_());
  906. }
  907. /**
  908. * @return {!Promise.<boolean>}
  909. * @private
  910. */
  911. async removeEmeSessions_() {
  912. this.requireSupport_();
  913. goog.asserts.assert(this.networkingEngine_, 'Cannot be destroyed');
  914. const net = this.networkingEngine_;
  915. const config = this.config_.drm;
  916. /** @type {!shaka.offline.StorageMuxer} */
  917. const muxer = new shaka.offline.StorageMuxer();
  918. /** @type {!shaka.offline.SessionDeleter} */
  919. const deleter = new shaka.offline.SessionDeleter();
  920. let hasRemaining = false;
  921. try {
  922. await muxer.init();
  923. /** @type {!Array.<shaka.extern.EmeSessionStorageCell>} */
  924. const cells = [];
  925. muxer.forEachEmeSessionCell((c) => cells.push(c));
  926. // Run these sequentially to avoid creating too many DrmEngine instances
  927. // and having multiple CDMs alive at once. Some embedded platforms may
  928. // not support that.
  929. for (const sessionIdCell of cells) {
  930. /* eslint-disable no-await-in-loop */
  931. const sessions = await sessionIdCell.getAll();
  932. const deletedSessionIds = await deleter.delete(config, net, sessions);
  933. await sessionIdCell.remove(deletedSessionIds);
  934. if (deletedSessionIds.length != sessions.length) {
  935. hasRemaining = true;
  936. }
  937. /* eslint-enable no-await-in-loop */
  938. }
  939. } finally {
  940. await muxer.destroy();
  941. }
  942. return !hasRemaining;
  943. }
  944. /**
  945. * Lists all the stored content available.
  946. *
  947. * @return {!Promise.<!Array.<shaka.extern.StoredContent>>} A Promise to an
  948. * array of structures representing all stored content. The "offlineUri"
  949. * member of the structure is the URI that should be given to Player.load()
  950. * to play this piece of content offline. The "appMetadata" member is the
  951. * appMetadata argument you passed to store().
  952. * @export
  953. */
  954. list() {
  955. return this.startOperation_(this.list_());
  956. }
  957. /**
  958. * See |shaka.offline.Storage.list| for details.
  959. *
  960. * @return {!Promise.<!Array.<shaka.extern.StoredContent>>}
  961. * @private
  962. */
  963. async list_() {
  964. this.requireSupport_();
  965. /** @type {!Array.<shaka.extern.StoredContent>} */
  966. const result = [];
  967. /** @type {!shaka.offline.StorageMuxer} */
  968. const muxer = new shaka.offline.StorageMuxer();
  969. try {
  970. await muxer.init();
  971. let p = Promise.resolve();
  972. muxer.forEachCell((path, cell) => {
  973. p = p.then(async () => {
  974. const manifests = await cell.getAllManifests();
  975. manifests.forEach((manifest, key) => {
  976. const uri = shaka.offline.OfflineUri.manifest(
  977. path.mechanism,
  978. path.cell,
  979. key);
  980. const content = shaka.offline.StoredContentUtils.fromManifestDB(
  981. uri,
  982. manifest);
  983. result.push(content);
  984. });
  985. });
  986. });
  987. await p;
  988. } finally {
  989. await muxer.destroy();
  990. }
  991. return result;
  992. }
  993. /**
  994. * This method is public so that it can be overridden in testing.
  995. *
  996. * @param {string} uri
  997. * @param {shaka.extern.ManifestParser} parser
  998. * @param {shaka.extern.PlayerConfiguration} config
  999. * @return {!Promise.<shaka.extern.Manifest>}
  1000. */
  1001. async parseManifest(uri, parser, config) {
  1002. let error = null;
  1003. const networkingEngine = this.networkingEngine_;
  1004. goog.asserts.assert(networkingEngine, 'Should be initialized!');
  1005. /** @type {shaka.extern.ManifestParser.PlayerInterface} */
  1006. const playerInterface = {
  1007. networkingEngine: networkingEngine,
  1008. // Don't bother filtering now. We will do that later when we have all the
  1009. // information we need to filter.
  1010. filter: () => Promise.resolve(),
  1011. // The responsibility for making mock text streams for closed captions is
  1012. // handled inside shaka.offline.OfflineManifestParser, before playback.
  1013. makeTextStreamsForClosedCaptions: (manifest) => {},
  1014. onTimelineRegionAdded: () => {},
  1015. onEvent: () => {},
  1016. // Used to capture an error from the manifest parser. We will check the
  1017. // error before returning.
  1018. onError: (e) => {
  1019. error = e;
  1020. },
  1021. isLowLatencyMode: () => false,
  1022. isAutoLowLatencyMode: () => false,
  1023. enableLowLatencyMode: () => {},
  1024. updateDuration: () => {},
  1025. newDrmInfo: (stream) => {},
  1026. onManifestUpdated: () => {},
  1027. getBandwidthEstimate: () => config.abr.defaultBandwidthEstimate,
  1028. };
  1029. parser.configure(config.manifest);
  1030. // We may have been destroyed while we were waiting on |getParser| to
  1031. // resolve.
  1032. this.ensureNotDestroyed_();
  1033. const manifest = await parser.start(uri, playerInterface);
  1034. // We may have been destroyed while we were waiting on |start| to
  1035. // resolve.
  1036. this.ensureNotDestroyed_();
  1037. // Get all the streams that are used in the manifest.
  1038. const streams =
  1039. shaka.offline.Storage.getAllStreamsFromManifest_(manifest);
  1040. // Wait for each stream to create their segment indexes.
  1041. await Promise.all(shaka.util.Iterables.map(streams, (stream) => {
  1042. return stream.createSegmentIndex();
  1043. }));
  1044. // We may have been destroyed while we were waiting on
  1045. // |createSegmentIndex| to resolve for each stream.
  1046. this.ensureNotDestroyed_();
  1047. // If we saw an error while parsing, surface the error.
  1048. if (error) {
  1049. throw error;
  1050. }
  1051. return manifest;
  1052. }
  1053. /**
  1054. * This method is public so that it can be override in testing.
  1055. *
  1056. * @param {shaka.extern.Manifest} manifest
  1057. * @param {function(shaka.util.Error)} onError
  1058. * @param {shaka.extern.PlayerConfiguration} config
  1059. * @return {!Promise.<!shaka.media.DrmEngine>}
  1060. */
  1061. async createDrmEngine(manifest, onError, config) {
  1062. goog.asserts.assert(
  1063. this.networkingEngine_,
  1064. 'Cannot call |createDrmEngine| after |destroy|');
  1065. /** @type {!shaka.media.DrmEngine} */
  1066. const drmEngine = new shaka.media.DrmEngine({
  1067. netEngine: this.networkingEngine_,
  1068. onError: onError,
  1069. onKeyStatus: () => {},
  1070. onExpirationUpdated: () => {},
  1071. onEvent: () => {},
  1072. });
  1073. drmEngine.configure(config.drm);
  1074. await drmEngine.initForStorage(
  1075. manifest.variants, config.offline.usePersistentLicense);
  1076. await drmEngine.createOrLoad();
  1077. return drmEngine;
  1078. }
  1079. /**
  1080. * Converts manifest Streams to database Streams.
  1081. *
  1082. * @param {!shaka.offline.DownloadManager} downloader
  1083. * @param {shaka.offline.StreamBandwidthEstimator} estimator
  1084. * @param {!shaka.media.DrmEngine} drmEngine
  1085. * @param {shaka.extern.Manifest} manifest
  1086. * @param {shaka.extern.PlayerConfiguration} config
  1087. * @return {{
  1088. * streams: !Array.<shaka.extern.StreamDB>,
  1089. * toDownload: !Array.<!shaka.offline.DownloadInfo>
  1090. * }}
  1091. * @private
  1092. */
  1093. createStreams_(downloader, estimator, drmEngine, manifest, config) {
  1094. // Download infos are stored based on their refId, to dedup them.
  1095. /** @type {!Map.<string, !shaka.offline.DownloadInfo>} */
  1096. const toDownload = new Map();
  1097. // Find the streams we want to download and create a stream db instance
  1098. // for each of them.
  1099. const streamSet =
  1100. shaka.offline.Storage.getAllStreamsFromManifest_(manifest);
  1101. const streamDBs = new Map();
  1102. for (const stream of streamSet) {
  1103. const streamDB = this.createStream_(
  1104. downloader, estimator, manifest, stream, config, toDownload);
  1105. streamDBs.set(stream.id, streamDB);
  1106. }
  1107. // Connect streams and variants together.
  1108. for (const variant of manifest.variants) {
  1109. if (variant.audio) {
  1110. streamDBs.get(variant.audio.id).variantIds.push(variant.id);
  1111. }
  1112. if (variant.video) {
  1113. streamDBs.get(variant.video.id).variantIds.push(variant.id);
  1114. }
  1115. }
  1116. return {
  1117. streams: Array.from(streamDBs.values()),
  1118. toDownload: Array.from(toDownload.values()),
  1119. };
  1120. }
  1121. /**
  1122. * Converts a manifest stream to a database stream. This will search the
  1123. * segment index and add all the segments to the download infos.
  1124. *
  1125. * @param {!shaka.offline.DownloadManager} downloader
  1126. * @param {shaka.offline.StreamBandwidthEstimator} estimator
  1127. * @param {shaka.extern.Manifest} manifest
  1128. * @param {shaka.extern.Stream} stream
  1129. * @param {shaka.extern.PlayerConfiguration} config
  1130. * @param {!Map.<string, !shaka.offline.DownloadInfo>} toDownload
  1131. * @return {shaka.extern.StreamDB}
  1132. * @private
  1133. */
  1134. createStream_(downloader, estimator, manifest, stream, config, toDownload) {
  1135. /** @type {shaka.extern.StreamDB} */
  1136. const streamDb = {
  1137. id: stream.id,
  1138. originalId: stream.originalId,
  1139. groupId: stream.groupId,
  1140. primary: stream.primary,
  1141. type: stream.type,
  1142. mimeType: stream.mimeType,
  1143. codecs: stream.codecs,
  1144. frameRate: stream.frameRate,
  1145. pixelAspectRatio: stream.pixelAspectRatio,
  1146. hdr: stream.hdr,
  1147. colorGamut: stream.colorGamut,
  1148. videoLayout: stream.videoLayout,
  1149. kind: stream.kind,
  1150. language: stream.language,
  1151. originalLanguage: stream.originalLanguage,
  1152. label: stream.label,
  1153. width: stream.width || null,
  1154. height: stream.height || null,
  1155. encrypted: stream.encrypted,
  1156. keyIds: stream.keyIds,
  1157. segments: [],
  1158. variantIds: [],
  1159. roles: stream.roles,
  1160. forced: stream.forced,
  1161. channelsCount: stream.channelsCount,
  1162. audioSamplingRate: stream.audioSamplingRate,
  1163. spatialAudio: stream.spatialAudio,
  1164. closedCaptions: stream.closedCaptions,
  1165. tilesLayout: stream.tilesLayout,
  1166. external: stream.external,
  1167. fastSwitching: stream.fastSwitching,
  1168. };
  1169. const startTime =
  1170. manifest.presentationTimeline.getSegmentAvailabilityStart();
  1171. const numberOfParallelDownloads = config.offline.numberOfParallelDownloads;
  1172. let groupId = numberOfParallelDownloads === 0 ? stream.id : 0;
  1173. shaka.offline.Storage.forEachSegment_(stream, startTime, (segment) => {
  1174. const pendingSegmentRefId =
  1175. shaka.offline.DownloadInfo.idForSegmentRef(segment);
  1176. let pendingInitSegmentRefId = undefined;
  1177. // Set up the download for the segment, which will be downloaded later,
  1178. // perhaps in a service worker.
  1179. if (!toDownload.has(pendingSegmentRefId)) {
  1180. const estimateId = downloader.addDownloadEstimate(
  1181. estimator.getSegmentEstimate(stream.id, segment));
  1182. const segmentDownload = new shaka.offline.DownloadInfo(
  1183. segment,
  1184. estimateId,
  1185. groupId,
  1186. /* isInitSegment= */ false);
  1187. toDownload.set(pendingSegmentRefId, segmentDownload);
  1188. }
  1189. // Set up the download for the init segment, similarly, if there is one.
  1190. if (segment.initSegmentReference) {
  1191. pendingInitSegmentRefId = shaka.offline.DownloadInfo.idForSegmentRef(
  1192. segment.initSegmentReference);
  1193. if (!toDownload.has(pendingInitSegmentRefId)) {
  1194. const estimateId = downloader.addDownloadEstimate(
  1195. estimator.getInitSegmentEstimate(stream.id));
  1196. const initDownload = new shaka.offline.DownloadInfo(
  1197. segment.initSegmentReference,
  1198. estimateId,
  1199. groupId,
  1200. /* isInitSegment= */ true);
  1201. toDownload.set(pendingInitSegmentRefId, initDownload);
  1202. }
  1203. }
  1204. /** @type {!shaka.extern.SegmentDB} */
  1205. const segmentDB = {
  1206. pendingInitSegmentRefId,
  1207. initSegmentKey: pendingInitSegmentRefId ? 0 : null,
  1208. startTime: segment.startTime,
  1209. endTime: segment.endTime,
  1210. appendWindowStart: segment.appendWindowStart,
  1211. appendWindowEnd: segment.appendWindowEnd,
  1212. timestampOffset: segment.timestampOffset,
  1213. tilesLayout: segment.tilesLayout,
  1214. pendingSegmentRefId,
  1215. dataKey: 0,
  1216. mimeType: segment.mimeType,
  1217. codecs: segment.codecs,
  1218. };
  1219. streamDb.segments.push(segmentDB);
  1220. if (numberOfParallelDownloads !== 0) {
  1221. groupId = (groupId + 1) % numberOfParallelDownloads;
  1222. }
  1223. });
  1224. return streamDb;
  1225. }
  1226. /**
  1227. * @param {shaka.extern.Stream} stream
  1228. * @param {number} startTime
  1229. * @param {function(!shaka.media.SegmentReference)} callback
  1230. * @private
  1231. */
  1232. static forEachSegment_(stream, startTime, callback) {
  1233. /** @type {?number} */
  1234. let i = stream.segmentIndex.find(startTime);
  1235. if (i == null) {
  1236. return;
  1237. }
  1238. /** @type {?shaka.media.SegmentReference} */
  1239. let ref = stream.segmentIndex.get(i);
  1240. while (ref) {
  1241. callback(ref);
  1242. ref = stream.segmentIndex.get(++i);
  1243. }
  1244. }
  1245. /**
  1246. * Throws an error if the object is destroyed.
  1247. * @private
  1248. */
  1249. ensureNotDestroyed_() {
  1250. if (this.destroyer_.destroyed()) {
  1251. throw new shaka.util.Error(
  1252. shaka.util.Error.Severity.CRITICAL,
  1253. shaka.util.Error.Category.STORAGE,
  1254. shaka.util.Error.Code.OPERATION_ABORTED);
  1255. }
  1256. }
  1257. /**
  1258. * Used by functions that need storage support to ensure that the current
  1259. * platform has storage support before continuing. This should only be
  1260. * needed to be used at the start of public methods.
  1261. *
  1262. * @private
  1263. */
  1264. requireSupport_() {
  1265. if (!shaka.offline.Storage.support()) {
  1266. throw new shaka.util.Error(
  1267. shaka.util.Error.Severity.CRITICAL,
  1268. shaka.util.Error.Category.STORAGE,
  1269. shaka.util.Error.Code.STORAGE_NOT_SUPPORTED);
  1270. }
  1271. }
  1272. /**
  1273. * Perform an action. Track the action's progress so that when we destroy
  1274. * we will wait until all the actions have completed before allowing destroy
  1275. * to resolve.
  1276. *
  1277. * @param {!Promise<T>} action
  1278. * @return {!Promise<T>}
  1279. * @template T
  1280. * @private
  1281. */
  1282. async startOperation_(action) {
  1283. this.openOperations_.push(action);
  1284. try {
  1285. // Await |action| so we can use the finally statement to remove |action|
  1286. // from |openOperations_| when we still have a reference to |action|.
  1287. return await action;
  1288. } finally {
  1289. shaka.util.ArrayUtils.remove(this.openOperations_, action);
  1290. }
  1291. }
  1292. /**
  1293. * The equivalent of startOperation_, but for abortable operations.
  1294. *
  1295. * @param {!shaka.extern.IAbortableOperation<T>} action
  1296. * @return {!shaka.extern.IAbortableOperation<T>}
  1297. * @template T
  1298. * @private
  1299. */
  1300. startAbortableOperation_(action) {
  1301. const promise = action.promise;
  1302. this.openOperations_.push(promise);
  1303. // Remove the open operation once the action has completed. So that we
  1304. // can still return the AbortableOperation, this is done using a |finally|
  1305. // block, rather than awaiting the result.
  1306. return action.finally(() => {
  1307. shaka.util.ArrayUtils.remove(this.openOperations_, promise);
  1308. });
  1309. }
  1310. /**
  1311. * @param {shaka.extern.ManifestDB} manifest
  1312. * @return {!Array.<number>}
  1313. * @private
  1314. */
  1315. static getAllSegmentIds_(manifest) {
  1316. /** @type {!Set.<number>} */
  1317. const ids = new Set();
  1318. // Get every segment for every stream in the manifest.
  1319. for (const stream of manifest.streams) {
  1320. for (const segment of stream.segments) {
  1321. if (segment.initSegmentKey != null) {
  1322. ids.add(segment.initSegmentKey);
  1323. }
  1324. ids.add(segment.dataKey);
  1325. }
  1326. }
  1327. return Array.from(ids);
  1328. }
  1329. /**
  1330. * Delete the on-disk storage and all the content it contains. This should not
  1331. * be done in normal circumstances. Only do it when storage is rendered
  1332. * unusable, such as by a version mismatch. No business logic will be run, and
  1333. * licenses will not be released.
  1334. *
  1335. * @return {!Promise}
  1336. * @export
  1337. */
  1338. static async deleteAll() {
  1339. /** @type {!shaka.offline.StorageMuxer} */
  1340. const muxer = new shaka.offline.StorageMuxer();
  1341. try {
  1342. // Wipe all content from all storage mechanisms.
  1343. await muxer.erase();
  1344. } finally {
  1345. // Destroy the muxer, whether or not erase() succeeded.
  1346. await muxer.destroy();
  1347. }
  1348. }
  1349. /**
  1350. * @param {!shaka.net.NetworkingEngine} net
  1351. * @param {!shaka.extern.DrmConfiguration} drmConfig
  1352. * @param {!shaka.offline.StorageMuxer} muxer
  1353. * @param {shaka.extern.ManifestDB} manifestDb
  1354. * @return {!Promise}
  1355. * @private
  1356. */
  1357. static async deleteLicenseFor_(net, drmConfig, muxer, manifestDb) {
  1358. if (!manifestDb.drmInfo) {
  1359. return;
  1360. }
  1361. const sessionIdCell = muxer.getEmeSessionCell();
  1362. /** @type {!Array.<shaka.extern.EmeSessionDB>} */
  1363. const sessions = manifestDb.sessionIds.map((sessionId) => {
  1364. return {
  1365. sessionId: sessionId,
  1366. keySystem: manifestDb.drmInfo.keySystem,
  1367. licenseUri: manifestDb.drmInfo.licenseServerUri,
  1368. serverCertificate: manifestDb.drmInfo.serverCertificate,
  1369. audioCapabilities: shaka.offline.Storage.getCapabilities_(
  1370. manifestDb,
  1371. /* isVideo= */ false),
  1372. videoCapabilities: shaka.offline.Storage.getCapabilities_(
  1373. manifestDb,
  1374. /* isVideo= */ true),
  1375. };
  1376. });
  1377. // Try to delete the sessions; any sessions that weren't deleted get stored
  1378. // in the database so we can try to remove them again later. This allows us
  1379. // to still delete the stored content but not "forget" about these sessions.
  1380. // Later, we can remove the sessions to free up space.
  1381. const deleter = new shaka.offline.SessionDeleter();
  1382. const deletedSessionIds = await deleter.delete(drmConfig, net, sessions);
  1383. await sessionIdCell.remove(deletedSessionIds);
  1384. await sessionIdCell.add(sessions.filter(
  1385. (session) => !deletedSessionIds.includes(session.sessionId)));
  1386. }
  1387. /**
  1388. * Get the set of all streams in |manifest|.
  1389. *
  1390. * @param {shaka.extern.Manifest} manifest
  1391. * @return {!Set.<shaka.extern.Stream>}
  1392. * @private
  1393. */
  1394. static getAllStreamsFromManifest_(manifest) {
  1395. /** @type {!Set.<shaka.extern.Stream>} */
  1396. const set = new Set();
  1397. for (const variant of manifest.variants) {
  1398. if (variant.audio) {
  1399. set.add(variant.audio);
  1400. }
  1401. if (variant.video) {
  1402. set.add(variant.video);
  1403. }
  1404. }
  1405. for (const text of manifest.textStreams) {
  1406. set.add(text);
  1407. }
  1408. for (const image of manifest.imageStreams) {
  1409. set.add(image);
  1410. }
  1411. return set;
  1412. }
  1413. /**
  1414. * Go over a manifest and issue warnings for any suspicious properties.
  1415. *
  1416. * @param {shaka.extern.Manifest} manifest
  1417. * @private
  1418. */
  1419. static validateManifest_(manifest) {
  1420. const videos = new Set(manifest.variants.map((v) => v.video));
  1421. const audios = new Set(manifest.variants.map((v) => v.audio));
  1422. const texts = manifest.textStreams;
  1423. if (videos.size > 1) {
  1424. shaka.log.warning('Multiple video tracks selected to be stored');
  1425. }
  1426. for (const audio1 of audios) {
  1427. for (const audio2 of audios) {
  1428. if (audio1 != audio2 && audio1.language == audio2.language) {
  1429. shaka.log.warning(
  1430. 'Similar audio tracks were selected to be stored',
  1431. audio1.id,
  1432. audio2.id);
  1433. }
  1434. }
  1435. }
  1436. for (const text1 of texts) {
  1437. for (const text2 of texts) {
  1438. if (text1 != text2 && text1.language == text2.language) {
  1439. shaka.log.warning(
  1440. 'Similar text tracks were selected to be stored',
  1441. text1.id,
  1442. text2.id);
  1443. }
  1444. }
  1445. }
  1446. }
  1447. };
  1448. shaka.offline.Storage.defaultSystemIds_ = new Map()
  1449. .set('org.w3.clearkey', '1077efecc0b24d02ace33c1e52e2fb4b')
  1450. .set('com.widevine.alpha', 'edef8ba979d64acea3c827dcd51d21ed')
  1451. .set('com.microsoft.playready', '9a04f07998404286ab92e65be0885f95')
  1452. .set('com.microsoft.playready.recommendation',
  1453. '9a04f07998404286ab92e65be0885f95')
  1454. .set('com.microsoft.playready.software',
  1455. '9a04f07998404286ab92e65be0885f95')
  1456. .set('com.microsoft.playready.hardware',
  1457. '9a04f07998404286ab92e65be0885f95');
  1458. shaka.Player.registerSupportPlugin('offline', shaka.offline.Storage.support);