/*! @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 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; 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'; }; // 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(3); variant.addVideo(4); }); }); shaka.media.ManifestParser.registerParserByMime( fakeMimeType, () => new shaka.test.FakeManifestParser(manifest)); abrManager = new shaka.test.FakeAbrManager(); textDisplayer = createTextDisplayer(); 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()), open: jasmine.createSpy('open').and.returnValue(Promise.resolve()), destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()), setUseEmbeddedText: jasmine.createSpy('setUseEmbeddedText'), getUseEmbeddedText: jasmine.createSpy('getUseEmbeddedText'), getTextDisplayer: () => textDisplayer, ended: jasmine.createSpy('ended').and.returnValue(false), }; player.createDrmEngine = () => drmEngine; player.createNetworkingEngine = () => networkingEngine; player.createPlayhead = () => 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); } }); describe('destroy', () => { it('cleans up all dependencies', async () => { goog.asserts.assert(manifest, 'Manifest should be non-null'); await player.load(fakeManifestUri, 0, fakeMimeType); await player.destroy(); expect(abrManager.stop).toHaveBeenCalled(); expect(networkingEngine.destroy).toHaveBeenCalled(); expect(drmEngine.destroy).toHaveBeenCalled(); expect(playhead.release).toHaveBeenCalled(); expect(mediaSourceEngine.destroy).toHaveBeenCalled(); expect(streamingEngine.destroy).toHaveBeenCalled(); 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) { 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(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(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/google/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('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('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({ streaming: 5, }); newConfig = player.getConfiguration(); expect(newConfig).toEqual(defaultConfig); expect(logErrorSpy).toHaveBeenCalledWith( stringContaining('.streaming')); }); 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/google/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 () => { player.configure({playRangeStart: 5, playRangeEnd: 10}); 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(5); expect(seekRange.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/google/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); }); }); 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.} */ let variantTracks; /** @type {!Array.} */ let textTracks; 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.roles = ['main']; }); }); manifest.addVariant(103, (variant) => { // main stereo, high res variant.bandwidth = 2100; variant.language = 'en'; variant.addExistingStream(2); // video variant.addExistingStream(4); // audio }); manifest.addVariant(104, (variant) => { // commentary stereo, low res variant.bandwidth = 1100; variant.language = 'en'; variant.addExistingStream(1); // video variant.addAudio(5, (stream) => { stream.originalId = 'audio-commentary'; stream.bandwidth = 100; stream.channelsCount = 2; stream.audioSamplingRate = 48000; stream.roles = ['commentary']; }); }); manifest.addVariant(105, (variant) => { // commentary stereo, low res variant.bandwidth = 2100; variant.language = 'en'; variant.addExistingStream(2); // video variant.addExistingStream(5); // audio }); manifest.addVariant(106, (variant) => { // spanish stereo, low res variant.language = 'es'; variant.bandwidth = 1100; variant.addExistingStream(1); // video variant.addAudio(6, (stream) => { stream.originalId = 'audio-es'; stream.bandwidth = 100; stream.channelsCount = 2; stream.audioSamplingRate = 48000; }); }); manifest.addVariant(107, (variant) => { // spanish stereo, high res variant.language = 'es'; variant.bandwidth = 2100; variant.addExistingStream(2); // video variant.addExistingStream(6); // audio }); // All text tracks should remain, even with different MIME types. manifest.addTextStream(50, (stream) => { stream.originalId = 'text-es'; stream.language = 'es'; stream.label = 'Spanish'; stream.bandwidth = 10; stream.mimeType = 'text/vtt'; stream.kind = 'caption'; }); manifest.addTextStream(51, (stream) => { stream.originalId = 'text-en'; stream.language = 'en'; stream.label = 'English'; stream.bandwidth = 10; stream.mimeType = 'application/ttml+xml'; stream.kind = 'caption'; stream.roles = ['main']; }); manifest.addTextStream(52, (stream) => { stream.originalId = 'text-commentary'; stream.language = 'en'; stream.label = 'English'; stream.bandwidth = 10; stream.mimeType = 'application/ttml+xml'; stream.kind = 'caption'; stream.roles = ['commentary']; }); }); variantTracks = [ { id: 100, active: true, type: 'variant', bandwidth: 1300, language: 'en', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], audioRoles: ['main'], videoId: 1, audioId: 3, channelsCount: 6, audioSamplingRate: 48000, audioBandwidth: 300, videoBandwidth: 1000, originalAudioId: 'audio-en-6c', originalVideoId: 'video-1kbps', originalTextId: null, }, { id: 101, active: false, type: 'variant', bandwidth: 2300, language: 'en', label: null, kind: null, width: 200, height: 400, frameRate: 24, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], audioRoles: ['main'], videoId: 2, audioId: 3, channelsCount: 6, audioSamplingRate: 48000, audioBandwidth: 300, videoBandwidth: 2000, originalAudioId: 'audio-en-6c', originalVideoId: 'video-2kbps', originalTextId: null, }, { id: 102, active: false, type: 'variant', bandwidth: 1100, language: 'en', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], audioRoles: ['main'], videoId: 1, audioId: 4, channelsCount: 2, audioSamplingRate: 48000, audioBandwidth: 100, videoBandwidth: 1000, originalAudioId: 'audio-en-2c', originalVideoId: 'video-1kbps', originalTextId: null, }, { id: 103, active: false, type: 'variant', bandwidth: 2100, language: 'en', label: null, kind: null, width: 200, height: 400, frameRate: 24, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], audioRoles: ['main'], videoId: 2, audioId: 4, channelsCount: 2, audioSamplingRate: 48000, audioBandwidth: 100, videoBandwidth: 2000, originalAudioId: 'audio-en-2c', originalVideoId: 'video-2kbps', originalTextId: null, }, { id: 104, active: false, type: 'variant', bandwidth: 1100, language: 'en', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: ['commentary', 'main'], audioRoles: ['commentary'], videoId: 1, audioId: 5, channelsCount: 2, audioSamplingRate: 48000, audioBandwidth: 100, videoBandwidth: 1000, originalAudioId: 'audio-commentary', originalVideoId: 'video-1kbps', originalTextId: null, }, { id: 105, active: false, type: 'variant', bandwidth: 2100, language: 'en', label: null, kind: null, width: 200, height: 400, frameRate: 24, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: ['commentary'], audioRoles: ['commentary'], videoId: 2, audioId: 5, channelsCount: 2, audioSamplingRate: 48000, audioBandwidth: 100, videoBandwidth: 2000, originalAudioId: 'audio-commentary', originalVideoId: 'video-2kbps', originalTextId: null, }, { id: 106, active: false, type: 'variant', bandwidth: 1100, language: 'es', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], audioRoles: [], videoId: 1, audioId: 6, channelsCount: 2, audioSamplingRate: 48000, audioBandwidth: 100, videoBandwidth: 1000, originalAudioId: 'audio-es', originalVideoId: 'video-1kbps', originalTextId: null, }, { id: 107, active: false, type: 'variant', bandwidth: 2100, language: 'es', label: null, kind: null, width: 200, height: 400, frameRate: 24, pixelAspectRatio: '59:54', mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], audioRoles: [], videoId: 2, audioId: 6, channelsCount: 2, audioSamplingRate: 48000, audioBandwidth: 100, videoBandwidth: 2000, originalAudioId: 'audio-es', originalVideoId: 'video-2kbps', originalTextId: null, }, ]; textTracks = [ { id: 50, active: true, type: ContentType.TEXT, language: 'es', label: 'Spanish', kind: 'caption', mimeType: 'text/vtt', codecs: null, audioCodec: null, videoCodec: null, primary: false, roles: [], audioRoles: null, channelsCount: null, audioSamplingRate: null, audioBandwidth: null, videoBandwidth: null, bandwidth: 0, width: null, height: null, frameRate: null, pixelAspectRatio: null, videoId: null, audioId: null, originalAudioId: null, originalVideoId: null, originalTextId: 'text-es', }, { id: 51, active: false, type: ContentType.TEXT, language: 'en', label: 'English', kind: 'caption', mimeType: 'application/ttml+xml', codecs: null, audioCodec: null, videoCodec: null, primary: false, roles: ['main'], audioRoles: null, channelsCount: null, audioSamplingRate: null, audioBandwidth: null, videoBandwidth: null, bandwidth: 0, width: null, height: null, frameRate: null, pixelAspectRatio: null, videoId: null, audioId: null, originalAudioId: null, originalVideoId: null, originalTextId: 'text-en', }, { id: 52, active: false, type: ContentType.TEXT, language: 'en', label: 'English', kind: 'caption', mimeType: 'application/ttml+xml', codecs: null, audioCodec: null, videoCodec: null, primary: false, roles: ['commentary'], audioRoles: null, channelsCount: null, audioSamplingRate: null, audioBandwidth: null, videoBandwidth: null, bandwidth: 0, width: null, height: null, frameRate: null, pixelAspectRatio: null, videoId: null, audioId: null, originalAudioId: null, originalVideoId: null, originalTextId: 'text-commentary', }, ]; goog.asserts.assert(manifest, 'manifest must be non-null'); // Language/channel prefs must be set before load. Used in // select*Language() tests. player.configure({ preferredAudioLanguage: 'en', preferredTextLanguage: 'es', preferredAudioChannelCount: 6, }); await player.load(fakeManifestUri, 0, fakeMimeType); streamingEngine.switchVariant.calls.reset(); streamingEngine.switchTextStream.calls.reset(); }); it('returns the correct tracks', () => { expect(player.getVariantTracks()).toEqual(variantTracks); expect(player.getTextTracks()).toEqual(textTracks); }); it('returns empty arrays before tracks can be determined', async () => { const parser = new shaka.test.FakeManifestParser(manifest); parser.start.and.callFake((manifestUri, playerInterface) => { // The player does not yet have a manifest. expect(player.getVariantTracks()).toEqual([]); expect(player.getTextTracks()).toEqual([]); parser.playerInterface = playerInterface; return Promise.resolve(manifest); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks()).toEqual(variantTracks); expect(player.getTextTracks()).toEqual(textTracks); }); it('doesn\'t disable AbrManager if switching variants', () => { let config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; player.selectVariantTrack(newTrack); config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); }); it('doesn\'t disable AbrManager if switching text', () => { let config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); const newTrack = player.getTextTracks().filter((t) => !t.active)[0]; player.selectTextTrack(newTrack); config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); }); it('switches streams', () => { const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; player.selectVariantTrack(newTrack); expect(streamingEngine.switchVariant).toHaveBeenCalled(); const variant = streamingEngine.switchVariant.calls.argsFor(0)[0]; expect(variant.id).toBe(newTrack.id); }); it('switching audio doesn\'t change selected text track', () => { player.configure({ preferredTextLanguage: 'es', }); // We will manually switch from Spanish to English. const englishTextTrack = player.getTextTracks().filter((t) => t.language == 'en')[0]; player.selectTextTrack(englishTextTrack); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); // We have selected an English text track explicitly. expect(getActiveTextTrack().id).toBe(englishTextTrack.id); const newVariantTrack = player.getVariantTracks().filter((t) => !t.active)[0]; player.selectVariantTrack(newVariantTrack); // The active text track has not changed, even though the text language // preference is Spanish. expect(getActiveTextTrack().id).toBe(englishTextTrack.id); }); it('selectAudioLanguage() takes precedence over ' + 'preferredAudioLanguage', () => { // This preference is set in beforeEach, before load(). expect(player.getConfiguration().preferredAudioLanguage).toBe('en'); expect(getActiveVariantTrack().language).toBe('en'); player.selectAudioLanguage('es'); expect(streamingEngine.switchVariant).toHaveBeenCalled(); const args = streamingEngine.switchVariant.calls.argsFor(0); expect(args[0].language).toBe('es'); expect(args[1]).toBe(true); expect(getActiveVariantTrack().language).toBe('es'); }); it('selectAudioLanguage() respects selected role', () => { expect(getActiveVariantTrack().roles).not.toContain('commentary'); player.selectAudioLanguage('en', 'commentary'); expect(streamingEngine.switchVariant).toHaveBeenCalled(); const args = streamingEngine.switchVariant.calls.argsFor(0); expect(args[0].audio.roles).toContain('commentary'); expect(args[1]).toBe(true); expect(getActiveVariantTrack().roles).toContain('commentary'); }); it('selectAudioLanguage() applies role only to audio', () => { expect(getActiveVariantTrack().roles).not.toContain('commentary'); player.selectAudioLanguage('en', 'commentary'); let args = streamingEngine.switchVariant.calls.argsFor(0); expect(args[0].audio.roles).toContain('commentary'); expect(args[0].video.roles).toContain('main'); // Switch audio role from 'commentary' to 'main'. streamingEngine.switchVariant.calls.reset(); player.selectAudioLanguage('en', 'main'); expect(streamingEngine.switchVariant).toHaveBeenCalled(); args = streamingEngine.switchVariant.calls.argsFor(0); expect(args[0].audio.roles).toContain('main'); expect(args[0].video.roles).toContain('main'); }); it('selectAudioLanguage() does not change selected text track', () => { // This came up in a custom application that allows to select // from all tracks regardless of selected language. // We imitate this behavior by calling selectTextLanguage() // with one language and then selecting a track in a different // language. player.selectTextLanguage('en'); const spanishTextTrack = textTracks.filter((t) => t.language == 'es')[0]; player.selectTextTrack(spanishTextTrack); player.selectAudioLanguage('es'); expect(getActiveTextTrack().id).toBe(spanishTextTrack.id); }); it('selectTextLanguage() does not change selected variant track', () => { // This came up in a custom application that allows to select // from all tracks regardless of selected language. // We imitate this behavior by calling selectAudioLanguage() // with one language and then selecting a track in a different // language. player.selectAudioLanguage('es'); const englishVariantTrack = variantTracks.filter((t) => t.language == 'en')[0]; player.selectVariantTrack(englishVariantTrack); player.selectTextLanguage('es'); expect(getActiveVariantTrack().id).toBe(englishVariantTrack.id); }); it('selectTextLanguage() takes precedence over ' + 'preferredTextLanguage', () => { // This preference is set in beforeEach, before load(). expect(player.getConfiguration().preferredTextLanguage).toBe('es'); expect(getActiveTextTrack().language).toBe('es'); player.selectTextLanguage('en'); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); const args = streamingEngine.switchTextStream.calls.argsFor(0); expect(args[0].language).toBe('en'); expect(getActiveTextTrack().language).toBe('en'); }); it('selectTextLanguage() respects selected role', () => { expect(getActiveTextTrack().roles).not.toContain('commentary'); player.selectTextLanguage('en', 'commentary'); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); const args = streamingEngine.switchTextStream.calls.argsFor(0); expect(args[0].roles).toContain('commentary'); expect(getActiveTextTrack().roles).toContain('commentary'); }); it('changing current audio language changes active stream', () => { expect(getActiveVariantTrack().language).not.toBe('es'); expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); player.selectAudioLanguage('es'); expect(streamingEngine.switchVariant).toHaveBeenCalled(); const args = streamingEngine.switchVariant.calls.argsFor(0); expect(args[0].language).toBe('es'); expect(args[1]).toBe(true); expect(getActiveVariantTrack().language).toBe('es'); }); it('changing current text language changes active stream', () => { expect(getActiveTextTrack().language).not.toBe('en'); expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); player.selectTextLanguage('en'); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); const args = streamingEngine.switchTextStream.calls.argsFor(0); expect(args[0].language).toBe('en'); expect(getActiveTextTrack().language).toBe('en'); }); // https://github.com/google/shaka-player/issues/2010 it('changing text lang changes active stream when not streaming', () => { player.setTextTrackVisibility(false); expect(getActiveTextTrack()).toBe(null); expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); player.selectTextLanguage('en'); player.setTextTrackVisibility(true); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); expect(getActiveTextTrack().language).toBe('en'); }); it('remembers the channel count when ABR is reenabled', () => { // We prefer 6 channels, and we are currently playing 6 channels. expect(player.getConfiguration().preferredAudioChannelCount).toBe(6); expect(getActiveVariantTrack().channelsCount).toBe(6); // Manually turn off ABR and select a 2-channel track. player.configure({abr: {enabled: false}}); const newTrack = player.getVariantTracks().filter((t) => t.channelsCount == 2)[0]; player.selectVariantTrack(newTrack); // See that we are playing a 2-channel track now. expect(getActiveVariantTrack().channelsCount).toBe(2); // See that AbrManager has a list of 2-channel tracks now. expect(abrManager.variants.length).toBeGreaterThan(0); for (const v of abrManager.variants) { expect(v.audio.channelsCount).toBe(2); } // Re-enable ABR. player.configure({abr: {enabled: true}}); // See that AbrManager still has a list of 2-channel tracks. expect(abrManager.variants.length).toBeGreaterThan(0); for (const v of abrManager.variants) { expect(v.audio.channelsCount).toBe(2); } // See that we are still playing a 2-channel track. expect(getActiveVariantTrack().channelsCount).toBe(2); }); it('remembers the channel count across key status changes', () => { // Simulate an encrypted stream. Mark half of the audio streams with key // ID 'aaa', and the other half with 'bbb'. Remove all roles, so that our // choices are limited only by channel count and key status. for (const variant of manifest.variants) { const keyId = (variant.audio.id % 2) ? 'aaa' : 'bbb'; variant.audio.keyIds = new Set([keyId]); variant.video.roles = []; variant.audio.roles = []; } // We prefer 6 channels, and we are currently playing 6 channels. expect(player.getConfiguration().preferredAudioChannelCount).toBe(6); expect(getActiveVariantTrack().channelsCount).toBe(6); // Manually select a 2-channel track. const newTrack = player.getVariantTracks().filter((t) => t.channelsCount == 2)[0]; player.selectVariantTrack(newTrack); // See that we are playing a 2-channel track now. expect(getActiveVariantTrack().channelsCount).toBe(2); // See that AbrManager has a list of 2-channel tracks now. expect(abrManager.variants.length).toBeGreaterThan(0); for (const v of abrManager.variants) { expect(v.audio.channelsCount).toBe(2); } // Simulate a key status event that would trigger the removal of some // tracks. onKeyStatus({ 'aaa': 'usable', 'bbb': 'output-restricted', }); // See that AbrManager still has a list of 2-channel tracks. expect(abrManager.variants.length).toBeGreaterThan(0); for (const v of abrManager.variants) { expect(v.audio.channelsCount).toBe(2); } // See that we are still playing a 2-channel track. expect(getActiveVariantTrack().channelsCount).toBe(2); }); describe('only fires change event when something changes', () => { /** @type {jasmine.Spy} */ let textChanged; /** @type {jasmine.Spy} */ let variantChanged; beforeEach(() => { textChanged = jasmine.createSpy('textChanged'); player.addEventListener('textchanged', Util.spyFunc(textChanged)); variantChanged = jasmine.createSpy('variantChanged'); player.addEventListener('variantchanged', Util.spyFunc(variantChanged)); }); it('in selectTextTrack', async () => { // Any text track we're not already streaming. const newTrack = player.getTextTracks().filter((t) => !t.active)[0]; // Call selectTextTrack with a new track. Expect an event to fire. player.selectTextTrack(newTrack); await shaka.test.Util.shortDelay(); expect(textChanged).toHaveBeenCalled(); textChanged.calls.reset(); // Call again with the same track, and expect no event to fire, since // nothing changed this time. player.selectTextTrack(newTrack); await shaka.test.Util.shortDelay(); expect(textChanged).not.toHaveBeenCalled(); }); it('in selectVariantTrack', async () => { // Any variant track we're not already streaming. const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; // Call selectVariantTrack with a new track. Expect an event to fire. player.selectVariantTrack(newTrack); await shaka.test.Util.shortDelay(); expect(variantChanged).toHaveBeenCalled(); variantChanged.calls.reset(); // Call again with the same track, and expect no event to fire, since // nothing changed this time. player.selectVariantTrack(newTrack); await shaka.test.Util.shortDelay(); expect(variantChanged).not.toHaveBeenCalled(); }); it('in selectTextLanguage', async () => { // The current text language. const currentLanguage = player.getTextTracks() .filter((t) => t.active)[0].language; const newLanguage = player.getTextTracks() .filter((t) => t.language != currentLanguage)[0].language; // Call selectTextLanguage with a new language. Expect an event to // fire. player.selectTextLanguage(newLanguage); await shaka.test.Util.shortDelay(); expect(textChanged).toHaveBeenCalled(); textChanged.calls.reset(); // Call again with the same language, and expect no event to fire, // since nothing changed this time. player.selectTextLanguage(newLanguage); await shaka.test.Util.shortDelay(); expect(textChanged).not.toHaveBeenCalled(); }); it('in selectAudioLanguage', async () => { // The current audio language. const currentLanguage = player.getVariantTracks() .filter((t) => t.active)[0].language; const newLanguage = player.getVariantTracks() .filter((t) => t.language != currentLanguage)[0].language; // Call selectAudioLanguage with a new language. Expect an event to // fire. player.selectAudioLanguage(newLanguage); await shaka.test.Util.shortDelay(); expect(variantChanged).toHaveBeenCalled(); variantChanged.calls.reset(); // Call again with the same language, and expect no event to fire, // since nothing changed this time. player.selectAudioLanguage(newLanguage); await shaka.test.Util.shortDelay(); expect(variantChanged).not.toHaveBeenCalled(); }); }); }); // describe('tracks') describe('languages', () => { it('chooses the first as default', async () => { await runTest(['en', 'es'], 'pt', 0); }); it('chooses the primary track', async () => { await runTest(['en', 'es', '*fr'], 'pt', 2); }); it('chooses exact match for main language', async () => { await runTest(['en', 'pt', 'pt-BR'], 'pt', 1); }); it('chooses exact match for subtags', async () => { await runTest(['en', 'pt', 'pt-BR'], 'PT-BR', 2); }); it('chooses base language if exact does not exist', async () => { await runTest(['en', 'es', 'pt'], 'pt-BR', 2); }); it('chooses other subtags if base language does not exist', async () => { await runTest(['en', 'es', 'pt-BR'], 'pt-PT', 2); }); it('enables text if its language differs from audio at start', async () => { // A manifest we can use to test text visibility. manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.language = 'pt'; variant.addAudio(0); }); manifest.addVariant(1, (variant) => { variant.language = 'en'; variant.addAudio(1); }); manifest.addTextStream(2, (stream) => { stream.language = 'pt'; }); manifest.addTextStream(3, (stream) => { stream.language = 'fr'; }); }); player.configure({ preferredAudioLanguage: 'en', preferredTextLanguage: 'fr', }); expect(player.isTextTrackVisible()).toBe(false); await player.load(fakeManifestUri, 0, fakeMimeType); // Text was turned on during startup. expect(player.isTextTrackVisible()).toBe(true); // Turn text back off. await player.setTextTrackVisibility(false); expect(player.isTextTrackVisible()).toBe(false); // Change text languages after startup. player.selectTextLanguage('pt'); // This should not turn text back on. expect(player.isTextTrackVisible()).toBe(false); }); it('chooses an arbitrary language when none given', async () => { // The Player shouldn't allow changing between languages, so it should // choose an arbitrary language when none is given. manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.language = 'pt'; variant.addAudio(0); }); manifest.addVariant(1, (variant) => { variant.language = 'en'; variant.addAudio(1); }); }); player.configure({ preferredAudioLanguage: undefined, }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.setVariants).toHaveBeenCalled(); // If we have chosen any arbitrary language, setVariants is provided // with exactly one variant. const variants = abrManager.setVariants.calls.argsFor(0)[0]; expect(variants.length).toBe(1); }); /** * @param {!Array.} languages * @param {string} preference * @param {number} expectedIndex * @return {!Promise} */ async function runTest(languages, preference, expectedIndex) { // A manifest we can use to test language selection. manifest = shaka.test.ManifestGenerator.generate((manifest) => { const enumerate = (it) => shaka.util.Iterables.enumerate(it); for (const {i, item: lang} of enumerate(languages)) { if (lang.charAt(0) == '*') { manifest.addVariant(i, (variant) => { variant.primary = true; variant.language = lang.substr(1); variant.addAudio(i); }); } else { manifest.addVariant(i, (variant) => { variant.language = lang; variant.addAudio(i); }); } } }); // Set the user preferences, which must happen before load(). player.configure({ preferredAudioLanguage: preference, }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(getActiveVariantTrack().id).toBe(expectedIndex); } }); // describe('languages') describe('getStats', () => { const oldDateNow = Date.now; beforeEach(async () => { Date.now = () => 0; // The media element may be paused in a test, make sure that it is reset // to avoid cross-test contamination. video.paused = false; // A manifest we can use to test stats. manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.bandwidth = 200; variant.addAudio(1, (stream) => { stream.bandwidth = 100; }); variant.addVideo(2, (stream) => { stream.bandwidth = 100; stream.size(100, 200); }); }); manifest.addVariant(1, (variant) => { variant.bandwidth = 300; variant.addExistingStream(1); // audio variant.addVideo(3, (stream) => { stream.bandwidth = 200; stream.size(200, 400); }); }); manifest.addVariant(2, (variant) => { variant.bandwidth = 300; variant.addAudio(4, (stream) => { stream.bandwidth = 200; }); variant.addExistingStream(2); // video }); manifest.addVariant(3, (variant) => { variant.bandwidth = 400; variant.addExistingStream(4); // audio variant.addExistingStream(3); // video }); }); await player.load(fakeManifestUri, 0, fakeMimeType); }); afterEach(() => { Date.now = oldDateNow; }); it('tracks estimated bandwidth', () => { abrManager.getBandwidthEstimate.and.returnValue(25); const stats = player.getStats(); expect(stats.estimatedBandwidth).toBe(25); }); it('tracks info about current stream', () => { const stats = player.getStats(); // Should have chosen the first of each type of stream. expect(stats.width).toBe(100); expect(stats.height).toBe(200); expect(stats.streamBandwidth).toBe(200); }); it('tracks frame info', () => { // getVideoPlaybackQuality does not exist yet. let stats = player.getStats(); expect(stats.decodedFrames).toBeNaN(); expect(stats.droppedFrames).toBeNaN(); expect(stats.corruptedFrames).toBeNaN(); video.getVideoPlaybackQuality = () => { return { corruptedVideoFrames: 10, creationTime: 0, totalFrameDelay: 0, totalVideoFrames: 75, droppedVideoFrames: 125, }; }; // Now that it exists, theses stats should exist. stats = player.getStats(); expect(stats.decodedFrames).toBe(75); expect(stats.droppedFrames).toBe(125); expect(stats.corruptedFrames).toBe(10); }); describe('buffer/play times', () => { it('tracks play time', () => { let stats = player.getStats(); expect(stats.playTime).toBeCloseTo(0); expect(stats.bufferingTime).toBeCloseTo(0); // Stop buffering and start "playing". forceBufferingTo(false); Date.now = () => 5000; stats = player.getStats(); expect(stats.playTime).toBeCloseTo(5); expect(stats.bufferingTime).toBeCloseTo(0); }); it('tracks buffering time', () => { let stats = player.getStats(); expect(stats.playTime).toBeCloseTo(0); expect(stats.bufferingTime).toBeCloseTo(0); forceBufferingTo(true); Date.now = () => 5000; stats = player.getStats(); expect(stats.playTime).toBeCloseTo(0); expect(stats.bufferingTime).toBeCloseTo(5); }); it('tracks correct time when switching states', () => { forceBufferingTo(false); Date.now = () => 3000; forceBufferingTo(true); Date.now = () => 8000; forceBufferingTo(true); Date.now = () => 17000; forceBufferingTo(false); Date.now = () => 18000; const stats = player.getStats(); expect(stats.playTime).toBeCloseTo(4); expect(stats.bufferingTime).toBeCloseTo(14); }); }); describe('.switchHistory', () => { it('includes original choices', () => { // checkHistory prepends the initial stream selections. checkHistory([]); }); it('includes selectVariantTrack choices', () => { const track = player.getVariantTracks()[3]; const variants = manifest.variants; const variant = variants.find((variant) => variant.id == track.id); player.selectVariantTrack(track); checkHistory([{ // We are using a mock date, so this is not a race. timestamp: Date.now() / 1000, id: variant.id, type: 'variant', fromAdaptation: false, bandwidth: variant.bandwidth, }]); }); it('includes adaptation choices', () => { const variant = manifest.variants[3]; switch_(variant); checkHistory(jasmine.arrayContaining([ { timestamp: Date.now() / 1000, id: variant.id, type: 'variant', fromAdaptation: true, bandwidth: variant.bandwidth, }, ])); }); /** * Checks that the switch history is correct. * @param {!Array.} additional */ function checkHistory(additional) { const prefix = { timestamp: jasmine.any(Number), id: 0, type: 'variant', fromAdaptation: true, bandwidth: 200, }; const switchHistory = player.getStats().switchHistory; expect(switchHistory[0]).toEqual(prefix); expect(switchHistory.slice(1)).toEqual(additional); } /** * @param {shaka.extern.Variant} variant * @suppress {accessControls} */ function switch_(variant) { player.switch_(variant); } }); describe('.stateHistory', () => { function history() { return player.getStats().stateHistory; } // We expect that the player will start us in the buffering state after // loading. We should see that the only entry is a buffering entry. it('begins with buffering state', () => { expect(history()).toEqual([ { timestamp: jasmine.any(Number), duration: 0, state: 'buffering', }, ]); }); // We expect that the player will start us in the buffering state, but // when the media element is paused, we should see that we change to the // paused state. it('transitions to paused if the video is paused', () => { // Start playback, we must be playing in order to be paused. The // buffering state takes precedent over other states, so if we are // buffering and then paused, it will only report buffering. forceBufferingTo(false); // Trigger a pause event. video.paused = true; video.on['pause'](); expect(history()).toEqual([ { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'buffering', }, { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'playing', }, { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'paused', }, ]); }); // We expect that the player will start us in the buffering state, but // once we leave that state, we should be playing. it('transitions to playing if the video is playing', () => { // Leave buffering (and enter playing). forceBufferingTo(false); expect(history()).toEqual([ { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'buffering', }, { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'playing', }, ]); }); // We expect that the player will start us in the buffering state. We // expect that once we reach the end, we will enter the ended state. Since // we must be first playing before reaching the end, this test will // reflect that. it('transitions to ended when the video ends', () => { // Stop buffering (and start playing); forceBufferingTo(false); // Signal the playhead reaching the end of the content. video.ended = true; video.on['ended'](); expect(history()).toEqual([ { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'buffering', }, { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'playing', }, { timestamp: jasmine.any(Number), duration: jasmine.any(Number), state: 'ended', }, ]); }); }); /** * @param {boolean} buffering * @suppress {accessControls} */ function forceBufferingTo(buffering) { const State = shaka.media.BufferingObserver.State; // Replace the |getState| method on the buffer controllers so that any // others calls relying on the state will get the state that we want them // to have. player.bufferObserver_.getState = () => { return buffering ? State.STARVING : State.SATISFIED; }; // Force the update. player.updateBufferState_(); } }); describe('unplayable content', () => { it('throws CONTENT_UNSUPPORTED_BY_BROWSER', async () => { window.MediaSource.isTypeSupported = (mimeType) => false; manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(0); }); }); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER)); const load = player.load(fakeManifestUri, 0, fakeMimeType); await expectAsync(load).toBeRejectedWith(expected); }); }); describe('restrictions', () => { it('switches if active is restricted by application', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.bandwidth = 500; variant.addVideo(1); }); manifest.addVariant(1, (variant) => { variant.bandwidth = 100; variant.addVideo(2); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); let activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(0); // Ask AbrManager to choose the 0th variant from those it is given. abrManager.chooseIndex = 0; abrManager.chooseVariant.calls.reset(); // This restriction should make it so that the first variant (bandwidth // 500, id 0) cannot be selected. player.configure({ restrictions: {maxBandwidth: 200}, }); // The restriction change should trigger a call to AbrManager. expect(abrManager.chooseVariant).toHaveBeenCalled(); // The first variant is disallowed. expect(manifest.variants[0].id).toBe(0); expect(manifest.variants[0].allowedByApplication) .toBe(false); // AbrManager chose the second variant (id 1). activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(1); }); it('updates AbrManager for restriction changes', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(1, (variant) => { variant.bandwidth = 500; variant.addVideo(10); }); manifest.addVariant(2, (variant) => { variant.bandwidth = 100; variant.addVideo(20); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); abrManager.setVariants.calls.reset(); player.configure({restrictions: {maxBandwidth: 200}}); // AbrManager should have been updated with the restricted tracks. // The first variant is disallowed. expect(abrManager.setVariants).toHaveBeenCalled(); const variants = abrManager.setVariants.calls.argsFor(0)[0]; expect(variants.length).toBe(1); expect(variants[0].id).toBe(2); // Now increase the restriction, AbrManager should still be updated. // https://github.com/google/shaka-player/issues/1533 abrManager.setVariants.calls.reset(); player.configure({restrictions: {maxBandwidth: Infinity}}); expect(abrManager.setVariants).toHaveBeenCalledTimes(1); const newVariants = abrManager.setVariants.calls.argsFor(0)[0]; expect(newVariants.length).toBe(2); }); it('switches if active key status is "output-restricted"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); let activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(0); abrManager.chooseIndex = 0; abrManager.chooseVariant.calls.reset(); // This restricts the first variant, which triggers chooseVariant. onKeyStatus({'abc': 'output-restricted'}); expect(abrManager.chooseVariant).toHaveBeenCalled(); // The first variant is disallowed. expect(manifest.variants[0].id).toBe(0); expect(manifest.variants[0].allowedByKeySystem).toBe(false); // The second variant was chosen. activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(1); }); it('switches if active key status is "internal-error"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); let activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(0); // AbrManager should choose the second track since the first is // restricted. abrManager.chooseIndex = 0; abrManager.chooseVariant.calls.reset(); onKeyStatus({'abc': 'internal-error'}); expect(abrManager.chooseVariant).toHaveBeenCalled(); expect(manifest.variants[0].id).toBe(0); expect(manifest.variants[0].allowedByKeySystem).toBe(false); activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(1); }); it('doesn\'t switch if the active stream isn\'t restricted', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); abrManager.chooseVariant.calls.reset(); let activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(0); onKeyStatus({'abc': 'usable'}); expect(abrManager.chooseVariant).not.toHaveBeenCalled(); activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(0); }); it('removes if key status is "output-restricted"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); onKeyStatus({'abc': 'output-restricted'}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(1); }); it('removes if key status is "internal-error"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); onKeyStatus({'abc': 'internal-error'}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(1); }); it('removes if we don\'t have the required key', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // We have some key statuses, but not for the key IDs we know. onKeyStatus({'foo': 'usable'}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(2); }); it('updates key statuses for multi-key content', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3, (stream) => { stream.keyIds = new Set(['def']); }); }); manifest.addVariant(4, (variant) => { variant.addVideo(5, (stream) => { stream.keyIds = new Set(['abc', 'def']); }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); onKeyStatus({'abc': 'usable'}); expect(manifest.variants[0].allowedByKeySystem).toBe(true); expect(manifest.variants[1].allowedByKeySystem).toBe(false); expect(manifest.variants[2].allowedByKeySystem).toBe(false); }); it('does not restrict if no key statuses are available', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // This simulates, for example, the lack of key status on Chromecast // when using PlayReady. See #1070. onKeyStatus({}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(2); }); it('doesn\'t remove when using synthetic key status', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // A synthetic key status contains a single key status with key '00'. onKeyStatus({'00': 'usable'}); expect(player.getVariantTracks().length).toBe(2); }); it('removes all encrypted tracks for errors with synthetic key status', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3, (stream) => { stream.keyIds = new Set(['xyz']); }); }); manifest.addVariant(4, (variant) => { variant.addVideo(5); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); // A synthetic key status contains a single key status with key '00'. onKeyStatus({'00': 'internal-error'}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(4); }); it('removes if key system does not support codec', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.encrypted = true; stream.mimeType = 'video/unsupported'; stream.addDrmInfo('foo.bar'); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2, (stream) => { stream.encrypted = true; stream.addDrmInfo('foo.bar'); }); }); }); // We must be careful that our video/unsupported was not filtered out // because of MSE support. We are specifically testing EME-based // filtering of codecs. expect(MediaSource.isTypeSupported('video/unsupported')).toBe(true); // Make sure that drm engine will reject the variant with an unsupported // video mime type. drmEngine.supportsVariant.and.callFake((variant) => { return variant.video.mimeType != 'video/unsupported'; }); await player.load(fakeManifestUri, 0, fakeMimeType); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(1); }); it('removes based on bandwidth', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.bandwidth = 10; variant.addVideo(1); }); manifest.addVariant(1, (variant) => { variant.bandwidth = 1500; variant.addVideo(2); }); manifest.addVariant(2, (variant) => { variant.bandwidth = 500; variant.addVideo(3); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minBandwidth: 100, maxBandwidth: 1000}}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(2); }); it('removes based on pixels', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.size(900, 900); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2, (stream) => { stream.size(5, 5); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3, (stream) => { stream.size(190, 190); }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minPixels: 100, maxPixels: 800 * 800}}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(2); }); it('removes based on width', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.size(5, 5); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2, (stream) => { stream.size(1500, 200); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3, (stream) => { stream.size(190, 190); }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minWidth: 100, maxWidth: 1000}}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(2); }); it('removes based on height', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.size(5, 5); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2, (stream) => { stream.size(200, 1500); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3, (stream) => { stream.size(190, 190); }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minHeight: 100, maxHeight: 1000}}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(2); }); it('removes the whole variant if one stream is restricted', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.size(5, 5); }); variant.addAudio(2); }); manifest.addVariant(1, (variant) => { variant.addVideo(3, (stream) => { stream.size(190, 190); }); variant.addAudio(4); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); player.configure({restrictions: {minHeight: 100, maxHeight: 1000}}); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(1); }); it('issues error if no streams are playable', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.size(5, 5); }); }); manifest.addVariant(1, (variant) => { variant.addVideo(2, (stream) => { stream.size(200, 300); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3, (stream) => { stream.size(190, 190); }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); onError.and.callFake((e) => { const error = e.detail; shaka.test.Util.expectToEqualError( error, new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET, { hasAppRestrictions: true, missingKeys: [], restrictedKeyStatuses: [], })); }); player.configure({restrictions: {minHeight: 1000, maxHeight: 2000}}); expect(onError).toHaveBeenCalled(); }); it('chooses efficient codecs and removes the rest', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { // More efficient codecs manifest.addVariant(0, (variant) => { variant.bandwidth = 100; variant.addVideo(0, (stream) => { stream.codecs = 'good'; }); }); manifest.addVariant(1, (variant) => { variant.bandwidth = 200; variant.addVideo(1, (stream) => { stream.codecs = 'good'; }); }); manifest.addVariant(2, (variant) => { variant.bandwidth = 300; variant.addVideo(2, (stream) => { stream.codecs = 'good'; }); }); // Less efficient codecs manifest.addVariant(3, (variant) => { variant.bandwidth = 10000; variant.addVideo(3, (stream) => { stream.codecs = 'bad'; }); }); manifest.addVariant(4, (variant) => { variant.bandwidth = 20000; variant.addVideo(4, (stream) => { stream.codecs = 'bad'; }); }); manifest.addVariant(5, (variant) => { variant.bandwidth = 30000; variant.addVideo(5, (stream) => { stream.codecs = 'bad'; }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.setVariants).toHaveBeenCalled(); const variants = abrManager.setVariants.calls.argsFor(0)[0]; // We've already chosen codecs, so only 3 tracks should remain. expect(variants.length).toBe(3); // They should be the low-bandwidth ones. expect(variants[0].video.codecs).toBe('good'); expect(variants[1].video.codecs).toBe('good'); expect(variants[2].video.codecs).toBe('good'); }); it('updates AbrManager about restricted variants', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.keyIds = new Set(['abc']); }); }); manifest.addVariant(2, (variant) => { variant.addVideo(3); }); }); /** @type {!shaka.test.FakeAbrManager} */ const abrManager = new shaka.test.FakeAbrManager(); player.configure('abrFactory', () => abrManager); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // We have some key statuses, but not for the key IDs we know. abrManager.setVariants.calls.reset(); onKeyStatus({'foo': 'usable'}); expect(abrManager.setVariants).toHaveBeenCalled(); const variants = abrManager.setVariants.calls.argsFor(0)[0]; expect(variants.length).toBe(1); expect(variants[0].id).toBe(2); }); it('chooses codecs after considering 6-channel preference', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { // Surround sound AC-3, preferred by config manifest.addVariant(0, (variant) => { variant.bandwidth = 300; variant.addAudio(0, (stream) => { stream.channelsCount = 6; stream.audioSamplingRate = 48000; stream.codecs = 'ac-3'; }); }); // Stereo AAC, would win out based on bandwidth alone manifest.addVariant(1, (variant) => { variant.bandwidth = 100; variant.addAudio(1, (stream) => { stream.channelsCount = 2; stream.audioSamplingRate = 48000; stream.codecs = 'mp4a.40.2'; }); }); }); // Configure for 6 channels. player.configure({ preferredAudioChannelCount: 6, }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.setVariants).toHaveBeenCalled(); // We've chosen codecs, so only 1 track should remain. expect(abrManager.variants.length).toBe(1); // It should be the 6-channel variant, based on our preference. expect(abrManager.variants[0].audio.channelsCount).toBe(6); expect(abrManager.variants[0].audio.codecs).toBe('ac-3'); }); }); describe('getPlayheadTimeAsDate()', () => { beforeEach(async () => { const timeline = new shaka.media.PresentationTimeline(300, 0); timeline.setStatic(false); 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); }); it('gets current wall clock time in UTC', () => { playhead.getTime.and.returnValue(20); const liveTimeUtc = player.getPlayheadTimeAsDate(); // (300 (presentation start time) + 20 (playhead time)) * 1000 (ms/sec) expect(liveTimeUtc).toEqual(new Date(320 * 1000)); }); }); it('rejects empty manifests', async () => { manifest = shaka.test.ManifestGenerator.generate(); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.NO_VARIANTS)); await expectAsync(player.load(fakeManifestUri, 0, fakeMimeType)) .toBeRejectedWith(expected); }); it('does not assert when adapting', async () => { // Most of our Player unit tests never adapt. This allowed some assertions // to creep in that went uncaught until they happened during manual testing. // Repro only happens with audio+video variants in which we only adapt one // type. This test covers https://github.com/google/shaka-player/issues/954 manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.bandwidth = 100; variant.addVideo(0); variant.addAudio(9); }); manifest.addVariant(1, (variant) => { variant.bandwidth = 200; variant.addVideo(1); variant.addExistingStream(9); // audio }); manifest.addVariant(2, (variant) => { variant.bandwidth = 300; variant.addVideo(2); variant.addExistingStream(9); // audio }); }); await player.load(fakeManifestUri, 0, fakeMimeType); // We've already loaded variants[0]. Switch to [1] and [2]. abrManager.switchCallback(manifest.variants[1]); abrManager.switchCallback(manifest.variants[2]); }); describe('isTextTrackVisible', () => { it('does not throw before load', () => { player.isTextTrackVisible(); }); }); describe('setTextTrackVisibility', () => { it('does not throw before load', async () => { await player.setTextTrackVisibility(true); }); }); describe('isAudioOnly', () => { it('detects audio-only content', async () => { // False before we've loaded anything. expect(player.isAudioOnly()).toBe(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(0); variant.addAudio(1); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); // We have audio & video tracks, so this is not audio-only. expect(player.isAudioOnly()).toBe(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(0); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); // We have video-only tracks, so this is not audio-only. expect(player.isAudioOnly()).toBe(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addAudio(1); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); // We have audio-only tracks, so this is audio-only. expect(player.isAudioOnly()).toBe(true); await player.unload(); // When we have nothing loaded, we go back to not audio-only status. expect(player.isAudioOnly()).toBe(false); }); }); describe('load', () => { it('tolerates bandwidth of NaN, undefined, or 0', async () => { // Regression test for https://github.com/google/shaka-player/issues/938 manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.bandwidth = /** @type {?} */(undefined); variant.addVideo(0); }); manifest.addVariant(1, (variant) => { variant.bandwidth = NaN; variant.addVideo(1); }); manifest.addVariant(2, (variant) => { variant.bandwidth = 0; variant.addVideo(2); }); }); // Before the fix, load() would fail assertions and throw errors. await player.load(fakeManifestUri, 0, fakeMimeType); }); it('respects startTime of 0', async () => { // What we shouldn't do is treat start time of 0 as the same as startTime // of null/undefined. 0 means timestamp 0, whereas null/undefined means // "default". For VOD, the default is 0, but for live streams, the // default is the live edge. // Create a live timeline and manifest. const timeline = new shaka.media.PresentationTimeline(300, 0); timeline.setStatic(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline = timeline; manifest.addVariant(0, (variant) => { variant.addVideo(1); }); }); // To ensure that Playhead is correctly created, we must use the original // playhead injector. To inspect the real Playhead instance, though, we // must shim this method and keep a copy of the real Playhead. Otherwise, // we would be merely inspecting the mock Playhead. /** @type {shaka.media.Playhead} */ let realPlayhead = null; /** * @this {shaka.Player} * @return {!shaka.media.Playhead} */ // eslint-disable-next-line no-restricted-syntax player.createPlayhead = function() { realPlayhead = // eslint-disable-next-line no-restricted-syntax shaka.Player.prototype.createPlayhead.apply(this, arguments); return realPlayhead; }; await player.load(fakeManifestUri, /* startTime= */ 0, fakeMimeType); // Ensure this is seen as a live stream, or else the test is invalid. expect(player.isLive()).toBe(true); // If startTime of 0 was treated as null, then getTime() would point to // the live edge instead of 0. expect(realPlayhead.getTime()).toBe(0); }); }); describe('language methods', () => { let videoOnlyManifest; beforeEach(() => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(1, (variant) => { variant.language = 'fr'; variant.addVideo(0); variant.addAudio(1, (stream) => { stream.language = 'fr'; }); }); manifest.addVariant(2, (variant) => { variant.language = 'en'; variant.addExistingStream(0); // video variant.addAudio(2, (stream) => { stream.language = 'en'; stream.roles = ['main']; }); }); manifest.addVariant(3, (variant) => { variant.language = 'en'; variant.addExistingStream(0); // video variant.addAudio(3, (stream) => { stream.language = 'en'; stream.roles = ['commentary']; }); }); manifest.addVariant(4, (variant) => { variant.language = 'de'; variant.addExistingStream(0); // video variant.addAudio(4, (stream) => { stream.language = 'de'; stream.roles = ['foo', 'bar']; }); }); manifest.addTextStream(5, (stream) => { stream.language = 'es'; stream.roles = ['baz', 'qwerty']; }); manifest.addTextStream(6, (stream) => { stream.language = 'en'; stream.kind = 'caption'; stream.roles = ['main', 'caption']; }); manifest.addTextStream(7, (stream) => { stream.language = 'en'; stream.kind = 'subtitle'; stream.roles = ['main', 'subtitle']; }); }); videoOnlyManifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(1, (variant) => { variant.bandwidth = 400; variant.addVideo(1); }); manifest.addVariant(2, (variant) => { variant.bandwidth = 800; variant.addVideo(2); }); }); }); describe('get*Languages', () => { it('returns a list of languages', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getAudioLanguages()).toEqual(['fr', 'en', 'de']); expect(player.getTextLanguages()).toEqual(['es', 'en']); }); it('returns "und" for video-only tracks', async () => { manifest = videoOnlyManifest; await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getAudioLanguages()).toEqual(['und']); expect(player.getTextLanguages()).toEqual([]); }); }); describe('getAudioLanguagesAndRoles', () => { it('ignores video roles', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.language = 'en'; variant.addVideo(1, (stream) => { stream.roles = ['video-only-role']; }); variant.addAudio(2, (stream) => { stream.roles = ['audio-only-role']; stream.language = 'en'; }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getAudioLanguagesAndRoles()).toEqual([ {language: 'en', role: 'audio-only-role'}, ]); }); it('lists all language-role combinations', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getAudioLanguagesAndRoles()).toEqual([ {language: 'fr', role: ''}, {language: 'en', role: 'main'}, {language: 'en', role: 'commentary'}, {language: 'de', role: 'foo'}, {language: 'de', role: 'bar'}, ]); }); it('uses "und" for video-only tracks', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { stream.roles = ['video-only-role']; }); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getAudioLanguagesAndRoles()).toEqual([ {language: 'und', role: ''}, ]); }); }); describe('getTextLanguageAndRoles', () => { it('lists all language-role combinations', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getTextLanguagesAndRoles()).toEqual([ {language: 'es', role: 'baz'}, {language: 'es', role: 'qwerty'}, {language: 'en', role: 'main'}, {language: 'en', role: 'caption'}, {language: 'en', role: 'subtitle'}, ]); }); }); }); /** * Gets the currently active variant track. * @return {shaka.extern.Track} */ function getActiveVariantTrack() { const activeTracks = player.getVariantTracks().filter((track) => { return track.active; }); expect(activeTracks.length).toBe(1); return activeTracks[0]; } /** * Gets the currently active text track. * @return {?shaka.extern.Track} */ function getActiveTextTrack() { const activeTracks = player.getTextTracks().filter((track) => { return track.active; }); return activeTracks[0] || null; } /** * A Jasmine asymmetric matcher for substring matches. * @param {string} substring * @return {!Object} */ function stringContaining(substring) { return { asymmetricMatch: (actual) => actual.includes(substring), }; } /** * @param {!Object.} keyStatusMap * @suppress {accessControls} */ function onKeyStatus(keyStatusMap) { player.onKeyStatus_(keyStatusMap); } /** * Create a fake text displayer with a little extra logic to work with the * test cases. * * @return {!shaka.test.FakeTextDisplayer} */ function createTextDisplayer() { /** @type {!shaka.test.FakeTextDisplayer} */ const displayer = new shaka.test.FakeTextDisplayer(); // Allow the tests to set and check if we are showing text. let isVisible = false; displayer.isTextVisibleSpy.and.callFake(() => { return isVisible; }); displayer.setTextVisibilitySpy.and.callFake((on) => { isVisible = on; }); return displayer; } });