/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ // Test DRM-related parsing. describe('DashParser ContentProtection', () => { const Dash = shaka.test.Dash; const ContentProtection = shaka.dash.ContentProtection; const strToXml = (str) => { const parser = new DOMParser(); return parser.parseFromString(str, 'application/xml').documentElement; }; /** * Tests that the parser produces the correct results. * * @param {string} manifestText * @param {Object} expected A Manifest-like object. The parser output is * expected to match this. * @param {boolean=} ignoreDrmInfo * @return {!Promise} */ async function testDashParser(manifestText, expected, ignoreDrmInfo = false) { const netEngine = new shaka.test.FakeNetworkingEngine(); netEngine.setDefaultText(manifestText); const dashParser = new shaka.dash.DashParser(); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.ignoreDrmInfo = ignoreDrmInfo || false; dashParser.configure(config); const playerInterface = { networkingEngine: netEngine, filter: (manifest) => Promise.resolve(), onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, }; const actual = await dashParser.start( 'http://example.com', playerInterface); expect(actual).toEqual(expected); // When the above expectation fails, it is far too hard to read the output // and debug the test failure. So we also do these more targetted // comparisons below, which will be easier to read and debug. The full // comparison above remains to catch anything we haven't written a more // targetted expectation for below. for (let i = 0; i < actual.variants.length; ++i) { // NOTE: ['sample'] is how we get access to the partial object given to // jasmine.objectContaining(). const actualVariant = actual.variants[i]; const expectedVariant = expected['sample'].variants[i]; const actualVideo = actualVariant.video; const expectedVideo = expectedVariant['sample'].video; const actualDrmInfos = actualVideo.drmInfos; const expectedDrmInfos = expectedVideo['sample'].drmInfos; expect(actualDrmInfos).withContext(`video drmInfos, i=${i}`) .toEqual(expectedDrmInfos); const actualKeyIds = actualVideo.keyIds; const expectedKeyIds = expectedVideo['sample'].keyIds; expect(actualKeyIds).withContext(`video keyIds, i=${i}`) .toEqual(expectedKeyIds); } } /** * Build a simple manifest with ContentProtection lines inserted into the * AdaptationSet and each Representation. * * @param {!Array.} adaptationSetLines * @param {!Array.} representation1Lines * @param {!Array.} representation2Lines * @return {string} */ function buildManifestText( adaptationSetLines, representation1Lines, representation2Lines) { const template = [ '', ' ', ' ', ' ', '%(adaptationSetLines)s', ' ', '%(representation1Lines)s', ' ', ' ', '%(representation2Lines)s', ' ', ' ', ' ', '', ].join('\n'); return sprintf(template, { adaptationSetLines: adaptationSetLines.join('\n'), representation1Lines: representation1Lines.join('\n'), representation2Lines: representation2Lines.join('\n'), }); } /** * Build an expected manifest which checks DRM-related fields. * * @param {!Array.} drmInfos A list of DrmInfo-like objects. * @param {!Array.=} keyIds The key IDs to attach to each variant. * Will default to the keyIds from the first drmInfo object. * @return {Object} A Manifest-like object. */ function buildExpectedManifest(drmInfos, keyIds) { if (!keyIds) { if (drmInfos.length) { // NOTE: ['sample'] is how we get access to the partial object given to // jasmine.objectContaining(). keyIds = Array.from(drmInfos[0]['sample'].keyIds); } else { keyIds = []; } } const variants = []; const numVariants = 2; for (const i of shaka.util.Iterables.range(numVariants)) { const variant = jasmine.objectContaining({ video: jasmine.objectContaining({ keyIds: new Set(keyIds[i] ? [keyIds[i]] : []), drmInfos, }), }); variants.push(variant); } return jasmine.objectContaining({ variants: variants, textStreams: [], }); } /** * Build an expected DrmInfo based on a key system and optional key IDs and * init data. * * @param {string} keySystem * @param {!Array.=} keyIds * @param {!Array.=} initData * @return {Object} A DrmInfo-like object. */ function buildDrmInfo(keySystem, keyIds = [], initData = []) { return jasmine.objectContaining({ keySystem, keyIds: new Set(keyIds), initData, }); } /** * Build an expected InitDataOverride based on base-64-encoded PSSHs and * optional key IDs. * * @param {!Array.} base64Psshs * @param {!Array.=} keyIds * @return {!Array.} */ function buildInitData(base64Psshs, keyIds = []) { return base64Psshs.map((base64, index) => { /** @type {shaka.extern.InitDataOverride} */ const initData = { initDataType: 'cenc', initData: shaka.util.Uint8ArrayUtils.fromBase64(base64), keyId: keyIds[index] || null, }; return initData; }); } it('handles clear content', async () => { const source = buildManifestText([], [], []); const expected = buildExpectedManifest([]); await testDashParser(source, expected); }); describe('maps standard scheme IDs', () => { /** * @param {string} name A name for the test * @param {!Array.} uuids DRM scheme UUIDs * @param {!Array.} keySystems expected key system IDs */ function testKeySystemMappings(name, uuids, keySystems) { it(name, async () => { const adaptationSetLines = uuids.map((uri) => { return sprintf('', uri); }); const source = buildManifestText(adaptationSetLines, [], []); const drmInfos = keySystems.map((keySystem) => { return buildDrmInfo(keySystem); }); const expected = buildExpectedManifest(drmInfos); await testDashParser(source, expected); }); } testKeySystemMappings('for Widevine', ['edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'], ['com.widevine.alpha']); testKeySystemMappings('for PlayReady', ['9a04f079-9840-4286-ab92-e65be0885f95'], ['com.microsoft.playready']); testKeySystemMappings('for old PlayReady', ['79f0049a-4098-8642-ab92-e65be0885f95'], ['com.microsoft.playready']); testKeySystemMappings('for Adobe Primetime', ['f239e769-efa3-4850-9c16-a903c6932efb'], ['com.adobe.primetime']); testKeySystemMappings('for multiple DRMs in the specified order', [ 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', '9a04f079-9840-4286-ab92-e65be0885f95', ], [ 'com.widevine.alpha', 'com.microsoft.playready', ]); testKeySystemMappings('in a case-insensitive way', [ 'EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED', '9A04F079-9840-4286-AB92-E65BE0885F95', 'F239E769-EFA3-4850-9C16-A903C6932EFB', ], [ 'com.widevine.alpha', 'com.microsoft.playready', 'com.adobe.primetime', ]); }); it('inherits key IDs from AdaptationSet to Representation', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', ], [], []); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', [ // Representation 1 & 2 key ID deduplicated in DrmInfo 'deadbeeffeedbaadf00d000008675309', ]), ], [ // Representation 1 key ID 'deadbeeffeedbaadf00d000008675309', // Representation 2 key ID 'deadbeeffeedbaadf00d000008675309', ]); await testDashParser(source, expected); }); it('sets key IDs for the init data', async () => { const source = buildManifestText([ // AdaptationSet lines ], [ // Representation 1 lines '', ' bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcmVjdGx5', '', ], []); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', ['deadbeeffeedbaadf00d000008675309'], // key ID buildInitData( ['bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcmVjdGx5'], // PSSHs ['deadbeeffeedbaadf00d000008675309'] // key ID for init data ) ), ]); await testDashParser(source, expected); }); it('lets Representations override key IDs', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', ], [ // Representation 1 lines '', ], [ // Representation 2 lines '', ]); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', [ // Representation 1 key ID 'baadf00dfeeddeafbeef000004390116', // Representation 2 key ID 'baadf00dfeeddeafbeef018006492568', ]), ]); await testDashParser(source, expected); }); it('extracts embedded PSSHs', async () => { const source = buildManifestText([ // AdaptationSet lines '', ' ZmFrZSBXaWRldmluZSBQU1NI', '', '', ' bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcmVjdGx5', '', ], [], []); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', [], // key IDs buildInitData(['ZmFrZSBXaWRldmluZSBQU1NI']) ), buildDrmInfo('com.microsoft.playready', [], // key IDs buildInitData(['bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcmVjdGx5']) ), ]); 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', [], // key IDs buildInitData([ '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', [], // key IDs buildInitData(['bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcmVjdGx5']) ), ]); await testDashParser(source, expected); }); it('assumes all known key systems for generic CENC', async () => { const source = buildManifestText([ // AdaptationSet lines '', ], [], []); // The order does not matter here, so use arrayContaining. // NOTE: the buildDrmInfo calls here specify no init data const drmInfos = jasmine.arrayContaining([ buildDrmInfo('com.widevine.alpha'), buildDrmInfo('com.microsoft.playready'), buildDrmInfo('com.adobe.primetime'), ]); const expected = buildExpectedManifest( /** @type {!Array.} */(drmInfos), [], // key IDs ); await testDashParser(source, expected); }); it('assumes all known key systems when ignoreDrmInfo is set', async () => { const source = buildManifestText([ // AdaptationSet lines '', ' ZmFrZSBXaWRldmluZSBQU1NI', '', '', ' bm8gaHVtYW4gY2FuIHJlYWQgYmFzZTY0IGRpcm', '', ], [], []); // The order does not matter here, so use arrayContaining. // NOTE: the buildDrmInfo calls here specify no init data const drmInfos = jasmine.arrayContaining([ buildDrmInfo('com.widevine.alpha'), buildDrmInfo('com.microsoft.playready'), buildDrmInfo('com.adobe.primetime'), ]); const expected = buildExpectedManifest( /** @type {!Array.} */(drmInfos), [], // key IDs ); await testDashParser(source, expected, /* ignoreDrmInfo= */ true); }); it('parses key IDs when ignoreDrmInfo flag is set', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', ], [], []); const keyIds = [ // Representation 1 & 2 key ID deduplicated in DrmInfo 'deadbeeffeedbaadf00d000008675309', ]; const variantKeyIds = [ // Representation 1 key ID 'deadbeeffeedbaadf00d000008675309', // Representation 2 key ID 'deadbeeffeedbaadf00d000008675309', ]; const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', keyIds), // PlayReady has two associated UUIDs, so it appears twice. buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.adobe.primetime', keyIds), ], variantKeyIds); await testDashParser(source, expected, /* ignoreDrmInfo= */ true); }); it('inherits PSSH from generic CENC into all key systems', async () => { const source = buildManifestText([ // AdaptationSet lines '', ' b25lIGhlYWRlciB0byBydWxlIHRoZW0gYWxs', '', '', '', ], [], []); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', [], // key IDs buildInitData(['b25lIGhlYWRlciB0byBydWxlIHRoZW0gYWxs']) ), buildDrmInfo('com.microsoft.playready', [], // key IDs buildInitData(['b25lIGhlYWRlciB0byBydWxlIHRoZW0gYWxs']) ), ]); await testDashParser(source, expected); }); it('lets key systems override generic PSSH', async () => { const source = buildManifestText([ // AdaptationSet lines '', ' b25lIGhlYWRlciB0byBydWxlIHRoZW0gYWxs', '', '', ' ', ' VGltZSBpcyBhbiBpbGx1c2lvbi4gTHVuY2h0aW1lIGRvdWJseSBzby4=', ' ', '', '', ], [], []); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', [], // key IDs buildInitData( ['VGltZSBpcyBhbiBpbGx1c2lvbi4gTHVuY2h0aW1lIGRvdWJseSBzby4='], ) ), buildDrmInfo('com.microsoft.playready', [], // key IDs buildInitData(['b25lIGhlYWRlciB0byBydWxlIHRoZW0gYWxs']), ), ]); await testDashParser(source, expected); }); it('ignores custom or unknown schemes', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', '', ], [], []); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha'), ]); await testDashParser(source, expected); }); it('inserts a placeholder for unrecognized schemes', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', '', ], [], []); const expected = buildExpectedManifest([ // placeholder: only unrecognized schemes found buildDrmInfo('', [ // Representation 1 & 2 key ID deduplicated in DrmInfo 'deadbeeffeedbaadf00d000008675309', ]), ], [ // Representation 1 key ID 'deadbeeffeedbaadf00d000008675309', // Representation 2 key ID 'deadbeeffeedbaadf00d000008675309', ]); await testDashParser(source, expected); }); it('can specify ContentProtection in Representation only', async () => { const source = buildManifestText([ // AdaptationSet lines ], [ // Representation 1 lines '', ], [ // Representation 2 lines '', ]); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha'), ]); await testDashParser(source, expected); }); it('still keeps per-Representation key IDs when merging', async () => { const source = buildManifestText([ // AdaptationSet lines ], [ // Representation 1 lines '', '', ], [ // Representation 2 lines '', '', ]); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', [ // Representation 1 key ID 'deadbeeffeedbaadf00d000008675309', // Representation 2 key ID 'baadf00dfeeddeafbeef000004390116', ]), ]); await testDashParser(source, expected); }); it('parses key IDs from non-cenc in Representation', async () => { const source = buildManifestText([ // AdaptationSet lines ], [ // Representation 1 lines '', '', ], [ // Representation 2 lines '', '', ]); const keyIds = [ // Representation 1 key ID 'deadbeeffeedbaadf00d000008675309', // Representation 2 key ID 'baadf00dfeeddeafbeef000004390116', ]; const expected = buildExpectedManifest([ buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.widevine.alpha', keyIds), ]); await testDashParser(source, expected); }); it('parses key IDs from non-cenc in AdaptationSet', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', ], [], []); const keyIds = [ // Representation 1 & 2 key ID deduplicated in DrmInfo 'deadbeeffeedbaadf00d000008675309', ]; const variantKeyIds = [ // Representation 1 key ID 'deadbeeffeedbaadf00d000008675309', // Representation 2 key ID 'deadbeeffeedbaadf00d000008675309', ]; const expected = buildExpectedManifest([ buildDrmInfo('com.microsoft.playready', keyIds), buildDrmInfo('com.widevine.alpha', keyIds), ], variantKeyIds); await testDashParser(source, expected); }); it('ignores elements missing @schemeIdUri', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', ], [], []); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha'), ]); await testDashParser(source, expected); }); it('handles non-default namespace names', async () => { const source = [ '', ' ', ' ', ' ', ' ', ' b25lIGhlYWRlciB0byBydWxlIHRoZW0gYWxs', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); const expected = buildExpectedManifest([ buildDrmInfo('com.widevine.alpha', [], // key IDs buildInitData(['b25lIGhlYWRlciB0byBydWxlIHRoZW0gYWxs']) ), ]); await testDashParser(source, expected); }); it('fails for no schemes common', async () => { const source = buildManifestText([ // AdaptationSet lines ], [ // Representation 1 lines '', ], [ // Representation 2 lines '', ]); const expected = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM); await Dash.testFails(source, expected); }); it('fails for invalid PSSH encoding', async () => { const source = buildManifestText([ // AdaptationSet lines '', ' foobar!', '', ], [], []); const expected = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING); await Dash.testFails(source, expected); }); it('fails for conflicting default key IDs', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', ], [], []); const expected = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS); await Dash.testFails(source, expected); }); it('fails for multiple key IDs', async () => { const source = buildManifestText([ // AdaptationSet lines '', '', ], [], []); const expected = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED); await Dash.testFails(source, expected); }); describe('getWidevineLicenseUrl', () => { it('valid ms:laurl node', () => { const input = { init: null, keyId: null, schemeUri: '', node: strToXml([ '', ' ', '', ].join('\n')), }; const actual = ContentProtection.getWidevineLicenseUrl(input); expect(actual).toBe('www.example.com'); }); it('ms:laurl without license url', () => { const input = { init: null, keyId: null, schemeUri: '', node: strToXml([ '', ' ', '', ].join('\n')), }; const actual = ContentProtection.getWidevineLicenseUrl(input); expect(actual).toBe(''); }); it('no ms:laurl node', () => { const input = { init: null, keyId: null, schemeUri: '', node: strToXml(''), }; const actual = ContentProtection.getWidevineLicenseUrl(input); expect(actual).toBe(''); }); }); describe('getPlayReadyLicenseURL', () => { it('mspro', () => { const laurl = [ '', ' ', ' www.example.com', ' ', '', ].join('\n'); const laurlCodes = laurl.split('').map((c) => { return c.charCodeAt(); }); const prBytes = new Uint16Array([ // pr object size (in num bytes). // + 10 for PRO size, count, and type laurl.length * 2 + 10, 0, // record count 1, // type ContentProtection.PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT, // record size (in num bytes) laurl.length * 2, // value ].concat(laurlCodes)); const encodedPrObject = shaka.util.Uint8ArrayUtils.toBase64(prBytes); const input = { init: null, keyId: null, schemeUri: '', node: strToXml([ '', ' ' + encodedPrObject + '', '', ].join('\n')), }; const actual = ContentProtection.getPlayReadyLicenseUrl(input); expect(actual).toBe('www.example.com'); }); it('no mspro', () => { const input = { init: null, keyId: null, schemeUri: '', node: strToXml(''), }; const actual = ContentProtection.getPlayReadyLicenseUrl(input); expect(actual).toBe(''); }); }); });