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.Player');
  20. goog.require('shaka.log');
  21. goog.require('shaka.media.DrmEngine');
  22. goog.require('shaka.media.ManifestParser');
  23. goog.require('shaka.offline.DownloadManager');
  24. goog.require('shaka.offline.IStorageEngine');
  25. goog.require('shaka.offline.OfflineManifestParser');
  26. goog.require('shaka.offline.OfflineUtils');
  27. goog.require('shaka.util.ConfigUtils');
  28. goog.require('shaka.util.Error');
  29. goog.require('shaka.util.Functional');
  30. goog.require('shaka.util.IDestroyable');
  31. goog.require('shaka.util.LanguageUtils');
  32. goog.require('shaka.util.ManifestParserUtils');
  33. goog.require('shaka.util.StreamUtils');
  34. /**
  35. * This manages persistent offline data including storage, listing, and deleting
  36. * stored manifests. Playback of offline manifests are done using Player
  37. * using the special URI (e.g. 'offline:12').
  38. *
  39. * First, check support() to see if offline is supported by the platform.
  40. * Second, configure() the storage object with callbacks to your application.
  41. * Third, call store(), remove(), or list() as needed.
  42. * When done, call destroy().
  43. *
  44. * @param {shaka.Player} player
  45. * The player instance to pull configuration data from.
  46. *
  47. * @struct
  48. * @constructor
  49. * @implements {shaka.util.IDestroyable}
  50. * @export
  51. */
  52. shaka.offline.Storage = function(player) {
  53. // It is an easy mistake to make to pass a Player proxy from CastProxy.
  54. // Rather than throw a vague exception later, throw an explicit and clear one
  55. // now.
  56. if (!player || player.constructor != shaka.Player) {
  57. throw new shaka.util.Error(
  58. shaka.util.Error.Severity.CRITICAL,
  59. shaka.util.Error.Category.STORAGE,
  60. shaka.util.Error.Code.LOCAL_PLAYER_INSTANCE_REQUIRED);
  61. }
  62. /** @private {shaka.offline.IStorageEngine} */
  63. this.storageEngine_ = shaka.offline.OfflineUtils.createStorageEngine();
  64. /** @private {shaka.Player} */
  65. this.player_ = player;
  66. /** @private {?shakaExtern.OfflineConfiguration} */
  67. this.config_ = this.defaultConfig_();
  68. /** @private {shaka.media.DrmEngine} */
  69. this.drmEngine_ = null;
  70. /** @private {boolean} */
  71. this.storeInProgress_ = false;
  72. /** @private {Array.<shakaExtern.Track>} */
  73. this.firstPeriodTracks_ = null;
  74. /** @private {number} */
  75. this.manifestId_ = -1;
  76. /** @private {number} */
  77. this.duration_ = 0;
  78. /** @private {?shakaExtern.Manifest} */
  79. this.manifest_ = null;
  80. var netEngine = player.getNetworkingEngine();
  81. goog.asserts.assert(netEngine, 'Player must not be destroyed');
  82. /** @private {shaka.offline.DownloadManager} */
  83. this.downloadManager_ = new shaka.offline.DownloadManager(
  84. this.storageEngine_, netEngine,
  85. player.getConfiguration().streaming.retryParameters, this.config_);
  86. };
  87. /**
  88. * Gets whether offline storage is supported. Returns true if offline storage
  89. * is supported for clear content. Support for offline storage of encrypted
  90. * content will not be determined until storage is attempted.
  91. *
  92. * @return {boolean}
  93. * @export
  94. */
  95. shaka.offline.Storage.support = function() {
  96. return shaka.offline.OfflineUtils.isStorageEngineSupported();
  97. };
  98. /**
  99. * @override
  100. * @export
  101. */
  102. shaka.offline.Storage.prototype.destroy = function() {
  103. var storageEngine = this.storageEngine_;
  104. // Destroy the download manager first since it needs the StorageEngine to
  105. // clean up old segments.
  106. var ret = !this.downloadManager_ ?
  107. Promise.resolve() :
  108. this.downloadManager_.destroy()
  109. .catch(function() {})
  110. .then(function() {
  111. if (storageEngine) return storageEngine.destroy();
  112. });
  113. this.storageEngine_ = null;
  114. this.downloadManager_ = null;
  115. this.player_ = null;
  116. this.config_ = null;
  117. return ret;
  118. };
  119. /**
  120. * Sets configuration values for Storage. This is not associated with
  121. * Player.configure and will not change Player.
  122. *
  123. * There are two important callbacks configured here: one for download progress,
  124. * and one to decide which tracks to store.
  125. *
  126. * The default track selection callback will store the largest SD video track.
  127. * Provide your own callback to choose the tracks you want to store.
  128. *
  129. * @param {!Object} config This should follow the form of
  130. * {@link shakaExtern.OfflineConfiguration}, but you may omit any field you do
  131. * not wish to change.
  132. * @export
  133. */
  134. shaka.offline.Storage.prototype.configure = function(config) {
  135. goog.asserts.assert(this.config_, 'Storage must not be destroyed');
  136. shaka.util.ConfigUtils.mergeConfigObjects(
  137. this.config_, config, this.defaultConfig_(), {}, '');
  138. };
  139. /**
  140. * Stores the given manifest. If the content is encrypted, and encrypted
  141. * content cannot be stored on this platform, the Promise will be rejected with
  142. * error code 6001, REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE.
  143. *
  144. * @param {string} manifestUri The URI of the manifest to store.
  145. * @param {!Object=} opt_appMetadata An arbitrary object from the application
  146. * that will be stored along-side the offline content. Use this for any
  147. * application-specific metadata you need associated with the stored content.
  148. * For details on the data types that can be stored here, please refer to
  149. * https://goo.gl/h62coS
  150. * @param {!shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
  151. * @return {!Promise.<shakaExtern.StoredContent>} A Promise to a structure
  152. * representing what was stored. The "offlineUri" member is the URI that
  153. * should be given to Player.load() to play this piece of content offline.
  154. * The "appMetadata" member is the appMetadata argument you passed to store().
  155. * @export
  156. */
  157. shaka.offline.Storage.prototype.store = function(
  158. manifestUri, opt_appMetadata, opt_manifestParserFactory) {
  159. if (this.storeInProgress_) {
  160. return Promise.reject(new shaka.util.Error(
  161. shaka.util.Error.Severity.CRITICAL,
  162. shaka.util.Error.Category.STORAGE,
  163. shaka.util.Error.Code.STORE_ALREADY_IN_PROGRESS));
  164. }
  165. this.storeInProgress_ = true;
  166. /** @type {shakaExtern.ManifestDB} */
  167. var manifestDb;
  168. var error = null;
  169. var onError = function(e) { error = e; };
  170. return this.initIfNeeded_()
  171. .then(function() {
  172. this.checkDestroyed_();
  173. return this.loadInternal(
  174. manifestUri, onError, opt_manifestParserFactory);
  175. }.bind(this)).then((
  176. /**
  177. * @param {{manifest: shakaExtern.Manifest,
  178. * drmEngine: !shaka.media.DrmEngine}} data
  179. * @return {!Promise}
  180. */
  181. function(data) {
  182. this.checkDestroyed_();
  183. this.manifest_ = data.manifest;
  184. this.drmEngine_ = data.drmEngine;
  185. if (this.manifest_.presentationTimeline.isLive() ||
  186. this.manifest_.presentationTimeline.isInProgress()) {
  187. throw new shaka.util.Error(
  188. shaka.util.Error.Severity.CRITICAL,
  189. shaka.util.Error.Category.STORAGE,
  190. shaka.util.Error.Code.CANNOT_STORE_LIVE_OFFLINE, manifestUri);
  191. }
  192. // Re-filter now that DrmEngine is initialized.
  193. this.filterAllPeriods_(this.manifest_.periods);
  194. this.manifestId_ = this.storageEngine_.reserveId('manifest');
  195. this.duration_ = 0;
  196. manifestDb =
  197. this.createOfflineManifest_(manifestUri, opt_appMetadata || {});
  198. return this.downloadManager_.downloadAndStore(manifestDb);
  199. })
  200. .bind(this))
  201. .then(function() {
  202. this.checkDestroyed_();
  203. // Throw any errors from the manifest parser or DrmEngine.
  204. if (error)
  205. throw error;
  206. return this.cleanup_();
  207. }.bind(this))
  208. .then(function() {
  209. return shaka.offline.OfflineUtils.getStoredContent(manifestDb);
  210. }.bind(this))
  211. .catch(function(err) {
  212. var Functional = shaka.util.Functional;
  213. return this.cleanup_().catch(Functional.noop).then(function() {
  214. throw err;
  215. });
  216. }.bind(this));
  217. };
  218. /**
  219. * Removes the given stored content.
  220. *
  221. * @param {shakaExtern.StoredContent} content
  222. * @return {!Promise}
  223. * @export
  224. */
  225. shaka.offline.Storage.prototype.remove = function(content) {
  226. var uri = content.offlineUri;
  227. var parts = /^offline:([0-9]+)$/.exec(uri);
  228. if (!parts) {
  229. return Promise.reject(new shaka.util.Error(
  230. shaka.util.Error.Severity.CRITICAL,
  231. shaka.util.Error.Category.STORAGE,
  232. shaka.util.Error.Code.MALFORMED_OFFLINE_URI, uri));
  233. }
  234. var error = null;
  235. var onError = function(e) {
  236. // Ignore errors if the session was already removed.
  237. if (e.code != shaka.util.Error.Code.OFFLINE_SESSION_REMOVED)
  238. error = e;
  239. };
  240. /** @type {shakaExtern.ManifestDB} */
  241. var manifestDb;
  242. /** @type {!shaka.media.DrmEngine} */
  243. var drmEngine;
  244. var manifestId = Number(parts[1]);
  245. return this.initIfNeeded_().then(function() {
  246. this.checkDestroyed_();
  247. return this.storageEngine_.get('manifest', manifestId);
  248. }.bind(this)).then((
  249. /**
  250. * @param {?shakaExtern.ManifestDB} data
  251. * @return {!Promise}
  252. */
  253. function(data) {
  254. this.checkDestroyed_();
  255. if (!data) {
  256. throw new shaka.util.Error(
  257. shaka.util.Error.Severity.CRITICAL,
  258. shaka.util.Error.Category.STORAGE,
  259. shaka.util.Error.Code.REQUESTED_ITEM_NOT_FOUND, uri);
  260. }
  261. manifestDb = data;
  262. var manifest =
  263. shaka.offline.OfflineManifestParser.reconstructManifest(manifestDb);
  264. var netEngine = this.player_.getNetworkingEngine();
  265. goog.asserts.assert(netEngine, 'Player must not be destroyed');
  266. drmEngine = new shaka.media.DrmEngine({
  267. netEngine: netEngine,
  268. onError: onError,
  269. onKeyStatus: function() {},
  270. onExpirationUpdated: function() {},
  271. onEvent: function() {}
  272. });
  273. drmEngine.configure(this.player_.getConfiguration().drm);
  274. var isOffline = this.config_.usePersistentLicense || false;
  275. return drmEngine.init(manifest, isOffline);
  276. })
  277. .bind(this)).then(function() {
  278. return drmEngine.removeSessions(manifestDb.sessionIds);
  279. }.bind(this)).then(function() {
  280. return drmEngine.destroy();
  281. }.bind(this)).then(function() {
  282. this.checkDestroyed_();
  283. if (error) throw error;
  284. var Functional = shaka.util.Functional;
  285. // Get every segment for every stream in the manifest.
  286. /** @type {!Array.<number>} */
  287. var segments = manifestDb.periods.map(function(period) {
  288. return period.streams.map(function(stream) {
  289. var segments = stream.segments.map(function(segment) {
  290. var parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(segment.uri);
  291. goog.asserts.assert(parts, 'Invalid offline URI');
  292. return Number(parts[1]);
  293. });
  294. if (stream.initSegmentUri) {
  295. var parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(
  296. stream.initSegmentUri);
  297. goog.asserts.assert(parts, 'Invalid offline URI');
  298. segments.push(Number(parts[1]));
  299. }
  300. return segments;
  301. }).reduce(Functional.collapseArrays, []);
  302. }).reduce(Functional.collapseArrays, []);
  303. // Delete all the segments.
  304. var deleteCount = 0;
  305. var segmentCount = segments.length;
  306. var callback = this.config_.progressCallback;
  307. return this.storageEngine_.removeKeys('segment', segments, function() {
  308. deleteCount++;
  309. callback(content, deleteCount / segmentCount);
  310. });
  311. }.bind(this)).then(function() {
  312. this.checkDestroyed_();
  313. this.config_.progressCallback(content, 1);
  314. return this.storageEngine_.remove('manifest', manifestId);
  315. }.bind(this));
  316. };
  317. /**
  318. * Lists all the stored content available.
  319. *
  320. * @return {!Promise.<!Array.<shakaExtern.StoredContent>>} A Promise to an
  321. * array of structures representing all stored content. The "offlineUri"
  322. * member of the structure is the URI that should be given to Player.load()
  323. * to play this piece of content offline. The "appMetadata" member is the
  324. * appMetadata argument you passed to store().
  325. * @export
  326. */
  327. shaka.offline.Storage.prototype.list = function() {
  328. /** @type {!Array.<shakaExtern.StoredContent>} */
  329. var storedContents = [];
  330. return this.initIfNeeded_()
  331. .then(function() {
  332. this.checkDestroyed_();
  333. return this.storageEngine_.forEach(
  334. 'manifest', function(/** shakaExtern.ManifestDB */ manifest) {
  335. storedContents.push(
  336. shaka.offline.OfflineUtils.getStoredContent(manifest));
  337. });
  338. }.bind(this))
  339. .then(function() { return storedContents; });
  340. };
  341. /**
  342. * Loads the given manifest, parses it, and constructs the DrmEngine. This
  343. * stops the manifest parser. This may be replaced by tests.
  344. *
  345. * @param {string} manifestUri
  346. * @param {function(*)} onError
  347. * @param {!shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
  348. * @return {!Promise.<{
  349. * manifest: shakaExtern.Manifest,
  350. * drmEngine: !shaka.media.DrmEngine
  351. * }>}
  352. */
  353. shaka.offline.Storage.prototype.loadInternal = function(
  354. manifestUri, onError, opt_manifestParserFactory) {
  355. var netEngine = /** @type {!shaka.net.NetworkingEngine} */ (
  356. this.player_.getNetworkingEngine());
  357. var config = this.player_.getConfiguration();
  358. /** @type {shakaExtern.Manifest} */
  359. var manifest;
  360. /** @type {!shaka.media.DrmEngine} */
  361. var drmEngine;
  362. /** @type {!shakaExtern.ManifestParser} */
  363. var manifestParser;
  364. var onKeyStatusChange = function() {};
  365. return shaka.media.ManifestParser
  366. .getFactory(
  367. manifestUri, netEngine, config.manifest.retryParameters,
  368. opt_manifestParserFactory)
  369. .then(function(factory) {
  370. this.checkDestroyed_();
  371. manifestParser = new factory();
  372. manifestParser.configure(config.manifest);
  373. var playerInterface = {
  374. networkingEngine: netEngine,
  375. filterAllPeriods: this.filterAllPeriods_.bind(this),
  376. filterNewPeriod: this.filterPeriod_.bind(this),
  377. onTimelineRegionAdded: function() {},
  378. onEvent: function() {},
  379. onError: onError
  380. };
  381. return manifestParser.start(manifestUri, playerInterface);
  382. }.bind(this))
  383. .then(function(data) {
  384. this.checkDestroyed_();
  385. manifest = data;
  386. drmEngine = new shaka.media.DrmEngine({
  387. netEngine: netEngine,
  388. onError: onError,
  389. onKeyStatus: onKeyStatusChange,
  390. onExpirationUpdated: function() {},
  391. onEvent: function() {}
  392. });
  393. drmEngine.configure(config.drm);
  394. var isOffline = this.config_.usePersistentLicense || false;
  395. return drmEngine.init(manifest, isOffline);
  396. }.bind(this))
  397. .then(function() {
  398. this.checkDestroyed_();
  399. return this.createSegmentIndex_(manifest);
  400. }.bind(this))
  401. .then(function() {
  402. this.checkDestroyed_();
  403. return drmEngine.createOrLoad();
  404. }.bind(this))
  405. .then(function() {
  406. this.checkDestroyed_();
  407. return manifestParser.stop();
  408. }.bind(this))
  409. .then(function() {
  410. this.checkDestroyed_();
  411. return {manifest: manifest, drmEngine: drmEngine};
  412. }.bind(this))
  413. .catch(function(error) {
  414. if (manifestParser)
  415. return manifestParser.stop().then(function() { throw error; });
  416. else
  417. throw error;
  418. });
  419. };
  420. /**
  421. * The default track selection function.
  422. *
  423. * @param {!Array.<shakaExtern.Track>} tracks
  424. * @return {!Array.<shakaExtern.Track>}
  425. * @private
  426. */
  427. shaka.offline.Storage.prototype.defaultTrackSelect_ = function(tracks) {
  428. var LanguageUtils = shaka.util.LanguageUtils;
  429. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  430. var selectedTracks = [];
  431. // Select variants with best language match.
  432. var audioLangPref = LanguageUtils.normalize(
  433. this.player_.getConfiguration().preferredAudioLanguage);
  434. var matchTypes = [
  435. LanguageUtils.MatchType.EXACT,
  436. LanguageUtils.MatchType.BASE_LANGUAGE_OKAY,
  437. LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY
  438. ];
  439. var allVariantTracks =
  440. tracks.filter(function(track) { return track.type == 'variant'; });
  441. // For each match type, get the tracks that match the audio preference for
  442. // that match type.
  443. var tracksByMatchType = matchTypes.map(function(match) {
  444. return allVariantTracks.filter(function(track) {
  445. var lang = LanguageUtils.normalize(track.language);
  446. return LanguageUtils.match(match, audioLangPref, lang);
  447. });
  448. });
  449. // Find the best match type that has any matches.
  450. var variantTracks;
  451. for (var i = 0; i < tracksByMatchType.length; i++) {
  452. if (tracksByMatchType[i].length) {
  453. variantTracks = tracksByMatchType[i];
  454. break;
  455. }
  456. }
  457. // Fall back to "primary" audio tracks, if present.
  458. if (!variantTracks) {
  459. var primaryTracks = allVariantTracks.filter(function(track) {
  460. return track.primary;
  461. });
  462. if (primaryTracks.length)
  463. variantTracks = primaryTracks;
  464. }
  465. // Otherwise, there is no good way to choose the language, so we don't choose
  466. // a language at all.
  467. if (!variantTracks) {
  468. variantTracks = allVariantTracks;
  469. // Issue a warning, but only if the content has multiple languages.
  470. // Otherwise, this warning would just be noise.
  471. var languages = allVariantTracks
  472. .map(function(track) { return track.language; })
  473. .filter(shaka.util.Functional.isNotDuplicate);
  474. if (languages.length > 1) {
  475. shaka.log.warning('Could not choose a good audio track based on ' +
  476. 'language preferences or primary tracks. An ' +
  477. 'arbitrary language will be stored!');
  478. }
  479. }
  480. // From previously selected variants, choose the SD ones (height <= 480).
  481. var tracksByHeight = variantTracks.filter(function(track) {
  482. return track.height && track.height <= 480;
  483. });
  484. // If variants don't have video or no video with height <= 480 was
  485. // found, proceed with the previously selected tracks.
  486. if (tracksByHeight.length) {
  487. // Sort by resolution, then select all variants which match the height
  488. // of the highest SD res. There may be multiple audio bitrates for the
  489. // same video resolution.
  490. tracksByHeight.sort(function(a, b) { return b.height - a.height; });
  491. variantTracks = tracksByHeight.filter(function(track) {
  492. return track.height == tracksByHeight[0].height;
  493. });
  494. }
  495. // Now sort by bandwidth.
  496. variantTracks.sort(function(a, b) { return a.bandwidth - b.bandwidth; });
  497. // In case there are multiple matches at different audio bitrates, select the
  498. // middle bandwidth one.
  499. if (variantTracks.length)
  500. selectedTracks.push(variantTracks[Math.floor(variantTracks.length / 2)]);
  501. // Since this default callback is used primarily by our own demo app and by
  502. // app developers who haven't thought about which tracks they want, we should
  503. // select all text tracks, regardless of language. This makes for a better
  504. // demo for us, and does not rely on user preferences for the unconfigured
  505. // app.
  506. selectedTracks.push.apply(selectedTracks, tracks.filter(function(track) {
  507. return track.type == ContentType.TEXT;
  508. }));
  509. return selectedTracks;
  510. };
  511. /**
  512. * @return {shakaExtern.OfflineConfiguration}
  513. * @private
  514. */
  515. shaka.offline.Storage.prototype.defaultConfig_ = function() {
  516. return {
  517. trackSelectionCallback: this.defaultTrackSelect_.bind(this),
  518. progressCallback: function(storedContent, percent) {
  519. // Reference arguments to keep closure from removing it.
  520. // If the argument is removed, it breaks our function length check
  521. // in mergeConfigObjects_().
  522. // NOTE: Chrome App Content Security Policy prohibits usage of new
  523. // Function().
  524. if (storedContent || percent) return null;
  525. },
  526. usePersistentLicense: true
  527. };
  528. };
  529. /**
  530. * Initializes the IStorageEngine if it is not already.
  531. *
  532. * @return {!Promise}
  533. * @private
  534. */
  535. shaka.offline.Storage.prototype.initIfNeeded_ = function() {
  536. if (!this.storageEngine_) {
  537. return Promise.reject(new shaka.util.Error(
  538. shaka.util.Error.Severity.CRITICAL,
  539. shaka.util.Error.Category.STORAGE,
  540. shaka.util.Error.Code.STORAGE_NOT_SUPPORTED));
  541. } else if (this.storageEngine_.initialized()) {
  542. return Promise.resolve();
  543. } else {
  544. var scheme = shaka.offline.OfflineUtils.DB_SCHEME;
  545. return this.storageEngine_.init(scheme);
  546. }
  547. };
  548. /**
  549. * @param {!Array.<shakaExtern.Period>} periods
  550. * @private
  551. */
  552. shaka.offline.Storage.prototype.filterAllPeriods_ = function(periods) {
  553. periods.forEach(this.filterPeriod_.bind(this));
  554. };
  555. /**
  556. * @param {shakaExtern.Period} period
  557. * @private
  558. */
  559. shaka.offline.Storage.prototype.filterPeriod_ = function(period) {
  560. var StreamUtils = shaka.util.StreamUtils;
  561. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  562. var activeStreams = {};
  563. if (this.firstPeriodTracks_) {
  564. var variantTracks = this.firstPeriodTracks_.filter(function(track) {
  565. return track.type == 'variant';
  566. });
  567. var variant = null;
  568. if (variantTracks.length)
  569. variant = StreamUtils.findVariantForTrack(period, variantTracks[0]);
  570. if (variant) {
  571. // Use the first variant as the container of "active streams". This
  572. // is then used to filter out the streams that are not compatible with it.
  573. // This ensures that in multi-Period content, all Periods have streams
  574. // with compatible MIME types.
  575. if (variant.video) activeStreams[ContentType.VIDEO] = variant.video;
  576. if (variant.audio) activeStreams[ContentType.AUDIO] = variant.audio;
  577. }
  578. }
  579. StreamUtils.filterNewPeriod(this.drmEngine_, activeStreams, period);
  580. StreamUtils.applyRestrictions(
  581. period, this.player_.getConfiguration().restrictions,
  582. /* maxHwRes */ { width: Infinity, height: Infinity });
  583. };
  584. /**
  585. * Cleans up the current store and destroys any objects. This object is still
  586. * usable after this.
  587. *
  588. * @return {!Promise}
  589. * @private
  590. */
  591. shaka.offline.Storage.prototype.cleanup_ = function() {
  592. var ret = this.drmEngine_ ? this.drmEngine_.destroy() : Promise.resolve();
  593. this.drmEngine_ = null;
  594. this.manifest_ = null;
  595. this.storeInProgress_ = false;
  596. this.firstPeriodTracks_ = null;
  597. this.manifestId_ = -1;
  598. return ret;
  599. };
  600. /**
  601. * Calls createSegmentIndex for all streams in the manifest.
  602. *
  603. * @param {shakaExtern.Manifest} manifest
  604. * @return {!Promise}
  605. * @private
  606. */
  607. shaka.offline.Storage.prototype.createSegmentIndex_ = function(manifest) {
  608. var Functional = shaka.util.Functional;
  609. var streams = manifest.periods
  610. .map(function(period) { return period.variants; })
  611. .reduce(Functional.collapseArrays, [])
  612. .map(function(variant) {
  613. var variantStreams = [];
  614. if (variant.audio) variantStreams.push(variant.audio);
  615. if (variant.video) variantStreams.push(variant.video);
  616. return variantStreams;
  617. })
  618. .reduce(Functional.collapseArrays, [])
  619. .filter(Functional.isNotDuplicate);
  620. var textStreams = manifest.periods
  621. .map(function(period) { return period.textStreams; })
  622. .reduce(Functional.collapseArrays, []);
  623. streams.push.apply(streams, textStreams);
  624. return Promise.all(
  625. streams.map(function(stream) { return stream.createSegmentIndex(); }));
  626. };
  627. /**
  628. * Creates an offline 'manifest' for the real manifest. This does not store
  629. * the segments yet, only adds them to the download manager through
  630. * createPeriod_.
  631. *
  632. * @param {string} originalManifestUri
  633. * @param {!Object} appMetadata
  634. * @return {shakaExtern.ManifestDB}
  635. * @private
  636. */
  637. shaka.offline.Storage.prototype.createOfflineManifest_ = function(
  638. originalManifestUri, appMetadata) {
  639. var periods = this.manifest_.periods.map(this.createPeriod_.bind(this));
  640. var drmInfo = this.drmEngine_.getDrmInfo();
  641. var sessions = this.drmEngine_.getSessionIds();
  642. if (drmInfo && this.config_.usePersistentLicense) {
  643. if (!sessions.length) {
  644. throw new shaka.util.Error(
  645. shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE,
  646. shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE, originalManifestUri);
  647. }
  648. // Don't store init data since we have stored sessions.
  649. drmInfo.initData = [];
  650. }
  651. return {
  652. key: this.manifestId_,
  653. originalManifestUri: originalManifestUri,
  654. duration: this.duration_,
  655. size: 0,
  656. expiration: this.drmEngine_.getExpiration(),
  657. periods: periods,
  658. sessionIds: this.config_.usePersistentLicense ? sessions : [],
  659. drmInfo: drmInfo,
  660. appMetadata: appMetadata
  661. };
  662. };
  663. /**
  664. * Converts a manifest Period to a database Period. This will use the current
  665. * configuration to get the tracks to use, then it will search each segment
  666. * index and add all the segments to the download manager through createStream_.
  667. *
  668. * @param {shakaExtern.Period} period
  669. * @return {shakaExtern.PeriodDB}
  670. * @private
  671. */
  672. shaka.offline.Storage.prototype.createPeriod_ = function(period) {
  673. var StreamUtils = shaka.util.StreamUtils;
  674. var variantTracks = StreamUtils.getVariantTracks(period, null, null);
  675. var textTracks = StreamUtils.getTextTracks(period, null);
  676. var allTracks = variantTracks.concat(textTracks);
  677. var chosenTracks = this.config_.trackSelectionCallback(allTracks);
  678. if (this.firstPeriodTracks_ == null) {
  679. this.firstPeriodTracks_ = chosenTracks;
  680. // Now that the first tracks are chosen, filter again. This ensures all
  681. // Periods have compatible content types.
  682. this.filterAllPeriods_(this.manifest_.periods);
  683. }
  684. for (var i = chosenTracks.length - 1; i > 0; --i) {
  685. var foundSimilarTracks = false;
  686. for (var j = i - 1; j >= 0; --j) {
  687. if (chosenTracks[i].type == chosenTracks[j].type &&
  688. chosenTracks[i].kind == chosenTracks[j].kind &&
  689. chosenTracks[i].language == chosenTracks[j].language) {
  690. shaka.log.warning(
  691. 'Multiple tracks of the same type/kind/language given.');
  692. foundSimilarTracks = true;
  693. break;
  694. }
  695. }
  696. if (foundSimilarTracks) break;
  697. }
  698. var streams = [];
  699. for (var i = 0; i < chosenTracks.length; i++) {
  700. var variant = StreamUtils.findVariantForTrack(period, chosenTracks[i]);
  701. if (variant) {
  702. // Make a rough estimation of the streams' bandwidth so download manager
  703. // can track the progress of the download.
  704. var bandwidthEstimation;
  705. if (variant.audio) {
  706. // If the audio stream has already been added to the DB
  707. // as part of another variant, add the ID to the list.
  708. // Otherwise, add it to the DB.
  709. var stream = streams.filter(function(s) {
  710. return s.id == variant.audio.id;
  711. })[0];
  712. if (stream) {
  713. stream.variantIds.push(variant.id);
  714. } else {
  715. // If variant has both audio and video, roughly estimate them
  716. // both to be 1/2 of the variant's bandwidth.
  717. // If variant only has one stream, it's bandwidth equals to
  718. // the bandwidth of the variant.
  719. bandwidthEstimation =
  720. variant.video ? variant.bandwidth / 2 : variant.bandwidth;
  721. streams.push(this.createStream_(period,
  722. variant.audio,
  723. bandwidthEstimation,
  724. variant.id));
  725. }
  726. }
  727. if (variant.video) {
  728. var stream = streams.filter(function(s) {
  729. return s.id == variant.video.id;
  730. })[0];
  731. if (stream) {
  732. stream.variantIds.push(variant.id);
  733. } else {
  734. bandwidthEstimation =
  735. variant.audio ? variant.bandwidth / 2 : variant.bandwidth;
  736. streams.push(this.createStream_(period,
  737. variant.video,
  738. bandwidthEstimation,
  739. variant.id));
  740. }
  741. }
  742. } else {
  743. var textStream =
  744. StreamUtils.findTextStreamForTrack(period, chosenTracks[i]);
  745. goog.asserts.assert(
  746. textStream, 'Could not find track with id ' + chosenTracks[i].id);
  747. streams.push(this.createStream_(
  748. period, textStream, 0 /* estimatedStreamBandwidth */));
  749. }
  750. }
  751. return {
  752. startTime: period.startTime,
  753. streams: streams
  754. };
  755. };
  756. /**
  757. * Converts a manifest stream to a database stream. This will search the
  758. * segment index and add all the segments to the download manager.
  759. *
  760. * @param {shakaExtern.Period} period
  761. * @param {shakaExtern.Stream} stream
  762. * @param {number} estimatedStreamBandwidth
  763. * @param {number=} opt_variantId
  764. * @return {shakaExtern.StreamDB}
  765. * @private
  766. */
  767. shaka.offline.Storage.prototype.createStream_ = function(
  768. period, stream, estimatedStreamBandwidth, opt_variantId) {
  769. /** @type {!Array.<shakaExtern.SegmentDB>} */
  770. var segmentsDb = [];
  771. var startTime =
  772. this.manifest_.presentationTimeline.getSegmentAvailabilityStart();
  773. var endTime = startTime;
  774. var i = stream.findSegmentPosition(startTime);
  775. var ref = (i != null ? stream.getSegmentReference(i) : null);
  776. while (ref) {
  777. var id = this.storageEngine_.reserveId('segment');
  778. var bandwidthSize =
  779. (ref.endTime - ref.startTime) * estimatedStreamBandwidth / 8;
  780. /** @type {shakaExtern.SegmentDataDB} */
  781. var segmentDataDb = {
  782. key: id,
  783. data: null,
  784. manifestKey: this.manifestId_,
  785. streamNumber: stream.id,
  786. segmentNumber: id
  787. };
  788. this.downloadManager_.addSegment(
  789. stream.type, ref, bandwidthSize, segmentDataDb);
  790. segmentsDb.push({
  791. startTime: ref.startTime,
  792. endTime: ref.endTime,
  793. uri: 'offline:' + this.manifestId_ + '/' + stream.id + '/' + id
  794. });
  795. endTime = ref.endTime + period.startTime;
  796. ref = stream.getSegmentReference(++i);
  797. }
  798. this.duration_ = Math.max(this.duration_, (endTime - startTime));
  799. var initUri = null;
  800. if (stream.initSegmentReference) {
  801. var id = this.storageEngine_.reserveId('segment');
  802. initUri = 'offline:' + this.manifestId_ + '/' + stream.id + '/' + id;
  803. /** @type {shakaExtern.SegmentDataDB} */
  804. var initDataDb = {
  805. key: id,
  806. data: null,
  807. manifestKey: this.manifestId_,
  808. streamNumber: stream.id,
  809. segmentNumber: -1
  810. };
  811. this.downloadManager_.addSegment(
  812. stream.contentType, stream.initSegmentReference, 0, initDataDb);
  813. }
  814. var variantIds = [];
  815. if (opt_variantId != null) variantIds.push(opt_variantId);
  816. return {
  817. id: stream.id,
  818. primary: stream.primary,
  819. presentationTimeOffset: stream.presentationTimeOffset || 0,
  820. contentType: stream.type,
  821. mimeType: stream.mimeType,
  822. codecs: stream.codecs,
  823. frameRate: stream.frameRate,
  824. kind: stream.kind,
  825. language: stream.language,
  826. label: stream.label,
  827. width: stream.width || null,
  828. height: stream.height || null,
  829. initSegmentUri: initUri,
  830. encrypted: stream.encrypted,
  831. keyId: stream.keyId,
  832. segments: segmentsDb,
  833. variantIds: variantIds
  834. };
  835. };
  836. /**
  837. * Throws an error if the object is destroyed.
  838. * @private
  839. */
  840. shaka.offline.Storage.prototype.checkDestroyed_ = function() {
  841. if (!this.player_) {
  842. throw new shaka.util.Error(
  843. shaka.util.Error.Severity.CRITICAL,
  844. shaka.util.Error.Category.STORAGE,
  845. shaka.util.Error.Code.OPERATION_ABORTED);
  846. }
  847. };
  848. shaka.Player.registerSupportPlugin('offline', shaka.offline.Storage.support);