Files
shaka-player/lib/offline/storage.js
T
Aaron Vaage 1b90e0f4c1 Integrate Deprecation Tool With Library
This integrates the deprecation tool with our library and updates
all the deprecated warnings to use our new tool.

As some features needed to be removed in 2.5, this change removes them
since their deprecation checks would make us fail.

Change-Id: I28310bbe9df6839719f4763a96026d5968bdcea6
2018-12-12 00:26:50 +00:00

1320 lines
41 KiB
JavaScript

/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.Storage');
goog.require('goog.asserts');
goog.require('shaka.Deprecate');
goog.require('shaka.Player');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.offline.DownloadManager');
goog.require('shaka.offline.OfflineUri');
goog.require('shaka.offline.SessionDeleter');
goog.require('shaka.offline.StorageCellPath');
goog.require('shaka.offline.StorageMuxer');
goog.require('shaka.offline.StoredContentUtils');
goog.require('shaka.offline.StreamBandwidthEstimator');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.Destroyer');
goog.require('shaka.util.Error');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.ManifestFilter');
goog.require('shaka.util.PlayerConfiguration');
goog.require('shaka.util.StreamUtils');
/**
* This manages persistent offline data including storage, listing, and deleting
* stored manifests. Playback of offline manifests are done through the Player
* using a special URI (see shaka.offline.OfflineUri).
*
* First, check support() to see if offline is supported by the platform.
* Second, configure() the storage object with callbacks to your application.
* Third, call store(), remove(), or list() as needed.
* When done, call destroy().
*
* @param {!shaka.Player=} player
* A player instance to share a networking engine and configuration with.
* When initializing with a player, storage is only valid as long as
* |destroy| has not been called on the player instance. When omitted,
* storage will manage its own networking engine and configuration.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
* @export
*/
shaka.offline.Storage = function(player) {
// It is an easy mistake to make to pass a Player proxy from CastProxy.
// Rather than throw a vague exception later, throw an explicit and clear one
// now.
//
// TODO(vaage): After we decide whether or not we want to support
// initializing storage with a player proxy, we should either remove
// this error or rename the error.
if (player && player.constructor != shaka.Player) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.LOCAL_PLAYER_INSTANCE_REQUIRED);
}
/** @private {?shaka.extern.PlayerConfiguration} */
this.config_ = null;
/** @private {shaka.net.NetworkingEngine} */
this.networkingEngine_ = null;
// Initialize |config_| and |networkingEngine_| based on whether or not
// we were given a player instance.
if (player) {
this.config_ = player.getSharedConfiguration();
this.networkingEngine_ = player.getNetworkingEngine();
goog.asserts.assert(
this.networkingEngine_,
'Storage should not be initialized with a player that had |destroy| ' +
'called on it.');
} else {
this.config_ = shaka.util.PlayerConfiguration.createDefault();
this.networkingEngine_ = new shaka.net.NetworkingEngine();
}
/** @private {boolean} */
this.storeInProgress_ = false;
/**
* A list of segment ids for all the segments that were added during the
* current store. If the store fails or is aborted, these need to be
* removed from storage.
* @private {!Array.<number>}
*/
this.segmentsFromStore_ = [];
/**
* A list of open operations that are being performed by this instance of
* |shaka.offline.Storage|.
*
* @private {!Array.<!Promise>}
*/
this.openOperations_ = [];
/**
* Storage should only destroy the networking engine if it was initialized
* without a player instance. Store this as a flag here to avoid including
* the player object in the destoyer's closure.
*
* @type {boolean}
*/
const destroyNetworkingEngine = !player;
/** @private {!shaka.util.Destroyer} */
this.destroyer_ = new shaka.util.Destroyer(async () => {
// Wait for all the open operations to end. Wrap each operations so that a
// single rejected promise won't cause |Promise.all| to return early or to
// return a rejected Promise.
const noop = () => {};
await Promise.all(this.openOperations_.map((op) => op.then(noop, noop)));
// Wait until after all the operations have finished before we destroy
// the networking engine to avoid any unexpected errors.
if (destroyNetworkingEngine) {
await this.networkingEngine_.destroy();
}
// Drop all references to internal objects to help with GC.
this.config_ = null;
this.networkingEngine_ = null;
});
};
/**
* Gets whether offline storage is supported. Returns true if offline storage
* is supported for clear content. Support for offline storage of encrypted
* content will not be determined until storage is attempted.
*
* @return {boolean}
* @export
*/
shaka.offline.Storage.support = function() {
return shaka.offline.StorageMuxer.support();
};
/**
* @override
* @export
*/
shaka.offline.Storage.prototype.destroy = function() {
return this.destroyer_.destroy();
};
/**
* Sets configuration values for Storage. This is associated with
* Player.configure and will change the player instance given at
* initialization.
*
* @param {!Object} config This should follow the form of
* {@link shaka.extern.PlayerConfiguration}, but you may omit any field
* you do not wish to change.
* @return {boolean}
* @export
*/
shaka.offline.Storage.prototype.configure = function(config) {
shaka.offline.Storage.verifyConfig_(config);
goog.asserts.assert(
this.config_, 'Cannot reconfigure stroage after calling destroy.');
return shaka.util.PlayerConfiguration.mergeConfigObjects(
this.config_ /* destination */, config /* updates */);
};
/**
* Return the networking engine that storage is using. If storage was
* initialized with a player instance, then the networking engine returned
* will be the same as |player.getNetworkingEngine()|.
*
* The returned value will only be null if |destroy| was called before
* |getNetworkingEngine|.
*
* @return {shaka.net.NetworkingEngine}
* @export
*/
shaka.offline.Storage.prototype.getNetworkingEngine = function() {
return this.networkingEngine_;
};
/**
* Stores the given manifest. If the content is encrypted, and encrypted
* content cannot be stored on this platform, the Promise will be rejected with
* error code 6001, REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE.
*
* @param {string} uri The URI of the manifest to store.
* @param {!Object=} appMetadata An arbitrary object from the application
* that will be stored along-side the offline content. Use this for any
* application-specific metadata you need associated with the stored content.
* For details on the data types that can be stored here, please refer to
* {@link https://bit.ly/StructClone}
* @param {string|shaka.extern.ManifestParser.Factory=} mimeType
* The mime type for the content |manifestUri| points to or a manifest parser
* factory to override auto-detection or use an unregistered parser. Passing
* a manifest parser factory is deprecated and will be removed.
* @return {!Promise.<shaka.extern.StoredContent>} A Promise to a structure
* representing what was stored. The "offlineUri" member is the URI that
* should be given to Player.load() to play this piece of content offline.
* The "appMetadata" member is the appMetadata argument you passed to store().
* @export
*/
shaka.offline.Storage.prototype.store = function(uri, appMetadata, mimeType) {
const getParser = async () => {
let Factory;
if (mimeType && typeof mimeType != 'string') {
shaka.Deprecate.deprecateFeature(
2, 6,
'Storing with a manifest parser factory',
'Please register a manifest parser and for the mime-type.');
Factory = /** @type {shaka.extern.ManifestParser.Factory} */(mimeType);
} else {
goog.asserts.assert(
this.networkingEngine_, 'Should not call |store| after |destroy|');
Factory = await shaka.media.ManifestParser.getFactory(
uri,
this.networkingEngine_,
this.config_.manifest.retryParameters,
/** @type {?string} */ (mimeType));
}
return new Factory();
};
return this.startOperation_(this.store_(uri, appMetadata || {}, getParser));
};
/**
* See |shaka.offline.Storage.store| for details.
*
* @param {string} uri
* @param {!Object} appMetadata
* @param {function():!Promise.<shaka.extern.ManifestParser>} getParser
* @return {!Promise.<shaka.extern.StoredContent>}
* @private
*/
shaka.offline.Storage.prototype.store_ = async function(
uri, appMetadata, getParser) {
// TODO: Create a way for a download to be canceled while being downloaded.
this.requireSupport_();
if (this.storeInProgress_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.STORE_ALREADY_IN_PROGRESS));
}
this.storeInProgress_ = true;
const manifest = await this.parseManifest(uri, getParser);
// Check if we were asked to destroy ourselves while we were "away"
// downloading the manifest.
this.checkDestroyed_();
// Check if we can even download this type of manifest before trying to
// create the drm engine.
const canDownload = !manifest.presentationTimeline.isLive() &&
!manifest.presentationTimeline.isInProgress();
if (!canDownload) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.CANNOT_STORE_LIVE_OFFLINE,
uri);
}
// Since we will need to use |drmEngine|, |activeHandle|, and |muxer| in the
// catch/finally blocks, we need to define them out here. Since they may not
// get initialized when we enter the catch/finally block, we need to assume
// that they may be null/undefined when we get there.
/** @type {?shaka.media.DrmEngine} */
let drmEngine = null;
/** @type {shaka.offline.StorageMuxer} */
let muxer = new shaka.offline.StorageMuxer();
/** @type {?shaka.offline.StorageCellHandle} */
let activeHandle = null;
// This will be used to store any errors from drm engine. Whenever drm engine
// is passed to another function to do work, we should check if this was
// set.
let drmError = null;
try {
drmEngine = await this.createDrmEngine(
manifest,
(e) => { drmError = drmError || e; });
// We could have been asked to destroy ourselves while we were "away"
// creating the drm engine.
this.checkDestroyed_();
if (drmError) { throw drmError; }
this.filterManifest_(manifest, drmEngine);
await muxer.init();
this.checkDestroyed_();
// Get the cell that we are saving the manifest to. Once we get a cell
// we will only reference the cell and not the muxer so that the manifest
// and segments will all be saved to the same cell.
activeHandle = await muxer.getActive();
this.checkDestroyed_();
goog.asserts.assert(drmEngine, 'drmEngine should be non-null here.');
const manifestDB = await this.downloadManifest_(
activeHandle.cell, drmEngine, manifest, uri, appMetadata);
this.checkDestroyed_();
if (drmError) { throw drmError; }
const ids = await activeHandle.cell.addManifests([manifestDB]);
this.checkDestroyed_();
const offlineUri = shaka.offline.OfflineUri.manifest(
activeHandle.path.mechanism, activeHandle.path.cell, ids[0]);
return shaka.offline.StoredContentUtils.fromManifestDB(
offlineUri, manifestDB);
} catch (e) {
// If we did start saving some data, we need to remove it all to avoid
// wasting storage. However if the muxer did not manage to initialize, then
// we won't have an active cell to remove the segments from.
if (activeHandle) {
await activeHandle.cell.removeSegments(this.segmentsFromStore_, () => {});
}
// If we already had an error, ignore this error to avoid hiding
// the original error.
throw drmError || e;
} finally {
this.storeInProgress_ = false;
this.segmentsFromStore_ = [];
await muxer.destroy();
if (drmEngine) {
await drmEngine.destroy();
}
}
};
/**
* Filter |manifest| such that it will only contain the variants and text
* streams that we want to store and can actually play.
*
* @param {shaka.extern.Manifest} manifest
* @param {!shaka.media.DrmEngine} drmEngine
* @private
*/
shaka.offline.Storage.prototype.filterManifest_ = function(
manifest, drmEngine) {
// Filter the manifest based on the restrictions given in the player
// configuration.
const maxHwRes = {width: Infinity, height: Infinity};
shaka.util.ManifestFilter.filterByRestrictions(
manifest, this.config_.restrictions, maxHwRes);
// Filter the manifest based on what we know media source will be able to
// play later (no point storing something we can't play).
shaka.util.ManifestFilter.filterByMediaSourceSupport(manifest);
// Filter the manifest based on what we know our drm system will support
// playing later.
shaka.util.ManifestFilter.filterByDrmSupport(manifest, drmEngine);
// Filter the manifest so that it will only use codecs that are available in
// all periods.
shaka.util.ManifestFilter.filterByCommonCodecs(manifest);
// Filter each variant based on what the app says they want to store. The app
// will only be given variants that are compatible with all previous
// post-filtered periods.
shaka.util.ManifestFilter.rollingFilter(manifest, (period) => {
const StreamUtils = shaka.util.StreamUtils;
let tracks = [];
tracks = tracks.concat(StreamUtils.getVariantTracks(period, null, null));
tracks = tracks.concat(StreamUtils.getTextTracks(period, null));
const chosenTracks = this.config_.offline.trackSelectionCallback(tracks);
/** @type {!Set.<number>} */
const variantIds = new Set();
/** @type {!Set.<number>} */
const textIds = new Set();
for (const track of chosenTracks) {
if (track.type == 'variant') { variantIds.add(track.id); }
if (track.type == 'text') { textIds.add(track.id); }
}
period.variants =
period.variants.filter((variant) => variantIds.has(variant.id));
period.textStreams =
period.textStreams.filter((stream) => textIds.has(stream.id));
});
// Check the post-filtered manifest for characteristics that may indicate
// issues with how the app selected tracks.
shaka.offline.Storage.validateManifest_(manifest);
};
/**
* Create a download manager and download the manifest.
*
* @param {shaka.extern.StorageCell} storage
* @param {!shaka.media.DrmEngine} drmEngine
* @param {shaka.extern.Manifest} manifest
* @param {string} uri
* @param {!Object} metadata
* @return {!Promise.<shaka.extern.ManifestDB>}
* @private
*/
shaka.offline.Storage.prototype.downloadManifest_ = async function(
storage, drmEngine, manifest, uri, metadata) {
goog.asserts.assert(
this.networkingEngine_,
'Cannot call |downloadManifest_| after calling |destroy|.');
const pendingContent = shaka.offline.StoredContentUtils.fromManifest(
uri, manifest, /* size */ 0, metadata);
/** @type {!shaka.offline.DownloadManager} */
const downloader = new shaka.offline.DownloadManager((progress, size) => {
// Update the size of the stored content before issuing a progress update.
pendingContent.size = size;
this.config_.offline.progressCallback(pendingContent, progress);
});
try {
const manifestDB = this.createOfflineManifest_(
downloader, storage, drmEngine, manifest, uri, metadata);
// Downloader should now be populated with all the download requests needed
// to download the manifest. Wait for them all the finish.
await downloader.download(this.networkingEngine_);
// Now that we have downloaded all the streams, we can save the actual size
// of the content.
manifestDB.size = pendingContent.size;
return manifestDB;
} finally {
await downloader.destroy();
}
};
/**
* Removes the given stored content. This will also attempt to release the
* licenses, if any.
*
* @param {string} contentUri
* @return {!Promise}
* @export
*/
shaka.offline.Storage.prototype.remove = function(contentUri) {
return this.startOperation_(this.remove_(contentUri));
};
/**
* See |shaka.offline.Storage.remove| for details.
*
* @param {string} contentUri
* @return {!Promise}
* @private
*/
shaka.offline.Storage.prototype.remove_ = function(contentUri) {
this.requireSupport_();
let nullableUri = shaka.offline.OfflineUri.parse(contentUri);
if (nullableUri == null || !nullableUri.isManifest()) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
contentUri));
}
let uri = /** @type {!shaka.offline.OfflineUri} */ (nullableUri);
let muxer = new shaka.offline.StorageMuxer();
return shaka.util.Destroyer.with([muxer], async () => {
await muxer.init();
let cell = await muxer.getCell(uri.mechanism(), uri.cell());
let manifests = await cell.getManifests([uri.key()]);
let manifest = manifests[0];
await Promise.all([
this.removeFromDRM_(uri, manifest, muxer),
this.removeFromStorage_(cell, uri, manifest),
]);
});
};
/**
* @param {shaka.extern.ManifestDB} manifestDb
* @param {boolean} isVideo
* @return {!Array.<MediaKeySystemMediaCapability>}
* @private
*/
shaka.offline.Storage.getCapabilities_ = function(manifestDb, isVideo) {
const getFullType = shaka.util.MimeUtils.getFullType;
const ret = [];
for (const period of manifestDb.periods) {
for (const stream of period.streams) {
if (isVideo && stream.contentType == 'video') {
ret.push({
contentType: getFullType(stream.mimeType, stream.codecs),
robustness: manifestDb.drmInfo.videoRobustness,
});
} else if (!isVideo && stream.contentType == 'audio') {
ret.push({
contentType: getFullType(stream.mimeType, stream.codecs),
robustness: manifestDb.drmInfo.audioRobustness,
});
}
}
}
return ret;
};
/**
* @param {!shaka.offline.OfflineUri} uri
* @param {shaka.extern.ManifestDB} manifestDb
* @param {!shaka.offline.StorageMuxer} muxer
* @return {!Promise}
* @private
*/
shaka.offline.Storage.prototype.removeFromDRM_ = async function(
uri, manifestDb, muxer) {
goog.asserts.assert(this.networkingEngine_, 'Cannot be destroyed');
await shaka.offline.Storage.deleteLicenseFor_(
this.networkingEngine_, this.config_.drm, muxer, manifestDb);
};
/**
* @param {shaka.extern.StorageCell} storage
* @param {!shaka.offline.OfflineUri} uri
* @param {shaka.extern.ManifestDB} manifest
* @return {!Promise}
* @private
*/
shaka.offline.Storage.prototype.removeFromStorage_ = function(
storage, uri, manifest) {
/** @type {!Array.<number>} */
let segmentIds = shaka.offline.Storage.getAllSegmentIds_(manifest);
// Count(segments) + Count(manifests)
let toRemove = segmentIds.length + 1;
let removed = 0;
let pendingContent = shaka.offline.StoredContentUtils.fromManifestDB(
uri, manifest);
let onRemove = (key) => {
removed += 1;
this.config_.offline.progressCallback(pendingContent, removed / toRemove);
};
return Promise.all([
storage.removeSegments(segmentIds, onRemove),
storage.removeManifests([uri.key()], onRemove),
]);
};
/**
* Removes any EME sessions that were not successfully removed before. This
* returns whether all the sessions were successfully removed.
*
* @return {!Promise.<boolean>}
* @export
*/
shaka.offline.Storage.prototype.removeEmeSessions = function() {
return this.startOperation_(this.removeEmeSessions_());
};
/**
* @return {!Promise.<boolean>}
* @private
*/
shaka.offline.Storage.prototype.removeEmeSessions_ = function() {
this.requireSupport_();
goog.asserts.assert(this.networkingEngine_, 'Cannot be destroyed');
const net = this.networkingEngine_;
const config = this.config_.drm;
const muxer = new shaka.offline.StorageMuxer();
return shaka.util.Destroyer.with([muxer], async () => {
await muxer.init();
let hasRemaining = false;
/** @type {!Array.<shaka.extern.EmeSessionStorageCell>} */
const cells = [];
muxer.forEachEmeSessionCell((c) => cells.push(c));
for (const sessionIdCell of cells) {
// Run these sequentially to avoid creating too many DrmEngine instances
// and having multiple CDMs alive at once. Some embedded platforms may
// not support that.
/* eslint-disable no-await-in-loop */
const sessions = await sessionIdCell.getAll();
const deletedSessionIds =
await shaka.offline.SessionDeleter.delete(config, net, sessions);
await sessionIdCell.remove(deletedSessionIds);
if (deletedSessionIds.length != sessions.length) {
hasRemaining = true;
}
/* eslint-enable no-await-in-loop */
}
return !hasRemaining;
});
};
/**
* Lists all the stored content available.
*
* @return {!Promise.<!Array.<shaka.extern.StoredContent>>} A Promise to an
* array of structures representing all stored content. The "offlineUri"
* member of the structure is the URI that should be given to Player.load()
* to play this piece of content offline. The "appMetadata" member is the
* appMetadata argument you passed to store().
* @export
*/
shaka.offline.Storage.prototype.list = function() {
return this.startOperation_(this.list_());
};
/**
* See |shaka.offline.Storage.list| for details.
*
* @return {!Promise.<!Array.<shaka.extern.StoredContent>>}
* @private
*/
shaka.offline.Storage.prototype.list_ = function() {
this.requireSupport_();
/** @type {!Array.<shaka.extern.StoredContent>} */
let result = [];
/**
* @param {!shaka.offline.StorageCellPath} path
* @param {shaka.extern.StorageCell} cell
*/
async function onCell(path, cell) {
const manifests = await cell.getAllManifests();
manifests.forEach((manifest, key) => {
const uri = shaka.offline.OfflineUri.manifest(
path.mechanism, path.cell, key);
const content = shaka.offline.StoredContentUtils.fromManifestDB(
uri, manifest);
result.push(content);
});
}
// Go over each storage cell and call |onCell| to create our list of
// stored content.
let muxer = new shaka.offline.StorageMuxer();
return shaka.util.Destroyer.with([muxer], async () => {
await muxer.init();
let p = Promise.resolve();
muxer.forEachCell((path, cell) => {
p = p.then(() => onCell(path, cell));
});
await p;
}).then(() => result);
};
/**
* This method is public so that it can be overridden in testing.
*
* @param {string} uri
* @param {function():!Promise.<shaka.extern.ManifestParser>} getParser
* @return {!Promise.<shaka.extern.Manifest>}
*/
shaka.offline.Storage.prototype.parseManifest = async function(
uri, getParser) {
let error = null;
const networkingEngine = this.networkingEngine_;
goog.asserts.assert(networkingEngine, 'Should be initialized!');
/** @type {shaka.extern.ManifestParser.PlayerInterface} */
const playerInterface = {
networkingEngine: networkingEngine,
// Don't bother filtering now. We will do that later when we have all the
// information we need to filter.
filterAllPeriods: () => {},
filterNewPeriod: () => {},
onTimelineRegionAdded: () => {},
onEvent: () => {},
// Used to capture an error from the manifest parser. We will check the
// error before returning.
onError: (e) => {
error = e;
},
};
const parser = await getParser();
parser.configure(this.config_.manifest);
// We may have been destroyed while we were waiting on |getParser| to
// resolve.
this.checkDestroyed_();
try {
const manifest = await parser.start(uri, playerInterface);
// We may have been destroyed while we were waiting on |start| to
// resolve.
this.checkDestroyed_();
// Get all the streams that are used in the manifest.
const streams = new Set();
for (const period of manifest.periods) {
for (const variant of period.variants) {
if (variant.audio) { streams.add(variant.audio); }
if (variant.video) { streams.add(variant.video); }
}
for (const text of period.textStreams) {
streams.add(text);
}
}
// Wait for each stream to create their segment indexes.
await Promise.all(Array.from(streams).map((stream) => {
return stream.createSegmentIndex();
}));
// We may have been destroyed while we were waiting on |createSegmentIndex|
// to resolve for each stream.
this.checkDestroyed_();
// If we saw an error while parsing, surface the error.
if (error) {
throw error;
}
return manifest;
} finally {
await parser.stop();
}
};
/**
* This method is public so that it can be override in testing.
*
* @param {shaka.extern.Manifest} manifest
* @param {function(shaka.util.Error)} onError
* @return {!Promise.<!shaka.media.DrmEngine>}
*/
shaka.offline.Storage.prototype.createDrmEngine = async function(
manifest, onError) {
goog.asserts.assert(
this.networkingEngine_, 'Cannot call |createDrmEngine| after |destroy|');
/** @type {!shaka.media.DrmEngine} */
const drmEngine = new shaka.media.DrmEngine({
netEngine: this.networkingEngine_,
onError: onError,
onKeyStatus: () => {},
onExpirationUpdated: () => {},
onEvent: () => {},
});
const variants = shaka.util.StreamUtils.getAllVariants(manifest);
const config = this.config_;
drmEngine.configure(config.drm);
await drmEngine.initForStorage(variants, config.offline.usePersistentLicense);
await drmEngine.setServerCertificate();
await drmEngine.createOrLoad();
return drmEngine;
};
/**
* Creates an offline 'manifest' for the real manifest. This does not store the
* segments yet, only adds them to the download manager through createPeriod_.
*
* @param {!shaka.offline.DownloadManager} downloader
* @param {shaka.extern.StorageCell} storage
* @param {!shaka.media.DrmEngine} drmEngine
* @param {shaka.extern.Manifest} manifest
* @param {string} originalManifestUri
* @param {!Object} metadata
* @return {shaka.extern.ManifestDB}
* @private
*/
shaka.offline.Storage.prototype.createOfflineManifest_ = function(
downloader, storage, drmEngine, manifest, originalManifestUri, metadata) {
let estimator = new shaka.offline.StreamBandwidthEstimator();
let periods = manifest.periods.map((period) => {
return this.createPeriod_(
downloader, storage, estimator, drmEngine, manifest, period);
});
let drmInfo = drmEngine.getDrmInfo();
let sessions = drmEngine.getSessionIds();
if (drmInfo && this.config_.offline.usePersistentLicense) {
if (!sessions.length) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE, originalManifestUri);
}
// Don't store init data, since we have stored sessions.
drmInfo.initData = [];
}
return {
originalManifestUri: originalManifestUri,
duration: manifest.presentationTimeline.getDuration(),
size: 0,
expiration: drmEngine.getExpiration(),
periods: periods,
sessionIds: this.config_.offline.usePersistentLicense ? sessions : [],
drmInfo: drmInfo,
appMetadata: metadata,
};
};
/**
* Converts a manifest Period to a database Period. This will use the current
* configuration to get the tracks to use, then it will search each segment
* index and add all the segments to the download manager through createStream_.
*
* @param {!shaka.offline.DownloadManager} downloader
* @param {shaka.extern.StorageCell} storage
* @param {shaka.offline.StreamBandwidthEstimator} estimator
* @param {!shaka.media.DrmEngine} drmEngine
* @param {shaka.extern.Manifest} manifest
* @param {shaka.extern.Period} period
* @return {shaka.extern.PeriodDB}
* @private
*/
shaka.offline.Storage.prototype.createPeriod_ = function(
downloader, storage, estimator, drmEngine, manifest, period) {
// Pass all variants and text streams to the estimator so that we can
// get the best estimate for each stream later.
manifest.periods.forEach((period) => {
period.variants.forEach((variant) => { estimator.addVariant(variant); });
period.textStreams.forEach((text) => { estimator.addText(text); });
});
// Find the streams we want to download and create a stream db instance
// for each of them.
const streamSet = shaka.offline.Storage.getStreamSet_(manifest);
const streamDBs = new Map();
for (const stream of streamSet) {
const streamDB = this.createStream_(
downloader, storage, estimator, manifest, period, stream);
streamDBs.set(stream.id, streamDB);
}
// Connect streams and variants together.
period.variants.forEach((variant) => {
if (variant.audio) {
streamDBs.get(variant.audio.id).variantIds.push(variant.id);
}
if (variant.video) {
streamDBs.get(variant.video.id).variantIds.push(variant.id);
}
});
return {
startTime: period.startTime,
streams: Array.from(streamDBs.values()),
};
};
/**
* Converts a manifest stream to a database stream. This will search the
* segment index and add all the segments to the download manager.
*
* @param {!shaka.offline.DownloadManager} downloader
* @param {shaka.extern.StorageCell} storage
* @param {shaka.offline.StreamBandwidthEstimator} estimator
* @param {shaka.extern.Manifest} manifest
* @param {shaka.extern.Period} period
* @param {shaka.extern.Stream} stream
* @return {shaka.extern.StreamDB}
* @private
*/
shaka.offline.Storage.prototype.createStream_ = function(
downloader, storage, estimator, manifest, period, stream) {
/** @type {shaka.extern.StreamDB} */
let streamDb = {
id: stream.id,
originalId: stream.originalId,
primary: stream.primary,
presentationTimeOffset: stream.presentationTimeOffset || 0,
contentType: stream.type,
mimeType: stream.mimeType,
codecs: stream.codecs,
frameRate: stream.frameRate,
kind: stream.kind,
language: stream.language,
label: stream.label,
width: stream.width || null,
height: stream.height || null,
initSegmentKey: null,
encrypted: stream.encrypted,
keyId: stream.keyId,
segments: [],
variantIds: [],
};
/** @type {number} */
let startTime =
manifest.presentationTimeline.getSegmentAvailabilityStart();
// Download each stream in parallel.
let downloadGroup = stream.id;
shaka.offline.Storage.forEachSegment_(stream, startTime, (segment) => {
downloader.queue(
downloadGroup,
this.createRequest_(segment),
estimator.getSegmentEstimate(stream.id, segment),
(data) => {
return storage.addSegments([{data: data}]).then((ids) => {
this.segmentsFromStore_.push(ids[0]);
streamDb.segments.push({
startTime: segment.startTime,
endTime: segment.endTime,
dataKey: ids[0],
});
});
});
});
let initSegment = stream.initSegmentReference;
if (initSegment) {
downloader.queue(
downloadGroup,
this.createRequest_(initSegment),
estimator.getInitSegmentEstimate(stream.id),
(data) => {
return storage.addSegments([{data: data}]).then((ids) => {
this.segmentsFromStore_.push(ids[0]);
streamDb.initSegmentKey = ids[0];
});
});
}
return streamDb;
};
/**
* @param {shaka.extern.Stream} stream
* @param {number} startTime
* @param {function(!shaka.media.SegmentReference)} callback
* @private
*/
shaka.offline.Storage.forEachSegment_ = function(stream, startTime, callback) {
/** @type {?number} */
let i = stream.findSegmentPosition(startTime);
/** @type {?shaka.media.SegmentReference} */
let ref = i == null ? null : stream.getSegmentReference(i);
while (ref) {
callback(ref);
ref = stream.getSegmentReference(++i);
}
};
/**
* Throws an error if the object is destroyed.
* @private
*/
shaka.offline.Storage.prototype.checkDestroyed_ = function() {
if (this.destroyer_.destroyed()) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.OPERATION_ABORTED);
}
};
/**
* Used by functions that need storage support to ensure that the current
* platform has storage support before continuing. This should only be
* needed to be used at the start of public methods.
*
* @private
*/
shaka.offline.Storage.prototype.requireSupport_ = function() {
if (!shaka.offline.Storage.support()) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.STORAGE_NOT_SUPPORTED);
}
};
/**
* @param {!shaka.media.SegmentReference|
* !shaka.media.InitSegmentReference} segment
* @return {shaka.extern.Request}
* @private
*/
shaka.offline.Storage.prototype.createRequest_ = function(segment) {
const retryParams = this.config_.streaming.retryParameters;
let request = shaka.net.NetworkingEngine.makeRequest(
segment.getUris(), retryParams);
if (segment.startByte != 0 || segment.endByte != null) {
let end = segment.endByte == null ? '' : segment.endByte;
request.headers['Range'] = 'bytes=' + segment.startByte + '-' + end;
}
return request;
};
/**
* Perform an action. Track the action's progress so that when we destroy
* we will wait until all the actions have completed before allowing destroy
* to resolve.
*
* @param {!Promise<T>} action
* @return {!Promise<T>}
* @template T
* @private
*/
shaka.offline.Storage.prototype.startOperation_ = async function(action) {
this.openOperations_.push(action);
try {
// Await |action| so we can use the finally statement to remove |action|
// from |openOperations_| when we still have a reference to |action|.
return await action;
} finally {
shaka.util.ArrayUtils.remove(this.openOperations_, action);
}
};
/**
* @param {shaka.extern.ManifestDB} manifest
* @return {!Array.<number>}
* @private
*/
shaka.offline.Storage.getAllSegmentIds_ = function(manifest) {
/** @type {!Array.<number>} */
let ids = [];
// Get every segment for every stream in the manifest.
manifest.periods.forEach(function(period) {
period.streams.forEach(function(stream) {
if (stream.initSegmentKey != null) {
ids.push(stream.initSegmentKey);
}
stream.segments.forEach(function(segment) {
ids.push(segment.dataKey);
});
});
});
return ids;
};
/**
* Delete the on-disk storage and all the content it contains. This should not
* be done in normal circumstances. Only do it when storage is rendered
* unusable, such as by a version mismatch. No business logic will be run, and
* licenses will not be released.
*
* @return {!Promise}
* @export
*/
shaka.offline.Storage.deleteAll = async function() {
/** @type {!shaka.offline.StorageMuxer} */
const muxer = new shaka.offline.StorageMuxer();
try {
// Wipe all content from all storage mechanisms.
await muxer.erase();
} finally {
// Destroy the muxer, whether or not erase() succeeded.
await muxer.destroy();
}
};
/**
* @param {!shaka.net.NetworkingEngine} net
* @param {!shaka.extern.DrmConfiguration} drmConfig
* @param {!shaka.offline.StorageMuxer} muxer
* @param {shaka.extern.ManifestDB} manifestDb
* @return {!Promise}
* @private
*/
shaka.offline.Storage.deleteLicenseFor_ = async function(
net, drmConfig, muxer, manifestDb) {
if (!manifestDb.drmInfo) {
return;
}
const sessionIdCell = muxer.getEmeSessionCell();
/** @type {!Array.<shaka.extern.EmeSessionDB>} */
const sessions = manifestDb.sessionIds.map((sessionId) => {
return {
sessionId: sessionId,
keySystem: manifestDb.drmInfo.keySystem,
licenseUri: manifestDb.drmInfo.licenseServerUri,
serverCertificate: manifestDb.drmInfo.serverCertificate,
audioCapabilities: shaka.offline.Storage.getCapabilities_(
manifestDb,
/* isVideo */ false),
videoCapabilities: shaka.offline.Storage.getCapabilities_(
manifestDb,
/* isVideo */ true),
};
});
// Try to delete the sessions; any sessions that weren't deleted get stored
// in the database so we can try to remove them again later. This allows us
// to still delete the stored content but not "forget" about these sessions.
// Later, we can remove the sessions to free up space.
const deletedSessionIds =
await shaka.offline.SessionDeleter.delete(drmConfig, net, sessions);
await sessionIdCell.remove(deletedSessionIds);
await sessionIdCell.add(sessions.filter(
(session) => deletedSessionIds.indexOf(session.sessionId) == -1));
};
/**
* Get the set of all streams in |manifest|.
*
* @param {shaka.extern.Manifest} manifest
* @return {!Set.<shaka.extern.Stream>}
* @private
*/
shaka.offline.Storage.getStreamSet_ = function(manifest) {
/** @type {!Set.<shaka.extern.Stream>} */
const set = new Set();
manifest.periods.forEach((period) => {
period.textStreams.forEach((text) => { set.add(text); });
period.variants.forEach((variant) => {
if (variant.audio) { set.add(variant.audio); }
if (variant.video) { set.add(variant.video); }
});
});
return set;
};
/**
* Make sure that the given configuration object follows the correct structure
* expected by |configure|. This function should be removed in v2.6 when
* backward-compatibility is no longer needed.
*
* @param {!Object} config
* The config fields that the app wants to update. This object will be
* change by this function.
* @private
*/
shaka.offline.Storage.verifyConfig_ = function(config) {
// To avoid printing a deprecated warning multiple times, track all
// infractions and then print it once at the end.
let usedLegacyConfig = false;
// For each field in the legacy config structure
// (shaka.extern.OfflineConfiguration), move any occurances to the correct
// location in the player configuration.
if (config.trackSelectionCallback != null) {
usedLegacyConfig = true;
config.offline = config.offline || {};
config.offline.trackSelectionCallback = config.trackSelectionCallback;
}
if (config.progressCallback != null) {
usedLegacyConfig = true;
config.offline = config.offline || {};
config.offline.progressCallback = config.progressCallback;
}
if (config.usePersistentLicense != null) {
usedLegacyConfig = true;
config.offline = config.offline || {};
config.offline.usePersistentLicense = config.usePersistentLicense;
}
if (usedLegacyConfig) {
shaka.Deprecate.deprecateFeature(
2, 6,
'Storage.configure with OfflineConfig',
'Please configure storage with a player configuration.');
}
};
/**
* Go over a manifest and issue warnings for any suspicious properties.
*
* @param {shaka.extern.Manifest} manifest
* @private
*/
shaka.offline.Storage.validateManifest_ = function(manifest) {
// Make sure that the period has not been reduced to nothing.
if (manifest.periods.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.NO_PERIODS);
}
for (const period of manifest.periods) {
shaka.offline.Storage.validatePeriod_(period);
}
};
/**
* Go over a period and issue warnings for any suspicious properties.
*
* @param {shaka.extern.Period} period
* @private
*/
shaka.offline.Storage.validatePeriod_ = function(period) {
const videos = new Set(period.variants.map((v) => v.video));
const audios = new Set(period.variants.map((v) => v.audio));
const texts = period.textStreams;
if (videos.size > 1) {
shaka.log.warning('Multiple video tracks selected to be stored');
}
for (const audio1 of audios) {
for (const audio2 of audios) {
if (audio1 != audio2 && audio1.language == audio2.language) {
shaka.log.warning(
'Similar audio tracks were selected to be stored',
audio1.id,
audio2.id);
}
}
}
for (const text1 of texts) {
for (const text2 of texts) {
if (text1 != text2 && text1.language == text2.language) {
shaka.log.warning(
'Similar text tracks were selected to be stored',
text1.id,
text2.id);
}
}
}
};
shaka.Player.registerSupportPlugin('offline', shaka.offline.Storage.support);