mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-26 17:46:26 +03:00
1c58dee0c2
Add new ContentProtection interpretation API that allows applications to return multiple DRM configurations for each ContentProtection element and to parse raw ContentProtection XML elements. This patch deprecates DrmSchemeInfo in favor of DrmInfo. Furthermore, DrmSchemeInfo will be removed post v1.5.0. * Replace DrmSchemeInfo with DrmInfo. * Move Restrictions class definition into its own file. * Populate initData values from explicit PSSHs without application intervention. * Allow explicit PSSHs to differ between Representations Issue #71 Issue #137 Closes b/23428584 Change-Id: Ib8d6ba630b930ee64f923a3f4a3e518abacccf88
544 lines
18 KiB
JavaScript
544 lines
18 KiB
JavaScript
/**
|
|
* Copyright 2015 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.
|
|
*
|
|
* @fileoverview Implements an offline video source.
|
|
*/
|
|
|
|
goog.provide('shaka.player.OfflineVideoSource');
|
|
|
|
goog.require('goog.Uri');
|
|
goog.require('shaka.asserts');
|
|
goog.require('shaka.dash.MpdProcessor');
|
|
goog.require('shaka.dash.MpdRequest');
|
|
goog.require('shaka.dash.mpd');
|
|
goog.require('shaka.features');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.media.EmeManager');
|
|
goog.require('shaka.media.IAbrManager');
|
|
goog.require('shaka.media.OfflineSegmentIndexSource');
|
|
goog.require('shaka.media.SegmentIndex');
|
|
goog.require('shaka.media.SimpleAbrManager');
|
|
goog.require('shaka.media.StreamInfo');
|
|
goog.require('shaka.player.StreamVideoSource');
|
|
goog.require('shaka.util.ContentDatabaseReader');
|
|
goog.require('shaka.util.ContentDatabaseWriter');
|
|
goog.require('shaka.util.FailoverUri');
|
|
goog.require('shaka.util.IBandwidthEstimator');
|
|
goog.require('shaka.util.TypedBind');
|
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
|
|
|
|
|
|
/**
|
|
* Creates an OfflineVideoSource.
|
|
* @param {?number} groupId The unique ID of the group of streams
|
|
* in this source.
|
|
* @param {shaka.util.IBandwidthEstimator} estimator
|
|
* @param {shaka.media.IAbrManager} abrManager
|
|
*
|
|
* @struct
|
|
* @constructor
|
|
* @extends {shaka.player.StreamVideoSource}
|
|
* @exportDoc
|
|
*/
|
|
shaka.player.OfflineVideoSource = function(groupId, estimator, abrManager) {
|
|
if (!estimator) {
|
|
// For backward compatibility, provide an instance of the default
|
|
// implementation if none is provided.
|
|
estimator = new shaka.util.EWMABandwidthEstimator();
|
|
}
|
|
if (!abrManager) {
|
|
abrManager = new shaka.media.SimpleAbrManager();
|
|
}
|
|
|
|
shaka.player.StreamVideoSource.call(this, null, estimator, abrManager);
|
|
|
|
/** @private {?number} */
|
|
this.groupId_ = groupId;
|
|
|
|
/** @private {!Array.<string>} */
|
|
this.sessionIds_ = [];
|
|
|
|
/**
|
|
* The timeout, in milliseconds, for downloading and storing offline licenses
|
|
* for encrypted content.
|
|
* @type {number}
|
|
* @expose
|
|
*/
|
|
this.timeoutMs = 30000;
|
|
|
|
/** @private {!Object.<string, *>} */
|
|
this.config_ = {};
|
|
|
|
/** @private {shaka.util.FailoverUri.NetworkCallback} */
|
|
this.networkCallback_ = null;
|
|
};
|
|
goog.inherits(shaka.player.OfflineVideoSource, shaka.player.StreamVideoSource);
|
|
if (shaka.features.Offline) {
|
|
goog.exportSymbol('shaka.player.OfflineVideoSource',
|
|
shaka.player.OfflineVideoSource);
|
|
}
|
|
|
|
|
|
/**
|
|
* A callback to the application to choose the tracks which will be stored
|
|
* offline. Returns a Promise to an array of track IDs. This uses Promises
|
|
* so that the application can, if it chooses, display some dialog to the user
|
|
* to drive the choice of tracks.
|
|
*
|
|
* @typedef {function():!Promise.<!Array.<number>>}
|
|
* @expose
|
|
*/
|
|
shaka.player.OfflineVideoSource.ChooseTracksCallback;
|
|
|
|
|
|
/**
|
|
* Configures the OfflineVideoSource options.
|
|
* Options are set via key-value pairs.
|
|
*
|
|
* The following configuration options are supported:
|
|
* licenseRequestTimeout: number
|
|
* Sets the license request timeout in seconds.
|
|
* mpdRequestTimeout: number
|
|
* Sets the MPD request timeout in seconds.
|
|
* segmentRequestTimeout: number
|
|
* Sets the segment request timeout in seconds.
|
|
*
|
|
* @example
|
|
* offlineVideoSouce.configure({'licenseRequestTimeout': 20});
|
|
*
|
|
* @param {!Object.<string, *>} config A configuration object, which contains
|
|
* the configuration options as key-value pairs. All fields should have
|
|
* already been validated.
|
|
* @override
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.configure = function(config) {
|
|
if (config['licenseRequestTimeout'] != null) {
|
|
this.config_['licenseRequestTimeout'] = config['licenseRequestTimeout'];
|
|
}
|
|
if (config['segmentRequestTimeout'] != null) {
|
|
this.config_['segmentRequestTimeout'] = config['segmentRequestTimeout'];
|
|
}
|
|
var baseClassConfigure = shaka.player.StreamVideoSource.prototype.configure;
|
|
baseClassConfigure.call(this, config);
|
|
};
|
|
|
|
|
|
/**
|
|
* Retrieves an array of all stored group IDs.
|
|
* @return {!Promise.<!Array.<number>>} The unique IDs of all of the
|
|
* stored groups.
|
|
* @export
|
|
*/
|
|
shaka.player.OfflineVideoSource.retrieveGroupIds = function() {
|
|
var contentDatabase = new shaka.util.ContentDatabaseReader();
|
|
|
|
var p = contentDatabase.setUpDatabase().then(
|
|
function() {
|
|
return contentDatabase.retrieveGroupIds();
|
|
});
|
|
|
|
p.then(
|
|
function() {
|
|
contentDatabase.closeDatabaseConnection();
|
|
}
|
|
).catch(
|
|
function() {
|
|
contentDatabase.closeDatabaseConnection();
|
|
}
|
|
);
|
|
|
|
return p;
|
|
};
|
|
|
|
|
|
/**
|
|
* Stores the content described by the MPD for offline playback.
|
|
* @param {string} mpdUrl The MPD URL.
|
|
* @param {string} preferredLanguage The user's preferred language tag.
|
|
* @param {?shaka.player.DashVideoSource.ContentProtectionCallback}
|
|
* interpretContentProtection A callback to interpret the ContentProtection
|
|
* elements in the MPD.
|
|
* @param {shaka.player.OfflineVideoSource.ChooseTracksCallback} chooseTracks
|
|
* @return {!Promise.<number>} The group ID of the stored content.
|
|
* @exportDoc
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.store = function(
|
|
mpdUrl, preferredLanguage, interpretContentProtection, chooseTracks) {
|
|
var emeManager;
|
|
|
|
/** @type {!Object.<number, !shaka.media.StreamInfo>} */
|
|
var streamIdMap = {};
|
|
|
|
/** @type {!Array.<!shaka.media.StreamInfo>} */
|
|
var selectedStreams = [];
|
|
|
|
var failover = new shaka.util.FailoverUri(this.networkCallback_,
|
|
[new goog.Uri(mpdUrl)]);
|
|
var mpdRequest = new shaka.dash.MpdRequest(failover, this.mpdRequestTimeout);
|
|
|
|
return mpdRequest.send().then(shaka.util.TypedBind(this,
|
|
/** @param {!shaka.dash.mpd.Mpd} mpd */
|
|
function(mpd) {
|
|
var mpdProcessor =
|
|
new shaka.dash.MpdProcessor(interpretContentProtection);
|
|
this.manifestInfo = mpdProcessor.process(mpd, this.networkCallback_);
|
|
|
|
if (this.manifestInfo.live) {
|
|
var error = new Error('Unable to store live streams offline.');
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
this.configure({'preferredLanguage': preferredLanguage});
|
|
var baseClassLoad = shaka.player.StreamVideoSource.prototype.load;
|
|
return baseClassLoad.call(this);
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
function() {
|
|
var fakeVideoElement = /** @type {!HTMLVideoElement} */ (
|
|
document.createElement('video'));
|
|
fakeVideoElement.src = window.URL.createObjectURL(this.mediaSource);
|
|
|
|
emeManager =
|
|
new shaka.media.EmeManager(null, fakeVideoElement, this);
|
|
if (this.config_['licenseRequestTimeout'] != null) {
|
|
emeManager.setLicenseRequestTimeout(
|
|
Number(this.config_['licenseRequestTimeout']));
|
|
}
|
|
this.eventManager.listen(
|
|
emeManager, 'sessionReady', this.onSessionReady_.bind(this));
|
|
return emeManager.initialize();
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
function() {
|
|
// Build a map of stream IDs.
|
|
var streamSetInfos = this.streamSetsByType.getAll();
|
|
for (var i = 0; i < streamSetInfos.length; ++i) {
|
|
var streamSetInfo = streamSetInfos[i];
|
|
for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) {
|
|
var streamInfo = streamSetInfo.streamInfos[j];
|
|
streamIdMap[streamInfo.uniqueId] = streamInfo;
|
|
}
|
|
}
|
|
|
|
// Ask the application to choose streams.
|
|
return chooseTracks();
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
/** @param {!Array.<number>} trackIds */
|
|
function(trackIds) {
|
|
// Map the track IDs back to streams.
|
|
for (var i = 0; i < trackIds.length; ++i) {
|
|
var id = trackIds[i];
|
|
var selectedStream = streamIdMap[id];
|
|
if (selectedStream) {
|
|
selectedStreams.push(selectedStream);
|
|
} else {
|
|
return Promise.reject(new Error('Invalid stream ID chosen: ' + id));
|
|
}
|
|
}
|
|
|
|
// Only keep those which are supported types.
|
|
// TODO(natalieharris): Add text support.
|
|
var supportedTypes = ['audio', 'video'];
|
|
selectedStreams = selectedStreams.filter(function(streamInfo) {
|
|
if (supportedTypes.indexOf(streamInfo.getContentType()) < 0) {
|
|
shaka.log.warning('Ignoring track ID ' + streamInfo.uniqueId +
|
|
' due to unsupported type: ' +
|
|
streamInfo.getContentType());
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
var async = selectedStreams.map(
|
|
function(streamInfo) {
|
|
return streamInfo.segmentInitSource.create();
|
|
});
|
|
return Promise.all(async);
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
/** @param {!Array.<ArrayBuffer>} initDatas */
|
|
function(initDatas) {
|
|
return this.initializeStreams_(selectedStreams, initDatas);
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
function() {
|
|
return emeManager.allSessionsReady(this.timeoutMs);
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
function() {
|
|
var drmInfo = emeManager.getDrmInfo();
|
|
// TODO(story 1890046): Support multiple periods.
|
|
var duration = this.manifestInfo.periodInfos[0].duration;
|
|
if (!duration) {
|
|
shaka.log.warning('The duration of the stream being stored is null.');
|
|
}
|
|
shaka.asserts.assert(duration != Number.POSITIVE_INFINITY);
|
|
|
|
return this.insertGroup_(selectedStreams, drmInfo, duration);
|
|
})
|
|
);
|
|
};
|
|
if (shaka.features.Dash) {
|
|
goog.exportSymbol('shaka.player.OfflineVideoSource.prototype.store',
|
|
shaka.player.OfflineVideoSource.prototype.store);
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the callback used to intercept the URL in network requests.
|
|
*
|
|
* @param {!shaka.util.FailoverUri.NetworkCallback} callback
|
|
* @export
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.setNetworkCallback =
|
|
function(callback) {
|
|
this.networkCallback_ = callback;
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates sourceBuffers and appends init data for each of the given streams.
|
|
* This should trigger encrypted events for any encrypted streams.
|
|
* @param {!Array.<!shaka.media.StreamInfo>} streamInfos The streams to
|
|
* initialize.
|
|
* @param {!Array.<ArrayBuffer>} initDatas |streamInfos| corresponding
|
|
* initialization data.
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.initializeStreams_ =
|
|
function(streamInfos, initDatas) {
|
|
shaka.asserts.assert(streamInfos.length == initDatas.length);
|
|
|
|
var sourceBuffers = [];
|
|
for (var i = 0; i < streamInfos.length; ++i) {
|
|
try {
|
|
var fullMimeType = streamInfos[i].getFullMimeType();
|
|
sourceBuffers[i] = this.mediaSource.addSourceBuffer(fullMimeType);
|
|
} catch (exception) {
|
|
shaka.log.error('addSourceBuffer() failed', exception);
|
|
}
|
|
}
|
|
|
|
if (streamInfos.length != sourceBuffers.length) {
|
|
var error = new Error('Error initializing streams.');
|
|
error.type = 'storage';
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
for (var i = 0; i < initDatas.length; ++i) {
|
|
var initData = initDatas[i];
|
|
if (initData) {
|
|
sourceBuffers[i].appendBuffer(initData);
|
|
}
|
|
}
|
|
|
|
return Promise.resolve();
|
|
};
|
|
|
|
|
|
/**
|
|
* Event handler for sessionReady events.
|
|
* @param {Event} event A sessionReady event.
|
|
* @private
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.onSessionReady_ = function(event) {
|
|
var session = /** @type {MediaKeySession} */ (event.detail);
|
|
this.sessionIds_.push(session.sessionId);
|
|
};
|
|
|
|
|
|
/**
|
|
* Inserts a group of streams into the database.
|
|
* @param {!Array.<!shaka.media.StreamInfo>} selectedStreams The streams to
|
|
* insert.
|
|
* @param {shaka.player.DrmInfo} drmInfo
|
|
* @param {?number} duration The duration of the entire stream.
|
|
* @return {!Promise.<number>} The unique id assigned to the group.
|
|
* @private
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.insertGroup_ =
|
|
function(selectedStreams, drmInfo, duration) {
|
|
var contentDatabase =
|
|
new shaka.util.ContentDatabaseWriter(this.estimator, this);
|
|
if (this.config_['segmentRequestTimeout'] != null) {
|
|
contentDatabase.setSegmentRequestTimeout(
|
|
Number(this.config_['segmentRequestTimeout']));
|
|
}
|
|
|
|
// Insert the group of streams into the database and close the connection.
|
|
return contentDatabase.setUpDatabase().then(shaka.util.TypedBind(this,
|
|
function() {
|
|
return contentDatabase.insertGroup(
|
|
selectedStreams, this.sessionIds_, duration, drmInfo);
|
|
})
|
|
).then(
|
|
/** @param {number} groupId */
|
|
function(groupId) {
|
|
contentDatabase.closeDatabaseConnection();
|
|
return Promise.resolve(groupId);
|
|
}
|
|
).catch(
|
|
/** @param {*} e */
|
|
function(e) {
|
|
contentDatabase.closeDatabaseConnection();
|
|
return Promise.reject(e);
|
|
}
|
|
);
|
|
};
|
|
|
|
|
|
/** @override */
|
|
shaka.player.OfflineVideoSource.prototype.load = function() {
|
|
shaka.asserts.assert(this.groupId_ >= 0);
|
|
var contentDatabase = new shaka.util.ContentDatabaseReader();
|
|
var duration, keySystem;
|
|
|
|
return contentDatabase.setUpDatabase().then(shaka.util.TypedBind(this,
|
|
function() {
|
|
return contentDatabase.retrieveGroup(
|
|
/** @type {number} */(this.groupId_));
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
/** @param {shaka.util.ContentDatabase.GroupInformation} group */
|
|
function(group) {
|
|
var async = [];
|
|
this.sessionIds_ = group['session_ids'];
|
|
duration = group['duration'];
|
|
keySystem = group['key_system'];
|
|
for (var i = 0; i < group['stream_ids'].length; ++i) {
|
|
var streamId = group['stream_ids'][i];
|
|
async.push(contentDatabase.retrieveStreamIndex(streamId));
|
|
}
|
|
return Promise.all(async);
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
/** @param {!Array.<shaka.util.ContentDatabase.StreamIndex>} indexes */
|
|
function(indexes) {
|
|
this.manifestInfo =
|
|
this.reconstructManifestInfo_(indexes, duration, keySystem);
|
|
|
|
var baseClassLoad = shaka.player.StreamVideoSource.prototype.load;
|
|
return baseClassLoad.call(this);
|
|
})
|
|
).then(
|
|
function() {
|
|
contentDatabase.closeDatabaseConnection();
|
|
return Promise.resolve();
|
|
}
|
|
).catch(
|
|
/** @param {*} e */
|
|
function(e) {
|
|
contentDatabase.closeDatabaseConnection();
|
|
return Promise.reject(e);
|
|
}
|
|
);
|
|
};
|
|
|
|
|
|
/**
|
|
* Reconstructs a ManifestInfo object with data from storage.
|
|
* @param {!Array.<shaka.util.ContentDatabase.StreamIndex>} indexes The indexes
|
|
* of the streams in this manifest.
|
|
* @param {number} duration The max stream's entire duration in the group.
|
|
* @param {string} keySystem The group's DRM key system.
|
|
* @return {!shaka.media.ManifestInfo}
|
|
* @private
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.reconstructManifestInfo_ =
|
|
function(indexes, duration, keySystem) {
|
|
var manifestInfo = new shaka.media.ManifestInfo();
|
|
manifestInfo.minBufferTime = 5;
|
|
// TODO(story 1890046): Support multiple periods.
|
|
var periodInfo = new shaka.media.PeriodInfo();
|
|
|
|
for (var i = 0; i < indexes.length; ++i) {
|
|
var storedStreamInfo = indexes[i];
|
|
|
|
// Will only have one streamInfo per streamSetInfo stored.
|
|
var streamInfo = new shaka.media.StreamInfo();
|
|
|
|
var segmentIndexSource = new shaka.media.OfflineSegmentIndexSource(
|
|
storedStreamInfo['references']);
|
|
|
|
var initData = new Uint8Array(storedStreamInfo['init_segment']);
|
|
// TODO: Use idb:// URI directly instead of transforming into a data URI.
|
|
var segmentInitUrl =
|
|
new goog.Uri('data:application/octet-stream;base64,' +
|
|
shaka.util.Uint8ArrayUtils.toBase64(initData));
|
|
var segmentInitSource = new shaka.media.SegmentInitSource(
|
|
new shaka.util.FailoverUri(this.networkCallback_, [segmentInitUrl],
|
|
0, null));
|
|
|
|
streamInfo.segmentIndexSource = segmentIndexSource;
|
|
streamInfo.segmentInitSource = segmentInitSource;
|
|
streamInfo.mimeType = storedStreamInfo['mime_type'];
|
|
streamInfo.codecs = storedStreamInfo['codecs'];
|
|
|
|
var drmInfo = shaka.player.DrmInfo.createFromConfig(
|
|
{'keySystem': keySystem});
|
|
var streamSetInfo = new shaka.media.StreamSetInfo();
|
|
streamSetInfo.streamInfos.push(streamInfo);
|
|
streamSetInfo.drmInfos.push(drmInfo);
|
|
streamSetInfo.contentType = streamInfo.mimeType.split('/')[0];
|
|
periodInfo.streamSetInfos.push(streamSetInfo);
|
|
periodInfo.duration = duration;
|
|
}
|
|
manifestInfo.periodInfos.push(periodInfo);
|
|
return manifestInfo;
|
|
};
|
|
|
|
|
|
/**
|
|
* Deletes a group of streams from storage.
|
|
* @return {!Promise}
|
|
* @export
|
|
*/
|
|
shaka.player.OfflineVideoSource.prototype.deleteGroup = function() {
|
|
shaka.asserts.assert(this.groupId_ >= 0);
|
|
var contentDatabase = new shaka.util.ContentDatabaseWriter(null, null);
|
|
|
|
return contentDatabase.setUpDatabase().then(shaka.util.TypedBind(this,
|
|
function() {
|
|
return contentDatabase.deleteGroup(
|
|
/** @type {number} */(this.groupId_));
|
|
})
|
|
).then(
|
|
function() {
|
|
contentDatabase.closeDatabaseConnection();
|
|
return Promise.resolve();
|
|
}
|
|
).catch(
|
|
/** @param {*} e */
|
|
function(e) {
|
|
contentDatabase.closeDatabaseConnection();
|
|
return Promise.reject(e);
|
|
});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
shaka.player.OfflineVideoSource.prototype.getSessionIds = function() {
|
|
return this.sessionIds_;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
shaka.player.OfflineVideoSource.prototype.isOffline = function() {
|
|
return true;
|
|
};
|