UNPKG

shaka-player

Version:
360 lines (323 loc) 12.1 kB
/** * @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'; /** * 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) { var ContentProtection = shaka.dash.ContentProtection; var Functional = shaka.util.Functional; var MapUtils = shaka.util.MapUtils; var ManifestParserUtils = shaka.util.ManifestParserUtils; var parsed = ContentProtection.parseElements_(elems); /** @type {Array.<shakaExtern.InitDataOverride>} */ var defaultInit = null; /** @type {!Array.<shakaExtern.DrmInfo>} */ var drmInfos = []; var parsedNonCenc = []; // Get the default key ID; if there are multiple, they must all match. var 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)) { var keySystems = ContentProtection.defaultKeySystems_; drmInfos = MapUtils.values(keySystems) .map(function(keySystem) { return ManifestParserUtils.createDrmInfo(keySystem, defaultInit); }); } /** @type {?string} */ var 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) { var ContentProtection = shaka.dash.ContentProtection; var repContext = ContentProtection.parseFromAdaptationSet( elems, callback, ignoreDrmInfo); if (context.firstRepresentation) { var asUnknown = context.drmInfos.length == 1 && !context.drmInfos[0].keySystem; var asUnencrypted = context.drmInfos.length == 0; var repUnencrypted = repContext.drmInfos.length == 0; // There are two cases when we need to replace the |drmInfos| in the context // with those in the Representation: // * The AdaptationSet does not list any ContentProtection. // * 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) { var Functional = shaka.util.Functional; return elements.map( /** * @param {shaka.dash.ContentProtection.Element} element * @return {!Array.<shakaExtern.DrmInfo>} */ function(element) { var ManifestParserUtils = shaka.util.ManifestParserUtils; var ContentProtection = shaka.dash.ContentProtection; var keySystem = ContentProtection.defaultKeySystems_[element.schemeUri]; if (keySystem) { goog.asserts.assert(!element.init || element.init.length, 'Init data must be null or non-empty.'); var 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) { var Functional = shaka.util.Functional; return elems.map( /** * @param {!Element} elem * @return {?shaka.dash.ContentProtection.Element} */ function(elem) { /** @type {?string} */ var schemeUri = elem.getAttribute('schemeIdUri'); /** @type {?string} */ var keyId = elem.getAttribute('cenc:default_KID'); /** @type {!Array.<string>} */ var psshs = shaka.util.XmlUtils.findChildren(elem, 'cenc: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>} */ var init = []; try { init = psshs.map(function(pssh) { /** @type {shakaExtern.InitDataOverride} */ var 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} */ var element = { node: elem, schemeUri: schemeUri, keyId: keyId, init: (init.length > 0 ? init : null) }; return element; }).filter(Functional.isNotNull); };