From b83b4120f46ae94e3ce194f43b13517b7a736f07 Mon Sep 17 00:00:00 2001 From: sr90 Date: Wed, 13 Apr 2022 08:34:39 -0700 Subject: [PATCH] feat(dash): Construct ClearKey PSSH based on MPD ContentProtection (#4104) This PR parses default_KID from ContentProtection in the manifest and constructs a PSSH box from it to feed to the ClearKey CDM only if clear key content is detected. Example: ``` https://drm-clearkey-testvectors.axtest.net/AcquireLicense ``` PSSH is based on https://www.w3.org/TR/eme-initdata-cenc/ Tested content: https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p_ClearKey.mpd --- lib/dash/content_protection.js | 58 +++++++++++++++++++++++++++++++--- lib/hls/hls_parser.js | 5 ++- lib/util/pssh.js | 27 +++++++++++++--- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/lib/dash/content_protection.js b/lib/dash/content_protection.js index 8e3fe75b39..721362ab1a 100644 --- a/lib/dash/content_protection.js +++ b/lib/dash/content_protection.js @@ -68,7 +68,7 @@ shaka.dash.ContentProtection = class { if (parsedNonCenc.length) { drmInfos = ContentProtection.convertElements_( - defaultInit, parsedNonCenc, keySystemsByURI); + defaultInit, parsedNonCenc, keySystemsByURI, keyIds); // If there are no drmInfos after parsing, then add a dummy entry. // This may be removed in parseKeyIds. @@ -356,7 +356,41 @@ shaka.dash.ContentProtection = class { 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95, ]); - const pssh = shaka.util.Pssh.createPssh(data, systemId); + const keyIds = new Set(); + const psshVersion = 0; + const pssh = + shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion); + return [ + { + initData: pssh, + initDataType: 'cenc', + keyId: element.keyId, + }, + ]; + } + + /** + * Creates ClearKey initData from Default_KID value retrieved from previously + * parsed ContentProtection tag. + * @param {shaka.dash.ContentProtection.Element} element + * @param {!Set.} keyIds + * @return {?Array.} + * @private + */ + static getInitDataClearKey_(element, keyIds) { + if (keyIds.size == 0) { + return null; + } + + const systemId = new Uint8Array([ + 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, + 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, + ]); + const data = new Uint8Array([]); + const psshVersion = 1; + const pssh = + shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion); + return [ { initData: pssh, @@ -372,10 +406,11 @@ shaka.dash.ContentProtection = class { * @param {Array.} defaultInit * @param {!Array.} elements * @param {!Object.} keySystemsByURI + * @param {!Set.} keyIds * @return {!Array.} * @private */ - static convertElements_(defaultInit, elements, keySystemsByURI) { + static convertElements_(defaultInit, elements, keySystemsByURI, keyIds) { const ContentProtection = shaka.dash.ContentProtection; const ManifestParserUtils = shaka.util.ManifestParserUtils; const licenseUrlParsers = ContentProtection.licenseUrlParsers_; @@ -391,7 +426,14 @@ shaka.dash.ContentProtection = class { 'Init data must be null or non-empty.'); const proInitData = ContentProtection.getInitDataFromPro_(element); - const initData = element.init || defaultInit || proInitData; + let clearKeyInitData = null; + if (element.schemeUri === + shaka.dash.ContentProtection.ClearKeySchemeUri_) { + clearKeyInitData = + ContentProtection.getInitDataClearKey_(element, keyIds); + } + const initData = element.init || defaultInit || proInitData || + clearKeyInitData; const info = ManifestParserUtils.createDrmInfo(keySystem, initData); const licenseParser = licenseUrlParsers.get(keySystem); if (licenseParser) { @@ -607,3 +649,11 @@ shaka.dash.ContentProtection.CencNamespaceUri_ = 'urn:mpeg:cenc:2013'; */ shaka.dash.ContentProtection.ClearKeyNamespaceUri_ = 'http://dashif.org/guidelines/clearKey'; + + +/** + * @const {string} + * @private + */ +shaka.dash.ContentProtection.ClearKeySchemeUri_ = + 'urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e'; diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 5657e618a2..16589c739f 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -2529,7 +2529,10 @@ shaka.hls.HlsParser = class { 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95, ]); - const pssh = shaka.util.Pssh.createPssh(data, systemId); + const keyIds = new Set(); + const psshVersion = 0; + const pssh = + shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion); const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo( 'com.microsoft.playready', [ {initDataType: 'cenc', initData: pssh}, diff --git a/lib/util/pssh.js b/lib/util/pssh.js index 0f829fb09d..296c98f3df 100644 --- a/lib/util/pssh.js +++ b/lib/util/pssh.js @@ -90,16 +90,21 @@ shaka.util.Pssh = class { } /** - * Creates a pssh blob from the given system ID and data. + * Creates a pssh blob from the given system ID, data, keyIds and version. * * @param {!Uint8Array} data * @param {!Uint8Array} systemId + * @param {!Set.} keyIds + * @param {number} version * @return {!Uint8Array} */ - static createPssh(data, systemId) { + static createPssh(data, systemId, keyIds, version) { goog.asserts.assert(systemId.byteLength == 16, 'Invalid system ID length'); const dataLength = data.length; - const psshSize = 0x4 + 0x4 + 0x4 + systemId.length + 0x4 + dataLength; + let psshSize = 0x4 + 0x4 + 0x4 + systemId.length + 0x4 + dataLength; + if (version > 0) { + psshSize += 0x4 + (16 * keyIds.size); + } /** @type {!Uint8Array} */ const psshBox = new Uint8Array(psshSize); @@ -111,10 +116,24 @@ shaka.util.Pssh = class { byteCursor += 0x4; psshData.setUint32(byteCursor, 0x70737368); // 'pssh' byteCursor += 0x4; - psshData.setUint32(byteCursor, 0); // flags + (version < 1) ? psshData.setUint32(byteCursor, 0) : + psshData.setUint32(byteCursor, 0x01000000); // version + flags byteCursor += 0x4; psshBox.set(systemId, byteCursor); byteCursor += systemId.length; + + // if version > 0, add KID count and kid values. + if (version > 0) { + psshData.setUint32(byteCursor, keyIds.size); // KID_count + byteCursor += 0x4; + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + for (const keyId of keyIds) { + const KID = Uint8ArrayUtils.fromHex(keyId); + psshBox.set(KID, byteCursor); + byteCursor += KID.length; + } + } + psshData.setUint32(byteCursor, dataLength); byteCursor += 0x4; psshBox.set(data, byteCursor);