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