UNPKG

shaka-player

Version:
1,473 lines (1,299 loc) 95.4 kB
/** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ describe('Player', function() { /** @const */ var ContentType = shaka.util.ManifestParserUtils.ContentType; /** @const */ var Util = shaka.test.Util; /** @const */ var originalLogError = shaka.log.error; /** @const */ var originalLogWarn = shaka.log.warning; /** @const */ var originalLogAlwaysWarn = shaka.log.alwaysWarn; /** @const */ var originalIsTypeSupported = window.MediaSource.isTypeSupported; /** @type {!jasmine.Spy} */ var logErrorSpy; /** @type {!jasmine.Spy} */ var logWarnSpy; /** @type {!jasmine.Spy} */ var onError; /** @type {shakaExtern.Manifest} */ var manifest; /** @type {number} */ var periodIndex; /** @type {!shaka.Player} */ var player; /** @type {!shaka.test.FakeAbrManager} */ var abrManager; /** @type {function():shakaExtern.AbrManager} */ var abrFactory; /** @type {!shaka.test.FakeNetworkingEngine} */ var networkingEngine; /** @type {!shaka.test.FakeStreamingEngine} */ var streamingEngine; /** @type {!shaka.test.FakeDrmEngine} */ var drmEngine; /** @type {!shaka.test.FakePlayhead} */ var playhead; /** @type {!shaka.test.FakePlayheadObserver} */ var playheadObserver; /** @type {!shaka.test.FakeTextDisplayer} */ var textDisplayer; /** @type {function():shakaExtern.TextDisplayer} */ var textDisplayFactory; var mediaSourceEngine; /** @type {!shaka.test.FakeVideo} */ var video; beforeAll(function() { logErrorSpy = jasmine.createSpy('shaka.log.error'); shaka.log.error = shaka.test.Util.spyFunc(logErrorSpy); logWarnSpy = jasmine.createSpy('shaka.log.warning'); shaka.log.warning = shaka.test.Util.spyFunc(logWarnSpy); shaka.log.alwaysWarn = shaka.test.Util.spyFunc(logWarnSpy); }); beforeEach(function() { // By default, errors are a failure. logErrorSpy.calls.reset(); logErrorSpy.and.callFake(fail); logWarnSpy.calls.reset(); // 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) { var 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 = new shaka.test.FakeTextDisplayer(); textDisplayFactory = function() { return textDisplayer; }; function dependencyInjector(player) { networkingEngine = new shaka.test.FakeNetworkingEngine({}, new ArrayBuffer(0)); drmEngine = new shaka.test.FakeDrmEngine(); playhead = new shaka.test.FakePlayhead(); playheadObserver = new shaka.test.FakePlayheadObserver(); streamingEngine = new shaka.test.FakeStreamingEngine( onChooseStreams, onCanSwitch); mediaSourceEngine = { destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()) }; player.createDrmEngine = function() { return drmEngine; }; player.createNetworkingEngine = function() { return networkingEngine; }; player.createPlayhead = function() { return playhead; }; player.createPlayheadObserver = function() { return playheadObserver; }; player.createMediaSource = function() { return Promise.resolve(); }; player.createMediaSourceEngine = function() { return mediaSourceEngine; }; player.createStreamingEngine = function() { // This captures the variable |manifest| so this should only be used // after the manifest has been set. // Subtle: because this captures var manifest above, there cannot be any // other location for manifests in these tests. // TODO: fix this to use the manifest currently loaded by the player. var period = manifest.periods[0]; streamingEngine.getCurrentPeriod.and.returnValue(period); 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(function(done) { player.destroy().catch(fail).then(done); }); afterAll(function() { shaka.log.error = originalLogError; shaka.log.warning = originalLogWarn; shaka.log.alwaysWarn = originalLogAlwaysWarn; window.MediaSource.isTypeSupported = originalIsTypeSupported; }); describe('destroy', function() { it('cleans up all dependencies', function(done) { goog.asserts.assert(manifest, 'Manifest should be non-null'); var parser = new shaka.test.FakeManifestParser(manifest); var factory = function() { return parser; }; player.load('', 0, factory).then(function() { return player.destroy(); }).then(function() { expect(abrManager.stop).toHaveBeenCalled(); expect(networkingEngine.destroy).toHaveBeenCalled(); expect(drmEngine.destroy).toHaveBeenCalled(); expect(playhead.destroy).toHaveBeenCalled(); expect(playheadObserver.destroy).toHaveBeenCalled(); expect(mediaSourceEngine.destroy).toHaveBeenCalled(); expect(streamingEngine.destroy).toHaveBeenCalled(); expect(textDisplayer.destroy).toHaveBeenCalled(); }).catch(fail).then(done); }); it('destroys parser first when interrupting load', function(done) { var p = shaka.test.Util.delay(0.3); var 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(); expect(textDisplayer.destroy).not.toHaveBeenCalled(); }); var factory = function() { return parser; }; player.load('', 0, factory).then(fail); shaka.test.Util.delay(0.1).then(function() { player.destroy().catch(fail).then(function() { expect(abrManager.stop).toHaveBeenCalled(); expect(networkingEngine.destroy).toHaveBeenCalled(); expect(textDisplayer.destroy).toHaveBeenCalled(); expect(parser.stop).toHaveBeenCalled(); }).then(done); }); }); }); describe('load/unload', function() { /** @type {!shaka.test.FakeManifestParser} */ var parser1; /** @type {!shaka.test.FakeManifestParser} */ var parser2; /** @type {!Function} */ var factory1; /** @type {!Function} */ var factory2; /** @type {!jasmine.Spy} */ var checkError; beforeEach(function() { goog.asserts.assert(manifest, 'manifest must be non-null'); parser1 = new shaka.test.FakeManifestParser(manifest); parser2 = new shaka.test.FakeManifestParser(manifest); factory1 = function() { return parser1; }; factory2 = function() { return parser2; }; checkError = jasmine.createSpy('checkError'); checkError.and.callFake(function(error) { expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED); }); }); it('won\'t start loading until unloading is done', function(done) { // There was a bug when calling unload before calling load would cause // the load to continue before the (first) unload was complete. // https://github.com/google/shaka-player/issues/612 player.load('', 0, factory1).then(function() { // Delay the promise to destroy the parser. var p = new shaka.util.PublicPromise(); parser1.stop.and.returnValue(p); var unloadDone = false; spyOn(player, 'createMediaSourceEngine'); shaka.test.Util.delay(0.5).then(function() { // Should not start loading yet. expect(player.createMediaSourceEngine).not.toHaveBeenCalled(); // Unblock the unload chain. unloadDone = true; p.resolve(); }); // Explicitly unload the player first. When load calls unload, it // should wait until the parser is destroyed. player.unload(); player.load('', 0, factory2).then(function() { expect(unloadDone).toBe(true); done(); }); }); }); it('destroys TextDisplayer on unload', function(done) { // Regression test for https://github.com/google/shaka-player/issues/984 player.load('', 0, factory1).then(function() { textDisplayer.destroy.calls.reset(); player.unload().then(function() { expect(textDisplayer.destroy).toHaveBeenCalled(); done(); }); }); }); it('handles repeated load/unload', function(done) { player.load('', 0, factory1).then(function() { shaka.log.debug('finished load 1'); return player.unload(); }).then(function() { shaka.log.debug('finished unload 1'); expect(parser1.stop).toHaveBeenCalled(); return player.load('', 0, factory2); }).then(function() { shaka.log.debug('finished load 2'); return player.unload(); }).then(function() { shaka.log.debug('finished unload 2'); expect(parser2.stop).toHaveBeenCalled(); }).catch(fail).then(done); }); it('handles repeated loads', function(done) { player.load('', 0, factory1).then(function() { return player.load('', 0, factory1); }).then(function() { return player.load('', 0, factory2); }).then(function() { expect(parser1.stop.calls.count()).toBe(2); }).catch(fail).then(done); }); it('handles load interrupting load', function(done) { player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.load('', 0, factory2).catch(fail).then(function() { // Delay so the interrupted calls have time to reject themselves. return shaka.test.Util.delay(0.5); }).then(function() { expect(checkError.calls.count()).toBe(2); expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count()); done(); }); }); it('handles unload interrupting load', function(done) { player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.unload().catch(fail); player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.unload().catch(fail); player.load('', 0, factory2).catch(fail).then(function() { // Delay so the interrupted calls have time to reject themselves. return shaka.test.Util.delay(0.5); }).then(function() { expect(checkError.calls.count()).toBe(2); expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count()); done(); }); }); it('handles destroy interrupting load', function(done) { player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.destroy().catch(fail).then(function() { // Delay so the interrupted calls have time to reject themselves. return shaka.test.Util.delay(0.5); }).then(function() { expect(checkError.calls.count()).toBe(1); expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count()); done(); }); }); it('handles multiple unloads interrupting load', function(done) { player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.unload().catch(fail); player.unload().catch(fail); player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.unload().catch(fail); player.unload().catch(fail); player.unload().catch(fail); player.load('', 0, factory2).catch(fail).then(function() { // Delay so the interrupted calls have time to reject themselves. return shaka.test.Util.delay(0.5); }).then(function() { expect(checkError.calls.count()).toBe(2); expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count()); done(); }); }); it('handles multiple destroys interrupting load', function(done) { player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.destroy().catch(fail); player.destroy().catch(fail); player.destroy().catch(fail).then(function() { // Delay so the interrupted calls have time to reject themselves. return shaka.test.Util.delay(0.5); }).then(function() { expect(checkError.calls.count()).toBe(1); expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count()); done(); }); }); it('handles unload, then destroy interrupting load', function(done) { player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.unload().catch(fail); player.unload().catch(fail); player.destroy().catch(fail).then(function() { // Delay so the interrupted calls have time to reject themselves. return shaka.test.Util.delay(0.5); }).then(function() { expect(checkError.calls.count()).toBe(1); expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count()); done(); }); }); it('handles destroy, then unload interrupting load', function(done) { player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)); player.destroy().catch(fail).then(function() { // Delay so the interrupted calls have time to reject themselves. return shaka.test.Util.delay(0.5); }).then(function() { expect(checkError.calls.count()).toBe(1); expect(parser1.stop.calls.count()).toEqual(parser1.start.calls.count()); done(); }); player.unload().catch(fail); player.unload().catch(fail); }); it('streaming event', function(done) { var streamingListener = jasmine.createSpy('listener'); streamingListener.and.callFake(function() { var tracks = player.getVariantTracks(); expect(tracks).toBeDefined(); expect(tracks.length).toEqual(1); var activeTracks = player.getVariantTracks().filter(function(track) { return track.active; }); expect(activeTracks.length).toEqual(0); }); player.addEventListener('streaming', Util.spyFunc(streamingListener)); expect(streamingListener).not.toHaveBeenCalled(); player.load('', 0, factory1).then(function() { expect(streamingListener).toHaveBeenCalled(); }).catch(fail).then(done); }); describe('interruption during', function() { beforeEach(function() { checkError.and.callFake(function(error) { expect(error.code).toBe(shaka.util.Error.Code.LOAD_INTERRUPTED); expect(parser1.stop.calls.count()) .toEqual(parser1.start.calls.count()); expect(parser2.stop.calls.count()) .toEqual(parser2.start.calls.count()); }); }); it('manifest type check', function(done) { // Block the network request. var p = networkingEngine.delayNextRequest(); // Give the stage a factory so that it can succeed and get canceled. shaka.media.ManifestParser.registerParserByMime('undefined', factory1); player.load('', 0).then(fail).catch(Util.spyFunc(checkError)) .then(function() { // Unregister our parser factory. delete shaka.media.ManifestParser.parsersByMime['undefined']; done(); }); shaka.test.Util.delay(0.5).then(function() { // Make sure we're blocked. var requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST; networkingEngine.expectRequest('', requestType); // Interrupt load(). player.unload(); p.resolve(); }); }); it('parser startup', function(done) { // Block parser startup. var p = new shaka.util.PublicPromise(); parser1.start.and.returnValue(p); player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)) .then(done); shaka.test.Util.delay(0.5).then(function() { // Make sure we're blocked. expect(parser1.start).toHaveBeenCalled(); // Interrupt load(). player.unload(); p.resolve(); }); }); it('DrmEngine init', function(done) { // Block DrmEngine init. var p = new shaka.util.PublicPromise(); var drmEngine = new shaka.test.FakeDrmEngine(); drmEngine.init.and.returnValue(p); player.createDrmEngine = function() { return drmEngine; }; player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)) .then(done); shaka.test.Util.delay(1.0).then(function() { // Make sure we're blocked. expect(drmEngine.init).toHaveBeenCalled(); // Interrupt load(). player.unload(); p.resolve(); }); }); it('DrmEngine attach', function(done) { // Block DrmEngine attach. var p = new shaka.util.PublicPromise(); var drmEngine = new shaka.test.FakeDrmEngine(); drmEngine.attach.and.returnValue(p); player.createDrmEngine = function() { return drmEngine; }; player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)) .then(done); shaka.test.Util.delay(1.0).then(function() { // Make sure we're blocked. expect(drmEngine.attach).toHaveBeenCalled(); // Interrupt load(). player.unload(); p.resolve(); }); }); it('StreamingEngine init', function(done) { // Block StreamingEngine init. var p = new shaka.util.PublicPromise(); streamingEngine.init.and.returnValue(p); player.load('', 0, factory1).then(fail).catch(Util.spyFunc(checkError)) .then(done); shaka.test.Util.delay(1.5).then(function() { // Make sure we're blocked. expect(streamingEngine.init).toHaveBeenCalled(); // Interrupt load(). player.unload(); p.resolve(); }); }); }); // describe('interruption during') }); // describe('load/unload') describe('getConfiguration', function() { it('returns a copy of the configuration', function() { var config1 = player.getConfiguration(); config1.streaming.bufferBehind = -99; var config2 = player.getConfiguration(); expect(config1.streaming.bufferBehind).not.toEqual( config2.streaming.bufferBehind); }); }); describe('configure', function() { it('overwrites defaults', function() { var 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 } } }); var 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 } }); var 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(); var defaultConfig = player.getConfiguration(); // Try a bogus bufferBehind (string instead of number) player.configure({ streaming: { bufferBehind: '77' } }); var 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('expands dictionaries that allow arbitrary keys', function() { player.configure({ drm: { servers: { 'com.widevine.alpha': 'http://foo/widevine' } } }); var 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 } } }); var 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(); var 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' } } }); var 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() { var goodCustomScheme = function(node) {}; var badCustomScheme1 = function() {}; // too few args var badCustomScheme2 = function(x, y) {}; // too many args // Takes good callback. player.configure({ manifest: { dash: { customScheme: goodCustomScheme } } }); var 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', function(done) { player.configure({playRangeStart: 5, playRangeEnd: 10}); var 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'); var parser = new shaka.test.FakeManifestParser(manifest); var factory = function() { return parser; }; player.load('', 0, factory).then(function() { var seekRange = player.seekRange(); expect(seekRange.start).toBe(5); expect(seekRange.end).toBe(10); }) .catch(fail) .then(done); }); it('does not switch for plain configuration changes', function(done) { var parser = new shaka.test.FakeManifestParser(manifest); var factory = function() { return parser; }; var switchVariantSpy = spyOn(player, 'switchVariant_'); player.load('', 0, factory) .then(function() { player.configure({abr: {enabled: false}}); player.configure({streaming: {bufferingGoal: 9001}}); // Delay to ensure that the switch would have been called. return shaka.test.Util.delay(0.1); }) .then(function() { expect(switchVariantSpy).not.toHaveBeenCalled(); }) .catch(fail) .then(done); }); }); describe('AbrManager', function() { /** @type {!shaka.test.FakeManifestParser} */ var parser; /** @type {!Function} */ var 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', function(done) { player.load('', 0, parserFactory).then(function() { expect(abrManager.init).toHaveBeenCalled(); }) .catch(fail) .then(done); }); it('calls chooseVariant', function(done) { player.load('', 0, parserFactory).then(function() { expect(abrManager.chooseVariant).toHaveBeenCalled(); }) .catch(fail) .then(done); }); it('does not enable before stream startup', function(done) { player.load('', 0, parserFactory).then(function() { expect(abrManager.enable).not.toHaveBeenCalled(); streamingEngine.onCanSwitch(); expect(abrManager.enable).toHaveBeenCalled(); }) .catch(fail) .then(done); }); it('does not enable if adaptation is disabled', function(done) { player.configure({abr: {enabled: false}}); player.load('', 0, parserFactory).then(function() { streamingEngine.onCanSwitch(); expect(abrManager.enable).not.toHaveBeenCalled(); }) .catch(fail) .then(done); }); it('enables/disables though configure', function(done) { player.load('', 0, parserFactory).then(function() { 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(); }) .catch(fail) .then(done); }); it('waits to enable if in-between Periods', function(done) { player.configure({abr: {enabled: false}}); player.load('', 0, parserFactory).then(function() { 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(); }) .catch(fail) .then(done); }); it('can still be configured through deprecated config', function() { var managerInstance = new shaka.test.FakeAbrManager(); expect(logWarnSpy).not.toHaveBeenCalled(); player.configure({ abr: { manager: managerInstance } }); expect(logWarnSpy).toHaveBeenCalled(); var compatibilityFactory = player.getConfiguration().abrFactory; expect(new compatibilityFactory()).toBe(managerInstance); }); it('can still be given a custom v2.1.x AbrManager', function(done) { var managerInstance = new shaka.test.FakeAbrManager(); // Convert it back into the v2.1.x API. Use a compiler hack to get around // the @struct restrictions on FakeAbrManager. var notAStruct = /** @type {!Object} */(managerInstance); notAStruct['setDefaultEstimate'] = jasmine.createSpy('setDefaultEstimate'); notAStruct['setRestrictions'] = jasmine.createSpy('setRestrictions'); notAStruct['configure'] = null; notAStruct['chooseStreams'] = jasmine.createSpy('choostStreams'); notAStruct['chooseVariant'] = null; // The return value from this matters, so set a fake implementation. notAStruct['chooseStreams'].and.callFake(function(mediaTypes) { var period = manifest.periods[0]; var variant = period.variants[0]; var textStream = period.textStreams[0]; var map = {}; if (mediaTypes.indexOf('audio') >= 0) { map.audio = variant.audio; } if (mediaTypes.indexOf('video') >= 0) { map.video = variant.video; } if (mediaTypes.indexOf('text') >= 0) { map.text = textStream || null; } return map; }); expect(logWarnSpy).not.toHaveBeenCalled(); player.configure({ abrFactory: function() { return managerInstance; } }); // No warning yet. We're using the current configure interface, and the // factory isn't called until load. expect(logWarnSpy).not.toHaveBeenCalled(); player.load('', 0, parserFactory).then(function() { expect(managerInstance.init).toHaveBeenCalled(); expect(notAStruct['setDefaultEstimate']).toHaveBeenCalled(); expect(notAStruct['setRestrictions']).toHaveBeenCalled(); expect(notAStruct['chooseStreams']).toHaveBeenCalled(); expect(logWarnSpy).toHaveBeenCalled(); }).catch(fail).then(done); }); }); describe('filterTracks', function() { it('retains only video+audio variants if they exist', function(done) { manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(1) .bandwidth(200) .language('fr') .addAudio(2).bandwidth(200) .addVariant(2) .bandwidth(400) .language('en') .addAudio(1).bandwidth(200) .addVideo(4).bandwidth(200).size(100, 200) .frameRate(1000000 / 42000) .addVariant(3) .bandwidth(200) .addVideo(5).bandwidth(200).size(300, 400) .frameRate(1000000 / 42000) .addPeriod(1) .addVariant(1) .bandwidth(200) .language('fr') .addAudio(2).bandwidth(200) .addVariant(2) .bandwidth(200) .addVideo(5).bandwidth(200).size(300, 400) .frameRate(1000000 / 42000) .addVariant(3) .bandwidth(400) .language('en') .addAudio(1).bandwidth(200) .addVideo(4).bandwidth(200).size(100, 200) .frameRate(1000000 / 42000) .build(); var variantTracks1 = [ { id: 2, active: true, type: 'variant', bandwidth: 400, language: 'en', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], videoId: 4, audioId: 1, channelsCount: null, audioBandwidth: 200, videoBandwidth: 200 } ]; var variantTracks2 = [ { id: 3, active: false, type: 'variant', bandwidth: 400, language: 'en', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], videoId: 4, audioId: 1, channelsCount: null, audioBandwidth: 200, videoBandwidth: 200 } ]; var parser = new shaka.test.FakeManifestParser(manifest); var parserFactory = function() { return parser; }; player.load('', 0, parserFactory).catch(fail).then(function() { // Check the first period's variant tracks. var actualVariantTracks1 = player.getVariantTracks(); expect(actualVariantTracks1).toEqual(variantTracks1); // Check the second period's variant tracks. playhead.getTime.and.callFake(function() { return 100; }); var actualVariantTracks2 = player.getVariantTracks(); expect(actualVariantTracks2).toEqual(variantTracks2); }).then(done); }); }); describe('tracks', function() { /** @type {!Array.<shakaExtern.Track>} */ var variantTracks; /** @type {!Array.<shakaExtern.Track>} */ var textTracks; beforeEach(function(done) { // A manifest we can use to test track expectations. manifest = new shaka.test.ManifestGenerator() .addPeriod(0) .addVariant(1) .bandwidth(200) .language('en') .addAudio(1).bandwidth(100) .addVideo(4).bandwidth(100).size(100, 200) .frameRate(1000000 / 42000) .addVariant(2) .bandwidth(300) .language('en') .addAudio(1).bandwidth(100) .addVideo(5).bandwidth(200).size(200, 400).frameRate(24) .addVariant(3) .bandwidth(200) .language('en') .addAudio(2).bandwidth(100) .addVideo(4).bandwidth(100).size(100, 200) .frameRate(1000000 / 42000) .addVariant(4) .bandwidth(300) .language('en') .addAudio(2).bandwidth(100) .addVideo(5).bandwidth(200).size(200, 400).frameRate(24) .addVariant(5) .language('es') .bandwidth(300) .addAudio(8).bandwidth(100) .addVideo(5).bandwidth(200).size(200, 400).frameRate(24) .addTextStream(6) .language('es') .label('Spanish') .bandwidth(100).kind('caption') .mime('text/vtt') .addTextStream(7) .language('en') .label('English') .bandwidth(100).kind('caption') .mime('application/ttml+xml') // Both text tracks should remain, even with different MIME types. .addPeriod(1) .addVariant(8) .bandwidth(200) .language('en') .addAudio(9).bandwidth(100) .addVideo(10).bandwidth(100).size(100, 200) .build(); variantTracks = [ { id: 1, active: true, type: 'variant', bandwidth: 200, language: 'en', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], videoId: 4, audioId: 1, channelsCount: null, audioBandwidth: 100, videoBandwidth: 100 }, { id: 2, active: false, type: 'variant', bandwidth: 300, language: 'en', label: null, kind: null, width: 200, height: 400, frameRate: 24, mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], videoId: 5, audioId: 1, channelsCount: null, audioBandwidth: 100, videoBandwidth: 200 }, { id: 3, active: false, type: 'variant', bandwidth: 200, language: 'en', label: null, kind: null, width: 100, height: 200, frameRate: 1000000 / 42000, mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], videoId: 4, audioId: 2, channelsCount: null, audioBandwidth: 100, videoBandwidth: 100 }, { id: 4, active: false, type: 'variant', bandwidth: 300, language: 'en', label: null, kind: null, width: 200, height: 400, frameRate: 24, mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], videoId: 5, audioId: 2, channelsCount: null, audioBandwidth: 100, videoBandwidth: 200 }, { id: 5, active: false, type: 'variant', bandwidth: 300, language: 'es', label: null, kind: null, width: 200, height: 400, frameRate: 24, mimeType: 'video/mp4', codecs: 'avc1.4d401f, mp4a.40.2', audioCodec: 'mp4a.40.2', videoCodec: 'avc1.4d401f', primary: false, roles: [], videoId: 5, audioId: 8, channelsCount: null, audioBandwidth: 100, videoBandwidth: 200 } ]; textTracks = [ { id: 6, active: true, type: ContentType.TEXT, language: 'es', label: 'Spanish', kind: 'caption', mimeType: 'text/vtt', codecs: null, audioCodec: null, videoCodec: null, primary: false, roles: [], channelsCount: null, audioBandwidth: null, videoBandwidth: null }, { id: 7, active: false, type: ContentType.TEXT, language: 'en', label: 'English', kind: 'caption', mimeType: 'application/ttml+xml', codecs: null, audioCodec: null, videoCodec: null, primary: false, roles: [], channelsCount: null, audioBandwidth: null, videoBandwidth: null } ]; goog.asserts.assert(manifest, 'manifest must be non-null'); var parser = new shaka.test.FakeManifestParser(manifest); var parserFactory = function() { return parser; }; // Language prefs must be set before load. Used in select*Language() // tests. player.configure({ preferredAudioLanguage: 'en', preferredTextLanguage: 'es' }); player.load('', 0, parserFactory).catch(fail).then(done); }); 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', function(done) { var parser = new shaka.test.FakeManifestParser(manifest); var 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.init.and.callFake(function(manifest, isOffline) { // The player does not yet have a playhead. expect(player.getVariantTracks()).toEqual([]); expect(player.getTextTracks()).toEqual([]); }); player.load('', 0, parserFactory).catch(fail).then(function() { // Make sure the interruptions didn't mess up the tracks. streamingEngine.onCanSwitch(); expect(player.getVariantTracks()).toEqual(variantTracks); expect(player.getTextTracks()).toEqual(textTracks); }).then(done); }); it('doesn\'t disable AbrManager if switching variants', function() { streamingEngine.onCanSwitch(); var config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); expect(variantTracks[1].type).toBe('variant'); player.selectVariantTrack(variantTracks[1]); config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); }); it('doesn\'t disable AbrManager if switching text', function() { streamingEngine.onCanSwitch(); var config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); expect(textTracks[0].type).toBe(ContentType.TEXT); player.selectTextTrack(textTracks[0]); config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); }); it('switches streams', function() { streamingEngine.onCanSwitch(); var track = variantTracks[3]; var variant = manifest.periods[0].variants[3]; expect(track.id).toEqual(variant.id); player.selectVariantTrack(track); expect(streamingEngine.switchVariant) .toHaveBeenCalledWith(variant, false); }); it('still switches streams if called during startup', function() { // startup is not complete until onCanSwitch is called. // pick a track var track = variantTracks[1]; // ask the player to switch to it player.selectVariantTrack(track); // nothing happens yet expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); var variant = manifest.periods[0].variants[1]; expect(variant.id).toEqual(track.id); // after startup is complete, the manual selection takes effect. streamingEngine.onCanSwitch(); expect(streamingEngine.switchVariant) .toHaveBeenCalledWith(variant, false); }); 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(); var track = variantTracks[3]; var variant = manifest.periods[0].variants[3]; expect(variant.id).toEqual(track.id); // simulate the transition to period 1 transitionPeriod(1); // select the new track (from period 0, which is fine) player.selectVariantTrack(track); expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); // after transition is completed by onCanSwitch, switchVariant is called streamingEngine.onCanSwitch(); expect(streamingEngine.switchVariant) .toHaveBeenCalledWith(variant, false); }); it('switching audio doesn\'t change selected text track', function() { streamingEngine.onCanSwitch(); player.configure({ preferredTextLanguage: 'es' }); var textStream = manifest.periods[0].textStreams[1]; expect(textTracks[1].type).toBe(ContentType.TEXT); expect(textTracks[1].language).toBe('en'); var textTrack = textTracks[1]; streamingEngine.switchTextStream.calls.reset(); player.selectTextTrack(textTrack); expect(streamingEngine.switchTextStream).toHaveBeenCalledWith(textStream); // We have selected an English text track explicitly. expect(getActiveTextTrack().id).toBe(textTrack.id); var variantTrack = variantTracks[2]; var variant = manifest.periods[0].variants[2]; expect(variantTrack.id).toBe(variant.id); player.selectVariantTrack(variantTrack); // The active text track has not changed, even though the text language // preference is Spanish. expect(getActiveTextTrack().id).toBe(textTrack.id); }); it('selectAudioLanguage() takes precedence over preferredAudioLanguage', function() { streamingEngine.onCanSwitch(); // This preference is set in beforeEach, before load(). expect(player.getConfiguration().preferredAudioLanguage).toBe('en');