UNPKG

shaka-player

Version:
1,270 lines (1,081 loc) 85.4 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. */ describe('DrmEngine', function() { const Periods = shaka.util.Periods; const originalRequestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess; const originalLogError = shaka.log.error; /** @type {!jasmine.Spy} */ let requestMediaKeySystemAccessSpy; /** @type {!jasmine.Spy} */ let logErrorSpy; /** @type {!jasmine.Spy} */ let onErrorSpy; /** @type {!jasmine.Spy} */ let onKeyStatusSpy; /** @type {!jasmine.Spy} */ let onExpirationSpy; /** @type {!jasmine.Spy} */ let onEventSpy; /** @type {!shaka.test.FakeNetworkingEngine} */ let fakeNetEngine; /** @type {!shaka.media.DrmEngine} */ let drmEngine; /** @type {shaka.extern.Manifest} */ let manifest; /** @type {shaka.extern.DrmConfiguration} */ let config; let mockMediaKeySystemAccess; let mockMediaKeys; /** @type {!shaka.test.FakeVideo} */ let mockVideo; let session1; let session2; let session3; /** @type {!ArrayBuffer} */ let license; beforeEach(() => { requestMediaKeySystemAccessSpy = jasmine.createSpy('requestMediaKeySystemAccess'); navigator.requestMediaKeySystemAccess = shaka.test.Util.spyFunc(requestMediaKeySystemAccessSpy); logErrorSpy = jasmine.createSpy('shaka.log.error'); shaka.log.error = shaka.test.Util.spyFunc(logErrorSpy); onErrorSpy = jasmine.createSpy('onError'); onKeyStatusSpy = jasmine.createSpy('onKeyStatus'); onExpirationSpy = jasmine.createSpy('onExpirationUpdated'); onEventSpy = jasmine.createSpy('onEvent'); manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addDrmInfo('drm.abc') .addDrmInfo('drm.def') .addVideo(1).mime('video/foo', 'vbar').encrypted(true) .addAudio(2).mime('audio/foo', 'abar').encrypted(true) .build(); // By default, error logs and callbacks result in failure. onErrorSpy.and.callFake(fail); logErrorSpy.and.callFake(fail); // By default, allow keysystem drm.abc requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc'])); mockVideo = new shaka.test.FakeVideo(); session1 = createMockSession(); session2 = createMockSession(); session3 = createMockSession(); mockMediaKeySystemAccess = createMockMediaKeySystemAccess(); mockMediaKeys = createMockMediaKeys(); mockMediaKeys.createSession.and.callFake(function() { let index = mockMediaKeys.createSession.calls.count() - 1; return [session1, session2, session3][index]; }); mockMediaKeys.setServerCertificate.and.returnValue(Promise.resolve()); fakeNetEngine = new shaka.test.FakeNetworkingEngine(); license = (new Uint8Array(0)).buffer; fakeNetEngine.setResponseValue('http://abc.drm/license', license); let playerInterface = { netEngine: fakeNetEngine, onError: shaka.test.Util.spyFunc(onErrorSpy), onKeyStatus: shaka.test.Util.spyFunc(onKeyStatusSpy), onExpirationUpdated: shaka.test.Util.spyFunc(onExpirationSpy), onEvent: shaka.test.Util.spyFunc(onEventSpy), }; drmEngine = new shaka.media.DrmEngine(playerInterface); config = shaka.util.PlayerConfiguration.createDefault().drm; config.servers = { 'drm.abc': 'http://abc.drm/license', 'drm.def': 'http://def.drm/license', }; drmEngine.configure(config); }); afterEach(async () => { await drmEngine.destroy(); navigator.requestMediaKeySystemAccess = originalRequestMediaKeySystemAccess; shaka.log.error = originalLogError; }); describe('supportsVariants', function() { it('supports all clear variants', async function() { const manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addDrmInfo('drm.abc') .addDrmInfo('drm.def') .addVideo(1).mime('video/foo', 'vbar').encrypted(false) .build(); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.supportsVariant(variants[0])).toBeTruthy(); }); }); describe('init', function() { it('stops on first available key system', async () => { // Accept both drm.abc and drm.def. Only one can be chosen. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc', 'drm.def'])); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())) .toBe('drm.abc'); // Only one call, since the first key system worked. expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(1); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', jasmine.any(Object)); }); it('tries systems in the order they appear in', async () => { // Fail both key systems. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); // These should be in the same order as the key systems appear in the // manifest. let calls = requestMediaKeySystemAccessSpy.calls; expect(calls.argsFor(0)[0]).toBe('drm.abc'); expect(calls.argsFor(1)[0]).toBe('drm.def'); } }); it('tries systems with configured license servers first', async () => { // Fail both key systems. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); // Remove the server URI for drm.abc, which appears first in the manifest. delete config.servers['drm.abc']; drmEngine.configure(config); // Ignore error logs, which we expect to occur due to the missing server. logErrorSpy.and.stub(); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); // Although drm.def appears second in the manifest, it is queried first // because it has a server configured. let calls = requestMediaKeySystemAccessSpy.calls; expect(calls.argsFor(0)[0]).toBe('drm.def'); expect(calls.argsFor(1)[0]).toBe('drm.abc'); } }); it('overrides manifest with configured license servers', async () => { // Accept both drm.abc and drm.def. Only one can be chosen. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc', 'drm.def'])); // Add manifest-supplied license servers for both. for (const drmInfo of manifest.periods[0].variants[0].drmInfos) { if (drmInfo.keySystem == 'drm.abc') { drmInfo.licenseServerUri = 'http://foo.bar/abc'; } else if (drmInfo.keySystem == 'drm.def') { drmInfo.licenseServerUri = 'http://foo.bar/def'; } // Make sure we didn't somehow choose manifest-supplied values that // match the config. This would invalidate parts of the test. const configServer = config.servers[drmInfo.keySystem]; expect(drmInfo.licenseServerUri).not.toEqual(configServer); } // Remove the server URI for drm.abc from the config, so that only drm.def // could be used, in spite of the manifest-supplied license server URI. delete config.servers['drm.abc']; drmEngine.configure(config); // Ignore error logs, which we expect to occur due to the missing server. logErrorSpy.and.stub(); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); // Although drm.def appears second in the manifest, it is queried first // because it has a server configured. The manifest-supplied server for // drm.abc will not be used. expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(1); const selectedDrmInfo = drmEngine.getDrmInfo(); expect(selectedDrmInfo).not.toBe(null); expect(selectedDrmInfo.keySystem).toBe('drm.def'); expect(selectedDrmInfo.licenseServerUri).toBe(config.servers['drm.def']); }); it('detects content type capabilities of key system', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc'])); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(drmEngine.willSupport('audio/webm')).toBeTruthy(); expect(drmEngine.willSupport('video/mp4; codecs="fake"')).toBeTruthy(); expect(drmEngine.willSupport('video/mp4; codecs="FAKE"')).toBeTruthy(); // Because DrmEngine will err on being too accepting, make sure it will // reject something. However, we can only check that it is actually // thing on non-Edge browsers because of https://bit.ly/2IcEgv0 if (!shaka.util.Platform.isLegacyEdge()) { expect(drmEngine.willSupport('this-should-fail')).toBeFalsy(); } }); it('tries the second key system if the first fails', async () => { // Accept drm.def, but not drm.abc. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.def'])); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())) .toBe('drm.def'); // Both key systems were tried, since the first one failed. expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', jasmine.any(Object)); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', jasmine.any(Object)); }); it('fails to initialize if no key systems are available', async () => { // Accept no key systems. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); // Both key systems were tried, since the first one failed. expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', jasmine.any(Object)); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', jasmine.any(Object)); shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE)); } }); it('silences errors for unencrypted assets', async () => { manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addVideo(1).mime('video/foo', 'vbar') .addAudio(2).mime('audio/foo', 'abar') .build(); // Accept no key systems. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); // Both key systems were tried, since the first one failed. expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', jasmine.any(Object)); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', jasmine.any(Object)); }); it('fails to initialize if no key systems are recognized', async () => { // Simulate the DASH parser inserting a blank placeholder when only // unrecognized custom schemes are found. manifest.periods[0].variants[0].drmInfos[0].keySystem = ''; manifest.periods[0].variants[0].drmInfos[1].keySystem = ''; try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); // No key systems were tried, since the dummy placeholder was detected. expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(0); shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.NO_RECOGNIZED_KEY_SYSTEMS)); } }); it('fails to initialize if the CDM cannot be created', async () => { // The query succeeds, but we fail to create the CDM. mockMediaKeySystemAccess.createMediaKeys.and.throwError('whoops!'); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(1); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', jasmine.any(Object)); shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.FAILED_TO_CREATE_CDM, 'whoops!')); } }); it('queries audio/video capabilities', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ // audioCapabilities not present. videoCapabilities: [jasmine.objectContaining({ contentType: 'video/foo; codecs="vbar"', })], distinctiveIdentifier: 'optional', persistentState: 'optional', sessionTypes: ['temporary'], })]); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', [jasmine.objectContaining({ audioCapabilities: [jasmine.objectContaining({ contentType: 'audio/foo; codecs="abar"', })], // videoCapabilities not present. distinctiveIdentifier: 'optional', persistentState: 'optional', sessionTypes: ['temporary'], })]); } }); it('asks for persistent state and license for offline', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForStorage( variants, /* usePersistentLicense */ true); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ distinctiveIdentifier: 'optional', persistentState: 'required', sessionTypes: ['persistent-license'], })]); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', [jasmine.objectContaining({ distinctiveIdentifier: 'optional', persistentState: 'required', sessionTypes: ['persistent-license'], })]); } }); it('honors distinctive identifier and persistent state', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); manifest.periods[0].variants[0].drmInfos[0] .distinctiveIdentifierRequired = true; manifest.periods[0].variants[0].drmInfos[1] .persistentStateRequired = true; try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ distinctiveIdentifier: 'required', persistentState: 'optional', sessionTypes: ['temporary'], })]); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', [jasmine.objectContaining({ distinctiveIdentifier: 'optional', persistentState: 'required', sessionTypes: ['temporary'], })]); } }); it('makes no queries for clear content if no key config', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); manifest.periods[0].variants[0].drmInfos = []; config.servers = {}; config.advanced = {}; drmEngine.configure(config); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())).toBe(''); expect(requestMediaKeySystemAccessSpy).not.toHaveBeenCalled(); }); it('makes queries for clear content if key is configured', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc'])); manifest.periods[0].variants[0].drmInfos = []; config.servers = { 'drm.abc': 'http://abc.drm/license', }; drmEngine.configure(config); const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())) .toBe('drm.abc'); expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(1); }); it('uses advanced config to fill in DrmInfo', async () => { // Leave only one drmInfo manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addDrmInfo('drm.abc') .addVideo(1).mime('video/foo', 'vbar').encrypted(true) .addAudio(2).mime('audio/foo', 'abar').encrypted(true) .build(); requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); config.advanced['drm.abc'] = { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: null, individualizationServer: '', distinctiveIdentifierRequired: true, persistentStateRequired: true, }; drmEngine.configure(config); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(1); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ audioCapabilities: [jasmine.objectContaining({ robustness: 'good', })], videoCapabilities: [jasmine.objectContaining({ robustness: 'really_really_ridiculously_good', })], distinctiveIdentifier: 'required', persistentState: 'required', })]); } }); it('prefers advanced config from manifest if present', async () => { // Leave only one drmInfo manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addDrmInfo('drm.abc') .addVideo(1).mime('video/foo', 'vbar').encrypted(true) .addAudio(2).mime('audio/foo', 'abar').encrypted(true) .build(); requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); // DrmInfo directly sets advanced settings. manifest.periods[0].variants[0].drmInfos[0] .distinctiveIdentifierRequired = true; manifest.periods[0].variants[0].drmInfos[0] .persistentStateRequired = true; manifest.periods[0].variants[0].drmInfos[0] .audioRobustness = 'good'; manifest.periods[0].variants[0].drmInfos[0] .videoRobustness = 'really_really_ridiculously_good'; config.advanced['drm.abc'] = { audioRobustness: 'bad', videoRobustness: 'so_bad_it_hurts', serverCertificate: null, individualizationServer: '', distinctiveIdentifierRequired: false, persistentStateRequired: false, }; drmEngine.configure(config); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { expect(drmEngine.initialized()).toBe(false); expect(requestMediaKeySystemAccessSpy.calls.count()).toBe(1); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ audioCapabilities: [jasmine.objectContaining({ robustness: 'good', })], videoCapabilities: [jasmine.objectContaining({ robustness: 'really_really_ridiculously_good', })], distinctiveIdentifier: 'required', persistentState: 'required', })]); } }); it('fails if license server is not configured', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc'])); config.servers = {}; drmEngine.configure(config); try { const variants = Periods.getAllVariantsFrom(manifest.periods); await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); fail(); } catch (error) { shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.NO_LICENSE_SERVER_GIVEN, 'drm.abc')); } }); it('maps TS MIME types through the transmuxer', async () => { const originalIsSupported = shaka.media.Transmuxer.isSupported; try { // Mock out isSupported on Transmuxer so that we don't have to care // about what MediaSource supports under that. All we really care about // is the translation of MIME types. shaka.media.Transmuxer.isSupported = (mimeType, contentType) => { return mimeType.startsWith('video/mp2t'); }; // The default mock for this is so unrealistic, some of our test // conditions would always fail. Make it realistic enough for this // test case by returning the same types we are supposed to be querying // for. That way, supportsVariant() should work produce the correct // result after translating the types of the variant's streams. mockMediaKeySystemAccess.getConfiguration.and.callFake(() => { return { audioCapabilities: [{contentType: 'audio/mp4; codecs="abar"'}], videoCapabilities: [{contentType: 'video/mp4; codecs="vbar"'}], }; }); requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc'])); const variants = manifest.periods[0].variants; variants[0].video.mimeType = 'video/mp2t'; variants[0].audio.mimeType = 'video/mp2t'; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ audioCapabilities: [jasmine.objectContaining({ contentType: 'audio/mp4; codecs="abar"', })], videoCapabilities: [jasmine.objectContaining({ contentType: 'video/mp4; codecs="vbar"', })], })]); expect(drmEngine.supportsVariant(variants[0])).toBeTruthy(); } finally { // Restore the mock. shaka.media.Transmuxer.isSupported = originalIsSupported; } }); }); // describe('init') describe('attach', function() { beforeEach(function() { // Both audio and video with the same key system: manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addDrmInfo('drm.abc') .addVideo(1).mime('video/foo', 'vbar').encrypted(true) .addAudio(2).mime('audio/foo', 'abar').encrypted(true) .build(); }); it('does nothing for unencrypted content', async () => { requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); manifest.periods[0].variants[0].drmInfos = []; config.servers = {}; config.advanced = {}; await initAndAttach(); expect(mockVideo.setMediaKeys).not.toHaveBeenCalled(); }); it('sets MediaKeys for encrypted content', async () => { await initAndAttach(); expect(mockVideo.setMediaKeys).toHaveBeenCalledWith(mockMediaKeys); }); it('sets server certificate if present in config', async () => { let cert = new Uint8Array(1); config.advanced['drm.abc'] = createAdvancedConfig(cert); drmEngine.configure(config); await initAndAttach(); expect(mockMediaKeys.setServerCertificate).toHaveBeenCalledWith(cert); }); it('prefers server certificate from DrmInfo', async () => { let cert1 = new Uint8Array(5); let cert2 = new Uint8Array(1); manifest.periods[0].variants[0].drmInfos[0].serverCertificate = cert1; config.advanced['drm.abc'] = createAdvancedConfig(cert2); drmEngine.configure(config); await initAndAttach(); expect(mockMediaKeys.setServerCertificate).toHaveBeenCalledWith(cert1); }); it('does not set server certificate if absent', async () => { await initAndAttach(); expect(mockMediaKeys.setServerCertificate).not.toHaveBeenCalled(); }); it('creates sessions for init data overrides', function(done) { // Set up init data overrides in the manifest: let initData1 = new Uint8Array(5); let initData2 = new Uint8Array(0); let initData3 = new Uint8Array(10); manifest.periods[0].variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: null}, {initData: initData2, initDataType: 'webm', keyId: null}, {initData: initData3, initDataType: 'cenc', keyId: null}, ]; initAndAttach().then(function() { expect(mockMediaKeys.createSession.calls.count()).toBe(3); expect(session1.generateRequest) .toHaveBeenCalledWith('cenc', initData1); expect(session2.generateRequest) .toHaveBeenCalledWith('webm', initData2); expect(session3.generateRequest) .toHaveBeenCalledWith('cenc', initData3); }).catch(fail).then(done); }); it('ignores duplicate init data overrides', function(done) { // Set up init data overrides in the manifest; // The second initData has a different keyId from the first, // but the same initData. // The third initData has a different initData from the first, // but the same keyId. // Both should be discarded as duplicates. let initData1 = new Uint8Array(1); let initData2 = new Uint8Array(1); let initData3 = new Uint8Array(10); manifest.periods[0].variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: 'abc'}, {initData: initData2, initDataType: 'cenc', keyId: 'def'}, {initData: initData3, initDataType: 'cenc', keyId: 'abc'}, ]; initAndAttach().then(function() { expect(mockMediaKeys.createSession.calls.count()).toBe(1); expect(session1.generateRequest) .toHaveBeenCalledWith('cenc', initData1); }).catch(fail).then(done); }); // https://github.com/google/shaka-player/issues/2754 it('ignores duplicate init data from newInitData', async () => { /** @type {!Uint8Array} */ const initData = new Uint8Array(1); tweakDrmInfos((drmInfos) => { drmInfos[0].initData = [{initData: initData, initDataType: 'cenc', keyId: 'abc'}]; }); await drmEngine.initForPlayback( manifest.periods[0].variants, manifest.offlineSessionIds); drmEngine.newInitData('cenc', initData); await drmEngine.attach(mockVideo); expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); expect(session1.generateRequest) .toHaveBeenCalledWith('cenc', initData); }); it('uses clearKeys config to override DrmInfo', async () => { manifest.periods[0].variants[0].drmInfos[0].keySystem = 'com.fake.NOT.clearkey'; requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, ['org.w3.clearkey'])); // Configure clear keys (map of hex key IDs to keys) config.clearKeys = { 'deadbeefdeadbeefdeadbeefdeadbeef': '18675309186753091867530918675309', '02030507011013017019023029031037': '03050701302303204201080425098033', }; drmEngine.configure(config); let session = createMockSession(); mockMediaKeys.createSession.and.callFake(function() { expect(mockMediaKeys.createSession.calls.count()).toBe(1); return session; }); await initAndAttach(); let Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; expect(manifest.periods[0].variants[0].drmInfos.length).toBe(1); expect(manifest.periods[0].variants[0].drmInfos[0].keySystem). toBe('org.w3.clearkey'); expect(session.generateRequest) .toHaveBeenCalledWith('keyids', jasmine.any(Uint8Array)); let initData = JSON.parse(shaka.util.StringUtils.fromUTF8( session.generateRequest.calls.argsFor(0)[1])); let keyId1 = Uint8ArrayUtils.toHex( Uint8ArrayUtils.fromBase64(initData.kids[0])); let keyId2 = Uint8ArrayUtils.toHex( Uint8ArrayUtils.fromBase64(initData.kids[1])); expect(keyId1).toBe('deadbeefdeadbeefdeadbeefdeadbeef'); expect(keyId2).toBe('02030507011013017019023029031037'); }); // Regression test for #2139, in which we suppressed errors if drmInfos was // empty and clearKeys config was given it('fails if clearKeys config fails', async () => { manifest.periods[0].variants[0].drmInfos = []; // Make it so that clear key setup fails by pretending we don't have it. // In reality, it was failing because of missing codec info, but any // failure should do for testing purposes. requestMediaKeySystemAccessSpy.and.callFake( fakeRequestMediaKeySystemAccess.bind(null, [])); // Configure clear keys (map of hex key IDs to keys) config.clearKeys = { 'deadbeefdeadbeefdeadbeefdeadbeef': '18675309186753091867530918675309', '02030507011013017019023029031037': '03050701302303204201080425098033', }; drmEngine.configure(config); const variants = Periods.getAllVariantsFrom(manifest.periods); try { await drmEngine.initForPlayback(variants, []); fail(); } catch (error) { shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE)); } }); it('fails with an error if setMediaKeys fails', async () => { // Fail setMediaKeys. mockVideo.setMediaKeys.and.returnValue(Promise.reject({ message: 'whoops!', })); try { await initAndAttach(); fail(); } catch (error) { shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO, 'whoops!')); } }); it('fails with an error if setServerCertificate fails', async () => { let cert = new Uint8Array(1); config.advanced['drm.abc'] = createAdvancedConfig(cert); drmEngine.configure(config); // Fail setServerCertificate. mockMediaKeys.setServerCertificate.and.returnValue(Promise.reject({ message: 'whoops!', })); try { await initAndAttach(); fail(); } catch (error) { shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE, 'whoops!')); } }); it('dispatches an error if generateRequest fails', async () => { // Set up an init data override in the manifest to get an immediate call // to generateRequest: let initData1 = new Uint8Array(5); manifest.periods[0].variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: null}, ]; // Fail generateRequest. let session1 = createMockSession(); const nativeError = {message: 'whoops!'}; session1.generateRequest.and.returnValue(Promise.reject(nativeError)); mockMediaKeys.createSession.and.returnValue(session1); onErrorSpy.and.stub(); await initAndAttach(); expect(onErrorSpy).toHaveBeenCalled(); let error = onErrorSpy.calls.argsFor(0)[0]; shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.FAILED_TO_GENERATE_LICENSE_REQUEST, nativeError.message, nativeError, undefined)); }); }); // describe('attach') describe('events', function() { describe('encrypted', function() { it('is listened for', async () => { await initAndAttach(); expect(mockVideo.addEventListener).toHaveBeenCalledWith( 'encrypted', jasmine.any(Function), jasmine.anything()); }); it('triggers the creation of a session', async () => { await initAndAttach(); let initData1 = new Uint8Array(1); let initData2 = new Uint8Array(2); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData1, keyId: null}); mockVideo.on['encrypted']( {initDataType: 'cenc', initData: initData2, keyId: null}); expect(mockMediaKeys.createSession.calls.count()).toBe(2); expect(session1.generateRequest) .toHaveBeenCalledWith('webm', initData1); expect(session2.generateRequest) .toHaveBeenCalledWith('cenc', initData2); }); it('suppresses duplicate initDatas', async () => { await initAndAttach(); let initData1 = new Uint8Array(1); let initData2 = new Uint8Array(1); // identical to initData1 mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData1, keyId: null}); mockVideo.on['encrypted']( {initDataType: 'cenc', initData: initData2, keyId: null}); expect(mockMediaKeys.createSession.calls.count()).toBe(1); expect(session1.generateRequest) .toHaveBeenCalledWith('webm', initData1); }); it('is ignored when init data is in DrmInfo', async () => { // Set up an init data override in the manifest: manifest.periods[0].variants[0].drmInfos[0].initData = [ {initData: new Uint8Array(0), initDataType: 'cenc', keyId: null}, ]; await initAndAttach(); // We already created a session for the init data override. expect(mockMediaKeys.createSession.calls.count()).toBe(1); // We aren't even listening for 'encrypted' events. expect(mockVideo.on['encrypted']).toBe(undefined); }); it('dispatches an error if createSession fails', async () => { mockMediaKeys.createSession.and.throwError('whoops!'); onErrorSpy.and.stub(); await initAndAttach(); let initData1 = new Uint8Array(1); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData1, keyId: null}); expect(onErrorSpy).toHaveBeenCalled(); let error = onErrorSpy.calls.argsFor(0)[0]; shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.FAILED_TO_CREATE_SESSION, 'whoops!')); }); it('dispatches an error if manifest says unencrypted', async () => { manifest.periods[0].variants[0].drmInfos = []; config.servers = {}; config.advanced = {}; onErrorSpy.and.stub(); await initAndAttach(); let initData1 = new Uint8Array(1); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData1, keyId: null}); expect(onErrorSpy).toHaveBeenCalled(); let error = onErrorSpy.calls.argsFor(0)[0]; shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.ENCRYPTED_CONTENT_WITHOUT_DRM_INFO)); }); }); // describe('encrypted') describe('message', function() { it('is listened for', async () => { await initAndAttach(); let initData = new Uint8Array(0); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData, keyId: null}); expect(session1.addEventListener).toHaveBeenCalledWith( 'message', jasmine.any(Function), jasmine.anything()); }); it('triggers a license request', async () => { await sendMessageTest('http://abc.drm/license'); }); it('prefers a license server URI from configuration', async () => { manifest.periods[0].variants[0].drmInfos[0].licenseServerUri = 'http://foo.bar/drm'; await sendMessageTest('http://abc.drm/license'); }); it('handles "individualization-request" messages special', async () => { config.advanced['drm.abc'] = createAdvancedConfig(null); config.advanced['drm.abc'].individualizationServer = 'http://foo.bar/drm'; expect(config.servers['drm.abc']).not.toBe('http://foo.bar/drm'); await sendMessageTest( 'http://foo.bar/drm', 'individualization-request'); }); it('uses license server for "individualization-request" by default', async () => { config.advanced['drm.abc'] = createAdvancedConfig(null); config.advanced['drm.abc'].individualizationServer = ''; await sendMessageTest( 'http://abc.drm/license', 'individualization-request'); }); it('dispatches an error if license request fails', async () => { onErrorSpy.and.stub(); await initAndAttach(); let initData = new Uint8Array(0); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData, keyId: null}); // Simulate a permission error from the web server. let netError = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.BAD_HTTP_STATUS, 'http://abc.drm/license', 403); let operation = shaka.util.AbortableOperation.failed(netError); fakeNetEngine.request.and.returnValue(operation); let message = new Uint8Array(0); session1.on['message']({target: session1, message: message}); await shaka.test.Util.delay(0.5); expect(onErrorSpy).toHaveBeenCalled(); let error = onErrorSpy.calls.argsFor(0)[0]; shaka.test.Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.LICENSE_REQUEST_FAILED, jasmine.objectContaining({ category: shaka.util.Error.Category.NETWORK, code: shaka.util.Error.Code.BAD_HTTP_STATUS, data: ['http://abc.drm/license', 403], }))); }); /** * @param {string=} expectedUrl * @param {string=} messageType * @return {!Promise} */ async function sendMessageTest( expectedUrl, messageType = 'license-request') { await initAndAttach(); let initData = new Uint8Array(0); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData, keyId: null}); let operation = shaka.util.AbortableOperation.completed({}); fakeNetEngine.request.and.returnValue(operation); let message = new Uint8Array(0); session1.on['message']( {target: session1, message: message, messageType: messageType}); expect(fakeNetEngine.request).toHaveBeenCalledWith( shaka.net.NetworkingEngine.RequestType.LICENSE, jasmine.objectContaining({ uris: [expectedUrl], method: 'POST', body: message, licenseRequestType: messageType, })); } }); // describe('message') describe('keystatuseschange', function() { it('is listened for', async () => { await initAndAttach(); let initData = new Uint8Array(0); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData, keyId: null}); expect(session1.addEventListener).toHaveBeenCalledWith( 'keystatuseschange', jasmine.any(Function), jasmine.anything()); }); it('triggers callback', function(done) { initAndAttach().then(function() { let initData = new Uint8Array(0); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData, keyId: null}); let keyId1 = (new Uint8Array(1)).buffer; let keyId2 = (new Uint8Array(2)).buffer; let status1 = 'usable'; let status2 = 'expired'; session1.keyStatuses.forEach.and.callFake(function(callback) { callback(keyId1, status1); callback(keyId2, status2); }); onKeyStatusSpy.and.callFake(function(statusMap) { expect(statusMap).toEqual({ '00': status1, '0000': status2, }); done(); }); session1.on['keystatuseschange']({target: session1}); }).catch(fail); }); // See https://github.com/google/shaka-player/issues/1541 it('does not update public key statuses before callback', async () => { await initAndAttach(); let initData = new Uint8Array(0); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData, keyId: null}); let keyId1 = (new Uint8Array(1)).buffer; let keyId2 = (new Uint8Array(2)).buffer; let status1 = 'usable'; let status2 = 'expired'; session1.keyStatuses.forEach.and.callFake(function(callback) { callback(keyId1, status1); callback(keyId2, status2); }); session1.on['keystatuseschange']({target: session1}); // The callback waits for some time to pass, to batch up status changes. expect(onKeyStatusSpy).not.toHaveBeenCalled(); // The publicly-accessible key statuses should not show these new // changes yet. This shows that we have solved the race between the // callback and any polling done by any other component. let keyIds = Object.keys(drmEngine.getKeyStatuses()); expect(keyIds.length).toEqual(0); // Wait for the callback to occur, then end the test. await new Promise((resolve) => { onKeyStatusSpy.and.callFake(resolve); }); // Now key statuses are available. keyIds = Object.keys(drmEngine.getKeyStatuses()); expect(keyIds.length).toEqual(2); }); // See https://github.com/google/shaka-player/issues/1541 it('does not invoke callback until all sessions are loaded', async () => { // Set up init data overrides in the manifest so that we get multiple // sessions. let initData1 = new Uint8Array(10); let initData2 = new Uint8Array(11); manifest.periods[0].variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: null}, {initData: initData2, initDataType: 'cenc', keyId: null}, ]; let keyId1 = (new Uint8Array(1)).buffer; let keyId2 = (new Uint8Array(2)).buffer; session1.keyStatuses.forEach.and.callFake(function(callback) { callback(keyId1, 'usable'); }); session2.keyStatuses.forEach.and.callFake(function(callback) { callback(keyId2, 'usable'); }); await initAndAttach(); // The callback waits for some time to pass, to batch up status changes. // But even after some time has passed, we should not have invoked the // callback, because we don't have a status for session2 yet. session1.on['keystatuseschange']({target: session1}); await shaka.test.Util.delay(keyStatusBatchTime() + 0.5); expect(onKeyStatusSpy).not.toHaveBeenCalled(); // After both sessions have been loaded, we will finally invoke the // callback. session2.on['keystatuseschange']({target: session2}); await shaka.test.Util.delay(keyStatusBatchTime() + 0.5); expect(onKeyStatusSpy).toHaveBeenCalled(); }); it('causes an EXPIRED error when all keys expire', function(done) { onErrorSpy.and.stub(); initAndAttach().then(function() { expect(onErrorSpy).not.toHaveBeenCalled(); let initData = new Uint8Array(0); mockVideo.on['encrypted']( {initDataType: 'webm', initData: initData, keyId: null}); let keyId1 = (new Uint8Array(1)).buffer; let keyId2 = (new Uint8Array(2)).buffer; // Expire one key. session1.keyStatuses.forEach.and.callFake(function(callback) { callback(keyId1, 'usable'); callback(keyId2, 'expired'); }); onKeyStatusSpy.and.callFake(function(statusMap) { // One key is still usable. expect(onErrorSpy).not.toHaveBeenCalled(); // Expire both keys. session1.keyStatuses.forEach.and.callFake(function(callback) { callback(keyId1, 'expired'); callback(keyId2, 'expired'); }); onKeyStatusSpy.and.callFake(function(statusMap) { // Both keys are expired, so we should have an error.