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