Files
shaka-player/test/player_integration.js
T
Jacob Trimble cbcfba031d Avoid having two top-level describe() blocks.
This moves some of the tests around to have each file use only one
top-level describe() block.  This removes some Player integration tests
that are already tested elsewhere.

There are two cases where there are two top-level describe() blocks
left: the segment reference and http plugin tests.

Change-Id: Iab7365919cfba739d4710184f079b6d2638192b6
2019-09-12 15:50:43 +00:00

598 lines
23 KiB
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', () => {
const Util = shaka.test.Util;
const waitUntilPlayheadReaches = Util.waitUntilPlayheadReaches;
/** @type {!jasmine.Spy} */
let onErrorSpy;
/** @type {!HTMLVideoElement} */
let video;
/** @type {shaka.Player} */
let player;
/** @type {!shaka.util.EventManager} */
let eventManager;
let compiledShaka;
beforeAll(async () => {
video = shaka.util.Dom.createVideoElement();
document.body.appendChild(video);
compiledShaka = await Util.loadShaka(getClientArg('uncompiled'));
});
beforeEach(async () => {
await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
player = new compiledShaka.Player(video);
// Grab event manager from the uncompiled library:
eventManager = new shaka.util.EventManager();
onErrorSpy = jasmine.createSpy('onError');
onErrorSpy.and.callFake((event) => {
fail(event.detail);
});
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
});
afterEach(async () => {
eventManager.release();
await player.destroy();
});
afterAll(() => {
document.body.removeChild(video);
});
describe('attach', () => {
beforeEach(async () => {
// To test attach, we want to construct a player without a video element
// attached in advance. To do that, we destroy the player that was
// constructed in the outermost beforeEach(), then construct a new one
// without a video element.
await player.destroy();
player = new compiledShaka.Player();
});
it('can be used before load()', async () => {
await player.attach(video);
await player.load('test:sintel_compiled');
});
});
describe('getStats', () => {
it('gives stats about current stream', async () => {
// This is tested more in player_unit.js. This is here to test the public
// API and to check for renaming.
await player.load('test:sintel_compiled');
video.play();
await waitUntilPlayheadReaches(eventManager, video, 1, 10);
const stats = player.getStats();
const expected = {
width: jasmine.any(Number),
height: jasmine.any(Number),
streamBandwidth: jasmine.any(Number),
decodedFrames: jasmine.any(Number),
droppedFrames: jasmine.any(Number),
estimatedBandwidth: jasmine.any(Number),
loadLatency: jasmine.any(Number),
playTime: jasmine.any(Number),
pauseTime: jasmine.any(Number),
bufferingTime: jasmine.any(Number),
// We should have loaded the first Period by now, so we should have a
// history.
switchHistory: jasmine.arrayContaining([{
timestamp: jasmine.any(Number),
id: jasmine.any(Number),
type: 'variant',
fromAdaptation: true,
bandwidth: 0,
}]),
stateHistory: jasmine.arrayContaining([{
state: 'playing',
timestamp: jasmine.any(Number),
duration: jasmine.any(Number),
}]),
};
expect(stats).toEqual(expected);
});
// Regression test for Issue #968 where trying to get the stats before
// calling load would fail because not all components had been initialized.
it('can get stats before loading content', async () => {
// Destroy Player created in beforeEach.
await player.destroy();
// We are opting not to initialize the player with a video element so that
// it is in the least loaded state possible.
player = new shaka.Player();
const stats = player.getStats();
expect(stats).toBeTruthy();
await player.destroy();
});
});
describe('setTextTrackVisibility', () => {
// Using mode='disabled' on TextTrack causes cues to go null, which leads
// to a crash in TextEngine. This validates that we do not trigger this
// behavior when changing visibility of text.
it('does not cause cues to be null', async () => {
await player.load('test:sintel_compiled');
video.play();
await waitUntilPlayheadReaches(eventManager, video, 1, 10);
// This TextTrack was created as part of load() when we set up the
// TextDisplayer.
const textTrack = video.textTracks[0];
expect(textTrack).not.toBe(null);
if (textTrack) {
// This should not be null initially.
expect(textTrack.cues).not.toBe(null);
await player.setTextTrackVisibility(true);
// This should definitely not be null when visible.
expect(textTrack.cues).not.toBe(null);
await player.setTextTrackVisibility(false);
// This should not transition to null when invisible.
expect(textTrack.cues).not.toBe(null);
}
});
it('is called automatically if language prefs match', async () => {
// If the text is a match for the user's preferences, and audio differs
// from text, we enable text display automatically.
// NOTE: This is also a regression test for #1696, in which a change
// to this feature broke StreamingEngine initialization.
const preferredTextLanguage = 'fa'; // The same as in the content itself
player.configure({preferredTextLanguage: preferredTextLanguage});
// Now load a version of Sintel with delayed setup of video & audio
// streams and wait for completion.
await player.load('test:sintel_realistic_compiled');
// By this point, a MediaSource error would be thrown in a repro of bug
// #1696.
// Make sure the automatic setting took effect.
expect(player.isTextTrackVisible()).toBe(true);
// Make sure the content we tested with has text tracks, that the config
// we used matches the text language, and that the audio language differs.
// These will catch any changes to the underlying content that would
// invalidate the test setup.
expect(player.getTextTracks().length).not.toBe(0);
const textTrack = player.getTextTracks()[0];
expect(textTrack.language).toBe(preferredTextLanguage);
const variantTrack = player.getVariantTracks()[0];
expect(variantTrack.language).not.toBe(textTrack.language);
});
it('is not called automatically without language pref match', async () => {
// If the text preference doesn't match the content, we do not enable text
// display automatically.
const preferredTextLanguage = 'xx'; // Differs from the content itself
player.configure({preferredTextLanguage: preferredTextLanguage});
// Now load the content and wait for completion.
await player.load('test:sintel_realistic_compiled');
// Make sure the automatic setting did not happen.
expect(player.isTextTrackVisible()).toBe(false);
// Make sure the content we tested with has text tracks, that the config
// we used does not match the text language, and that the text and audio
// languages do not match each other (to keep this distinct from the next
// test case). This will catch any changes to the underlying content that
// would invalidate the test setup.
expect(player.getTextTracks().length).not.toBe(0);
const textTrack = player.getTextTracks()[0];
expect(textTrack.language).not.toBe(preferredTextLanguage);
const variantTrack = player.getVariantTracks()[0];
expect(variantTrack.language).not.toBe(textTrack.language);
});
it('is not called automatically with audio and text match', async () => {
// If the audio and text tracks use the same language, we do not enable
// text display automatically, no matter the text preference.
const preferredTextLanguage = 'und'; // The same as in the content itself
player.configure({preferredTextLanguage: preferredTextLanguage});
// Now load the content and wait for completion.
await player.load('test:sintel_compiled');
// Make sure the automatic setting did not happen.
expect(player.isTextTrackVisible()).toBe(false);
// Make sure the content we tested with has text tracks, that the
// config we used matches the content, and that the text and audio
// languages match each other. This will catch any changes to the
// underlying content that would invalidate the test setup.
expect(player.getTextTracks().length).not.toBe(0);
const textTrack = player.getTextTracks()[0];
expect(textTrack.language).toBe(preferredTextLanguage);
const variantTrack = player.getVariantTracks()[0];
expect(variantTrack.language).toBe(textTrack.language);
});
// Repro for https://github.com/google/shaka-player/issues/1879.
it('actually appends cues when enabled initially', async () => {
let cues = [];
/** @const {!shaka.test.FakeTextDisplayer} */
const displayer = new shaka.test.FakeTextDisplayer();
displayer.appendSpy.and.callFake((added) => {
cues = cues.concat(added);
});
player.configure('textDisplayFactory', Util.factoryReturns(displayer));
const preferredTextLanguage = 'fa'; // The same as in the content itself
player.configure({preferredTextLanguage: preferredTextLanguage});
await player.load('test:sintel_realistic_compiled');
await Util.delay(1); // Allow the first segments to be appended.
expect(player.isTextTrackVisible()).toBe(true);
expect(displayer.isTextVisible()).toBe(true);
expect(cues.length).toBeGreaterThan(0);
});
it('actually appends cues for external text', async () => {
let cues = [];
/** @const {!shaka.test.FakeTextDisplayer} */
const displayer = new shaka.test.FakeTextDisplayer();
displayer.appendSpy.and.callFake((added) => {
cues = cues.concat(added);
});
player.configure('textDisplayFactory', Util.factoryReturns(displayer));
const eventManager = new shaka.util.EventManager();
/** @type {shaka.test.Waiter} */
const waiter = new shaka.test.Waiter(eventManager);
await player.load('test:sintel_no_text_compiled');
const locationUri = new goog.Uri(location.href);
const partialUri = new goog.Uri('/base/test/test/assets/text-clip.vtt');
const absoluteUri = locationUri.resolve(partialUri);
await player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles',
'text/vtt');
const textTracks = player.getTextTracks();
expect(textTracks).toBeTruthy();
expect(textTracks.length).toBe(1);
player.setTextTrackVisibility(true);
await waiter.waitForEvent(player, 'texttrackvisibility');
// Wait for the text cues to get appended.
// TODO: this should be based on an event instead.
await Util.delay(1);
expect(player.isTextTrackVisible()).toBe(true);
expect(displayer.isTextVisible()).toBe(true);
expect(cues.length).toBeGreaterThan(0);
});
});
describe('plays', () => {
it('with external text tracks', async () => {
await player.load('test:sintel_no_text_compiled');
// For some reason, using path-absolute URLs (i.e. without the hostname)
// like this doesn't work on Safari. So manually resolve the URL.
const locationUri = new goog.Uri(location.href);
const partialUri = new goog.Uri('/base/test/test/assets/text-clip.vtt');
const absoluteUri = locationUri.resolve(partialUri);
await player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles',
'text/vtt');
const textTracks = player.getTextTracks();
expect(textTracks).toBeTruthy();
expect(textTracks.length).toBe(1);
expect(textTracks[0].active).toBe(true);
expect(textTracks[0].language).toBe('en');
});
it('with cea closed captions', async () => {
await player.load('test:cea-708_mp4_compiled');
const textTracks = player.getTextTracks();
expect(textTracks).toBeTruthy();
expect(textTracks.length).toBe(1);
expect(textTracks[0].language).toBe('en');
});
it('while changing languages with short Periods', async () => {
// See: https://github.com/google/shaka-player/issues/797
player.configure({preferredAudioLanguage: 'en'});
await player.load('test:sintel_short_periods_compiled');
video.play();
await waitUntilPlayheadReaches(eventManager, video, 8, 30);
// The Period changes at 10 seconds. Assert that we are in the previous
// Period and have buffered into the next one.
expect(video.currentTime).toBeLessThan(9);
// The two periods might not be in a single contiguous buffer, so don't
// check end(0). Gap-jumping will deal with any discontinuities.
const bufferEnd = video.buffered.end(video.buffered.length - 1);
expect(bufferEnd).toBeGreaterThan(11);
// Change to a different language; this should clear the buffers and
// cause a Period transition again.
expect(getActiveLanguage()).toBe('en');
player.selectAudioLanguage('es');
await waitUntilPlayheadReaches(eventManager, video, 21, 30);
// Should have gotten past the next Period transition and still be
// playing the new language.
expect(getActiveLanguage()).toBe('es');
});
it('at higher playback rates', async () => {
await player.load('test:sintel_compiled');
video.play();
await waitUntilPlayheadReaches(eventManager, video, 1, 10);
// Enabling trick play should change our playback rate to the same rate.
player.trickPlay(2);
expect(video.playbackRate).toBe(2);
// Let playback continue playing for a bit longer.
await shaka.test.Util.delay(2);
// Cancelling trick play should return our playback rate to normal.
player.cancelTrickPlay();
expect(video.playbackRate).toBe(1);
});
/**
* Gets the language of the active Variant.
* @return {string}
*/
function getActiveLanguage() {
const tracks = player.getVariantTracks().filter((t) => {
return t.active;
});
expect(tracks.length).toBeGreaterThan(0);
return tracks[0].language;
}
});
describe('TextDisplayer plugin', () => {
// Simulate the use of an external TextDisplayer plugin.
/** @type {shaka.test.FakeTextDisplayer} */
let textDisplayer;
beforeEach(() => {
textDisplayer = new shaka.test.FakeTextDisplayer();
textDisplayer.isTextVisibleSpy.and.callFake(() => {
return false;
});
textDisplayer.destroySpy.and.returnValue(Promise.resolve());
player.configure({
textDisplayFactory: Util.factoryReturns(textDisplayer),
});
// Make sure the configuration was taken.
const ConfiguredFactory = player.getConfiguration().textDisplayFactory;
const configuredTextDisplayer = new ConfiguredFactory();
expect(configuredTextDisplayer).toBe(textDisplayer);
});
// Regression test for https://github.com/google/shaka-player/issues/1187
it('does not throw on destroy', async () => {
await player.load('test:sintel_compiled');
video.play();
await waitUntilPlayheadReaches(eventManager, video, 1, 10);
await player.unload();
// Before we fixed #1187, the call to destroy() on textDisplayer was
// renamed in the compiled version and could not be called.
expect(textDisplayer.destroySpy).toHaveBeenCalled();
});
});
describe('TextAndRoles', () => {
// Regression Test. Makes sure that the language and role fields have been
// properly exported from the player.
it('exports language and roles fields', async () => {
await player.load('test:sintel_compiled');
const languagesAndRoles = player.getTextLanguagesAndRoles();
expect(languagesAndRoles.length).toBeTruthy();
for (const languageAndRole of languagesAndRoles) {
expect(languageAndRole.language).not.toBeUndefined();
expect(languageAndRole.role).not.toBeUndefined();
}
});
});
describe('streaming event', () => {
// Calling switch early during load() caused a failed assertion in Player
// and the track selection was ignored. Because this bug involved
// interactions between Player and StreamingEngine, it is an integration
// test and not a unit test.
// https://github.com/google/shaka-player/issues/1119
it('allows early selection of specific tracks', async () => {
/** @type {!jasmine.Spy} */
const streamingListener = jasmine.createSpy('listener');
// Because this is an issue with failed assertions, destroy the existing
// player from the compiled version, and create a new one using the
// uncompiled version. Then we will get assertions.
eventManager.unlisten(player, 'error');
await player.destroy();
player = new shaka.Player(video);
player.configure({abr: {enabled: false}});
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
// When 'streaming' fires, select the first track explicitly.
player.addEventListener('streaming', Util.spyFunc(streamingListener));
streamingListener.and.callFake(() => {
const tracks = player.getVariantTracks();
player.selectVariantTrack(tracks[0]);
});
// Now load the content.
await player.load('test:sintel');
// When the bug triggers, we fail assertions in Player.
// Make sure the listener was triggered, so that it could trigger the
// code path in this bug.
expect(streamingListener).toHaveBeenCalled();
});
// After fixing the issue above, calling switch early during a second load()
// caused a failed assertion in StreamingEngine, because we did not reset
// switchingPeriods_ in Player. Because this bug involved interactions
// between Player and StreamingEngine, it is an integration test and not a
// unit test.
// https://github.com/google/shaka-player/issues/1119
it('allows selection of tracks in subsequent loads', async () => {
/** @type {!jasmine.Spy} */
const streamingListener = jasmine.createSpy('listener');
// Because this is an issue with failed assertions, destroy the existing
// player from the compiled version, and create a new one using the
// uncompiled version. Then we will get assertions.
eventManager.unlisten(player, 'error');
await player.destroy();
player = new shaka.Player(video);
player.configure({abr: {enabled: false}});
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
// This bug only triggers when you do this on the second load.
// So we load one piece of content, then set up the streaming listener
// to change tracks, then we load a second piece of content.
await player.load('test:sintel');
// Give StreamingEngine time to complete all setup and to call back into
// the Player with canSwitch_. If you move on too quickly to the next
// load(), the bug does not reproduce.
await shaka.test.Util.delay(1);
player.addEventListener('streaming', Util.spyFunc(streamingListener));
streamingListener.and.callFake(() => {
const track = player.getVariantTracks()[0];
player.selectVariantTrack(track);
});
// Now load again to trigger the failed assertion.
await player.load('test:sintel');
// When the bug triggers, we fail assertions in StreamingEngine.
// So just make sure the listener was triggered, so that it could
// trigger the code path in this bug.
expect(streamingListener).toHaveBeenCalled();
});
});
describe('tracks', () => {
// This is a regression test for b/138941217, in which tracks briefly
// vanished during the loading process. On Chromecast devices, where the
// timing is very different from on desktop, this could occur such that
// there were no tracks after load() is resolved.
// This is an integration test so that we can check the behavior of the
// Player against actual platform behavior on all supported platforms.
it('remain available at every stage of loading', async () => {
let tracksFound = false;
/**
* @param {string} when When the check takes place.
*
* Will fail the test if tracks disappear after they first become
* available.
*/
const checkTracks = (when) => {
// If tracks have already been found, expect them to still be found.
const tracksNow = player.getVariantTracks().length != 0;
if (tracksFound) {
expect(tracksNow).withContext(when).toBe(true);
} else {
// If tracks are now found, they should not, at any point during
// the loading process, disappear again.
if (tracksNow) {
tracksFound = true;
}
}
shaka.log.debug(
'checkTracks', when,
'tracksFound=', tracksFound,
'tracksNow=', tracksNow);
};
/** @param {Event} event */
const checkOnEvent = (event) => {
checkTracks(event.type + ' event');
};
// On each of these events, we will notice when tracks first appear, and
// verify that they never disappear at any point in the loading sequence.
eventManager.listen(video, 'canplay', checkOnEvent);
eventManager.listen(video, 'canplaythrough', checkOnEvent);
eventManager.listen(video, 'durationchange', checkOnEvent);
eventManager.listen(video, 'emptied', checkOnEvent);
eventManager.listen(video, 'loadeddata', checkOnEvent);
eventManager.listen(video, 'loadedmetadata', checkOnEvent);
eventManager.listen(video, 'loadstart', checkOnEvent);
eventManager.listen(video, 'pause', checkOnEvent);
eventManager.listen(video, 'play', checkOnEvent);
eventManager.listen(video, 'playing', checkOnEvent);
eventManager.listen(video, 'seeked', checkOnEvent);
eventManager.listen(video, 'seeking', checkOnEvent);
eventManager.listen(video, 'stalled', checkOnEvent);
eventManager.listen(video, 'waiting', checkOnEvent);
eventManager.listen(player, 'trackschanged', checkOnEvent);
const waiter = (new shaka.test.Waiter(eventManager)).timeoutAfter(10);
const canPlayThrough = waiter.waitForEvent(video, 'canplaythrough');
// Important: use a stream that starts somewhere other than zero, so that
// the video element's time is initially different from the start time of
// playback, and there is no content at time zero.
await player.load('test:sintel_start_at_3_compiled', 5);
shaka.log.debug('load resolved');
// When load is resolved(), tracks should definitely exist.
expect(tracksFound).toBe(true);
// Let the test keep running until we can play through. In the original
// bug, tracks would disappear _after_ load() on some platforms.
await canPlayThrough;
});
});
});