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