/**
* @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.dash.ContentProtection');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.Uint8ArrayUtils');
goog.require('shaka.util.XmlUtils');
/**
* @namespace shaka.dash.ContentProtection
* @summary A set of functions for parsing and interpreting ContentProtection
* elements.
*/
/**
* @typedef {{
* defaultKeyId: ?string,
* defaultInit: Array.<shakaExtern.InitDataOverride>,
* drmInfos: !Array.<shakaExtern.DrmInfo>,
* firstRepresentation: boolean
* }}
*
* @description
* Contains information about the ContentProtection elements found at the
* AdaptationSet level.
*
* @property {?string} defaultKeyId
* The default key ID to use. This is used by parseKeyIds as a default. This
* can be null to indicate that there is no default.
* @property {Array.<shakaExtern.InitDataOverride>} defaultInit
* The default init data override. This can be null to indicate that there
* is no default.
* @property {!Array.<shakaExtern.DrmInfo>} drmInfos
* The DrmInfo objects.
* @property {boolean} firstRepresentation
* True when first parsed; changed to false after the first call to
* parseKeyIds. This is used to determine if a dummy key-system should be
* overwritten; namely that the first representation can replace the dummy
* from the AdaptationSet.
*/
shaka.dash.ContentProtection.Context;
/**
* @typedef {{
* node: !Element,
* schemeUri: string,
* keyId: ?string,
* init: Array.<shakaExtern.InitDataOverride>
* }}
*
* @description
* The parsed result of a single ContentProtection element.
*
* @property {!Element} node
* The ContentProtection XML element.
* @property {string} schemeUri
* The scheme URI.
* @property {?string} keyId
* The default key ID, if present.
* @property {Array.<shakaExtern.InitDataOverride>} init
* The init data, if present. If there is no init data, it will be null. If
* this is non-null, there is at least one element.
*/
shaka.dash.ContentProtection.Element;
/**
* A map of scheme URI to key system name.
*
* @const {!Object.<string, string>}
* @private
*/
shaka.dash.ContentProtection.defaultKeySystems_ = {
'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime'
};
/**
* @const {string}
* @private
*/
shaka.dash.ContentProtection.MP4Protection_ =
'urn:mpeg:dash:mp4protection:2011';
/**
* @const {string}
* @private
*/
shaka.dash.ContentProtection.CencNamespaceUri_ = 'urn:mpeg:cenc:2013';
/**
* Parses info from the ContentProtection elements at the AdaptationSet level.
*
* @param {!Array.<!Element>} elems
* @param {shakaExtern.DashContentProtectionCallback} callback
* @param {boolean} ignoreDrmInfo
* @return {shaka.dash.ContentProtection.Context}
*/
shaka.dash.ContentProtection.parseFromAdaptationSet = function(
elems, callback, ignoreDrmInfo) {
const ContentProtection = shaka.dash.ContentProtection;
const Functional = shaka.util.Functional;
const MapUtils = shaka.util.MapUtils;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
let parsed = ContentProtection.parseElements_(elems);
/** @type {Array.<shakaExtern.InitDataOverride>} */
let defaultInit = null;
/** @type {!Array.<shakaExtern.DrmInfo>} */
let drmInfos = [];
let parsedNonCenc = [];
// Get the default key ID; if there are multiple, they must all match.
let keyIds = parsed.map(function(elem) { return elem.keyId; })
.filter(Functional.isNotNull);
if (keyIds.length) {
if (keyIds.filter(Functional.isNotDuplicate).length > 1) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS);
}
}
if (!ignoreDrmInfo) {
// Find the default key ID and init data. Create a new array of all the
// non-CENC elements.
parsedNonCenc = parsed.filter(function(elem) {
if (elem.schemeUri == ContentProtection.MP4Protection_) {
goog.asserts.assert(!elem.init || elem.init.length,
'Init data must be null or non-empty.');
defaultInit = elem.init || defaultInit;
return false;
} else {
return true;
}
});
if (parsedNonCenc.length) {
drmInfos = ContentProtection.convertElements_(
defaultInit, callback, parsedNonCenc);
// If there are no drmInfos after parsing, then add a dummy entry.
// This may be removed in parseKeyIds.
if (drmInfos.length == 0) {
drmInfos = [ManifestParserUtils.createDrmInfo('', defaultInit)];
}
}
}
// If there are only CENC element(s) or ignoreDrmInfo flag is set, assume all
// key-systems are supported.
if (parsed.length && (ignoreDrmInfo || !parsedNonCenc.length)) {
let keySystems = ContentProtection.defaultKeySystems_;
drmInfos =
MapUtils.values(keySystems)
.filter(function(keySystem) {
// If the manifest doesn't specify any key systems, we shouldn't
// put clearkey in this list. Otherwise, it may be triggered when
// a real key system should be used instead.
return keySystem != 'org.w3.clearkey';
})
.map(function(keySystem) {
return ManifestParserUtils.createDrmInfo(keySystem, defaultInit);
});
}
/** @type {?string} */
let defaultKeyId = keyIds[0] || null;
// Attach the default keyId, if it exists, to every initData.
if (defaultKeyId) {
drmInfos.forEach(function(drmInfo) {
drmInfo.initData.forEach(function(initData) {
initData.keyId = defaultKeyId;
});
});
}
return {
defaultKeyId: defaultKeyId,
defaultInit: defaultInit,
drmInfos: drmInfos,
firstRepresentation: true
};
};
/**
* Parses the given ContentProtection elements found at the Representation
* level. This may update the |context|.
*
* @param {!Array.<!Element>} elems
* @param {shakaExtern.DashContentProtectionCallback} callback
* @param {shaka.dash.ContentProtection.Context} context
* @param {boolean} ignoreDrmInfo
* @return {?string} The parsed key ID
*/
shaka.dash.ContentProtection.parseFromRepresentation = function(
elems, callback, context, ignoreDrmInfo) {
const ContentProtection = shaka.dash.ContentProtection;
let repContext = ContentProtection.parseFromAdaptationSet(
elems, callback, ignoreDrmInfo);
if (context.firstRepresentation) {
let asUnknown = context.drmInfos.length == 1 &&
!context.drmInfos[0].keySystem;
let asUnencrypted = context.drmInfos.length == 0;
let repUnencrypted = repContext.drmInfos.length == 0;
// There are two cases where we need to replace the |drmInfos| in the
// context with those in the Representation:
// 1. The AdaptationSet does not list any ContentProtection.
// 2. The AdaptationSet only lists unknown key-systems.
if (asUnencrypted || (asUnknown && !repUnencrypted)) {
context.drmInfos = repContext.drmInfos;
}
context.firstRepresentation = false;
} else if (repContext.drmInfos.length > 0) {
// If this is not the first Representation, then we need to remove entries
// from the context that do not appear in this Representation.
context.drmInfos = context.drmInfos.filter(function(asInfo) {
return repContext.drmInfos.some(function(repInfo) {
return repInfo.keySystem == asInfo.keySystem;
});
});
// If we have filtered out all key-systems, throw an error.
if (context.drmInfos.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM);
}
}
return repContext.defaultKeyId || context.defaultKeyId;
};
/**
* Creates DrmInfo objects from the given element.
*
* @param {Array.<shakaExtern.InitDataOverride>} defaultInit
* @param {shakaExtern.DashContentProtectionCallback} callback
* @param {!Array.<shaka.dash.ContentProtection.Element>} elements
* @return {!Array.<shakaExtern.DrmInfo>}
* @private
*/
shaka.dash.ContentProtection.convertElements_ = function(
defaultInit, callback, elements) {
const Functional = shaka.util.Functional;
return elements.map(
/**
* @param {shaka.dash.ContentProtection.Element} element
* @return {!Array.<shakaExtern.DrmInfo>}
*/
function(element) {
const ManifestParserUtils = shaka.util.ManifestParserUtils;
const ContentProtection = shaka.dash.ContentProtection;
let keySystem = ContentProtection.defaultKeySystems_[element.schemeUri];
if (keySystem) {
goog.asserts.assert(!element.init || element.init.length,
'Init data must be null or non-empty.');
let initData = element.init || defaultInit;
return [ManifestParserUtils.createDrmInfo(keySystem, initData)];
} else {
goog.asserts.assert(
callback, 'ContentProtection callback is required');
return callback(element.node) || [];
}
}).reduce(Functional.collapseArrays, []);
};
/**
* Parses the given ContentProtection elements. If there is an error, it
* removes those elements.
*
* @param {!Array.<!Element>} elems
* @return {!Array.<shaka.dash.ContentProtection.Element>}
* @private
*/
shaka.dash.ContentProtection.parseElements_ = function(elems) {
const Functional = shaka.util.Functional;
return elems.map(
/**
* @param {!Element} elem
* @return {?shaka.dash.ContentProtection.Element}
*/
function(elem) {
const NS = shaka.dash.ContentProtection.CencNamespaceUri_;
/** @type {?string} */
let schemeUri = elem.getAttribute('schemeIdUri');
/** @type {?string} */
let keyId = shaka.util.XmlUtils.getAttributeNS(elem, NS, 'default_KID');
/** @type {!Array.<string>} */
let psshs = shaka.util.XmlUtils.findChildrenNS(elem, NS, 'pssh')
.map(shaka.util.XmlUtils.getContents);
if (!schemeUri) {
shaka.log.error('Missing required schemeIdUri attribute on',
'ContentProtection element', elem);
return null;
}
schemeUri = schemeUri.toLowerCase();
if (keyId) {
keyId = keyId.replace(/-/g, '').toLowerCase();
if (keyId.indexOf(' ') >= 0) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED);
}
}
/** @type {!Array.<shakaExtern.InitDataOverride>} */
let init = [];
try {
init = psshs.map(function(pssh) {
/** @type {shakaExtern.InitDataOverride} */
let ret = {
initDataType: 'cenc',
initData: shaka.util.Uint8ArrayUtils.fromBase64(pssh),
keyId: null
};
return ret;
});
} catch (e) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING);
}
/** @type {shaka.dash.ContentProtection.Element} */
let element = {
node: elem,
schemeUri: schemeUri,
keyId: keyId,
init: (init.length > 0 ? init : null)
};
return element;
}).filter(Functional.isNotNull);
};