From 5a5864c653a5f46e9f8b19640250e80a6f6c740c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Fri, 23 Aug 2019 17:46:56 +0200 Subject: [PATCH] Get PlayReady PSSH from in DASH (#2106) Creates "pssh" init data based on the element in the DASH manifest. Closes #2058 --- lib/dash/content_protection.js | 34 +++++++++++++++- lib/util/pssh.js | 40 +++++++++++++++++++ .../dash_parser_content_protection_unit.js | 36 ++++++++++++++++- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/lib/dash/content_protection.js b/lib/dash/content_protection.js index c9bcd538de..0e3251d95b 100644 --- a/lib/dash/content_protection.js +++ b/lib/dash/content_protection.js @@ -22,6 +22,7 @@ goog.require('shaka.log'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); +goog.require('shaka.util.Pssh'); goog.require('shaka.util.Uint8ArrayUtils'); goog.require('shaka.util.XmlUtils'); @@ -321,6 +322,36 @@ shaka.dash.ContentProtection = class { return ContentProtection.getLaurl_(rootElement); } + /** + * Gets a PlayReady initData from a content protection element + * containing a PlayReady Pro Object + * + * @param {shaka.dash.ContentProtection.Element} element + * @return {?Array.} + * @private + */ + static getInitDataFromPro_(element) { + const proNode = shaka.util.XmlUtils.findChildNS( + element.node, 'urn:microsoft:playready', 'pro'); + if (!proNode) { + return null; + } + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + const data = Uint8ArrayUtils.fromBase64(proNode.textContent); + const systemId = new Uint8Array([ + 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, + 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95, + ]); + const pssh = shaka.util.Pssh.createPssh(data, systemId); + return [ + { + initData: pssh, + initDataType: 'cenc', + keyId: element.keyId, + }, + ]; + } + /** * Creates DrmInfo objects from the given element. * @@ -346,7 +377,8 @@ shaka.dash.ContentProtection = class { !element.init || element.init.length, 'Init data must be null or non-empty.'); - const initData = element.init || defaultInit; + const proInitData = ContentProtection.getInitDataFromPro_(element); + const initData = element.init || defaultInit || proInitData; const info = ManifestParserUtils.createDrmInfo(keySystem, initData); const licenseParser = licenseUrlParsers.get(keySystem); if (licenseParser) { diff --git a/lib/util/pssh.js b/lib/util/pssh.js index bd72581bf4..7fdc625462 100644 --- a/lib/util/pssh.js +++ b/lib/util/pssh.js @@ -109,4 +109,44 @@ shaka.util.Pssh = class { shaka.log.warning('Mismatch between box size and data size!'); } } + + /** + * Creates a pssh blob from the given system ID and data. + * + * @param {!Uint8Array} data + * @param {!Uint8Array} systemId + * @return {!Uint8Array} + */ + static createPssh(data, systemId) { + const dataLength = data.length; + const psshSize = 0x4 + 0x4 + 0x4 + systemId.length + 0x4 + dataLength; + + /** @type {!ArrayBuffer} */ + const psshBoxBuffer = new ArrayBuffer(psshSize); + + /** @type {!Uint8Array} */ + const psshBox = new Uint8Array(psshBoxBuffer); + + /** @type {!DataView} */ + const psshData = new DataView(psshBoxBuffer); + + let byteCursor = 0; + psshData.setUint32(byteCursor, psshSize); + byteCursor += 0x4; + psshData.setUint32(byteCursor, 0x70737368); // 'pssh' + byteCursor += 0x4; + psshData.setUint32(byteCursor, 0); // flags + byteCursor += 0x4; + psshBox.set(systemId, byteCursor); + byteCursor += systemId.length; + psshData.setUint32(byteCursor, dataLength); + byteCursor += 0x4; + psshBox.set(data, byteCursor); + byteCursor += dataLength; + + goog.asserts.assert(byteCursor === psshSize, + 'MS PRO invalid length.'); + + return psshBox; + } }; diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 0c45c3cc79..3f327c3a56 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -68,7 +68,8 @@ describe('DashParser ContentProtection', () => { adaptationSetLines, representation1Lines, representation2Lines) { const template = [ '', + ' xmlns:cenc="urn:mpeg:cenc:2013"', + ' xmlns:mspr="urn:microsoft:playready">', ' ', ' ', ' ', @@ -299,6 +300,39 @@ describe('DashParser ContentProtection', () => { await testDashParser(source, expected); }); + it('extracts embedded PSSHs with mspr:pro', async () => { + const source = buildManifestText([ + // AdaptationSet lines + '', + ' UGxheXJlYWR5', + '', + ], [], []); + const expected = buildExpectedManifest([ + buildDrmInfo('com.microsoft.playready', [], [ + 'AAAAKXBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAAlQbGF5cmVhZHk=', + ]), + ]); + await testDashParser(source, expected); + }); + + it('extracts embedded PSSHs and prefer cenc:pssh over mspr:pro', async () => { + const source = buildManifestText([ + // AdaptationSet lines + '', + ' bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcmVjdGx5', + ' ZmFrZSBQbGF5cmVhZHkgUFJP', + '', + ], [], []); + const expected = buildExpectedManifest([ + buildDrmInfo('com.microsoft.playready', [], [ + 'bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcmVjdGx5', + ]), + ]); + await testDashParser(source, expected); + }); + it('assumes all known key systems for generic CENC', async () => { const source = buildManifestText([ // AdaptationSet lines