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