Source: lib/offline/storage.js

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