UNPKG

shaka-player

Version:
1,402 lines (1,191 loc) 150 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('Player', () => { const ContentType = shaka.util.ManifestParserUtils.ContentType; const Util = shaka.test.Util; const originalLogError = shaka.log.error; const originalLogWarn = shaka.log.warning; const originalLogAlwaysWarn = shaka.log.alwaysWarn; const originalIsTypeSupported = window.MediaSource.isTypeSupported; const originalDecodingInfo = navigator.mediaCapabilities.decodingInfo; const fakeManifestUri = 'fake-manifest-uri'; const fakeMimeType = 'application/test'; /** @type {!jasmine.Spy} */ let logErrorSpy; /** @type {!jasmine.Spy} */ let logWarnSpy; /** @type {!jasmine.Spy} */ let onError; /** @type {shaka.extern.Manifest} */ let manifest; /** @type {!shaka.Player} */ let player; /** @type {!shaka.test.FakeAbrManager} */ let abrManager; /** @type {!shaka.test.FakeNetworkingEngine} */ let networkingEngine; /** @type {!shaka.test.FakeStreamingEngine} */ let streamingEngine; /** @type {!shaka.test.FakeDrmEngine} */ let drmEngine; /** @type {!shaka.test.FakePlayhead} */ let playhead; /** @type {!shaka.test.FakeTextDisplayer} */ let textDisplayer; /** @type {shaka.extern.BufferedInfo} */ let bufferedInfo; let mediaSourceEngine; /** @type {!shaka.test.FakeVideo} */ let video; beforeEach(() => { // By default, errors are a failure. logErrorSpy = jasmine.createSpy('shaka.log.error'); logErrorSpy.calls.reset(); shaka.log.error = shaka.test.Util.spyFunc(logErrorSpy); shaka.log.alwaysError = shaka.test.Util.spyFunc(logErrorSpy); logWarnSpy = jasmine.createSpy('shaka.log.warning'); logErrorSpy.and.callFake(fail); shaka.log.warning = shaka.test.Util.spyFunc(logWarnSpy); shaka.log.alwaysWarn = shaka.test.Util.spyFunc(logWarnSpy); // Since this is not an integration test, we don't want MediaSourceEngine to // fail assertions based on browser support for types. Pretend that all // video and audio types are supported. window.MediaSource.isTypeSupported = (mimeType) => { const type = mimeType.split('/')[0]; return type == 'video' || type == 'audio'; }; // Since this is not an integration test, we don't want MediaCapabilities to // fail assertions based on browser support for types. Pretend that all // video and audio types are supported. navigator.mediaCapabilities.decodingInfo = async (config) => { await Promise.resolve(); const videoType = config['video'] ? config['video'].contentType.split('/')[0] : null; const audioType = config['audio'] ? config['audio'].contentType.split('/')[0] : null; if (videoType == 'video' || audioType == 'audio') { return {supported: true}; } else { return {supported: false}; } }; // Many tests assume the existence of a manifest, so create a basic one. // Test suites can override this with more specific manifests. manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1); variant.addVideo(2); }); manifest.addVariant(1, (variant) => { variant.addAudio(1); variant.addVideo(4); }); }); shaka.media.ManifestParser.registerParserByMime( fakeMimeType, () => new shaka.test.FakeManifestParser(manifest)); abrManager = new shaka.test.FakeAbrManager(); textDisplayer = createTextDisplayer(); bufferedInfo = { total: [], audio: [], video: [{start: 12, end: 26}], text: [], }; function dependencyInjector(player) { // Create a networking engine that always returns an empty buffer. networkingEngine = new shaka.test.FakeNetworkingEngine(); networkingEngine.setDefaultValue(new ArrayBuffer(0)); drmEngine = new shaka.test.FakeDrmEngine(); playhead = new shaka.test.FakePlayhead(); streamingEngine = new shaka.test.FakeStreamingEngine(); mediaSourceEngine = { init: jasmine.createSpy('init').and.returnValue(Promise.resolve()), configure: jasmine.createSpy('configure'), open: jasmine.createSpy('open').and.returnValue(Promise.resolve()), destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()), setUseEmbeddedText: jasmine.createSpy('setUseEmbeddedText'), getUseEmbeddedText: jasmine.createSpy('getUseEmbeddedText'), setSegmentRelativeVttTiming: jasmine.createSpy('setSegmentRelativeVttTiming'), updateLcevcDec: jasmine.createSpy('updateLcevcDec'), getTextDisplayer: () => textDisplayer, getBufferedInfo: () => bufferedInfo, ended: jasmine.createSpy('ended').and.returnValue(false), }; player.createDrmEngine = () => { return drmEngine; }; player.createNetworkingEngine = () => networkingEngine; player.createPlayhead = (startTime) => { const callableSetStartTime = shaka.test.Util.spyFunc(playhead.setStartTime); callableSetStartTime(startTime); playhead.setStartTime.calls.reset(); return playhead; }; player.createMediaSourceEngine = () => mediaSourceEngine; player.createStreamingEngine = () => streamingEngine; } video = new shaka.test.FakeVideo(20); player = new shaka.Player(video, dependencyInjector); player.configure({ // Ensures we don't get a warning about missing preference. preferredAudioLanguage: 'en', abrFactory: () => abrManager, textDisplayFactory: () => textDisplayer, }); onError = jasmine.createSpy('error event'); onError.and.callFake((event) => { fail(event.detail); }); player.addEventListener('error', shaka.test.Util.spyFunc(onError)); }); afterEach(async () => { try { await player.destroy(); } finally { shaka.log.error = originalLogError; shaka.log.alwaysError = originalLogError; shaka.log.warning = originalLogWarn; shaka.log.alwaysWarn = originalLogAlwaysWarn; window.MediaSource.isTypeSupported = originalIsTypeSupported; shaka.media.ManifestParser.unregisterParserByMime(fakeMimeType); navigator.mediaCapabilities.decodingInfo = originalDecodingInfo; onError.calls.reset(); } }); describe('destroy', () => { it('cleans up all dependencies', async () => { goog.asserts.assert(manifest, 'Manifest should be non-null'); await player.load(fakeManifestUri, 0, fakeMimeType); const segmentIndexes = []; for (const variant of manifest.variants) { if (variant.audio) { segmentIndexes.push(variant.audio.segmentIndex); } if (variant.video) { segmentIndexes.push(variant.video.segmentIndex); } } for (const textStream of manifest.textStreams) { segmentIndexes.push(textStream.segmentIndex); } for (const segmentIndex of segmentIndexes) { spyOn(segmentIndex, 'release'); } await player.load(fakeManifestUri, 0, fakeMimeType); await player.destroy(); expect(abrManager.stop).toHaveBeenCalled(); expect(abrManager.release).toHaveBeenCalled(); expect(networkingEngine.destroy).toHaveBeenCalled(); expect(drmEngine.destroy).toHaveBeenCalled(); expect(playhead.release).toHaveBeenCalled(); expect(mediaSourceEngine.destroy).toHaveBeenCalled(); expect(streamingEngine.destroy).toHaveBeenCalled(); for (const segmentIndex of segmentIndexes) { if (segmentIndex) { expect(segmentIndex.release).toHaveBeenCalled(); } } }); it('destroys mediaSourceEngine before drmEngine', async () => { goog.asserts.assert(manifest, 'Manifest should be non-null'); mediaSourceEngine.destroy.and.callFake(async () => { expect(drmEngine.destroy).not.toHaveBeenCalled(); await Util.shortDelay(); expect(drmEngine.destroy).not.toHaveBeenCalled(); }); await player.load(fakeManifestUri, 0, fakeMimeType); await player.destroy(); expect(mediaSourceEngine.destroy).toHaveBeenCalled(); expect(drmEngine.destroy).toHaveBeenCalled(); }); // TODO(vaage): Re-enable once the parser is integrated into the load graph // better. xit('destroys parser first when interrupting load', async () => { const p = shaka.test.Util.shortDelay(); /** @type {!shaka.test.FakeManifestParser} */ const parser = new shaka.test.FakeManifestParser(manifest); parser.start.and.returnValue(p); parser.stop.and.callFake(() => { expect(abrManager.stop).not.toHaveBeenCalled(); expect(abrManager.release).not.toHaveBeenCalled(); expect(networkingEngine.destroy).not.toHaveBeenCalled(); }); shaka.media.ManifestParser.registerParserByMime( fakeMimeType, () => parser); const load = player.load(fakeManifestUri, 0, fakeMimeType); await shaka.test.Util.shortDelay(); await player.destroy(); expect(abrManager.stop).toHaveBeenCalled(); expect(abrManager.release).toHaveBeenCalled(); expect(networkingEngine.destroy).toHaveBeenCalled(); expect(parser.stop).toHaveBeenCalled(); await expectAsync(load).toBeRejected(); }); }); describe('load/unload', () => { /** @type {!jasmine.Spy} */ let checkError; beforeEach(() => { goog.asserts.assert(manifest, 'manifest must be non-null'); checkError = jasmine.createSpy('checkError'); checkError.and.callFake((error) => { expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED); }); }); describe('streaming event', () => { /** @type {jasmine.Spy} */ let streamingListener; beforeEach(() => { streamingListener = jasmine.createSpy('listener'); player.addEventListener('streaming', Util.spyFunc(streamingListener)); // We must have two different sets of codecs for some of our tests. manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1, (stream) => { stream.mime('audio/mp4', 'mp4a.40.2'); }); variant.addVideo(2, (stream) => { stream.mime('video/mp4', 'avc1.4d401f'); }); }); manifest.addVariant(1, (variant) => { variant.addAudio(3, (stream) => { stream.mime('audio/webm', 'opus'); }); variant.addVideo(4, (stream) => { stream.mime('video/webm', 'vp9'); }); }); }); }); async function runTest() { expect(streamingListener).not.toHaveBeenCalled(); await player.load(fakeManifestUri, 0, fakeMimeType); expect(streamingListener).toHaveBeenCalled(); } it('fires after tracks exist', async () => { streamingListener.and.callFake(() => { const tracks = player.getVariantTracks(); expect(tracks).toBeDefined(); expect(tracks.length).toBeGreaterThan(0); }); await runTest(); }); it('fires before any tracks are active', async () => { streamingListener.and.callFake(() => { const activeTracks = player.getVariantTracks().filter((t) => t.active); expect(activeTracks.length).toBe(0); }); await runTest(); }); // We used to fire the event /before/ filtering, which meant that for // multi-codec content, the application might select something which will // later be removed during filtering. // https://github.com/shaka-project/shaka-player/issues/1119 it('fires after tracks have been filtered', async () => { streamingListener.and.callFake(() => { const tracks = player.getVariantTracks(); // Either WebM, or MP4, but not both. expect(tracks.length).toBe(1); }); await runTest(); }); }); describe('disableStream', () => { /** @type {number} */ let disableTimeInSeconds; /** @type {?jasmine.Spy} */ let getBufferedInfoSpy; beforeAll(() => { jasmine.clock().install(); jasmine.clock().mockDate(); }); afterAll(() => { jasmine.clock().uninstall(); }); beforeEach(() => { disableTimeInSeconds = 30; getBufferedInfoSpy = spyOn(player, 'getBufferedInfo') .and.returnValue(bufferedInfo); }); async function runTest(variantIndex, streamType, expectedStatus) { await player.load(fakeManifestUri, 0, fakeMimeType); const stream = /** @type {shaka.extern.Stream} */ (manifest.variants[variantIndex][streamType]); const status = player.disableStream(stream, 10); expect(status).toBe(expectedStatus); } function multiVariantManifest() { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1, (stream) => { stream.mime('audio/mp4', 'mp4a.40.2'); stream.language = 'en'; }); variant.addVideo(2, (stream) => { stream.size(10, 10); }); }); manifest.addVariant(1, (variant) => { variant.addExistingStream(1); variant.addVideo(3, (stream) => { stream.size(20, 20); }); }); manifest.addVariant(2, (variant) => { variant.addExistingStream(1); variant.addVideo(4, (stream) => { stream.size(30, 30); }); }); }); } it('disable and restore stream after configured time', async () => { multiVariantManifest(); await player.load(fakeManifestUri, 0, fakeMimeType); const variant = manifest.variants[0]; const videoStream = /** @type {shaka.extern.Stream} */ (variant.video); player.disableStream(videoStream, disableTimeInSeconds); expect(variant.disabledUntilTime).toBeGreaterThan(Date.now()/1000); await shaka.test.Util.fakeEventLoop(disableTimeInSeconds); expect(variant.disabledUntilTime).toBe(0); }); it('does not restore stream if disabled time did not elapsed', async () => { multiVariantManifest(); await player.load(fakeManifestUri, 0, fakeMimeType); const variant = manifest.variants[0]; const videoStream = /** @type {shaka.extern.Stream} */ (variant.video); player.disableStream(videoStream, disableTimeInSeconds); expect(variant.disabledUntilTime).toBeGreaterThan(Date.now()/1000); await shaka.test.Util.fakeEventLoop(disableTimeInSeconds - 5); expect(variant.disabledUntilTime).toBeGreaterThan(Date.now()/1000); }); it('updates abrManager and switch after disabling a stream', async () => { multiVariantManifest(); await player.load(fakeManifestUri, 0, fakeMimeType); const variantCount = manifest.variants.length; const variant = manifest.variants[0]; const videoStream = /** @type {shaka.extern.Stream} */ (variant.video); player.disableStream(videoStream, disableTimeInSeconds); // Disabled as expected? expect(variant.disabledUntilTime).toBeGreaterThan(Date.now()/1000); expect(abrManager.setVariants).toHaveBeenCalled(); expect(abrManager.chooseVariant).toHaveBeenCalled(); expect(streamingEngine.switchVariant).toHaveBeenCalled(); expect(getBufferedInfoSpy).toHaveBeenCalled(); const updatedVariants = abrManager.setVariants.calls.mostRecent().args[0]; const alternateVariant = streamingEngine.switchVariant.calls.mostRecent().args[0]; const safeMargin = streamingEngine.switchVariant.calls.mostRecent().args[2]; const forceSwitch = streamingEngine.switchVariant.calls.mostRecent().args[3]; const fromAdaptation = streamingEngine.switchVariant.calls.mostRecent().args[4]; expect(updatedVariants.length).toBe(variantCount - 1); expect(alternateVariant.video).not.toEqual(variant.video); expect(safeMargin).toBe(14); expect(forceSwitch).toBeTruthy(); expect(fromAdaptation).toBeFalsy(); }); it('updates abrManager and switch after restoring a stream', async () => { multiVariantManifest(); await player.load(fakeManifestUri, 0, fakeMimeType); const variantCount = manifest.variants.length; const variant = manifest.variants[0]; const videoStream = /** @type {shaka.extern.Stream} */ (variant.video); player.disableStream(videoStream, disableTimeInSeconds); await shaka.test.Util.fakeEventLoop(disableTimeInSeconds); // Restored as expected? expect(variant.disabledUntilTime).toBe(0); expect(abrManager.setVariants).toHaveBeenCalled(); expect(abrManager.chooseVariant).toHaveBeenCalled(); expect(streamingEngine.switchVariant).toHaveBeenCalled(); const updatedVariants = abrManager.setVariants.calls.mostRecent().args[0]; const forceSwitch = streamingEngine.switchVariant.calls.mostRecent().args[3]; const fromAdaptation = streamingEngine.switchVariant.calls.mostRecent().args[4]; expect(updatedVariants.length).toBe(variantCount); expect(forceSwitch).toBeFalsy(); expect(fromAdaptation).toBeFalsy(); }); describe('does not disable stream if there not alternate stream', () => { it('single audio multiple videos', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1, (stream) => { stream.mime('audio/mp4', 'mp4a.40.2'); stream.language = 'en'; }); variant.addVideo(2); }); manifest.addVariant(1, (variant) => { variant.addExistingStream(1); variant.addVideo(3); }); }); await runTest(0, 'audio', false); }); it('multiple audio different languages', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1, (stream) => { stream.mime('audio/mp4', 'mp4a.40.2'); stream.language = 'en'; }); variant.addVideo(2); }); manifest.addVariant(1, (variant) => { variant.addAudio(3, (stream) => { stream.mime('audio/mp4', 'mp4a.40.2'); stream.language = 'de'; }); variant.addExistingStream(2); }); }); await runTest(1, 'audio', false); }); it('single variant', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1); variant.addVideo(2); }); }); await runTest(0, 'audio', false); await runTest(0, 'video', false); }); it('single video multiple audio', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1, (stream) => { stream.mime('audio/mp4', 'mp4a.40.2'); stream.language = 'en'; stream.bandwidth = 10; }); variant.addVideo(2); }); manifest.addVariant(1, (variant) => { variant.addAudio(3, (stream) => { stream.mime('audio/mp4', 'mp4a.40.2'); stream.language = 'en'; stream.bandwidth = 20; }); variant.addExistingStream(2); }); }); await runTest(0, 'video', false); }); describe('or', () => { /** @type {!Object} */ let navigatorOnLineDescriptor; // eslint-disable-next-line no-restricted-syntax const navigatorPrototype = Navigator.prototype; beforeAll(() => { navigatorOnLineDescriptor = /** @type {!Object} */(Object.getOwnPropertyDescriptor( navigatorPrototype, 'onLine')); }); beforeEach(() => { // Redefine the property, replacing only the getter. Object.defineProperty(navigatorPrototype, 'onLine', Object.assign(navigatorOnLineDescriptor, { get: () => false, })); }); afterEach(() => { // Restore the original property definition. Object.defineProperty( navigatorPrototype, 'onLine', navigatorOnLineDescriptor); }); it('browser is truly offline', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(11, (variant) => { variant.addAudio(2); variant.addVideo(3); }); manifest.addVariant(12, (variant) => { variant.addAudio(4); variant.addVideo(5); }); }); await runTest(0, 'video', false); }); }); }); }); describe('setTextTrackVisibility', () => { beforeEach(() => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1); variant.addVideo(2); }); manifest.addTextStream(3, (stream) => { stream.bandwidth = 100; stream.kind = 'caption'; stream.label = 'Spanish'; stream.language = 'es'; }); }); }); it('load text stream if caption is visible', async () => { await player.setTextTrackVisibility(true); await player.load(fakeManifestUri, 0, fakeMimeType); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); expect(shaka.test.Util.invokeSpy(streamingEngine.getCurrentTextStream)) .not.toBe(null); }); it('does not load text stream if caption is invisible', async () => { await player.setTextTrackVisibility(false); await player.load(fakeManifestUri, 0, fakeMimeType); expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); expect(shaka.test.Util.invokeSpy(streamingEngine.getCurrentTextStream)) .toBe(null); }); it('loads text stream if alwaysStreamText is set', async () => { await player.setTextTrackVisibility(false); player.configure({streaming: {alwaysStreamText: true}}); await player.load(fakeManifestUri, 0, fakeMimeType); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); expect(shaka.test.Util.invokeSpy(streamingEngine.getCurrentTextStream)) .not.toBe(null); streamingEngine.switchTextStream.calls.reset(); await player.setTextTrackVisibility(true); expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled(); await player.setTextTrackVisibility(false); expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled(); }); }); describe('when config.streaming.preferNativeHls is set to true', () => { beforeEach(() => { shaka.media.ManifestParser.registerParserByMime( 'application/x-mpegurl', () => new shaka.test.FakeManifestParser(manifest)); }); afterEach(() => { shaka.media.ManifestParser.unregisterParserByMime( 'application/x-mpegurl'); video.canPlayType.calls.reset(); }); it('only applies to HLS streams', async () => { video.canPlayType.and.returnValue('maybe'); spyOn(shaka.util.Platform, 'anyMediaElement').and.returnValue(video); spyOn(shaka.util.Platform, 'supportsMediaSource').and.returnValue(true); spyOn(shaka.util.Platform, 'isApple').and.returnValue(false); // Make sure player.load() resolves for src= spyOn(shaka.util.MediaReadyState, 'waitForReadyState').and.callFake( (mediaElement, readyState, eventManager, callback) => { callback(); }); player.configure({ streaming: { preferNativeHls: true, useNativeHlsOnSafari: false, }, }); await player.load(fakeManifestUri, undefined, 'application/x-mpegurl'); expect(player.getLoadMode()).toBe(shaka.Player.LoadMode.SRC_EQUALS); }); it('does not apply to non-HLS streams', async () => { video.canPlayType.and.returnValue('maybe'); spyOn(shaka.util.Platform, 'supportsMediaSource').and.returnValue(true); spyOn(shaka.util.Platform, 'isApple').and.returnValue(false); player.configure({ streaming: { preferNativeHls: true, useNativeHlsOnSafari: false, }, }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getLoadMode()).toBe(shaka.Player.LoadMode.MEDIA_SOURCE); }); }); }); // describe('load/unload') describe('getConfiguration', () => { it('returns a copy of the configuration', () => { const config1 = player.getConfiguration(); config1.streaming.bufferBehind = -99; const config2 = player.getConfiguration(); expect(config1.streaming.bufferBehind).not.toBe( config2.streaming.bufferBehind); }); }); describe('configure', () => { it('overwrites defaults', () => { const defaultConfig = player.getConfiguration(); // Make sure the default differs from our test value: expect(defaultConfig.drm.retryParameters.backoffFactor).not.toBe(5); expect(defaultConfig.manifest.retryParameters.backoffFactor).not.toBe(5); player.configure({ drm: { retryParameters: {backoffFactor: 5}, }, }); const newConfig = player.getConfiguration(); // Make sure we changed the backoff for DRM, but not for manifests: expect(newConfig.drm.retryParameters.backoffFactor).toBe(5); expect(newConfig.manifest.retryParameters.backoffFactor).not.toBe(5); }); it('reverts to defaults when undefined is given', () => { player.configure({ streaming: { retryParameters: {backoffFactor: 5}, bufferBehind: 7, }, }); let newConfig = player.getConfiguration(); expect(newConfig.streaming.retryParameters.backoffFactor).toBe(5); expect(newConfig.streaming.bufferBehind).toBe(7); player.configure({ streaming: { retryParameters: undefined, }, }); newConfig = player.getConfiguration(); expect(newConfig.streaming.retryParameters.backoffFactor).not.toBe(5); expect(newConfig.streaming.bufferBehind).toBe(7); player.configure({streaming: undefined}); newConfig = player.getConfiguration(); expect(newConfig.streaming.bufferBehind).not.toBe(7); }); it('restricts the types of config values', () => { logErrorSpy.and.stub(); const defaultConfig = player.getConfiguration(); // Try a bogus bufferBehind (string instead of number) player.configure({ streaming: {bufferBehind: '77'}, }); let newConfig = player.getConfiguration(); expect(newConfig).toEqual(defaultConfig); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.streaming.bufferBehind')); // Try a bogus streaming config (number instead of Object) logErrorSpy.calls.reset(); player.configure({ drm: 5, }); newConfig = player.getConfiguration(); expect(newConfig).toEqual(defaultConfig); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.drm')); }); it('accepts synchronous function values for async function fields', () => { const defaultConfig = player.getConfiguration(); // Make sure the default is async, or the test is invalid. const AsyncFunction = (async () => {}).constructor; expect(defaultConfig.offline.trackSelectionCallback.constructor) .toBe(AsyncFunction); // Try a synchronous callback. player.configure('offline.trackSelectionCallback', () => {}); // If this fails, an error log will trigger test failure. }); it('expands dictionaries that allow arbitrary keys', () => { player.configure({ drm: {servers: {'com.widevine.alpha': 'http://foo/widevine'}}, }); let newConfig = player.getConfiguration(); expect(newConfig.drm.servers).toEqual({ 'com.widevine.alpha': 'http://foo/widevine', }); player.configure({ drm: {servers: {'com.microsoft.playready': 'http://foo/playready'}}, }); newConfig = player.getConfiguration(); expect(newConfig.drm.servers).toEqual({ 'com.widevine.alpha': 'http://foo/widevine', 'com.microsoft.playready': 'http://foo/playready', }); }); it('expands dictionaries but still restricts their values', () => { // Try a bogus server value (number instead of string) logErrorSpy.and.stub(); player.configure({ drm: {servers: {'com.widevine.alpha': 7}}, }); let newConfig = player.getConfiguration(); expect(newConfig.drm.servers).toEqual({}); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.drm.servers.com.widevine.alpha')); // Try a valid advanced config. logErrorSpy.calls.reset(); player.configure({ drm: {advanced: {'ks1': {distinctiveIdentifierRequired: true}}}, }); newConfig = player.getConfiguration(); expect(newConfig.drm.advanced).toEqual({ 'ks1': jasmine.objectContaining({distinctiveIdentifierRequired: true}), }); expect(logErrorSpy).not.toHaveBeenCalled(); const lastGoodConfig = newConfig; // Try an invalid advanced config key. player.configure({ drm: {advanced: {'ks1': {bogus: true}}}, }); newConfig = player.getConfiguration(); expect(newConfig).toEqual(lastGoodConfig); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.drm.advanced.ks1.bogus')); }); it('removes dictionary entries when undefined is given', () => { player.configure({ drm: { servers: { 'com.widevine.alpha': 'http://foo/widevine', 'com.microsoft.playready': 'http://foo/playready', }, }, }); let newConfig = player.getConfiguration(); expect(newConfig.drm.servers).toEqual({ 'com.widevine.alpha': 'http://foo/widevine', 'com.microsoft.playready': 'http://foo/playready', }); player.configure({ drm: {servers: {'com.widevine.alpha': undefined}}, }); newConfig = player.getConfiguration(); expect(newConfig.drm.servers).toEqual({ 'com.microsoft.playready': 'http://foo/playready', }); player.configure({ drm: {servers: undefined}, }); newConfig = player.getConfiguration(); expect(newConfig.drm.servers).toEqual({}); }); it('checks the number of arguments to functions', () => { const goodFailureCallback = (error) => {}; const badFailureCallback1 = () => {}; // too few args const badFailureCallback2 = (x, y) => {}; // too many args // Takes good callback. player.configure({ streaming: {failureCallback: goodFailureCallback}, }); let newConfig = player.getConfiguration(); expect(newConfig.streaming.failureCallback).toBe(goodFailureCallback); expect(logWarnSpy).not.toHaveBeenCalled(); // Warns about bad callback #1, still takes it. logWarnSpy.calls.reset(); player.configure({ streaming: {failureCallback: badFailureCallback1}, }); newConfig = player.getConfiguration(); expect(newConfig.streaming.failureCallback).toBe(badFailureCallback1); expect(logWarnSpy).toHaveBeenCalledWith( stringContaining('.streaming.failureCallback')); // Warns about bad callback #2, still takes it. logWarnSpy.calls.reset(); player.configure({ streaming: {failureCallback: badFailureCallback2}, }); newConfig = player.getConfiguration(); expect(newConfig.streaming.failureCallback).toBe(badFailureCallback2); expect(logWarnSpy).toHaveBeenCalledWith( stringContaining('.streaming.failureCallback')); // Resets to default if undefined. logWarnSpy.calls.reset(); player.configure({ streaming: {failureCallback: undefined}, }); newConfig = player.getConfiguration(); expect(newConfig.streaming.failureCallback).not.toBe(badFailureCallback2); expect(logWarnSpy).not.toHaveBeenCalled(); }); // Regression test for https://github.com/shaka-project/shaka-player/issues/784 it('does not throw when overwriting serverCertificate', () => { player.configure({ drm: { advanced: { 'com.widevine.alpha': { serverCertificate: new Uint8Array(1), }, }, }, }); player.configure({ drm: { advanced: { 'com.widevine.alpha': { serverCertificate: new Uint8Array(2), }, }, }, }); }); it('checks the type of serverCertificate', () => { logErrorSpy.and.stub(); player.configure({ drm: { advanced: { 'com.widevine.alpha': { serverCertificate: null, }, }, }, }); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.serverCertificate')); logErrorSpy.calls.reset(); player.configure({ drm: { advanced: { 'com.widevine.alpha': { serverCertificate: 'foobar', }, }, }, }); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.serverCertificate')); }); it('does not throw when null appears instead of an object', () => { logErrorSpy.and.stub(); player.configure({ drm: {advanced: null}, }); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.drm.advanced')); }); it('configures play and seek range for VOD', async () => { const timeline = new shaka.media.PresentationTimeline(300, 0); timeline.setStatic(true); timeline.setDuration(300); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline = timeline; manifest.addVariant(0, (variant) => { variant.addVideo(1); }); }); goog.asserts.assert(manifest, 'manifest must be non-null'); player.configure({playRangeStart: 5, playRangeEnd: 10}); await player.load(fakeManifestUri, 0, fakeMimeType); const seekRange = player.seekRange(); expect(seekRange.start).toBe(5); expect(seekRange.end).toBe(10); }); // Test for https://github.com/shaka-project/shaka-player/issues/4026 it('configures play and seek range with notifySegments', async () => { const timeline = new shaka.media.PresentationTimeline(300, 0); timeline.setStatic(true); // This duration is used by useSegmentTemplate below to decide how many // references to generate. timeline.setDuration(300); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline = timeline; manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.useSegmentTemplate( '$Number$.mp4', /* segmentDuration= */ 10); }); }); }); goog.asserts.assert(manifest, 'manifest must be non-null'); // Explicitly notify the timeline of the segment references. const videoStream = manifest.variants[0].video; await videoStream.createSegmentIndex(); goog.asserts.assert(videoStream.segmentIndex, 'SegmentIndex must be non-null'); const references = Array.from(videoStream.segmentIndex); goog.asserts.assert(references.length != 0, 'Must have references for this test!'); timeline.notifySegments(references); player.configure({playRangeStart: 5, playRangeEnd: 10}); await player.load(fakeManifestUri, 0, fakeMimeType); const seekRange = player.seekRange(); expect(seekRange.start).toBe(5); expect(seekRange.end).toBe(10); }); it('configures play and seek range after playback starts', async () => { const timeline = new shaka.media.PresentationTimeline(300, 0); timeline.setStatic(true); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline = timeline; manifest.addVariant(0, (variant) => { variant.addVideo(1); }); }); goog.asserts.assert(manifest, 'manifest must be non-null'); await player.load(fakeManifestUri, 0, fakeMimeType); const seekRange = player.seekRange(); expect(seekRange.start).toBe(0); expect(seekRange.end).toBe(Infinity); // Change the configuration after the playback starts. player.configure({playRangeStart: 5, playRangeEnd: 10}); const seekRange2 = player.seekRange(); expect(seekRange2.start).toBe(5); expect(seekRange2.end).toBe(10); }); it('does not switch for plain configuration changes', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); streamingEngine.switchVariant.calls.reset(); player.configure({abr: {enabled: false}}); player.configure({streaming: {bufferingGoal: 9001}}); // Delay to ensure that the switch would have been called. await shaka.test.Util.shortDelay(); expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); }); it('accepts parameters in a (fieldName, value) format', () => { const oldConfig = player.getConfiguration(); const oldDelayLicense = oldConfig.drm.delayLicenseRequestUntilPlayed; const oldSwitchInterval = oldConfig.abr.switchInterval; const oldPreferredLang = oldConfig.preferredAudioLanguage; expect(oldDelayLicense).toBe(false); expect(oldSwitchInterval).toBe(8); expect(oldPreferredLang).toBe('en'); player.configure('drm.delayLicenseRequestUntilPlayed', true); player.configure('abr.switchInterval', 10); player.configure('preferredAudioLanguage', 'fr'); const newConfig = player.getConfiguration(); const newDelayLicense = newConfig.drm.delayLicenseRequestUntilPlayed; const newSwitchInterval = newConfig.abr.switchInterval; const newPreferredLang = newConfig.preferredAudioLanguage; expect(newDelayLicense).toBe(true); expect(newSwitchInterval).toBe(10); expect(newPreferredLang).toBe('fr'); }); it('accepts escaped "." in names', () => { const convert = (name, value) => { return shaka.util.ConfigUtils.convertToConfigObject(name, value); }; expect(convert('foo', 1)).toEqual({foo: 1}); expect(convert('foo.bar', 1)).toEqual({foo: {bar: 1}}); expect(convert('foo..bar', 1)).toEqual({foo: {'': {bar: 1}}}); expect(convert('foo.bar.baz', 1)).toEqual({foo: {bar: {baz: 1}}}); expect(convert('foo.bar\\.baz', 1)).toEqual({foo: {'bar.baz': 1}}); expect(convert('foo.baz.', 1)).toEqual({foo: {baz: {'': 1}}}); expect(convert('foo.baz\\.', 1)).toEqual({foo: {'baz.': 1}}); expect(convert('foo\\.bar', 1)).toEqual({'foo.bar': 1}); expect(convert('.foo', 1)).toEqual({'': {foo: 1}}); expect(convert('\\.foo', 1)).toEqual({'.foo': 1}); }); it('returns whether the config was valid', () => { logErrorSpy.and.stub(); expect(player.configure({streaming: {bufferBehind: '77'}})).toBe(false); expect(player.configure({streaming: {bufferBehind: 77}})).toBe(true); }); it('still sets other fields when there are errors', () => { logErrorSpy.and.stub(); const changes = { manifest: {foobar: false}, streaming: {bufferBehind: 77}, }; expect(player.configure(changes)).toBe(false); const newConfig = player.getConfiguration(); expect(newConfig.streaming.bufferBehind).toBe(77); }); // https://github.com/shaka-project/shaka-player/issues/1524 it('does not pollute other advanced DRM configs', () => { player.configure('drm.advanced.foo', {}); player.configure('drm.advanced.bar', {}); const fooConfig1 = player.getConfiguration().drm.advanced['foo']; const barConfig1 = player.getConfiguration().drm.advanced['bar']; expect(fooConfig1.distinctiveIdentifierRequired).toBe(false); expect(barConfig1.distinctiveIdentifierRequired).toBe(false); player.configure('drm.advanced.foo.distinctiveIdentifierRequired', true); const fooConfig2 = player.getConfiguration().drm.advanced['foo']; const barConfig2 = player.getConfiguration().drm.advanced['bar']; expect(fooConfig2.distinctiveIdentifierRequired).toBe(true); expect(barConfig2.distinctiveIdentifierRequired).toBe(false); }); it('sets default streaming configuration with low latency mode', () => { player.configure({ streaming: { lowLatencyMode: true, rebufferingGoal: 1, inaccurateManifestTolerance: 1, segmentPrefetchLimit: 1, }, }); expect(player.getConfiguration().streaming.rebufferingGoal).toBe(1); expect(player.getConfiguration().streaming.inaccurateManifestTolerance) .toBe(1); expect(player.getConfiguration().streaming.segmentPrefetchLimit).toBe(1); // When low latency streaming gets enabled, rebufferingGoal will default // to 0.01 if unless specified, inaccurateManifestTolerance will // default to 0 unless specified, and segmentPrefetchLimit will // default to 2 unless specified. player.configure('streaming.lowLatencyMode', true); expect(player.getConfiguration().streaming.rebufferingGoal).toBe(0.01); expect(player.getConfiguration().streaming.inaccurateManifestTolerance) .toBe(0); expect(player.getConfiguration().streaming.segmentPrefetchLimit).toBe(2); }); }); describe('resetConfiguration', () => { it('resets configurations to default', () => { const default_ = player.getConfiguration().streaming.bufferingGoal; expect(default_).not.toBe(100); player.configure('streaming.bufferingGoal', 100); expect(player.getConfiguration().streaming.bufferingGoal).toBe(100); player.resetConfiguration(); expect(player.getConfiguration().streaming.bufferingGoal).toBe(default_); }); it('resets the arbitrary keys', () => { player.configure('drm.servers.org\\.w3\\.clearKey', 'http://foo.com'); expect(player.getConfiguration().drm.servers).toEqual({ 'org.w3.clearKey': 'http://foo.com', }); player.resetConfiguration(); expect(player.getConfiguration().drm.servers).toEqual({}); }); it('keeps shared configuration the same', () => { const config = player.getSharedConfiguration(); player.resetConfiguration(); expect(player.getSharedConfiguration()).toBe(config); }); }); describe('AbrManager', () => { beforeEach(() => { goog.asserts.assert(manifest, 'manifest must be non-null'); }); it('sets through load', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.init).toHaveBeenCalled(); }); it('calls chooseVariant', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.chooseVariant).toHaveBeenCalled(); }); it('enables automatically', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.enable).toHaveBeenCalled(); }); it('does not enable if adaptation is disabled', async () => { player.configure({abr: {enabled: false}}); await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.enable).not.toHaveBeenCalled(); }); it('enables/disables though configure', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); abrManager.enable.calls.reset(); abrManager.disable.calls.reset(); player.configure({abr: {enabled: false}}); expect(abrManager.disable).toHaveBeenCalled(); player.configure({abr: {enabled: true}}); expect(abrManager.enable).toHaveBeenCalled(); }); it('reuses AbrManager instance', async () => { /** @type {!jasmine.Spy} */ const spy = jasmine.createSpy('AbrManagerFactory').and.returnValue(abrManager); player.configure({abrFactory: spy}); await player.load(fakeManifestUri, 0, fakeMimeType); expect(spy).toHaveBeenCalled(); spy.calls.reset(); await player.load(fakeManifestUri, 0, fakeMimeType); expect(spy).not.toHaveBeenCalled(); }); it('creates new AbrManager if factory changes', async () => { /** @type {!jasmine.Spy} */ const spy1 = jasmine.createSpy('AbrManagerFactory').and.returnValue(abrManager); /** @type {!jasmine.Spy} */ const spy2 = jasmine.createSpy('AbrManagerFactory').and.returnValue(abrManager); player.configure({abrFactory: spy1}); await player.load(fakeManifestUri, 0, fakeMimeType); expect(spy1).toHaveBeenCalled(); expect(spy2).not.toHaveBeenCalled(); spy1.calls.reset(); player.configure({abrFactory: spy2}); await player.load(fakeManifestUri, 0, fakeMimeType); expect(spy1).not.toHaveBeenCalled(); expect(spy2).toHaveBeenCalled(); }); }); describe('filterTracks', () => { it('retains only video+audio variants if they exist', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(10, (variant) => { variant.addAudio(1); }); manifest.addVariant(11, (variant) => { variant.addAudio(2); variant.addVideo(3); }); manifest.addVariant(12, (variant) => { variant.addVideo(4); }); }); const variantTracks = [ jasmine.objectContaining({ id: 11, active: true, type: 'variant', }), ]; await player.load(fakeManifestUri, 0, fakeMimeType); const actualVariantTracks = player.getVariantTracks(); expect(actualVariantTracks).toEqual(variantTracks); }); }); describe('tracks', () => { /** @type {!Array.<shaka.extern.Track>} */ let variantTracks; /** @type {!Array.<shaka.extern.Track>} */ let textTracks; /** @type {!Array.<shaka.extern.Track>} */ let imageTracks; beforeEach(async () => { // A manifest we can use to test track expectations. manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(100, (variant) => { // main surround, low res variant.bandwidth = 1300; variant.language = 'en'; variant.addVideo(1, (stream) => { stream.originalId = 'video-1kbps'; stream.bandwidth = 1000; stream.width = 100; stream.height = 200; stream.frameRate = 1000000 / 42000; stream.pixelAspectRatio = '59:54'; stream.roles = ['main']; }); variant.addAudio(3, (stream) => { stream.originalId = 'audio-en-6c'; stream.bandwidth = 300; stream.channelsCount = 6; stream.audioSamplingRate = 48000; stream.roles = ['main']; }); }); manifest.addVariant(101, (variant) => { // main surround, high res variant.bandwidth = 2300; variant.language = 'en'; variant.addVideo(2, (stream) => { stream.originalId = 'video-2kbps'; stream.bandwidth = 2000; stream.frameRate = 24; stream.pixelAspectRatio = '59:54'; stream.size(200, 400); }); variant.addExistingStream(3); // audio }); manifest.addVariant(102, (variant) => { // main stereo, low res variant.bandwidth = 1100; variant.language = 'en'; variant.addExistingStream(1); // video variant.addAudio(4, (stream) => { stream.originalId = 'audio-en-2c'; stream.bandwidth = 100; stream.channelsCount = 2; stream.audioSamplingRate = 48000; stream.ro