Source: lib/offline/storage.js

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