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