Files
shaka-player/test/media/streaming_engine_integration.js
T
Joey Parrish 5d07b5fbb7 Fix flake in high playback rate test
Something weird happens on some platforms (variously Chromecast, IE,
legacy Edge, and Safari) where the playhead can go past duration.  To
cope with this, don't fail on timeout.  If the video never got flagged
as "ended", check for the playhead to be near or past the end.

Also, wait for playback to begin before increasing the playback rate.
This improves test reliability on slow platforms like Chromecast.

Change-Id: If7d70de95b75e602853ec77ad1c285c118875db4
2020-04-09 19:22:16 +00:00

618 lines
20 KiB
JavaScript

/** @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('StreamingEngine', () => {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const Util = shaka.test.Util;
let metadata;
let generators;
/** @type {!shaka.util.EventManager} */
let eventManager;
/** @type {shaka.test.Waiter} */
let waiter;
/** @type {!HTMLVideoElement} */
let video;
let timeline;
/** @type {!shaka.media.Playhead} */
let playhead;
/** @type {shaka.extern.StreamingConfiguration} */
let config;
let netEngine;
/** @type {!shaka.media.MediaSourceEngine} */
let mediaSourceEngine;
/** @type {!shaka.media.StreamingEngine} */
let streamingEngine;
/** @type {shaka.extern.Variant} */
let variant;
/** @type {shaka.extern.Manifest} */
let manifest;
/** @type {!jasmine.Spy} */
let onError;
/** @type {!jasmine.Spy} */
let onEvent;
beforeAll(() => {
video = shaka.test.UiUtils.createVideoElement();
document.body.appendChild(video);
metadata = shaka.test.TestScheme.DATA['sintel'];
generators = {};
});
beforeEach(() => {
config = shaka.util.PlayerConfiguration.createDefault().streaming;
onError = jasmine.createSpy('onError');
onError.and.callFake(fail);
onEvent = jasmine.createSpy('onEvent');
eventManager = new shaka.util.EventManager();
waiter = new shaka.test.Waiter(eventManager);
mediaSourceEngine = new shaka.media.MediaSourceEngine(
video,
new shaka.test.FakeClosedCaptionParser(),
new shaka.test.FakeTextDisplayer());
});
afterEach(async () => {
eventManager.release();
await streamingEngine.destroy();
await mediaSourceEngine.destroy();
playhead.release();
});
afterAll(() => {
document.body.removeChild(video);
});
async function setupVod() {
await createVodStreamGenerator(metadata.audio, ContentType.AUDIO);
await createVodStreamGenerator(metadata.video, ContentType.VIDEO);
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
/* segmentAvailabilityStart= */ 0,
/* segmentAvailabilityEnd= */ 60,
/* presentationDuration= */ 60,
/* maxSegmentDuration= */ metadata.video.segmentDuration,
/* isLive= */ false);
setupNetworkingEngine(
/* presentationDuration= */ 60,
{
audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration,
});
setupManifest(
/* firstPeriodStartTime= */ 0,
/* secondPeriodStartTime= */ 30,
/* presentationDuration= */ 60);
setupPlayhead();
createStreamingEngine();
}
async function setupLive() {
await createLiveStreamGenerator(
metadata.audio,
ContentType.AUDIO,
/* timeShiftBufferDepth= */ 20);
await createLiveStreamGenerator(
metadata.video,
ContentType.VIDEO,
/* timeShiftBufferDepth= */ 20);
// The generator's AST is set to 295 seconds in the past, so the live-edge
// is at 295 - 10 seconds.
// -10 to account for maxSegmentDuration.
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
/* segmentAvailabilityStart= */ 275 - 10,
/* segmentAvailabilityEnd= */ 295 - 10,
/* presentationDuration= */ Infinity,
/* maxSegmentDuration= */ metadata.video.segmentDuration,
/* isLive= */ true);
setupNetworkingEngine(
/* presentationDuration= */ Infinity,
{
audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration,
});
setupManifest(
/* firstPeriodStartTime= */ 0,
/* secondPeriodStartTime= */ 300,
/* presentationDuration= */ Infinity);
setupPlayhead();
createStreamingEngine();
}
function createVodStreamGenerator(metadata, type) {
const generator = new shaka.test.Mp4VodStreamGenerator(
metadata.initSegmentUri,
metadata.mdhdOffset,
metadata.segmentUri,
metadata.tfdtOffset,
metadata.segmentDuration);
generators[type] = generator;
return generator.init();
}
function createLiveStreamGenerator(metadata, type, timeShiftBufferDepth) {
// Set the generator's AST to 295 seconds in the past so the
// StreamingEngine begins streaming close to the end of the first Period.
const now = Date.now() / 1000;
const generator = new shaka.test.Mp4LiveStreamGenerator(
metadata.initSegmentUri,
metadata.mdhdOffset,
metadata.segmentUri,
metadata.tfdtOffset,
metadata.segmentDuration,
/* broadcastStartTime= */ now - 295,
/* availabilityStartTime= */ now - 295,
timeShiftBufferDepth);
generators[type] = generator;
return generator.init();
}
function setupNetworkingEngine(presentationDuration, segmentDurations) {
// Create the fake NetworkingEngine. Note: the StreamingEngine should never
// request a segment that does not exist.
netEngine = shaka.test.StreamingEngineUtil.createFakeNetworkingEngine(
// Init segment generator:
(type, periodNumber) => {
const wallClockTime = Date.now() / 1000;
const segment = generators[type].getInitSegment(wallClockTime);
expect(segment).not.toBeNull();
return segment;
},
// Media segment generator:
(type, periodNumber, position) => {
const wallClockTime = Date.now() / 1000;
const segment = generators[type].getSegment(position, wallClockTime);
expect(segment).not.toBeNull();
return segment;
});
}
function setupPlayhead() {
const onSeek = () => {
streamingEngine.seeked();
};
playhead = new shaka.media.MediaSourcePlayhead(
/** @type {!HTMLVideoElement} */(video),
manifest,
config,
/* startTime= */ null,
onSeek,
shaka.test.Util.spyFunc(onEvent));
}
function setupManifest(
firstPeriodStartTime, secondPeriodStartTime, presentationDuration) {
manifest = shaka.test.StreamingEngineUtil.createManifest(
/** @type {!shaka.media.PresentationTimeline} */(timeline),
[firstPeriodStartTime, secondPeriodStartTime], presentationDuration,
/* segmentDurations= */ {
audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration,
},
/* initSegmentRanges= */ {
audio: [0, null],
video: [0, null],
});
variant = manifest.variants[0];
}
function createStreamingEngine() {
const playerInterface = {
getPresentationTime: () => playhead.getTime(),
getBandwidthEstimate: () => 1e6,
mediaSourceEngine: mediaSourceEngine,
netEngine: /** @type {!shaka.net.NetworkingEngine} */(netEngine),
onError: Util.spyFunc(onError),
onEvent: Util.spyFunc(onEvent),
onManifestUpdate: () => {},
onSegmentAppended: () => playhead.notifyOfBufferingChange(),
};
streamingEngine = new shaka.media.StreamingEngine(
/** @type {shaka.extern.Manifest} */(manifest), playerInterface);
streamingEngine.configure(config);
}
describe('VOD', () => {
beforeEach(async () => {
await setupVod();
});
it('plays', async () => {
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
await waiter.timeoutAfter(90).waitForEnd(video);
});
it('plays at high playback rates', async () => {
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
// Wait for playback to begin before increasing the playback rate. This
// improves test reliability on slow platforms like Chromecast.
await waiter.timeoutAfter(10).waitForMovement(video);
video.playbackRate = 10;
// Something weird happens on some platforms (variously Chromecast, IE,
// legacy Edge, and Safari) where the playhead can go past duration.
// To cope with this, don't fail on timeout. If the video never got
// flagged as "ended", check for the playhead to be near or past the end.
await waiter.timeoutAfter(30).failOnTimeout(false).waitForEnd(video);
if (!video.ended) {
expect(video.currentTime).toBeGreaterThan(video.duration - 0.1);
}
});
it('can handle buffered seeks', async () => {
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
// After 35 seconds seek back 10 seconds into the first Period.
await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 35);
video.currentTime = 25;
await waiter.timeoutAfter(60).waitForEnd(video);
});
it('can handle unbuffered seeks', async () => {
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 20);
video.currentTime = 40;
await waiter.timeoutAfter(60).waitForEnd(video);
});
});
describe('Live', () => {
/** @type {number} */
let slideSegmentAvailabilityWindow;
beforeEach(async () => {
await setupLive();
slideSegmentAvailabilityWindow = window.setInterval(() => {
timeline.segmentAvailabilityStart++;
timeline.segmentAvailabilityEnd++;
}, 1000);
});
afterEach(() => {
window.clearInterval(slideSegmentAvailabilityWindow);
});
it('plays through Period transition', async () => {
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 305);
const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
// firstSegmentNumber =
// [(segmentAvailabilityEnd - rebufferingGoal) / segmentDuration] + 1
netEngine.expectRequest('0_video_29', segmentType);
netEngine.expectRequest('0_audio_29', segmentType);
});
it('can handle seeks ahead of availability window', async () => {
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
// IE is sensitive and throws InvalidStateError when you seek while
// readyState is 0.
await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata');
// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityEnd + 120;
video.play();
// Wait until the repositioning is complete so we don't
// immediately hit this case.
await shaka.test.Util.delay(/* seconds= */ 1);
await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 305);
});
it('can handle seeks behind availability window', async () => {
let seekCount = 0;
eventManager.listen(video, 'seeking', () => {
seekCount++;
});
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
// IE is sensitive and throws InvalidStateError when you seek while
// readyState is 0.
await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata');
// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityStart - 120;
expect(video.currentTime).toBeGreaterThan(0);
video.play();
await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 305);
// We are playing close to the beginning of the availability window.
// We should be playing smoothly and not seeking repeatedly as we fall
// outside the window.
//
// Expected seeks:
// 1. seek to live stream start time during startup
// 2. explicit seek in the test to get outside the window
// 3. Playhead seeks to force us back inside the window
// 4. (maybe) seek if there is a gap at the period boundary
// 5. (maybe) seek to flush a pipeline stall
expect(seekCount).toBeGreaterThan(2);
expect(seekCount).toBeLessThan(6);
});
});
// This tests gaps created by missing segments.
// TODO: Consider also adding tests for missing frames.
describe('gap jumping', () => {
it('jumps small gaps at the beginning', async () => {
config.smallGapLimit = 5;
await setupGappyContent(/* gapAtStart= */ 1, /* dropSegment= */ false);
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
await waiter.timeoutAfter(5).waitUntilPlayheadReaches(video, 0.01);
expect(video.buffered.length).toBeGreaterThan(0);
expect(video.buffered.start(0)).toBeCloseTo(1);
await waiter.timeoutAfter(20).waitUntilPlayheadReaches(video, 5);
});
it('jumps large gaps at the beginning', async () => {
config.smallGapLimit = 1;
config.jumpLargeGaps = true;
await setupGappyContent(/* gapAtStart= */ 5, /* dropSegment= */ false);
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
await waiter.timeoutAfter(5).waitUntilPlayheadReaches(video, 0.01);
expect(video.buffered.length).toBeGreaterThan(0);
expect(video.buffered.start(0)).toBeCloseTo(5);
await waiter.timeoutAfter(20).waitUntilPlayheadReaches(video, 8);
});
it('jumps small gaps in the middle', async () => {
config.smallGapLimit = 20;
await setupGappyContent(/* gapAtStart= */ 0, /* dropSegment= */ true);
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
// IE is sensitive and throws InvalidStateError when you seek while
// readyState is 0.
await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata');
video.currentTime = 8;
video.play();
await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 23);
// Should be close enough to still have the gap buffered.
expect(video.buffered.length).toBe(2);
expect(onEvent).not.toHaveBeenCalled();
});
it('jumps large gaps in the middle', async () => {
config.jumpLargeGaps = true;
await setupGappyContent(/* gapAtStart= */ 0, /* dropSegment= */ true);
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
// IE is sensitive and throws InvalidStateError when you seek while
// readyState is 0.
await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata');
video.currentTime = 8;
video.play();
await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 23);
// Should be close enough to still have the gap buffered.
expect(video.buffered.length).toBe(2);
expect(onEvent).toHaveBeenCalled();
});
it('won\'t jump large gaps with preventDefault()', async () => {
config.jumpLargeGaps = true;
await setupGappyContent(/* gapAtStart= */ 0, /* dropSegment= */ true);
onEvent.and.callFake((event) => {
event.preventDefault();
});
// Let's go!
streamingEngine.switchVariant(variant);
await streamingEngine.start();
// IE is sensitive and throws InvalidStateError when you seek while
// readyState is 0.
await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata');
video.currentTime = 8;
video.play();
await shaka.test.Util.delay(5);
// IE/Edge somehow plays inside the gap. Just make sure we
// don't jump the gap.
expect(video.currentTime).toBeLessThan(20);
});
/**
* @param {number} gapAtStart The gap to introduce before start, in seconds.
* @param {boolean} dropSegment Whether to drop a segment in the middle.
* @return {!Promise}
*/
async function setupGappyContent(gapAtStart, dropSegment) {
// This uses "normal" stream generators and networking engine. The only
// difference is the segments are removed from the manifest. The segments
// should not be downloaded.
await createVodStreamGenerator(metadata.audio, ContentType.AUDIO);
await createVodStreamGenerator(metadata.video, ContentType.VIDEO);
timeline =
shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
/* segmentAvailabilityStart= */ 0,
/* segmentAvailabilityEnd= */ 30,
/* presentationDuration= */ 30,
/* maxSegmentDuration= */ metadata.video.segmentDuration,
/* isLive= */ false);
setupNetworkingEngine(
/* presentationDuration= */ 30,
{
audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration,
});
manifest = setupGappyManifest(gapAtStart, dropSegment);
variant = manifest.variants[0];
setupPlayhead();
createStreamingEngine();
}
/**
* TODO: Consolidate with StreamingEngineUtil.createManifest?
* @param {number} gapAtStart
* @param {boolean} dropSegment
* @return {shaka.extern.Manifest}
*/
function setupGappyManifest(gapAtStart, dropSegment) {
/**
* @param {string} type
* @param {shaka.media.InitSegmentReference} initSegmentReference
* @return {!shaka.media.SegmentIndex}
*/
function createIndex(type, initSegmentReference) {
const d = metadata[type].segmentDuration;
const refs = [];
let i = 0;
let time = gapAtStart;
while (time < 30) {
let end = time + d;
// Make segment 0 longer to make the manifest continuous, despite the
// dropped segment.
if (i == 0 && dropSegment) {
end += d;
}
let cur = i;
const getUris = () => {
// The times in the media are based on the URL; so to drop a
// segment, we change the URL.
if (cur >= 1 && dropSegment) {
cur++;
}
return ['0_' + type + '_' + cur];
};
refs.push(new shaka.media.SegmentReference(
/* startTime= */ time,
/* endTime= */ end,
getUris,
/* startByte= */ 0,
/* endByte= */ null,
initSegmentReference,
/* timestampOffset= */ gapAtStart,
/* appendWindowStart= */ 0,
/* appendWindowEnd= */ Infinity));
i++;
time = end;
}
return new shaka.media.SegmentIndex(refs);
}
function createInit(type) {
const getUris = () => {
return ['0_' + type + '_init'];
};
return new shaka.media.InitSegmentReference(getUris, 0, null);
}
const videoInit = createInit('video');
const videoIndex = createIndex('video', videoInit);
const audioInit = createInit('audio');
const audioIndex = createIndex('audio', audioInit);
return {
presentationTimeline: timeline,
offlineSessionIds: [],
minBufferTime: 2,
textStreams: [],
variants: [{
id: 1,
video: {
id: 2,
createSegmentIndex: () => Promise.resolve(),
segmentIndex: videoIndex,
mimeType: 'video/mp4',
codecs: 'avc1.42c01e',
bandwidth: 5000000,
width: 600,
height: 400,
type: shaka.util.ManifestParserUtils.ContentType.VIDEO,
},
audio: {
id: 3,
createSegmentIndex: () => Promise.resolve(),
segmentIndex: audioIndex,
mimeType: 'audio/mp4',
codecs: 'mp4a.40.2',
bandwidth: 192000,
type: shaka.util.ManifestParserUtils.ContentType.AUDIO,
},
}],
};
}
});
});