shaka-player
Version:
DASH/EME video player library
1,473 lines (1,299 loc) • 95.4 kB
JavaScript
/**
* @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');