Files
shaka-player/lib/player.js
T
Joey Parrish 813b746160 DrmEngine and associated tests
This is mostly complete, but needs additional integration with an
as-yet-unwritten AbrManager.

Change-Id: I3836040c6891fb774be800b53679f49e365c7e1c
2016-02-12 16:06:31 -08:00

440 lines
13 KiB
JavaScript

/**
* @license
* 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.
*/
goog.provide('shaka.Player');
goog.require('goog.Uri');
goog.require('shaka.asserts');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.MediaSourceEngine');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.IDestroyable');
/**
* @constructor
* @struct
* @implements {shaka.util.IDestroyable}
* @param {!HTMLMediaElement} video
* @export
*/
shaka.Player = function(video) {
/** @private {HTMLMediaElement} */
this.video_ = video;
/** @private {shaka.net.NetworkingEngine} */
this.networkingEngine_ = new shaka.net.NetworkingEngine();
/** @private {shaka.media.DrmEngine} */
this.drmEngine_ = new shaka.media.DrmEngine(
this.networkingEngine_,
this.onError_.bind(this));
/** @private {shaka.media.ManifestParser} */
this.parser_ = null;
/** @private {?shakaExtern.Manifest} */
this.manifest_ = null;
/** @private {?shakaExtern.PlayerConfiguration} */
this.config_ = this.defaultConfig_();
this.drmEngine_.configure(this.config_.drm);
};
/**
* @override
*/
shaka.Player.prototype.destroy = function() {
var p = Promise.all([
this.parser_ ? this.parser_.stop() : null,
this.drmEngine_.destroy(),
this.networkingEngine_.destroy()
]);
this.video_ = null;
this.networkingEngine_ = null;
this.drmEngine_ = null;
this.parser_ = null;
this.manifest_ = null;
this.config_ = null;
return p;
};
/**
* @define {string} A version number taken from git at compile time.
*/
goog.define('GIT_VERSION', 'v1.9.9-alpha-debug');
/**
* @const {string}
* @export
*/
shaka.Player.version = GIT_VERSION;
/**
* @return {!Promise.<!shakaExtern.SupportType>}
* @export
*/
shaka.Player.support = function() {
// Basic features needed for the library to be usable.
var basic = !!window.Promise && !!window.Uint8Array &&
!!Array.prototype.forEach;
if (basic) {
var manifest = shaka.media.ManifestParser.support();
var media = shaka.media.MediaSourceEngine.support();
return shaka.media.DrmEngine.support().then(function(drm) {
/** @type {!shakaExtern.SupportType} */
var support = {
manifest: manifest,
media: media,
drm: drm,
supported: manifest['basic'] && media['basic'] && drm['basic']
};
return support;
});
} else {
// Return something Promise-like so that the application can still check
// for support.
return /** @type {!Promise.<!shakaExtern.SupportType>} */({
'then': function(fn) {
fn({'supported': false});
}
});
}
};
/**
* @param {string} manifestUri
* @param {number=} opt_startTime
* @param {shaka.media.ManifestParser.Factory=} opt_manifestParserFactory
* @return {!Promise} Resolved when playback can begin.
* @export
*/
shaka.Player.prototype.load = function(manifestUri, opt_startTime,
opt_manifestParserFactory) {
var factory = opt_manifestParserFactory;
var p = Promise.resolve();
var extension;
if (!factory) {
// Try to choose a manifest parser by file extension.
var uriObj = new goog.Uri(manifestUri);
var uriPieces = uriObj.getPath().split('/');
var uriFilename = uriPieces.pop();
var filenamePieces = uriFilename.split('.');
// Only one piece means there is no extension.
if (filenamePieces.length > 1) {
extension = filenamePieces.pop().toLowerCase();
factory = shaka.media.ManifestParser.parsersByExtension[extension];
}
}
if (!factory) {
// Try to choose a manifest parser by MIME type.
var headRequest = shaka.net.NetworkingEngine.makeRequest(
[manifestUri], this.config_.manifest.retryParameters);
headRequest.method = 'HEAD';
var type = shaka.net.NetworkingEngine.RequestType.MANIFEST;
p = this.networkingEngine_.request(type, headRequest).then(
function(response) {
var mimeType = response.headers['content-type'];
// https://goo.gl/yzKDRx says this header should always be available,
// but just to be safe:
if (mimeType) {
mimeType = mimeType.toLowerCase();
}
factory = shaka.media.ManifestParser.parsersByMime[mimeType];
if (!factory) {
shaka.log.error(
'Unable to guess manifest type by file extension ' +
'or by MIME type.', extension, mimeType);
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.UNABLE_TO_GUESS_MANIFEST_TYPE,
manifestUri));
}
}, function(error) {
shaka.log.error('HEAD request to guess manifest type failed!', error);
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.UNABLE_TO_GUESS_MANIFEST_TYPE,
manifestUri));
});
}
var parser;
return p.then(function() {
shaka.asserts.assert(factory, 'Manifest factory should be set!');
shaka.asserts.assert(this.networkingEngine_,
'Networking engine should be set!');
var networkingEngine = /** @type {!shaka.net.NetworkingEngine} */(
this.networkingEngine_);
parser = new factory(networkingEngine,
this.filterPeriod_.bind(this),
this.onError_.bind(this));
parser.configure(this.config_.manifest);
return parser.start(manifestUri);
}.bind(this)).then(function(manifest) {
this.parser_ = parser;
this.manifest_ = manifest;
return this.drmEngine_.init(manifest, false /* offline */);
}.bind(this)).then(function() {
// Re-filter after DRM has been initialized.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));
// Attach to video.
return this.drmEngine_.attach(this.video_);
}.bind(this)).then(function() {
// TODO: validate manifest (all streams removed? etc.)
// TODO: StreamingEngine
}.bind(this));
};
/**
* @param {shakaExtern.PlayerConfiguration} config
* @export
*/
shaka.Player.prototype.configure = function(config) {
shaka.asserts.assert(this.config_, 'Config must not be null!');
this.mergeConfigObjects_(/** @type {!Object} */(this.config_), config,
this.defaultConfig_(), '');
if (this.parser_) {
this.parser_.configure(this.config_.manifest);
}
this.drmEngine_.configure(this.config_.drm);
};
/**
* @return {shakaExtern.PlayerConfiguration}
* @export
*/
shaka.Player.prototype.getConfiguration = function() {
return /** @type {shakaExtern.PlayerConfiguration} */(
this.cloneObject_(/** @type {!Object} */(this.config_)));
};
// TODO: consider moving config-parsing to another file.
/**
* @param {!Object} destination
* @param {!Object} source
* @param {!Object} template supplies default values
* @param {string} path to this part of the config
* @private
*/
shaka.Player.prototype.mergeConfigObjects_ =
function(destination, source, template, path) {
/**
* @type {boolean}
* If true, don't validate the keys in the next level.
*/
var ignoreKeys = !!({
'.drm.servers': true,
'.drm.clearKeys': true,
'.drm.advanced': true
})[path];
/**
* @type {string}
* If present, require this specific type instead of following the template.
*/
var requiredType = ({
'.drm.servers': 'string',
'.drm.clearKeys': 'string'
})[path] || '';
/**
* @type {Object}
* If present, use this object as the template for the next level.
*/
var overrideSubTemplate = ({
'.drm.advanced': this.defaultAdvancedDrmConfig_()
})[path];
shaka.asserts.assert(destination, 'Destination config must not be null!');
for (var k in source) {
var subPath = path + '.' + k;
var subTemplate = template[k];
if (overrideSubTemplate) {
subTemplate = overrideSubTemplate;
}
// The order of these checks is important.
if (!ignoreKeys && !(k in destination)) {
shaka.log.error('Invalid config, unrecognized key ' + subPath);
} else if (source[k] === undefined) {
// An explicit 'undefined' value causes the key to be deleted from the
// destination config and replaced with a default from the template if
// possible.
if (subTemplate === undefined) {
delete destination[k];
} else {
destination[k] = subTemplate;
}
} else if (typeof destination[k] == 'object' &&
typeof source[k] == 'object') {
this.mergeConfigObjects_(destination[k], source[k], subTemplate, subPath);
} else if (!ignoreKeys && (typeof source[k] != typeof destination[k])) {
shaka.log.error('Invalid config, wrong type for ' + subPath);
} else if (requiredType && (typeof source[k] != requiredType)) {
shaka.log.error('Invalid config, wrong type for ' + subPath);
} else if (typeof destination[k] == 'function' &&
destination[k].length != source[k].length) {
shaka.log.error('Invalid config, wrong number of arguments for ' +
subPath);
} else {
destination[k] = source[k];
}
}
};
/**
* @param {!Object} source
* @return {!Object}
* @private
*/
shaka.Player.prototype.cloneObject_ = function(source) {
var destination = {};
for (var k in source) {
if (typeof source[k] == 'object' && source[k] !== null) {
destination[k] = this.cloneObject_(source[k]);
} else {
destination[k] = source[k];
}
}
return destination;
};
/**
* @return {shakaExtern.PlayerConfiguration}
* @private
*/
shaka.Player.prototype.defaultConfig_ = function() {
return {
drm: {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
// These will all be verified by special cases in mergeConfigObjects_():
servers: {}, // key is arbitrary key system ID, value must be string
clearKeys: {}, // key is arbitrary key system ID, value must be string
advanced: {} // key is arbitrary key system ID, value is a record type
},
manifest: {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
dash: {
customScheme: function(node) { return null; }
}
},
streaming: {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
rebufferingGoal: 15,
bufferingGoal: 30,
byteLimit: 300 << 20 // 300MB
}
};
};
/**
* @return {shakaExtern.AdvancedDrmConfiguration}
* @private
*/
shaka.Player.prototype.defaultAdvancedDrmConfig_ = function() {
return {
distinctiveIdentifierRequired: false,
persistentStateRequired: false,
videoRobustness: '',
audioRobustness: '',
serverCertificate: null
};
};
/**
* @param {shakaExtern.Period} period
* @private
*/
shaka.Player.prototype.filterPeriod_ = function(period) {
for (var i = 0; i < period.streamSets.length; ++i) {
var streamSet = period.streamSets[i];
var keySystem = this.drmEngine_.keySystem();
if (this.drmEngine_.initialized() && keySystem) {
// A key system has been selected.
// Remove streamSets which can only be used with other key systems.
// Note that drmInfos == [] means unencrypted.
var match = streamSet.drmInfos.length == 0 ||
streamSet.drmInfos.some(function(drmInfo) {
return drmInfo.keySystem == keySystem; });
if (!match) {
shaka.log.debug('Dropping StreamSet, can\'t be used with ' + keySystem,
streamSet);
period.streamSets.splice(i, 1);
--i;
continue;
}
}
for (var j = 0; j < streamSet.streams.length; ++j) {
var stream = streamSet.streams[j];
var fullMimeType = stream.mimeType;
if (stream.codecs) {
fullMimeType += '; codecs="' + stream.codecs + '"';
}
if (!shaka.media.MediaSourceEngine.isTypeSupported(fullMimeType)) {
streamSet.streams.splice(j, 1);
--j;
continue;
}
}
if (streamSet.streams.length == 0) {
period.streamSets.splice(i, 1);
--i;
}
}
};
/**
* @param {!shaka.util.Error} error
* @private
*/
shaka.Player.prototype.onError_ = function(error) {
// TODO
};