/**
* @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.DrmInfo');
goog.require('shaka.asserts');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* Creates a DrmInfo, which represents a set of DRM configuration options.
* DrmInfo is an internal class, please see
* {@link shaka.player.DashVideoSource.ContentProtectionCallback},
* {@link shaka.player.HttpVideoSource}, and
* {@link shaka.player.DrmInfo.Config} for information on how to configure the
* key system from the application.
*
* @constructor
* @struct
* @see {shaka.player.DashVideoSource.ContentProtectionCallback}
* @see {shaka.player.DrmInfo.Config}
* @exportDoc
*/
shaka.player.DrmInfo = function() {
/**
* An empty string indicates no key system.
* @type {string}
*/
this.keySystem = '';
/** @type {string} */
this.licenseServerUrl = '';
/** @type {boolean} */
this.withCredentials = false;
/** @type {?shaka.player.DrmInfo.LicensePostProcessor} */
this.licensePostProcessor = null;
/** @type {?shaka.player.DrmInfo.LicensePreProcessor} */
this.licensePreProcessor = null;
/** @type {boolean} */
this.distinctiveIdentifierRequired = false;
/** @type {boolean} */
this.persistentStateRequired = false;
/** @type {string} */
this.audioRobustness = '';
/** @type {string} */
this.videoRobustness = '';
/** @type {Uint8Array} */
this.serverCertificate = null;
/** @type {!Array.<shaka.player.DrmInfo.InitData>} */
this.initDatas = [];
};
/**
* @typedef {{initData: !Uint8Array, initDataType: string}}
* @exportDoc
*/
shaka.player.DrmInfo.InitData;
/**
* <p>
* An object which represents a set of application provided DRM configuration
* options.
* </p>
*
* <p>
* For encrypted content, an application must provide one or more DrmInfo.Config
* objects to its VideoSource. For DASH content, the application can provide
* Config objects to {@link shaka.player.DashVideoSource} via a callback
* (see {@link shaka.player.DashVideoSource.ContentProtectionCallback}),
* and for HTTP content, the application can provide a single Config object to
* {@link shaka.player.HttpVideoSource} via HttpVideoSource's constructor.
* </p>
*
* The following options are supported:
* <ul>
* <li>
* <b>keySystem</b>: string (required) <br>
* The key system, e.g., "com.widevine.alpha".
* A blank string indicates unencrypted content.
*
* <li>
* <b>licenseServerUrl</b>: string (required for streaming encrypted content)
* <br>
* The license server URL.
*
* <li>
* <b>withCredentials:</b> boolean <br>
* True if license requests should include cookies when sent cross-domain
* (see {@link http://goo.gl/pzY9F7}). Defaults to false.
*
* <li>
* <b>licensePostProcessor:</b> shaka.player.DrmInfo.LicensePostProcessor <br>
* A license post-processor that does application-specific post-processing on
* license responses.
*
* <li>
* <b>licensePreProcessor:</b> shaka.player.DrmInfo.LicensePreProcessor <br>
* A license pre-processor that does application-specific pre-processing on
* license requests.
*
* <li>
* <b>distinctiveIdentifierRequired:</b> boolean <br>
* True if the application requires the key system to support distinctive
* identifiers. Defaults to false.
*
* <li>
* <b>persistentStateRequired:</b> boolean <br>
* True if the application requires the key system to support persistent
* state, e.g., for persistent license storage. Defaults to false.
*
* <li>
* <b>audioRobustness:</b> string <br>
* A key system specific string that specifies an audio decrypt/decode
* security level.
*
* <li>
* <b>videoRobustness:</b> string <br>
* A key system specific string that specifies a video decrypt/decode
* security level.
*
* <li>
* <b>serverCertificate:</b> Uint8Array <br>
* An key system specific server certificate for authenticating license
* requests.
*
* <li>
* <b>initData:</b> shaka.player.DrmInfo.InitData <br>
* Explicit key system initialization data (initData value), which overrides
* both the initData given in the manifest, if any, and the initData in the
* actual content (which may be inspected via an EME 'encrypted' event). The
* initDataType values and the formats that they correspond to are specified
* {@link http://goo.gl/hKBdff here}.
* </ul>
*
* @typedef {Object.<string, *>}
* @exportDoc
*/
shaka.player.DrmInfo.Config;
/**
* <p>
* A callback which does application-specific post-processing on license
* responses before they are passed to the key system. The application can
* set this callback in a {@link shaka.player.DrmInfo.Config} object.
* </p>
*
* <p>
* The parameter is the license response from the license server.
* Must return the raw license.
* </p>
*
* @example
* // Suppose the license server provides a JSON encoded license response
* // with the format {"header": header_string, "license": license_string}.
* // The application would need to use a license post-processor like
* // the following:
* var postProcessor = function(serverResponse) {
* // serverResponse is a Uint8Array, so decode it into an object.
* var json = String.fromCharCode.apply(null, serverResponse);
* var obj = JSON.parse(json);
*
* var headerString = obj['header'];
* // Do something with the header...
*
* // obj['license'] is a string, so encode it into a Uint8Array.
* var licenseString = obj['license'];
* var license = new Uint8Array(licenseString.split('').map(
* function(ch) { return ch.charCodeAt(0); }));
* return license;
* };
*
* @typedef {function(!Uint8Array):!Uint8Array}
* @exportDoc
*/
shaka.player.DrmInfo.LicensePostProcessor;
/**
* An object that describes a license request for license request
* pre-processing (see {@link shaka.player.DrmInfo.LicensePreProcessor}).
* <br>
*
* The following options are supported:
* <ul>
* <li>
* <b>url</b>: string (required) <br>
* The license server URL.
*
* <li>
* <b>body</b>: (ArrayBuffer|?string) <br>
* The license request's body.
*
* <li>
* <b>method</b>: string (required) <br>
* The HTTP request method, which must be either 'GET' or 'POST'.
*
* <li>
* <b>headers</b>: Object.<string, string> <br>
* Extra HTTP request headers as key-value pairs.
* </ul>
*
* @typedef {Object.<string, *>}
* @exportDoc
*/
shaka.player.DrmInfo.LicenseRequestInfo;
/**
* <p>
* A callback which does application-specific pre-processing on license
* requests before they are sent to the license server. The application can
* set this callback in a {@link shaka.player.DrmInfo.Config} object.
* </p>
*
* <p>
* The parameter is a {@link shaka.player.DrmInfo.LicenseRequestInfo}
* object. The callback may modify the object's fields as it requires; some
* fields are set to initial values:
* <ul>
* <li>
* The |url| field is initially set to the license server URL provided by the
* license request pre-processor's corresponding DrmInfo; it may be set
* to an arbitrary URL, or may be extended with extra query parameters.
*
* <li>
* The |body| field is initially set to the raw license request (an
* ArrayBuffer) emitted by the browser; it may be replaced (to another
* ArrayBuffer or string) or be removed entirely (e.g., if the server expects
* the raw license to be encoded in the URL).
*
* <li>
* The |method| field is initially set to 'POST', but may be set to 'GET'.
*
* <li>
* The |headers| field is initially an empty map; arbitrary request headers
* may be added as key-value pairs, e.g.,
* headers['Content-Type'] = 'application/x-www-form-urlencoded';
* </ul>
* </p>
*
* @example
* // Suppose the license server expects a license request to use a
* // base64 encoded payload and include special query parameters.
* // The application would need to use a license pre-processor like
* // the following:
* var preProcessor = function(requestInfo) {
* // Add query parameters.
* requestInfo.url += '?session=123&token=abc'
* // Encode the payload as base64.
* requestInfo.body = window.btoa(
* String.fromCharCode.apply(null, new Uint8Array(requestInfo.body)));
* };
* @typedef {function(!shaka.player.DrmInfo.LicenseRequestInfo)}
* @exportDoc
*/
shaka.player.DrmInfo.LicensePreProcessor;
/**
* Creates a DrmInfo object from a Config object.
*
* @param {shaka.player.DrmInfo.Config} config
* @throws TypeError if a configuration option has the wrong type.
* @throws Error if the application fails to provide any required fields.
* @return {!shaka.player.DrmInfo}
*/
shaka.player.DrmInfo.createFromConfig = function(config) {
var drmInfo = new shaka.player.DrmInfo();
if (!config) return drmInfo;
// Alias.
var MapUtils = shaka.util.MapUtils;
var keySystem = MapUtils.getString(config, 'keySystem');
if (keySystem != null) {
drmInfo.keySystem = keySystem;
} else {
throw new Error('\'keySystem\' cannot be null.');
}
var licenseServerUrl = MapUtils.getString(config, 'licenseServerUrl');
if (licenseServerUrl != null) {
drmInfo.licenseServerUrl = licenseServerUrl;
} else if (keySystem) {
throw new Error('For encrypted streaming content, \'licenseServerUrl\' ' +
'cannot be null or empty.');
}
var withCredentials = MapUtils.getBoolean(config, 'withCredentials');
if (withCredentials != null) {
drmInfo.withCredentials = withCredentials;
}
var licensePostProcessor = MapUtils.getAsInstanceType(
config, 'licensePostProcessor', Function);
if (licensePostProcessor != null) {
drmInfo.licensePostProcessor = licensePostProcessor;
}
var licensePreProcessor = MapUtils.getAsInstanceType(
config, 'licensePreProcessor', Function);
if (licensePreProcessor != null) {
drmInfo.licensePreProcessor = licensePreProcessor;
}
var distinctiveIdentifierRequired = MapUtils.getBoolean(
config, 'distinctiveIdentifierRequired');
if (distinctiveIdentifierRequired != null) {
drmInfo.distinctiveIdentifierRequired = distinctiveIdentifierRequired;
}
var persistentStateRequired = MapUtils.getBoolean(
config, 'persistentStateRequired');
if (persistentStateRequired != null) {
drmInfo.persistentStateRequired = persistentStateRequired;
}
var audioRobustness = MapUtils.getString(config, 'audioRobustness');
if (audioRobustness != null) {
drmInfo.audioRobustness = audioRobustness;
}
var videoRobustness = MapUtils.getString(config, 'videoRobustness');
if (videoRobustness != null) {
drmInfo.videoRobustness = videoRobustness;
}
var serverCertificate = MapUtils.getAsInstanceType(
config, 'serverCertificate', Uint8Array);
if (serverCertificate != null) {
drmInfo.serverCertificate = serverCertificate;
}
var initData = MapUtils.getAsInstanceType(config, 'initData', Object);
if (initData) {
var data = MapUtils.getAsInstanceType(initData, 'initData', Uint8Array);
if (data == null) {
throw new Error('\'initData.initData\' cannot be null.');
}
var dataType = MapUtils.getString(initData, 'initDataType');
if (dataType == null) {
throw new Error('\'initData.initDataType\' cannot be null.');
}
drmInfo.initDatas.push({
'initData': new Uint8Array(data.buffer),
'initDataType': dataType
});
}
return drmInfo;
};
/**
* Combines this DrmInfo object with another DrmInfo object to create a new
* DrmInfo object.
*
* @param {!shaka.player.DrmInfo} other The other DrmInfo object, which should
* be compatible with this DrmInfo object.
* @return {!shaka.player.DrmInfo}
*/
shaka.player.DrmInfo.prototype.combine = function(other) {
if (!COMPILED && !this.isCompatible(other)) {
shaka.log.warning(
'combine() should only be called with a compatible DrmInfo object:',
'this', this,
'other', other);
}
var drmInfo = new shaka.player.DrmInfo();
drmInfo.keySystem = this.keySystem;
drmInfo.licenseServerUrl = this.licenseServerUrl;
drmInfo.withCredentials = this.withCredentials;
drmInfo.licensePostProcessor = this.licensePostProcessor;
drmInfo.licensePreProcessor = this.licensePreProcessor;
drmInfo.distinctiveIdentifierRequired = this.distinctiveIdentifierRequired;
drmInfo.persistentStateRequired = this.persistentStateRequired;
drmInfo.audioRobustness = this.audioRobustness;
drmInfo.videoRobustness = this.videoRobustness;
drmInfo.serverCertificate =
this.serverCertificate ?
new Uint8Array(this.serverCertificate.buffer) :
null;
drmInfo.addInitDatas(this.initDatas);
drmInfo.addInitDatas(other.initDatas);
return drmInfo;
};
/**
* @param {!shaka.player.DrmInfo} other
* @return {boolean} True if this DrmInfo is compatible with |other|;
* otherwise, return false.
*/
shaka.player.DrmInfo.prototype.isCompatible = function(other) {
// NOTE: We can't check pre/post-processors, since this causes failures with
// bound functions.
return (this.keySystem == other.keySystem) &&
(this.licenseServerUrl == other.licenseServerUrl) &&
(this.withCredentials == other.withCredentials) &&
(this.distinctiveIdentifierRequired ==
other.distinctiveIdentifierRequired) &&
(this.persistentStateRequired == other.persistentStateRequired) &&
(this.audioRobustness == other.audioRobustness) &&
(this.videoRobustness == other.videoRobustness) &&
(shaka.util.Uint8ArrayUtils.equal(this.serverCertificate,
other.serverCertificate));
};
/**
* Adds the given initDatas (removing duplicates).
*
* @param {!Array.<shaka.player.DrmInfo.InitData>} otherInitDatas
*/
shaka.player.DrmInfo.prototype.addInitDatas = function(
otherInitDatas) {
var unfilteredInitDatas =
this.initDatas.concat(otherInitDatas.map(
function(initData) {
return {
'initData': new Uint8Array(initData.initData.buffer),
'initDataType': initData.initDataType
};
}));
this.initDatas = shaka.util.ArrayUtils.removeDuplicates(
unfilteredInitDatas, shaka.player.DrmInfo.compareInitDatas_);
};
/**
* @param {!shaka.player.DrmInfo.InitData} initDataA
* @param {!shaka.player.DrmInfo.InitData} initDataB
* @return {boolean}
* @private
*/
shaka.player.DrmInfo.compareInitDatas_ =
function(initDataA, initDataB) {
return initDataA.initDataType == initDataB.initDataType &&
shaka.util.Uint8ArrayUtils.equal(initDataA.initData,
initDataB.initData);
};