UNPKG

shaka-player

Version:
1,469 lines (1,286 loc) 115 kB
/** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ describe('Player', function() { 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'; /** @type {!jasmine.Spy} */ let logErrorSpy; /** @type {!jasmine.Spy} */ let logWarnSpy; /** @type {!jasmine.Spy} */ let onError; /** @type {shaka.extern.Manifest} */ let manifest; /** @type {number} */ let periodIndex; /** @type {!shaka.Player} */ let player; /** @type {!shaka.test.FakeAbrManager} */ let abrManager; /** @type {function():shaka.extern.AbrManager} */ let abrFactory; /** @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 {function():shaka.extern.TextDisplayer} */ let textDisplayFactory; 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 = function(mimeType) { let 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 = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addAudio(1) .addVideo(2) .addPeriod(1) .addVariant(1) .addAudio(3) .addVideo(4) .build(); periodIndex = 0; abrManager = new shaka.test.FakeAbrManager(); abrFactory = function() { return abrManager; }; textDisplayer = createTextDisplayer(); textDisplayFactory = function() { return textDisplayer; }; 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( onChooseStreams, onCanSwitch); 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 = function() { return drmEngine; }; player.createNetworkingEngine = function() { return networkingEngine; }; player.createPlayhead = function() { return playhead; }; player.createMediaSourceEngine = function() { return mediaSourceEngine; }; player.createStreamingEngine = function() { return 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: abrFactory, textDisplayFactory: textDisplayFactory, }); onError = jasmine.createSpy('error event'); onError.and.callFake(function(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; } }); describe('destroy', function() { it('cleans up all dependencies', async () => { goog.asserts.assert(manifest, 'Manifest should be non-null'); let parser = new shaka.test.FakeManifestParser(manifest); let factory = function() { return parser; }; await player.load(fakeManifestUri, 0, factory); 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(); }); it('destroys mediaSourceEngine before drmEngine', async () => { goog.asserts.assert(manifest, 'Manifest should be non-null'); let parser = new shaka.test.FakeManifestParser(manifest); let factory = function() { return parser; }; mediaSourceEngine.destroy.and.callFake(() => { expect(drmEngine.destroy).not.toHaveBeenCalled(); return Util.delay(0.01).then(() => { expect(drmEngine.destroy).not.toHaveBeenCalled(); }); }); await player.load(fakeManifestUri, 0, factory); 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', function(done) { let p = shaka.test.Util.delay(0.3); let parser = new shaka.test.FakeManifestParser(manifest); parser.start.and.returnValue(p); parser.stop.and.callFake(function() { expect(abrManager.stop).not.toHaveBeenCalled(); expect(networkingEngine.destroy).not.toHaveBeenCalled(); }); let factory = function() { return parser; }; player.load(fakeManifestUri, 0, factory).then(fail).catch(() => {}); shaka.test.Util.delay(0.1).then(function() { player.destroy().catch(fail).then(function() { expect(abrManager.stop).toHaveBeenCalled(); expect(networkingEngine.destroy).toHaveBeenCalled(); expect(parser.stop).toHaveBeenCalled(); }).then(done); }); }); }); describe('load/unload', function() { /** @type {!shaka.test.FakeManifestParser} */ let parser1; /** @type {!Function} */ let factory1; /** @type {!jasmine.Spy} */ let checkError; beforeEach(function() { goog.asserts.assert(manifest, 'manifest must be non-null'); parser1 = new shaka.test.FakeManifestParser(manifest); factory1 = function() { return parser1; }; checkError = jasmine.createSpy('checkError'); checkError.and.callFake(function(error) { expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED); }); }); describe('streaming event', function() { /** @type {jasmine.Spy} */ let streamingListener; beforeEach(function() { streamingListener = jasmine.createSpy('listener'); player.addEventListener('streaming', Util.spyFunc(streamingListener)); // We must have two different sets of codecs for some of our tests. manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addAudio(1).mime('audio/mp4', 'mp4a.40.2') .addVideo(2).mime('video/mp4', 'avc1.4d401f') .addVariant(1) .addAudio(3).mime('audio/webm', 'opus') .addVideo(4).mime('video/webm', 'vp9') .build(); parser1 = new shaka.test.FakeManifestParser(manifest); }); async function runTest() { expect(streamingListener).not.toHaveBeenCalled(); await player.load(fakeManifestUri, 0, factory1); expect(streamingListener).toHaveBeenCalled(); } it('fires after tracks exist', async () => { streamingListener.and.callFake(function() { 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(function() { const activeTracks = player.getVariantTracks().filter((t) => t.active); expect(activeTracks.length).toEqual(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(function() { const tracks = player.getVariantTracks(); // Either WebM, or MP4, but not both. expect(tracks.length).toEqual(1); }); await runTest(); }); }); describe('setTextTrackVisibility', function() { beforeEach(function() { manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(0) .addAudio(1) .addVideo(2) .addTextStream(3) .language('es').label('Spanish') .bandwidth(100).mime('text/vtt') .kind('caption') .build(); }); it('load text stream if caption is visible', async () => { await player.load(fakeManifestUri, 0, returnManifest(manifest)); await player.setTextTrackVisibility(true); expect(streamingEngine.loadNewTextStream).toHaveBeenCalled(); expect(streamingEngine.getBufferingText()).not.toBe(null); }); it('does not load text stream if caption is invisible', async () => { await player.load(fakeManifestUri, 0, returnManifest(manifest)); await player.setTextTrackVisibility(false); expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled(); expect(streamingEngine.getBufferingText()).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, returnManifest(manifest)); expect(streamingEngine.getBufferingText()).not.toBe(null); await player.setTextTrackVisibility(true); expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled(); expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled(); await player.setTextTrackVisibility(false); expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled(); expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled(); }); }); }); // describe('load/unload') describe('getConfiguration', function() { it('returns a copy of the configuration', function() { let config1 = player.getConfiguration(); config1.streaming.bufferBehind = -99; let config2 = player.getConfiguration(); expect(config1.streaming.bufferBehind).not.toEqual( config2.streaming.bufferBehind); }); }); describe('configure', function() { it('overwrites defaults', function() { let 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}, }, }); let 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', function() { 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', function() { logErrorSpy.and.stub(); let 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', function() { 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', function() { // 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(); let 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', function() { 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', function() { let goodCustomScheme = function(node) {}; let badCustomScheme1 = function() {}; // too few args let badCustomScheme2 = function(x, y) {}; // too many args // Takes good callback. player.configure({ manifest: {dash: {customScheme: goodCustomScheme}}, }); let newConfig = player.getConfiguration(); expect(newConfig.manifest.dash.customScheme).toBe(goodCustomScheme); expect(logWarnSpy).not.toHaveBeenCalled(); // Warns about bad callback #1, still takes it. logWarnSpy.calls.reset(); player.configure({ manifest: {dash: {customScheme: badCustomScheme1}}, }); newConfig = player.getConfiguration(); expect(newConfig.manifest.dash.customScheme).toBe(badCustomScheme1); expect(logWarnSpy).toHaveBeenCalledWith( stringContaining('.manifest.dash.customScheme')); // Warns about bad callback #2, still takes it. logWarnSpy.calls.reset(); player.configure({ manifest: {dash: {customScheme: badCustomScheme2}}, }); newConfig = player.getConfiguration(); expect(newConfig.manifest.dash.customScheme).toBe(badCustomScheme2); expect(logWarnSpy).toHaveBeenCalledWith( stringContaining('.manifest.dash.customScheme')); // Resets to default if undefined. logWarnSpy.calls.reset(); player.configure({ manifest: {dash: {customScheme: undefined}}, }); newConfig = player.getConfiguration(); expect(newConfig.manifest.dash.customScheme).not.toBe(badCustomScheme2); expect(logWarnSpy).not.toHaveBeenCalled(); }); // Regression test for https://github.com/google/shaka-player/issues/784 it('does not throw when overwriting serverCertificate', function() { 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', function() { 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', function() { 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}); let timeline = new shaka.media.PresentationTimeline(300, 0); timeline.setStatic(true); manifest = new shaka.test.ManifestGenerator() .setTimeline(timeline) .addPeriod(0) .addVariant(0) .addVideo(1) .build(); goog.asserts.assert(manifest, 'manifest must be non-null'); let parser = new shaka.test.FakeManifestParser(manifest); let factory = function() { return parser; }; await player.load(fakeManifestUri, 0, factory); let seekRange = player.seekRange(); expect(seekRange.start).toBe(5); expect(seekRange.end).toBe(10); }); it('does not switch for plain configuration changes', async () => { let parser = new shaka.test.FakeManifestParser(manifest); let factory = function() { return parser; }; let switchVariantSpy = spyOn(player, 'switchVariant_'); await player.load(fakeManifestUri, 0, factory); player.configure({abr: {enabled: false}}); player.configure({streaming: {bufferingGoal: 9001}}); // Delay to ensure that the switch would have been called. await shaka.test.Util.delay(0.1); expect(switchVariantSpy).not.toHaveBeenCalled(); }); it('accepts parameters in a (fieldName, value) format', function() { let oldConfig = player.getConfiguration(); let oldDelayLicense = oldConfig.drm.delayLicenseRequestUntilPlayed; let oldSwitchInterval = oldConfig.abr.switchInterval; let 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'); let newConfig = player.getConfiguration(); let newDelayLicense = newConfig.drm.delayLicenseRequestUntilPlayed; let newSwitchInterval = newConfig.abr.switchInterval; let 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', function() { 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', function() { logErrorSpy.and.stub(); let changes = { manifest: {foobar: false}, streaming: {bufferBehind: 77}, }; expect(player.configure(changes)).toBe(false); let newConfig = player.getConfiguration(); expect(newConfig.streaming.bufferBehind).toEqual(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).toEqual(false); expect(barConfig1.distinctiveIdentifierRequired).toEqual(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).toEqual(true); expect(barConfig2.distinctiveIdentifierRequired).toEqual(false); }); }); describe('resetConfiguration', function() { 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', function() { /** @type {!shaka.test.FakeManifestParser} */ let parser; /** @type {!Function} */ let parserFactory; beforeEach(function() { goog.asserts.assert(manifest, 'manifest must be non-null'); parser = new shaka.test.FakeManifestParser(manifest); parserFactory = function() { return parser; }; }); it('sets through load', async () => { await player.load(fakeManifestUri, 0, parserFactory); expect(abrManager.init).toHaveBeenCalled(); }); it('calls chooseVariant', async () => { await player.load(fakeManifestUri, 0, parserFactory); expect(abrManager.chooseVariant).toHaveBeenCalled(); }); it('does not enable before stream startup', async () => { await player.load(fakeManifestUri, 0, parserFactory); expect(abrManager.enable).not.toHaveBeenCalled(); streamingEngine.onCanSwitch(); expect(abrManager.enable).toHaveBeenCalled(); }); it('does not enable if adaptation is disabled', async () => { player.configure({abr: {enabled: false}}); await player.load(fakeManifestUri, 0, parserFactory); streamingEngine.onCanSwitch(); expect(abrManager.enable).not.toHaveBeenCalled(); }); it('enables/disables though configure', async () => { await player.load(fakeManifestUri, 0, parserFactory); streamingEngine.onCanSwitch(); 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('waits to enable if in-between Periods', async () => { player.configure({abr: {enabled: false}}); await player.load(fakeManifestUri, 0, parserFactory); player.configure({abr: {enabled: true}}); expect(abrManager.enable).not.toHaveBeenCalled(); // Until onCanSwitch is called, the first period hasn't been set up yet. streamingEngine.onCanSwitch(); 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, parserFactory); expect(spy).toHaveBeenCalled(); spy.calls.reset(); await player.load(fakeManifestUri, 0, parserFactory); 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, parserFactory); expect(spy1).toHaveBeenCalled(); expect(spy2).not.toHaveBeenCalled(); spy1.calls.reset(); player.configure({abrFactory: spy2}); await player.load(fakeManifestUri, 0, parserFactory); expect(spy1).not.toHaveBeenCalled(); expect(spy2).toHaveBeenCalled(); }); }); describe('filterTracks', function() { it('retains only video+audio variants if they exist', function(done) { manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(10) .addAudio(1) .addVariant(11) .addAudio(2) .addVideo(3) .addVariant(12) .addVideo(4) .addPeriod(1) .addVariant(20) .addAudio(5) .addVariant(21) .addVideo(6) .addVariant(22) .addAudio(7) .addVideo(8) .build(); let variantTracks1 = [ jasmine.objectContaining({ id: 11, active: true, type: 'variant', }), ]; let variantTracks2 = [ jasmine.objectContaining({ id: 22, active: false, type: 'variant', }), ]; let parser = new shaka.test.FakeManifestParser(manifest); let parserFactory = function() { return parser; }; player.load(fakeManifestUri, 0, parserFactory).catch(fail).then(() => { // Check the first period's variant tracks. let actualVariantTracks1 = player.getVariantTracks(); expect(actualVariantTracks1).toEqual(variantTracks1); // Check the second period's variant tracks. playhead.getTime.and.callFake(function() { return 100; }); let actualVariantTracks2 = player.getVariantTracks(); expect(actualVariantTracks2).toEqual(variantTracks2); }).then(done); }); }); describe('tracks', function() { /** @type {!Array.<shaka.extern.Track>} */ let variantTracks; /** @type {!Array.<shaka.extern.Track>} */ let textTracks; beforeEach(async () => { // A manifest we can use to test track expectations. manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(100) // main surround, low res .bandwidth(1300) .language('en') .addVideo(1).originalId('video-1kbps').bandwidth(1000) .size(100, 200).frameRate(1000000 / 42000) .pixelAspectRatio('59:54') .roles(['main']) .addAudio(3).originalId('audio-en-6c').bandwidth(300) .channelsCount(6).roles(['main']).audioSamplingRate(48000) .addVariant(101) // main surround, high res .bandwidth(2300) .language('en') .addVideo(2).originalId('video-2kbps').bandwidth(2000) .size(200, 400).frameRate(24) .pixelAspectRatio('59:54') .addExistingStream(3) // audio .addVariant(102) // main stereo, low res .bandwidth(1100) .language('en') .addExistingStream(1) // video .addAudio(4).originalId('audio-en-2c').bandwidth(100) .channelsCount(2).roles(['main']).audioSamplingRate(48000) .addVariant(103) // main stereo, high res .bandwidth(2100) .language('en') .addExistingStream(2) // video .addExistingStream(4) // audio .addVariant(104) // commentary stereo, low res .bandwidth(1100) .language('en') .addExistingStream(1) // video .addAudio(5).originalId('audio-commentary').bandwidth(100) .channelsCount(2).roles(['commentary']).audioSamplingRate(48000) .addVariant(105) // commentary stereo, low res .bandwidth(2100) .language('en') .addExistingStream(2) // video .addExistingStream(5) // audio .addVariant(106) // spanish stereo, low res .language('es') .bandwidth(1100) .addExistingStream(1) // video .addAudio(6).originalId('audio-es').bandwidth(100) .channelsCount(2).audioSamplingRate(48000) .addVariant(107) // spanish stereo, high res .language('es') .bandwidth(2100) .addExistingStream(2) // video .addExistingStream(6) // audio // All text tracks should remain, even with different MIME types. .addTextStream(50).originalId('text-es') .language('es').label('Spanish') .bandwidth(10).mime('text/vtt') .kind('caption') .addTextStream(51).originalId('text-en') .language('en').label('English') .bandwidth(10).mime('application/ttml+xml') .kind('caption').roles(['main']) .addTextStream(52).originalId('text-commentary') .language('en').label('English') .bandwidth(10).mime('application/ttml+xml') .kind('caption').roles(['commentary']) .addPeriod(1) .addVariant(200) .bandwidth(1100) .language('en') .addVideo(10).bandwidth(1000).size(100, 200) .addAudio(11) .bandwidth(100).channelsCount(2) .audioSamplingRate(48000) .addVariant(201) .bandwidth(1300) .language('en') .addExistingStream(10) // video .addAudio(12) .bandwidth(300).channelsCount(6) .audioSamplingRate(48000) .build(); 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'); let parser = new shaka.test.FakeManifestParser(manifest); let parserFactory = function() { return parser; }; // 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, parserFactory); }); it('returns the correct tracks', function() { streamingEngine.onCanSwitch(); expect(player.getVariantTracks()).toEqual(variantTracks); expect(player.getTextTracks()).toEqual(textTracks); }); it('returns empty arrays before tracks can be determined', async () => { let parser = new shaka.test.FakeManifestParser(manifest); let parserFactory = function() { return parser; }; parser.start.and.callFake(function(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); }); drmEngine.initForPlayback.and.callFake(() => { // The player does not yet have a playhead. expect(player.getVariantTracks()).toEqual([]); expect(player.getTextTracks()).toEqual([]); return Promise.resolve(); }); await player.load(fakeManifestUri, 0, parserFactory); // Make sure the interruptions didn't mess up the tracks. streamingEngine.onCanSwitch(); expect(player.getVariantTracks()).toEqual(variantTracks); expect(player.getTextTracks()).toEqual(textTracks); }); it('doesn\'t disable AbrManager if switching variants', function() { streamingEngine.onCanSwitch(); 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', function() { streamingEngine.onCanSwitch(); 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', function() { streamingEngine.onCanSwitch(); 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).toEqual(newTrack.id); }); it('still switches streams if called during startup', function() { // startup is not complete until onCanSwitch is called. // pick a track const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; // ask the player to switch to it player.selectVariantTrack(newTrack); // nothing happens yet expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); // after startup is complete, the manual selection takes effect. streamingEngine.onCanSwitch(); expect(streamingEngine.switchVariant).toHaveBeenCalled(); const variant = streamingEngine.switchVariant.calls.argsFor(0)[0]; expect(variant.id).toEqual(newTrack.id); }); it('still switches streams if called while switching Periods', function() { // startup is complete after onCanSwitch. streamingEngine.onCanSwitch(); // startup doesn't call switchVariant expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); // pick a track const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; // simulate the transition to period 1 transitionPeriod(1); // select the new track (from period 0, which is fine) player.selectVariantTrack(newTrack); expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); // after transition is completed by onCanSwitch, switchVariant is called streaming