mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-18 16:36:56 +03:00
60d2f1cc82
Fixes #1967 Change-Id: Ie2a41cb17427316ce65cf180507c1fb1f2c6f11a
3225 lines
110 KiB
JavaScript
3225 lines
110 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('StreamingEngine', () => {
|
|
const Util = shaka.test.Util;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
|
|
|
|
// Dummy byte ranges and sizes for initialization and media segments.
|
|
// Create empty object first and initialize the fields through
|
|
// [] to allow field names to be expressions.
|
|
/**
|
|
* @type {!Object.<shaka.util.ManifestParserUtils.ContentType,
|
|
* !Array.<number>>}
|
|
*/
|
|
const initSegmentRanges = {};
|
|
initSegmentRanges[ContentType.AUDIO] = [100, 1000];
|
|
initSegmentRanges[ContentType.VIDEO] = [200, 2000];
|
|
|
|
/** @type {!Object.<shaka.util.ManifestParserUtils.ContentType, number>} */
|
|
const segmentSizes = {};
|
|
segmentSizes[ContentType.AUDIO] = 1000;
|
|
segmentSizes[ContentType.VIDEO] = 10000;
|
|
segmentSizes[ContentType.TEXT] = 500;
|
|
|
|
/** @type {!Object.<string, shaka.test.FakeMediaSourceEngine.SegmentData>} */
|
|
let segmentData;
|
|
/** @type {number} */
|
|
let presentationTimeInSeconds;
|
|
/** @type {boolean} */
|
|
let playing;
|
|
|
|
/** @type {!shaka.test.FakeMediaSourceEngine} */
|
|
let mediaSourceEngine;
|
|
let netEngine;
|
|
let timeline;
|
|
|
|
let audioStream1;
|
|
let videoStream1;
|
|
let variant1;
|
|
let textStream1;
|
|
let alternateVideoStream1;
|
|
|
|
let audioStream2;
|
|
let videoStream2;
|
|
let variant2;
|
|
let textStream2;
|
|
|
|
/** @type {shaka.extern.Manifest} */
|
|
let manifest;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let onChooseStreams;
|
|
/** @type {!jasmine.Spy} */
|
|
let onCanSwitch;
|
|
/** @type {!jasmine.Spy} */
|
|
let onError;
|
|
/** @type {!jasmine.Spy} */
|
|
let onEvent;
|
|
/** @type {!jasmine.Spy} */
|
|
let onManifestUpdate;
|
|
/** @type {!jasmine.Spy} */
|
|
let onInitialStreamsSetup;
|
|
/** @type {!jasmine.Spy} */
|
|
let onStartupComplete;
|
|
/** @type {!jasmine.Spy} */
|
|
let onSegmentAppended;
|
|
/** @type {!jasmine.Spy} */
|
|
let getBandwidthEstimate;
|
|
/** @type {!shaka.media.StreamingEngine} */
|
|
let streamingEngine;
|
|
|
|
/**
|
|
* Runs the fake event loop.
|
|
* @param {function()=} callback An optional callback that is executed
|
|
* each time the clock ticks.
|
|
*/
|
|
function runTest(callback) {
|
|
function onTick(currentTime) {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
if (playing) {
|
|
presentationTimeInSeconds++;
|
|
}
|
|
}
|
|
// No test should require more than 60 seconds of simulated time.
|
|
Util.fakeEventLoop(60, onTick);
|
|
}
|
|
|
|
beforeAll(() => {
|
|
jasmine.clock().install();
|
|
jasmine.clock().mockDate();
|
|
// This mock is required for fakeEventLoop.
|
|
PromiseMock.install();
|
|
});
|
|
|
|
/** @param {boolean=} trickMode */
|
|
function setupVod(trickMode) {
|
|
// For VOD, we fake a presentation that has 2 Periods of equal duration
|
|
// (20 seconds), where each Period has 1 Variant and 1 text stream.
|
|
//
|
|
// There are 4 initialization segments: 1 audio and 1 video for the
|
|
// first Period, and 1 audio and 1 video for the second Period.
|
|
//
|
|
// There are 12 media segments: 2 audio, 2 video, and 2 text for the
|
|
// first Period, and 2 audio, 2 video, and 2 text for the second Period.
|
|
// All media segments are (by default) 10 seconds long.
|
|
|
|
// Create SegmentData map for FakeMediaSourceEngine.
|
|
const initSegmentSizeAudio = initSegmentRanges[ContentType.AUDIO][1] -
|
|
initSegmentRanges[ContentType.AUDIO][0] + 1;
|
|
const initSegmentSizeVideo = initSegmentRanges[ContentType.VIDEO][1] -
|
|
initSegmentRanges[ContentType.VIDEO][0] + 1;
|
|
|
|
const makeBuffer = (size) => new ArrayBuffer(size);
|
|
segmentData = {
|
|
audio: {
|
|
initSegments: [
|
|
makeBuffer(initSegmentSizeAudio), makeBuffer(initSegmentSizeAudio),
|
|
],
|
|
segments: [
|
|
makeBuffer(segmentSizes[ContentType.AUDIO]),
|
|
makeBuffer(segmentSizes[ContentType.AUDIO]),
|
|
makeBuffer(segmentSizes[ContentType.AUDIO]),
|
|
makeBuffer(segmentSizes[ContentType.AUDIO]),
|
|
],
|
|
segmentStartTimes: [0, 10, 0, 10],
|
|
segmentPeriodTimes: [0, 0, 20, 20],
|
|
segmentDuration: 10,
|
|
},
|
|
video: {
|
|
initSegments: [
|
|
makeBuffer(initSegmentSizeVideo), makeBuffer(initSegmentSizeVideo),
|
|
],
|
|
segments: [
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
],
|
|
segmentStartTimes: [0, 10, 0, 10],
|
|
segmentPeriodTimes: [0, 0, 20, 20],
|
|
segmentDuration: 10,
|
|
},
|
|
text: {
|
|
initSegments: [],
|
|
segments: [
|
|
makeBuffer(segmentSizes[ContentType.TEXT]),
|
|
makeBuffer(segmentSizes[ContentType.TEXT]),
|
|
makeBuffer(segmentSizes[ContentType.TEXT]),
|
|
makeBuffer(segmentSizes[ContentType.TEXT]),
|
|
],
|
|
segmentStartTimes: [0, 10, 0, 10],
|
|
segmentPeriodTimes: [0, 0, 20, 20],
|
|
segmentDuration: 10,
|
|
},
|
|
};
|
|
if (trickMode) {
|
|
segmentData.trickvideo = {
|
|
initSegments: [
|
|
makeBuffer(initSegmentSizeVideo), makeBuffer(initSegmentSizeVideo),
|
|
],
|
|
segments: [
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]),
|
|
],
|
|
segmentStartTimes: [0, 10, 0, 10],
|
|
segmentPeriodTimes: [0, 0, 20, 20],
|
|
segmentDuration: 10,
|
|
};
|
|
}
|
|
|
|
presentationTimeInSeconds = 0;
|
|
playing = false;
|
|
|
|
setupNetworkingEngine(
|
|
2 /* segmentsInFirstPeriod */,
|
|
2 /* segmentsInSecondPeriod */);
|
|
|
|
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
|
|
0 /* segmentAvailabilityStart */,
|
|
40 /* segmentAvailabilityEnd */,
|
|
40 /* presentationDuration */,
|
|
10 /* maxSegmentDuration */,
|
|
false /* isLive */);
|
|
|
|
setupManifest(
|
|
0 /* firstPeriodStartTime */,
|
|
20 /* secondPeriodStartTime */,
|
|
40 /* presentationDuration */);
|
|
}
|
|
|
|
function setupLive() {
|
|
// For live, we fake a presentation that has 2 Periods of different
|
|
// durations (120 seconds and 20 seconds respectively), where each Period
|
|
// has 1 Variant and 1 text stream.
|
|
//
|
|
// There are 4 initialization segments: 1 audio and 1 video for the
|
|
// first Period, and 1 audio and 1 video for the second Period.
|
|
//
|
|
// There are 14 media segments: 12 audio, 12 video, and 12 text for the
|
|
// first Period, and 2 audio, 2 video, and 2 text for the second Period.
|
|
// All media segments are (by default) 10 seconds long.
|
|
//
|
|
// The segment availability window starts at t=100 (segment 11) and extends
|
|
// to t=120 (segment 13).
|
|
|
|
// Create SegmentData map for FakeMediaSourceEngine.
|
|
const initSegmentSizeAudio = initSegmentRanges[ContentType.AUDIO][1] -
|
|
initSegmentRanges[ContentType.AUDIO][0] + 1;
|
|
const initSegmentSizeVideo = initSegmentRanges[ContentType.VIDEO][1] -
|
|
initSegmentRanges[ContentType.VIDEO][0] + 1;
|
|
|
|
const makeBuffer = (size) => new ArrayBuffer(size);
|
|
segmentData = {
|
|
audio: {
|
|
initSegments:
|
|
[makeBuffer(initSegmentSizeAudio),
|
|
makeBuffer(initSegmentSizeAudio)],
|
|
segments: [],
|
|
segmentStartTimes: [],
|
|
segmentPeriodTimes: [],
|
|
segmentDuration: 10,
|
|
},
|
|
video: {
|
|
initSegments:
|
|
[makeBuffer(initSegmentSizeVideo),
|
|
makeBuffer(initSegmentSizeVideo)],
|
|
segments: [],
|
|
segmentStartTimes: [],
|
|
segmentPeriodTimes: [],
|
|
segmentDuration: 10,
|
|
},
|
|
text: {
|
|
initSegments: [],
|
|
segments: [],
|
|
segmentStartTimes: [],
|
|
segmentPeriodTimes: [],
|
|
segmentDuration: 10,
|
|
},
|
|
};
|
|
|
|
const segmentsInFirstPeriod = 12;
|
|
for (let i = 0; i < segmentsInFirstPeriod; ++i) {
|
|
segmentData[ContentType.AUDIO].segments.push(
|
|
makeBuffer(segmentSizes[ContentType.AUDIO]));
|
|
segmentData[ContentType.VIDEO].segments.push(
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]));
|
|
segmentData[ContentType.TEXT].segments.push(
|
|
makeBuffer(segmentSizes[ContentType.TEXT]));
|
|
|
|
segmentData[ContentType.AUDIO].segmentStartTimes.push(i * 10);
|
|
segmentData[ContentType.VIDEO].segmentStartTimes.push(i * 10);
|
|
segmentData[ContentType.TEXT].segmentStartTimes.push(i * 10);
|
|
|
|
segmentData[ContentType.AUDIO].segmentPeriodTimes.push(0);
|
|
segmentData[ContentType.VIDEO].segmentPeriodTimes.push(0);
|
|
segmentData[ContentType.TEXT].segmentPeriodTimes.push(0);
|
|
}
|
|
|
|
const segmentsInSecondPeriod = 2;
|
|
for (let i = 0; i < segmentsInSecondPeriod; ++i) {
|
|
segmentData[ContentType.AUDIO].segments.push(
|
|
makeBuffer(segmentSizes[ContentType.AUDIO]));
|
|
segmentData[ContentType.VIDEO].segments.push(
|
|
makeBuffer(segmentSizes[ContentType.VIDEO]));
|
|
segmentData[ContentType.TEXT].segments.push(
|
|
makeBuffer(segmentSizes[ContentType.TEXT]));
|
|
|
|
segmentData[ContentType.AUDIO].segmentStartTimes.push(i * 10);
|
|
segmentData[ContentType.VIDEO].segmentStartTimes.push(i * 10);
|
|
segmentData[ContentType.TEXT].segmentStartTimes.push(i * 10);
|
|
|
|
segmentData[ContentType.AUDIO].segmentPeriodTimes.push(
|
|
segmentsInFirstPeriod * 10);
|
|
segmentData[ContentType.VIDEO].segmentPeriodTimes.push(
|
|
segmentsInFirstPeriod * 10);
|
|
segmentData[ContentType.TEXT].segmentPeriodTimes.push(
|
|
segmentsInFirstPeriod * 10);
|
|
}
|
|
|
|
presentationTimeInSeconds = 110;
|
|
playing = false;
|
|
|
|
setupNetworkingEngine(
|
|
12 /* segmentsInFirstPeriod */,
|
|
2 /* segmentsInSecondPeriod */);
|
|
|
|
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
|
|
100 /* segmentAvailabilityStart */,
|
|
140 /* segmentAvailabilityEnd */,
|
|
140 /* presentationDuration */,
|
|
10 /* maxSegmentDuration */,
|
|
true /* isLive */);
|
|
|
|
setupManifest(
|
|
0 /* firstPeriodStartTime */,
|
|
120 /* secondPeriodStartTime */,
|
|
140 /* presentationDuration */);
|
|
}
|
|
|
|
function setupNetworkingEngine(
|
|
segmentsInFirstPeriod, segmentsInSecondPeriod) {
|
|
// 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) => {
|
|
expect((periodNumber == 1) || (periodNumber == 2));
|
|
return segmentData[type].initSegments[periodNumber - 1];
|
|
},
|
|
// Media segment generator:
|
|
(type, periodNumber, position) => {
|
|
expect(position).toBeGreaterThan(0);
|
|
expect((periodNumber == 1 && position <= segmentsInFirstPeriod) ||
|
|
(periodNumber == 2 && position <= segmentsInSecondPeriod));
|
|
const i =
|
|
(segmentsInFirstPeriod * (periodNumber - 1)) + (position - 1);
|
|
return segmentData[type].segments[i];
|
|
});
|
|
}
|
|
|
|
function setupManifest(
|
|
firstPeriodStartTime, secondPeriodStartTime, presentationDuration) {
|
|
const segmentDurations = {
|
|
audio: segmentData[ContentType.AUDIO].segmentDuration,
|
|
video: segmentData[ContentType.VIDEO].segmentDuration,
|
|
text: segmentData[ContentType.TEXT].segmentDuration,
|
|
};
|
|
if (segmentData['trickvideo']) {
|
|
segmentDurations['trickvideo'] =
|
|
segmentData['trickvideo'].segmentDuration;
|
|
}
|
|
manifest = shaka.test.StreamingEngineUtil.createManifest(
|
|
[firstPeriodStartTime, secondPeriodStartTime], presentationDuration,
|
|
segmentDurations);
|
|
|
|
manifest.presentationTimeline =
|
|
/** @type {!shaka.media.PresentationTimeline} */ (timeline);
|
|
manifest.minBufferTime = 2;
|
|
|
|
// Create InitSegmentReferences.
|
|
manifest.periods[0].variants[0].audio.initSegmentReference =
|
|
new shaka.media.InitSegmentReference(
|
|
(() => { return ['1_audio_init']; }),
|
|
initSegmentRanges[ContentType.AUDIO][0],
|
|
initSegmentRanges[ContentType.AUDIO][1]);
|
|
manifest.periods[0].variants[0].video.initSegmentReference =
|
|
new shaka.media.InitSegmentReference(
|
|
(() => { return ['1_video_init']; }),
|
|
initSegmentRanges[ContentType.VIDEO][0],
|
|
initSegmentRanges[ContentType.VIDEO][1]);
|
|
if (manifest.periods[0].variants[0].video.trickModeVideo) {
|
|
manifest.periods[0].variants[0].video.trickModeVideo
|
|
.initSegmentReference = new shaka.media.InitSegmentReference(
|
|
(() => { return ['1_trickvideo_init']; }),
|
|
initSegmentRanges[ContentType.VIDEO][0],
|
|
initSegmentRanges[ContentType.VIDEO][1]);
|
|
}
|
|
manifest.periods[1].variants[0].audio.initSegmentReference =
|
|
new shaka.media.InitSegmentReference(
|
|
(() => { return ['2_audio_init']; }),
|
|
initSegmentRanges[ContentType.AUDIO][0],
|
|
initSegmentRanges[ContentType.AUDIO][1]);
|
|
manifest.periods[1].variants[0].video.initSegmentReference =
|
|
new shaka.media.InitSegmentReference(
|
|
(() => { return ['2_video_init']; }),
|
|
initSegmentRanges[ContentType.VIDEO][0],
|
|
initSegmentRanges[ContentType.VIDEO][1]);
|
|
if (manifest.periods[1].variants[0].video.trickModeVideo) {
|
|
manifest.periods[1].variants[0].video.trickModeVideo
|
|
.initSegmentReference = new shaka.media.InitSegmentReference(
|
|
(() => { return ['2_trickvideo_init']; }),
|
|
initSegmentRanges[ContentType.VIDEO][0],
|
|
initSegmentRanges[ContentType.VIDEO][1]);
|
|
}
|
|
|
|
audioStream1 = manifest.periods[0].variants[0].audio;
|
|
videoStream1 = manifest.periods[0].variants[0].video;
|
|
variant1 = manifest.periods[0].variants[0];
|
|
textStream1 = manifest.periods[0].textStreams[0];
|
|
|
|
// This Stream is only used to verify that StreamingEngine can setup
|
|
// Streams correctly. It does not have init or media segments.
|
|
alternateVideoStream1 =
|
|
shaka.test.StreamingEngineUtil.createMockVideoStream(8);
|
|
alternateVideoStream1.createSegmentIndex.and.returnValue(Promise.resolve());
|
|
alternateVideoStream1.findSegmentPosition.and.returnValue(null);
|
|
alternateVideoStream1.getSegmentReference.and.returnValue(null);
|
|
const variant = {
|
|
audio: null,
|
|
video: /** @type {shaka.extern.Stream} */ (alternateVideoStream1),
|
|
id: 0,
|
|
language: 'und',
|
|
primary: false,
|
|
bandwidth: 0,
|
|
drmInfos: [],
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
};
|
|
manifest.periods[0].variants.push(variant);
|
|
|
|
audioStream2 = manifest.periods[1].variants[0].audio;
|
|
videoStream2 = manifest.periods[1].variants[0].video;
|
|
variant2 = manifest.periods[1].variants[0];
|
|
textStream2 = manifest.periods[1].textStreams[0];
|
|
}
|
|
|
|
/**
|
|
* Creates the StreamingEngine.
|
|
**
|
|
* @param {shaka.extern.StreamingConfiguration=} config Optional
|
|
* configuration object which overrides the default one.
|
|
*/
|
|
function createStreamingEngine(config) {
|
|
onChooseStreams = jasmine.createSpy('onChooseStreams');
|
|
onCanSwitch = jasmine.createSpy('onCanSwitch');
|
|
onInitialStreamsSetup = jasmine.createSpy('onInitialStreamsSetup');
|
|
onStartupComplete = jasmine.createSpy('onStartupComplete');
|
|
onError = jasmine.createSpy('onError');
|
|
onError.and.callFake(fail);
|
|
onEvent = jasmine.createSpy('onEvent');
|
|
onManifestUpdate = jasmine.createSpy('onManifestUpdate');
|
|
onSegmentAppended = jasmine.createSpy('onSegmentAppended');
|
|
getBandwidthEstimate = jasmine.createSpy('getBandwidthEstimate');
|
|
getBandwidthEstimate.and.returnValue(1e3);
|
|
|
|
if (!config) {
|
|
config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.rebufferingGoal = 2;
|
|
config.bufferingGoal = 5;
|
|
config.bufferBehind = Infinity;
|
|
}
|
|
|
|
goog.asserts.assert(
|
|
presentationTimeInSeconds != undefined,
|
|
'All tests should have defined an initial presentation time by now!');
|
|
const playerInterface = {
|
|
getPresentationTime: () => presentationTimeInSeconds,
|
|
getBandwidthEstimate: Util.spyFunc(getBandwidthEstimate),
|
|
mediaSourceEngine: mediaSourceEngine,
|
|
netEngine: /** @type {!shaka.net.NetworkingEngine} */(netEngine),
|
|
onChooseStreams: Util.spyFunc(onChooseStreams),
|
|
onCanSwitch: Util.spyFunc(onCanSwitch),
|
|
onError: Util.spyFunc(onError),
|
|
onEvent: Util.spyFunc(onEvent),
|
|
onManifestUpdate: Util.spyFunc(onManifestUpdate),
|
|
onSegmentAppended: Util.spyFunc(onSegmentAppended),
|
|
onInitialStreamsSetup: Util.spyFunc(onInitialStreamsSetup),
|
|
onStartupComplete: Util.spyFunc(onStartupComplete),
|
|
};
|
|
streamingEngine = new shaka.media.StreamingEngine(
|
|
/** @type {shaka.extern.Manifest} */(manifest), playerInterface);
|
|
streamingEngine.configure(config);
|
|
}
|
|
|
|
afterEach(() => {
|
|
streamingEngine.destroy().catch(fail);
|
|
PromiseMock.flush();
|
|
});
|
|
|
|
afterAll(() => {
|
|
PromiseMock.uninstall();
|
|
jasmine.clock().uninstall();
|
|
});
|
|
|
|
// This test initializes the StreamingEngine (SE) and allows it to play
|
|
// through both Periods.
|
|
//
|
|
// After calling start() the following should occur:
|
|
// 1. SE should immediately call onChooseStreams() with the first Period.
|
|
// 2. SE should setup each of the initial Streams and then call
|
|
// onInitialStreamsSetup().
|
|
// 3. SE should start appending the initial Streams' segments and in
|
|
// parallel setup the remaining Streams within the Manifest.
|
|
// - SE should call onStartupComplete() after it has buffered at least 1
|
|
// segment of each type of content.
|
|
// - SE should call onCanSwitch() with the first Period after it has
|
|
// setup the remaining Streams within the first Period.
|
|
// 4. SE should call onChooseStreams() with the second Period after it has
|
|
// both segments within the first Period.
|
|
// - We must return the Streams within the second Period.
|
|
// 5. SE should call onCanSwitch() with the second Period shortly after
|
|
// step 4.
|
|
// 6. SE should call MediaSourceEngine.endOfStream() after it has appended
|
|
// both segments within the second Period. At this point the playhead
|
|
// should not be at the end of the presentation, but the test will be
|
|
// effectively over since SE will have nothing else to do.
|
|
it('initializes and plays VOD', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [true, false],
|
|
video: [true, false],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, false, false, false],
|
|
video: [true, false, false, false],
|
|
text: [true, false, false, false],
|
|
});
|
|
|
|
setupFakeGetTime(0);
|
|
});
|
|
|
|
expect(mediaSourceEngine.reinitText).not.toHaveBeenCalled();
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onCanSwitch.and.callFake(() => {
|
|
expect(alternateVideoStream1.createSegmentIndex).toHaveBeenCalled();
|
|
expect(mediaSourceEngine.reinitText).not.toHaveBeenCalled();
|
|
mediaSourceEngine.reinitText.calls.reset();
|
|
onCanSwitch.and.throwError(new Error());
|
|
});
|
|
|
|
// For second Period.
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [true, false],
|
|
video: [true, false],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, false, false],
|
|
video: [true, true, false, false],
|
|
text: [true, true, false, false],
|
|
});
|
|
|
|
verifyNetworkingEngineRequestCalls(1);
|
|
|
|
onCanSwitch.and.callFake(() => {
|
|
expect(audioStream2.createSegmentIndex).toHaveBeenCalled();
|
|
expect(videoStream2.createSegmentIndex).toHaveBeenCalled();
|
|
expect(textStream2.createSegmentIndex).toHaveBeenCalled();
|
|
expect(mediaSourceEngine.reinitText).toHaveBeenCalled();
|
|
mediaSourceEngine.reinitText.calls.reset();
|
|
onCanSwitch.and.throwError(new Error());
|
|
});
|
|
|
|
// Switch to the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
// Init the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
onInitialStreamsSetup.and.callFake(() => {
|
|
const expectedObject = new Map();
|
|
expectedObject.set(ContentType.AUDIO, audioStream1);
|
|
expectedObject.set(ContentType.VIDEO, videoStream1);
|
|
expectedObject.set(ContentType.TEXT, textStream1);
|
|
expect(mediaSourceEngine.init)
|
|
.toHaveBeenCalledWith(expectedObject, false);
|
|
expect(mediaSourceEngine.init).toHaveBeenCalledTimes(1);
|
|
mediaSourceEngine.init.calls.reset();
|
|
|
|
expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1);
|
|
expect(mediaSourceEngine.setDuration).toHaveBeenCalledWith(40);
|
|
mediaSourceEngine.setDuration.calls.reset();
|
|
|
|
expect(audioStream1.createSegmentIndex).toHaveBeenCalled();
|
|
expect(videoStream1.createSegmentIndex).toHaveBeenCalled();
|
|
expect(textStream1.createSegmentIndex).toHaveBeenCalled();
|
|
|
|
expect(alternateVideoStream1.createSegmentIndex).not.toHaveBeenCalled();
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
|
|
verifyNetworkingEngineRequestCalls(2);
|
|
});
|
|
|
|
describe('loadNewTextStream', () => {
|
|
it('clears MediaSourceEngine', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
});
|
|
onChooseStreams.and.callFake(onChooseStreamsWithUnloadedText);
|
|
|
|
streamingEngine.start();
|
|
|
|
runTest(() => {
|
|
if (presentationTimeInSeconds == 20) {
|
|
mediaSourceEngine.clear.calls.reset();
|
|
mediaSourceEngine.init.calls.reset();
|
|
streamingEngine.loadNewTextStream(textStream2);
|
|
PromiseMock.flush();
|
|
expect(mediaSourceEngine.clear).toHaveBeenCalledWith('text');
|
|
|
|
const expectedObject = new Map();
|
|
expectedObject.set(ContentType.TEXT, jasmine.any(Object));
|
|
expect(mediaSourceEngine.init).toHaveBeenCalledWith(
|
|
expectedObject, false);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('unloadTextStream', () => {
|
|
it('doesn\'t send requests for text after calling unload', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
});
|
|
onChooseStreams.and.callFake(onChooseStreamsWithUnloadedText);
|
|
|
|
streamingEngine.start();
|
|
const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
|
|
// Verify that after unloading text stream, no network request for text
|
|
// is sent.
|
|
runTest(() => {
|
|
if (presentationTimeInSeconds == 1) {
|
|
netEngine.expectRequest('1_text_1', segmentType);
|
|
netEngine.request.calls.reset();
|
|
streamingEngine.unloadTextStream();
|
|
} else if (presentationTimeInSeconds == 35) {
|
|
netEngine.expectNoRequest('1_text_1', segmentType);
|
|
netEngine.expectNoRequest('1_text_2', segmentType);
|
|
netEngine.expectNoRequest('2_text_1', segmentType);
|
|
netEngine.expectNoRequest('2_text_2', segmentType);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
it('initializes and plays live', () => {
|
|
setupLive();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
presentationTimeInSeconds = 100;
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(100);
|
|
});
|
|
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest(slideSegmentAvailabilityWindow);
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
|
|
// Since we started playback from segment 11, segments 10 through 14
|
|
// should be buffered.
|
|
for (let i = 0; i <= 8; ++i) {
|
|
expect(mediaSourceEngine.segments[ContentType.AUDIO][i]).toBeFalsy();
|
|
expect(mediaSourceEngine.segments[ContentType.VIDEO][i]).toBeFalsy();
|
|
expect(mediaSourceEngine.segments[ContentType.TEXT][i]).toBeFalsy();
|
|
}
|
|
|
|
for (let i = 9; i <= 13; ++i) {
|
|
expect(mediaSourceEngine.segments[ContentType.AUDIO][i]).toBeTruthy();
|
|
expect(mediaSourceEngine.segments[ContentType.VIDEO][i]).toBeTruthy();
|
|
expect(mediaSourceEngine.segments[ContentType.TEXT][i]).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
// Start the playhead in the first Period but pass start() Streams from the
|
|
// second Period.
|
|
it('plays from 1st Period when passed Streams from 2nd', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
});
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
// Start with Streams from the second Period even though the playhead is
|
|
// in the first Period. onChooseStreams() should be called again for the
|
|
// first Period and then eventually for the second Period.
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
return {variant: variant2, text: textStream2};
|
|
});
|
|
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
// Start the playhead in the second Period but pass start() Streams from the
|
|
// first Period.
|
|
it('plays from 2nd Period when passed Streams from 1st', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
presentationTimeInSeconds = 20;
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(20);
|
|
});
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Start with Streams from the first Period even though the playhead is
|
|
// in the second Period. onChooseStreams() should be called again for the
|
|
// second Period.
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
return {variant: variant1, text: textStream1};
|
|
});
|
|
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [false, false, true, true],
|
|
video: [false, false, true, true],
|
|
text: [false, false, true, true],
|
|
});
|
|
});
|
|
|
|
it('plays when a small gap is present at the beginning', () => {
|
|
const drift = 0.050; // 50 ms
|
|
|
|
setupVod();
|
|
mediaSourceEngine =
|
|
new shaka.test.FakeMediaSourceEngine(segmentData, drift);
|
|
createStreamingEngine();
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(onStartupComplete).toHaveBeenCalled();
|
|
});
|
|
|
|
it('plays when 1st Period doesn\'t have text streams', () => {
|
|
setupVod();
|
|
manifest.periods[0].textStreams = [];
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake((period) => {
|
|
const chosen = defaultOnChooseStreams(period);
|
|
if (period == manifest.periods[0]) {
|
|
chosen.text = null;
|
|
}
|
|
return chosen;
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [false, false, true, true],
|
|
});
|
|
});
|
|
|
|
it('doesn\'t get stuck when 2nd Period isn\'t available yet', () => {
|
|
// See: https://github.com/google/shaka-player/pull/839
|
|
setupVod();
|
|
manifest.periods[0].textStreams = [];
|
|
|
|
// For the first update, indicate the segment isn't available. This should
|
|
// not cause us to fallback to the Playhead time to determine which segment
|
|
// to start streaming.
|
|
const oldGet = textStream2.getSegmentReference;
|
|
textStream2.getSegmentReference = (idx) => {
|
|
if (idx == 1) {
|
|
textStream2.getSegmentReference = oldGet;
|
|
return null;
|
|
}
|
|
return oldGet(idx);
|
|
};
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake((period) => {
|
|
const chosen = defaultOnChooseStreams(period);
|
|
if (period == manifest.periods[0]) {
|
|
chosen.text = null;
|
|
}
|
|
return chosen;
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [false, false, true, true],
|
|
});
|
|
});
|
|
|
|
it('only reinitializes text when switching streams', () => {
|
|
// See: https://github.com/google/shaka-player/issues/910
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(defaultOnChooseStreams);
|
|
|
|
// When we can switch in the second Period, switch to the playing stream.
|
|
onCanSwitch.and.callFake(() => {
|
|
onCanSwitch.and.callFake(() => {
|
|
expect(streamingEngine.getBufferingText()).toBe(textStream2);
|
|
|
|
mediaSourceEngine.reinitText.calls.reset();
|
|
streamingEngine.switchTextStream(textStream2);
|
|
});
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(mediaSourceEngine.reinitText).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('plays when 2nd Period doesn\'t have text streams', () => {
|
|
setupVod();
|
|
manifest.periods[1].textStreams = [];
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake((period) => {
|
|
const chosen = defaultOnChooseStreams(period);
|
|
if (period == manifest.periods[1]) {
|
|
chosen.text = null;
|
|
}
|
|
return chosen;
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, false, false],
|
|
});
|
|
});
|
|
|
|
it('updates the timeline duration to match media duration', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(defaultOnChooseStreams);
|
|
|
|
mediaSourceEngine.endOfStream.and.callFake(() => {
|
|
expect(mediaSourceEngine.setDuration).toHaveBeenCalledWith(40);
|
|
expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1);
|
|
mediaSourceEngine.setDuration.calls.reset();
|
|
// Simulate the media ending BEFORE the expected (manifest) duration.
|
|
mediaSourceEngine.getDuration.and.returnValue(35);
|
|
return Promise.resolve();
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
expect(timeline.setDuration).toHaveBeenCalledWith(35);
|
|
});
|
|
|
|
// https://github.com/google/shaka-player/issues/979
|
|
it('does not expand the timeline duration', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(defaultOnChooseStreams);
|
|
|
|
mediaSourceEngine.endOfStream.and.callFake(() => {
|
|
expect(mediaSourceEngine.setDuration).toHaveBeenCalledWith(40);
|
|
expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1);
|
|
mediaSourceEngine.setDuration.calls.reset();
|
|
// Simulate the media ending AFTER the expected (manifest) duration.
|
|
mediaSourceEngine.getDuration.and.returnValue(41);
|
|
return Promise.resolve();
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
expect(timeline.setDuration).not.toHaveBeenCalled();
|
|
});
|
|
|
|
// https://github.com/google/shaka-player/issues/1967
|
|
it('does not change duration when 0', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(defaultOnChooseStreams);
|
|
|
|
// The duration can spuriously be set to 0, so we should ignore this and not
|
|
// update the duration.
|
|
mediaSourceEngine.getDuration.and.returnValue(0);
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
expect(timeline.setDuration).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('applies fudge factor for appendWindowStart', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(defaultOnChooseStreams);
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
// The second Period starts at 20, so we should set the appendWindowStart to
|
|
// 20, but reduced by a small fudge factor.
|
|
const lt20 = {
|
|
asymmetricMatch: (val) => val >= 19.9 && val < 20,
|
|
};
|
|
expect(mediaSourceEngine.setStreamProperties)
|
|
.toHaveBeenCalledWith('video', 20, lt20, 40);
|
|
});
|
|
|
|
it('does not buffer one media type ahead of another', () => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.bufferingGoal = 60;
|
|
config.failureCallback = () => streamingEngine.retry();
|
|
createStreamingEngine(config);
|
|
|
|
// Make requests for different types take different amounts of time.
|
|
// This would let some media types buffer faster than others if unchecked.
|
|
netEngine.delays.text = 0.1;
|
|
netEngine.delays.audio = 1.0;
|
|
netEngine.delays.video = 10.0;
|
|
|
|
mediaSourceEngine.appendBuffer.and.callFake((type, data, start, end) => {
|
|
// Call to the underlying implementation.
|
|
const p = mediaSourceEngine.appendBufferImpl(type, data, start, end);
|
|
|
|
// Validate that no one media type got ahead of any other.
|
|
let minBuffered = Infinity;
|
|
let maxBuffered = 0;
|
|
for (const t of ['audio', 'video', 'text']) {
|
|
const buffered = mediaSourceEngine.bufferedAheadOfImpl(t, 0);
|
|
minBuffered = Math.min(minBuffered, buffered);
|
|
maxBuffered = Math.max(maxBuffered, buffered);
|
|
}
|
|
|
|
// Sanity check.
|
|
expect(maxBuffered).not.toBeLessThan(minBuffered);
|
|
// Proof that we didn't get too far ahead (10s == 1 segment).
|
|
expect(maxBuffered - minBuffered).not.toBeGreaterThan(10);
|
|
|
|
return p;
|
|
});
|
|
|
|
// Here we go!
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(defaultOnChooseStreams);
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// Make sure appendBuffer was called, so that we know that we executed the
|
|
// checks in our fake above.
|
|
expect(mediaSourceEngine.appendBuffer).toHaveBeenCalled();
|
|
});
|
|
|
|
describe('switchVariant/switchTextStream', () => {
|
|
let initialVariant;
|
|
let sameAudioVariant;
|
|
let sameVideoVariant;
|
|
let initialTextStream;
|
|
|
|
beforeEach(() => {
|
|
// Set up a manifest with multiple variants and a text stream.
|
|
/* eslint-disable indent */
|
|
manifest = new shaka.test.ManifestGenerator()
|
|
.addPeriod(0)
|
|
.addVariant(0)
|
|
.addAudio(10).useSegmentTemplate('audio-10-%d.mp4', 10)
|
|
.addVideo(11).useSegmentTemplate('video-11-%d.mp4', 10)
|
|
.addVariant(1)
|
|
.addExistingStream(10) // audio
|
|
.addVideo(12).useSegmentTemplate('video-12-%d.mp4', 10)
|
|
.addVariant(2)
|
|
.addAudio(13).useSegmentTemplate('audio-13-%d.mp4', 10)
|
|
.addExistingStream(12) // video
|
|
.addTextStream(20).useSegmentTemplate('text-20-%d.mp4', 10)
|
|
.build();
|
|
/* eslint-enable indent */
|
|
|
|
initialVariant = manifest.periods[0].variants[0];
|
|
sameAudioVariant = manifest.periods[0].variants[1];
|
|
sameVideoVariant = manifest.periods[0].variants[2];
|
|
initialTextStream = manifest.periods[0].textStreams[0];
|
|
|
|
// For these tests, we don't care about specific data appended.
|
|
// Just return any old ArrayBuffer for any requested segment.
|
|
netEngine = {
|
|
request: (requestType, request) => {
|
|
const buffer = new ArrayBuffer(0);
|
|
const response = {uri: request.uris[0], data: buffer, headers: {}};
|
|
return shaka.util.AbortableOperation.completed(response);
|
|
},
|
|
};
|
|
|
|
// For these tests, we also don't need FakeMediaSourceEngine to verify
|
|
// its input data.
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine({});
|
|
mediaSourceEngine.clear.and.returnValue(Promise.resolve());
|
|
mediaSourceEngine.bufferedAheadOf.and.returnValue(0);
|
|
mediaSourceEngine.bufferStart.and.returnValue(0);
|
|
mediaSourceEngine.setStreamProperties.and.returnValue(Promise.resolve());
|
|
mediaSourceEngine.remove.and.returnValue(Promise.resolve());
|
|
|
|
const bufferEnd = {audio: 0, video: 0, text: 0};
|
|
mediaSourceEngine.appendBuffer.and.callFake(
|
|
(type, data, start, end) => {
|
|
bufferEnd[type] = end;
|
|
return Promise.resolve();
|
|
});
|
|
mediaSourceEngine.bufferEnd.and.callFake((type) => {
|
|
return bufferEnd[type];
|
|
});
|
|
mediaSourceEngine.bufferedAheadOf.and.callFake((type, start) => {
|
|
return Math.max(0, bufferEnd[type] - start);
|
|
});
|
|
mediaSourceEngine.isBuffered.and.callFake((type, time) => {
|
|
return time >= 0 && time < bufferEnd[type];
|
|
});
|
|
|
|
playing = false;
|
|
presentationTimeInSeconds = 0;
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(() => {
|
|
return {variant: initialVariant, text: initialTextStream};
|
|
});
|
|
});
|
|
|
|
it('will not clear buffers if streams have not changed', () => {
|
|
onCanSwitch.and.callFake(() => {
|
|
mediaSourceEngine.clear.calls.reset();
|
|
streamingEngine.switchVariant(
|
|
sameAudioVariant, /* clearBuffer */ true, /* safeMargin */ 0);
|
|
Util.fakeEventLoop(1);
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('audio');
|
|
expect(mediaSourceEngine.clear).toHaveBeenCalledWith('video');
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text');
|
|
|
|
mediaSourceEngine.clear.calls.reset();
|
|
streamingEngine.switchVariant(
|
|
sameVideoVariant, /* clearBuffer */ true, /* safeMargin */ 0);
|
|
Util.fakeEventLoop(1);
|
|
expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio');
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('video');
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text');
|
|
|
|
mediaSourceEngine.clear.calls.reset();
|
|
streamingEngine.switchTextStream(initialTextStream);
|
|
Util.fakeEventLoop(1);
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalled();
|
|
});
|
|
|
|
streamingEngine.start().catch(fail);
|
|
|
|
Util.fakeEventLoop(1);
|
|
|
|
expect(onCanSwitch).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handles seeks (VOD)', () => {
|
|
/** @type {!jasmine.Spy} */
|
|
let onTick;
|
|
|
|
beforeEach(() => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
onTick = jasmine.createSpy('onTick');
|
|
onTick.and.stub();
|
|
});
|
|
|
|
it('into buffered regions', () => {
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Seek backwards to a buffered region in the first Period. Note that
|
|
// since the buffering goal is 5 seconds and each segment is 10
|
|
// seconds long, the second segment of this Period will be required at
|
|
// 6 seconds. Then it will load the next Period, but not require the
|
|
// new segments.
|
|
expect(presentationTimeInSeconds).toBe(6);
|
|
presentationTimeInSeconds -= 5;
|
|
streamingEngine.seeked();
|
|
|
|
// Although we're seeking backwards we still have to return some
|
|
// Streams from the second Period here.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
// Init the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
it('into partially buffered regions in the same period', () => {
|
|
// When seeking into a region within the same period, or changing
|
|
// resolution, and after the seek some states are buffered and some
|
|
// are unbuffered, StreamingEngine should only clear the unbuffered
|
|
// states.
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
mediaSourceEngine.endOfStream.and.callFake(() => {
|
|
// Should have the first Period entirely buffered.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
|
|
// Fake the audio buffer being removed.
|
|
mediaSourceEngine.segments[ContentType.AUDIO] =
|
|
[true, true, false, false];
|
|
|
|
// Seek back into the second Period.
|
|
expect(presentationTimeInSeconds).toBe(26);
|
|
presentationTimeInSeconds -= 5;
|
|
streamingEngine.seeked();
|
|
|
|
|
|
mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve());
|
|
return Promise.resolve();
|
|
});
|
|
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
// When seeking within the same period, clear the buffer of the
|
|
// unbuffered streams.
|
|
expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio');
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('video');
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text');
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [false, false, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
|
|
it('into buffered regions across Periods', () => {
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Switch to the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
mediaSourceEngine.endOfStream.and.callFake(() => {
|
|
// Seek backwards to a buffered region in the first Period. Note
|
|
// that since the buffering goal is 5 seconds and each segment is
|
|
// 10 seconds long, the last segment should be required at 26 seconds.
|
|
// Then endOfStream() should be called.
|
|
expect(presentationTimeInSeconds).toBe(26);
|
|
presentationTimeInSeconds -= 20;
|
|
streamingEngine.seeked();
|
|
|
|
// Verify that buffers are not cleared.
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalled();
|
|
|
|
return Promise.resolve();
|
|
});
|
|
|
|
// Init the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
it('into unbuffered regions', () => {
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Init the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
|
|
// Seek forward to an unbuffered region in the first Period.
|
|
expect(presentationTimeInSeconds).toBe(0);
|
|
presentationTimeInSeconds += 15;
|
|
streamingEngine.seeked();
|
|
|
|
onTick.and.callFake(() => {
|
|
// Verify that all buffers have been cleared.
|
|
expect(mediaSourceEngine.clear)
|
|
.toHaveBeenCalledWith(ContentType.AUDIO);
|
|
expect(mediaSourceEngine.clear)
|
|
.toHaveBeenCalledWith(ContentType.VIDEO);
|
|
expect(mediaSourceEngine.clear)
|
|
.toHaveBeenCalledWith(ContentType.TEXT);
|
|
onTick.and.stub();
|
|
});
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [true, false],
|
|
video: [true, false],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, false, false],
|
|
video: [true, true, false, false],
|
|
text: [true, true, false, false],
|
|
});
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Switch to the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest(Util.spyFunc(onTick));
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
it('into unbuffered regions across Periods', () => {
|
|
// Start from the second Period.
|
|
presentationTimeInSeconds = 20;
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(20));
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Init the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
mediaSourceEngine.endOfStream.and.callFake(() => {
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [false, false, true, true],
|
|
video: [false, false, true, true],
|
|
text: [false, false, true, true],
|
|
});
|
|
|
|
// Seek backwards to an unbuffered region in the first Period. Note
|
|
// that since the buffering goal is 5 seconds and each segment is 10
|
|
// seconds long, the last segment should be required at 26 seconds.
|
|
// Then endOfStream() should be called.
|
|
expect(presentationTimeInSeconds).toBe(26);
|
|
presentationTimeInSeconds -= 20;
|
|
streamingEngine.seeked();
|
|
|
|
onTick.and.callFake(() => {
|
|
// Verify that all buffers have been cleared.
|
|
expect(mediaSourceEngine.clear)
|
|
.toHaveBeenCalledWith(ContentType.AUDIO);
|
|
expect(mediaSourceEngine.clear)
|
|
.toHaveBeenCalledWith(ContentType.VIDEO);
|
|
expect(mediaSourceEngine.clear)
|
|
.toHaveBeenCalledWith(ContentType.TEXT);
|
|
onTick.and.stub();
|
|
});
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [true, false],
|
|
video: [true, false],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, false, false],
|
|
video: [true, true, false, false],
|
|
text: [true, true, false, false],
|
|
});
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Switch to the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve());
|
|
|
|
// Switch to the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
return Promise.resolve();
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest(Util.spyFunc(onTick));
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
it('into unbuffered regions when nothing is buffered', () => {
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Init the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
onInitialStreamsSetup.and.callFake(() => {
|
|
// Seek forward to an unbuffered region in the first Period.
|
|
expect(presentationTimeInSeconds).toBe(0);
|
|
presentationTimeInSeconds = 15;
|
|
streamingEngine.seeked();
|
|
|
|
onTick.and.callFake(() => {
|
|
// Nothing should have been cleared.
|
|
expect(mediaSourceEngine.clear).not.toHaveBeenCalled();
|
|
onTick.and.stub();
|
|
});
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [true, false],
|
|
video: [true, false],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, false, false],
|
|
video: [true, true, false, false],
|
|
text: [true, true, false, false],
|
|
});
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Switch to the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
});
|
|
|
|
// This happens after onInitialStreamsSetup(), so pass 15 so the playhead
|
|
// resumes from 15.
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(15));
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest(Util.spyFunc(onTick));
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
// If we seek back into an unbuffered region but do not called seeked(),
|
|
// StreamingEngine should wait for seeked() to be called.
|
|
it('back into unbuffered regions without seeked() ', () => {
|
|
// Start from the second segment in the second Period.
|
|
presentationTimeInSeconds = 30;
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(20));
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Init the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
mediaSourceEngine.endOfStream.and.callFake(() => {
|
|
// Seek backwards to an unbuffered region in the second Period. Do not
|
|
// call seeked().
|
|
expect(presentationTimeInSeconds).toBe(26);
|
|
presentationTimeInSeconds -= 10;
|
|
return Promise.resolve();
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// Verify buffers. Segment 3 should not be buffered since we never
|
|
// called seeked().
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [false, false, true, true],
|
|
video: [false, false, true, true],
|
|
text: [false, false, true, true],
|
|
});
|
|
});
|
|
|
|
// If we seek forward into an unbuffered region but do not called seeked(),
|
|
// StreamingEngine should continue buffering. This test also exercises the
|
|
// case where the playhead moves past the end of the buffer, which may
|
|
// occur on some browsers depending on the playback rate.
|
|
it('forward into unbuffered regions without seeked()', () => {
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
// Init the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
|
|
// Seek forward to an unbuffered region in the first Period. Do not
|
|
// call seeked().
|
|
presentationTimeInSeconds += 15;
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Switch to the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
|
|
it('into partially buffered regions across periods', () => {
|
|
// Seeking into a region where some buffers (text) are buffered and some
|
|
// are not should work despite the media states requiring different
|
|
// periods.
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
// Should get another call for the unbuffered Period transition.
|
|
onChooseStreams.and.callFake(defaultOnChooseStreams);
|
|
|
|
mediaSourceEngine.endOfStream.and.callFake(() => {
|
|
// Should have the first Period entirely buffered.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
|
|
// Fake the audio/video buffers being removed.
|
|
mediaSourceEngine.segments[ContentType.AUDIO] =
|
|
[false, false, true, true];
|
|
mediaSourceEngine.segments[ContentType.VIDEO] =
|
|
[false, false, true, true];
|
|
|
|
// Seek back into the first Period.
|
|
expect(presentationTimeInSeconds).toBe(26);
|
|
presentationTimeInSeconds -= 20;
|
|
streamingEngine.seeked();
|
|
|
|
// When seeking across periods, if at least one stream is
|
|
// unbuffered, we clear all the buffers.
|
|
expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio');
|
|
expect(mediaSourceEngine.clear).toHaveBeenCalledWith('video');
|
|
expect(mediaSourceEngine.clear).toHaveBeenCalledWith('text');
|
|
|
|
mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve());
|
|
return Promise.resolve();
|
|
});
|
|
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handles seeks (live)', () => {
|
|
beforeEach(() => {
|
|
setupLive();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData, 0);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(100));
|
|
});
|
|
|
|
it('outside segment availability window', () => {
|
|
timeline.segmentAvailabilityStart = 90;
|
|
timeline.segmentAvailabilityEnd = 110;
|
|
|
|
presentationTimeInSeconds = 90;
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Init the first Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(90);
|
|
|
|
// Seek forward to an unbuffered and unavailable region in the second
|
|
// Period; set playing to false since the playhead can't move at the
|
|
// seek target.
|
|
expect(timeline.getSegmentAvailabilityStart()).toBe(90);
|
|
expect(timeline.getSegmentAvailabilityEnd()).toBe(110);
|
|
presentationTimeInSeconds += 35;
|
|
playing = false;
|
|
streamingEngine.seeked();
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[1]);
|
|
|
|
onChooseStreams.and.throwError(new Error());
|
|
|
|
// Switch to the second Period.
|
|
return defaultOnChooseStreams(period);
|
|
});
|
|
|
|
// Eventually StreamingEngine should request the first segment (since
|
|
// it needs the second segment) of the second Period when it becomes
|
|
// available.
|
|
const originalAppendBuffer =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl;
|
|
mediaSourceEngine.appendBuffer.and.callFake(
|
|
(type, data, startTime, endTime) => {
|
|
expect(presentationTimeInSeconds).toBe(125);
|
|
expect(timeline.getSegmentAvailabilityStart()).toBe(100);
|
|
expect(timeline.getSegmentAvailabilityEnd()).toBe(120);
|
|
playing = true;
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
const p = originalAppendBuffer.call(
|
|
mediaSourceEngine, type, data, startTime, endTime);
|
|
mediaSourceEngine.appendBuffer.and.callFake(originalAppendBuffer);
|
|
return p;
|
|
});
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
|
|
runTest(slideSegmentAvailabilityWindow);
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
|
|
// Since we performed an unbuffered seek into the second Period, the
|
|
// first 12 segments should not be buffered.
|
|
for (let i = 0; i <= 11; ++i) {
|
|
expect(mediaSourceEngine.segments[ContentType.AUDIO][i]).toBeFalsy();
|
|
expect(mediaSourceEngine.segments[ContentType.VIDEO][i]).toBeFalsy();
|
|
expect(mediaSourceEngine.segments[ContentType.TEXT][i]).toBeFalsy();
|
|
}
|
|
|
|
for (let i = 12; i <= 13; ++i) {
|
|
expect(mediaSourceEngine.segments[ContentType.AUDIO][i]).toBeTruthy();
|
|
expect(mediaSourceEngine.segments[ContentType.VIDEO][i]).toBeTruthy();
|
|
expect(mediaSourceEngine.segments[ContentType.TEXT][i]).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('handles errors', () => {
|
|
beforeEach(() => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
});
|
|
|
|
it('from initial Stream setup', () => {
|
|
videoStream1.createSegmentIndex.and.returnValue(
|
|
Promise.reject('FAKE_ERROR'));
|
|
|
|
const onInitError = jasmine.createSpy('onInitError');
|
|
onInitError.and.callFake((error) => {
|
|
expect(onInitialStreamsSetup).not.toHaveBeenCalled();
|
|
expect(onStartupComplete).not.toHaveBeenCalled();
|
|
expect(error).toBe('FAKE_ERROR');
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start().then(fail, Util.spyFunc(onInitError));
|
|
|
|
runTest();
|
|
expect(onInitError).toHaveBeenCalled();
|
|
expect(onError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('from post startup Stream setup', () => {
|
|
const expectedError = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.HTTP_ERROR);
|
|
alternateVideoStream1.createSegmentIndex.and.returnValue(
|
|
Promise.reject(expectedError));
|
|
|
|
onError.and.callFake((error) => {
|
|
expect(onInitialStreamsSetup).toHaveBeenCalled();
|
|
expect(onStartupComplete).toHaveBeenCalled();
|
|
expect(error).toBe(expectedError);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start().catch(fail);
|
|
runTest();
|
|
expect(onError).toHaveBeenCalled();
|
|
});
|
|
|
|
it('from failed init segment append during startup', () => {
|
|
const expectedError = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED);
|
|
|
|
onError.and.callFake((error) => {
|
|
expect(onInitialStreamsSetup).toHaveBeenCalled();
|
|
expect(onStartupComplete).not.toHaveBeenCalled();
|
|
Util.expectToEqualError(error, expectedError);
|
|
});
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
const streamsByType = defaultOnChooseStreams(period);
|
|
|
|
const originalAppendBuffer =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl;
|
|
mediaSourceEngine.appendBuffer.and.callFake(
|
|
(type, data, startTime, endTime) => {
|
|
// Reject the first video init segment.
|
|
if (data == segmentData[ContentType.VIDEO].initSegments[0]) {
|
|
return Promise.reject(expectedError);
|
|
} else {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
return originalAppendBuffer.call(
|
|
mediaSourceEngine, type, data, startTime, endTime);
|
|
}
|
|
});
|
|
|
|
return streamsByType;
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start().catch(fail);
|
|
runTest();
|
|
expect(onError).toHaveBeenCalled();
|
|
});
|
|
|
|
it('from failed media segment append during startup', () => {
|
|
const expectedError = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED);
|
|
|
|
onError.and.callFake((error) => {
|
|
expect(onInitialStreamsSetup).toHaveBeenCalled();
|
|
expect(onStartupComplete).not.toHaveBeenCalled();
|
|
Util.expectToEqualError(error, expectedError);
|
|
});
|
|
|
|
onChooseStreams.and.callFake((period) => {
|
|
expect(period).toBe(manifest.periods[0]);
|
|
|
|
const streamsByType = defaultOnChooseStreams(period);
|
|
|
|
const originalAppendBuffer =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl;
|
|
mediaSourceEngine.appendBuffer.and.callFake(
|
|
(type, data, startTime, endTime) => {
|
|
// Reject the first audio segment.
|
|
if (data == segmentData[ContentType.AUDIO].segments[0]) {
|
|
return Promise.reject(expectedError);
|
|
} else {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
return originalAppendBuffer.call(
|
|
mediaSourceEngine, type, data, startTime, endTime);
|
|
}
|
|
});
|
|
|
|
return streamsByType;
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start().catch(fail);
|
|
runTest();
|
|
expect(onError).toHaveBeenCalled();
|
|
});
|
|
|
|
it('from failed clear in switchVariant', () => {
|
|
const expectedError = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED);
|
|
mediaSourceEngine.clear.and.returnValue(Promise.reject(expectedError));
|
|
|
|
onError.and.stub();
|
|
onChooseStreams.and.callFake((period) => defaultOnChooseStreams(period));
|
|
onStartupComplete.and.callFake(() => {
|
|
streamingEngine.switchVariant(
|
|
variant2, /* clear_buffer= */ true, /* safe_margin= */ 0);
|
|
});
|
|
|
|
// Here we go!
|
|
streamingEngine.start().catch(fail);
|
|
runTest();
|
|
expect(onError).toHaveBeenCalledWith(Util.jasmineError(expectedError));
|
|
});
|
|
});
|
|
|
|
describe('handles network errors', () => {
|
|
it('ignores text stream failures if configured to', () => {
|
|
setupVod();
|
|
const textUri = '1_text_1';
|
|
const originalNetEngine = netEngine;
|
|
netEngine = {
|
|
request: jasmine.createSpy('request'),
|
|
};
|
|
netEngine.request.and.callFake((requestType, request) => {
|
|
if (request.uris[0] == textUri) {
|
|
return shaka.util.AbortableOperation.failed(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.BAD_HTTP_STATUS, textUri, 404));
|
|
}
|
|
return originalNetEngine.request(requestType, request);
|
|
});
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.ignoreTextStreamFailures = true;
|
|
createStreamingEngine(config);
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(onError).not.toHaveBeenCalled();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
});
|
|
|
|
it('retries if configured to', () => {
|
|
setupLive();
|
|
|
|
// Wrap the NetworkingEngine to cause errors.
|
|
const targetUri = '1_audio_init';
|
|
failFirstRequestForTarget(netEngine, targetUri,
|
|
shaka.util.Error.Code.BAD_HTTP_STATUS);
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.failureCallback = () => streamingEngine.retry();
|
|
createStreamingEngine(config);
|
|
|
|
presentationTimeInSeconds = 100;
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(100);
|
|
});
|
|
|
|
onError.and.callFake((error) => {
|
|
expect(error.severity).toBe(shaka.util.Error.Severity.CRITICAL);
|
|
expect(error.category).toBe(shaka.util.Error.Category.NETWORK);
|
|
expect(error.code).toBe(shaka.util.Error.Code.BAD_HTTP_STATUS);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
expect(netEngine.attempts).toBeGreaterThan(1);
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not retry if configured not to', () => {
|
|
setupLive();
|
|
|
|
// Wrap the NetworkingEngine to cause errors.
|
|
const targetUri = '1_audio_init';
|
|
failFirstRequestForTarget(netEngine, targetUri,
|
|
shaka.util.Error.Code.BAD_HTTP_STATUS);
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.failureCallback = () => {};
|
|
createStreamingEngine(config);
|
|
|
|
presentationTimeInSeconds = 100;
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(100);
|
|
});
|
|
|
|
onError.and.callFake((error) => {
|
|
expect(error.severity).toBe(shaka.util.Error.Severity.CRITICAL);
|
|
expect(error.category).toBe(shaka.util.Error.Category.NETWORK);
|
|
expect(error.code).toBe(shaka.util.Error.Code.BAD_HTTP_STATUS);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
expect(netEngine.attempts).toBe(1);
|
|
expect(mediaSourceEngine.endOfStream).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not invoke the callback if the error is handled', () => {
|
|
setupLive();
|
|
|
|
// Wrap the NetworkingEngine to cause errors.
|
|
const targetUri = '1_audio_init';
|
|
failFirstRequestForTarget(netEngine, targetUri,
|
|
shaka.util.Error.Code.BAD_HTTP_STATUS);
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
|
|
// Configure with a failure callback
|
|
const failureCallback = jasmine.createSpy('failureCallback');
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.failureCallback = shaka.test.Util.spyFunc(failureCallback);
|
|
createStreamingEngine(config);
|
|
|
|
presentationTimeInSeconds = 100;
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(100);
|
|
});
|
|
|
|
onError.and.callFake((error) => {
|
|
error.handled = true;
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
expect(failureCallback).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('waits to invoke the failure callback', () => {
|
|
setupLive();
|
|
|
|
// Wrap the NetworkingEngine to cause errors.
|
|
const targetUri = '1_audio_init';
|
|
failFirstRequestForTarget(netEngine, targetUri,
|
|
shaka.util.Error.Code.BAD_HTTP_STATUS);
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
|
|
// Configure with a failure callback that records the callback time.
|
|
let callbackTime = null;
|
|
const failureCallback = jasmine.createSpy('failureCallback');
|
|
failureCallback.and.callFake(() => {
|
|
callbackTime = Date.now();
|
|
});
|
|
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.failureCallback = shaka.test.Util.spyFunc(failureCallback);
|
|
config.retryParameters.maxAttempts = 2;
|
|
config.retryParameters.baseDelay = 10000;
|
|
config.retryParameters.fuzzFactor = 0;
|
|
createStreamingEngine(config);
|
|
|
|
presentationTimeInSeconds = 100;
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(100);
|
|
});
|
|
onError.and.stub();
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
const startTime = Date.now();
|
|
runTest();
|
|
expect(failureCallback).toHaveBeenCalled();
|
|
expect(callbackTime - startTime).toBe(10000); // baseDelay == 10000
|
|
});
|
|
});
|
|
|
|
describe('retry()', () => {
|
|
it('resumes streaming after failure', () => {
|
|
setupVod();
|
|
|
|
// Wrap the NetworkingEngine to cause errors.
|
|
const targetUri = '1_audio_init';
|
|
const originalNetEngineRequest = netEngine.request;
|
|
failFirstRequestForTarget(netEngine, targetUri,
|
|
shaka.util.Error.Code.BAD_HTTP_STATUS);
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
});
|
|
|
|
onError.and.callFake((error) => {
|
|
// Restore the original fake request function.
|
|
netEngine.request = originalNetEngineRequest;
|
|
netEngine.request.calls.reset();
|
|
|
|
// Retry streaming.
|
|
expect(streamingEngine.retry()).toBe(true);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
// We definitely called onError().
|
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
// We reset the request calls in onError() just before retry(), so this
|
|
// count reflects new calls since retry().
|
|
expect(netEngine.request).toHaveBeenCalled();
|
|
// The retry worked, so we should have reached the end of the stream.
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not resume streaming after quota error', () => {
|
|
setupVod();
|
|
|
|
const appendBufferSpy = jasmine.createSpy('appendBuffer');
|
|
// Throw QuotaExceededError on every segment to quickly trigger the quota
|
|
// error.
|
|
appendBufferSpy.and.callFake((type, data, startTime, endTime) => {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
|
|
type);
|
|
});
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
mediaSourceEngine.appendBuffer = appendBufferSpy;
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
});
|
|
|
|
onError.and.callFake((error) => {
|
|
expect(error.code).toBe(shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR);
|
|
|
|
// Retry streaming, which should fail and return false.
|
|
netEngine.request.calls.reset();
|
|
expect(streamingEngine.retry()).toBe(false);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
|
|
// We definitely called onError().
|
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
|
|
// We reset the request calls in onError() just before retry(), so this
|
|
// count reflects new calls since retry().
|
|
expect(netEngine.request).not.toHaveBeenCalled();
|
|
expect(mediaSourceEngine.endOfStream).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not resume streaming after destruction', () => {
|
|
setupVod();
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => {
|
|
setupFakeGetTime(0);
|
|
});
|
|
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
// Here we go!
|
|
let count = 0;
|
|
runTest(() => {
|
|
if (++count == 3) {
|
|
streamingEngine.destroy();
|
|
|
|
// Retry streaming, which should fail and return false.
|
|
netEngine.request.calls.reset();
|
|
expect(streamingEngine.retry()).toBe(false);
|
|
}
|
|
});
|
|
|
|
// We reset the request calls in onError() just before retry(), so this
|
|
// count reflects new calls since retry().
|
|
expect(netEngine.request).not.toHaveBeenCalled();
|
|
expect(mediaSourceEngine.endOfStream).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('eviction', () => {
|
|
let config;
|
|
|
|
beforeEach(() => {
|
|
setupVod();
|
|
|
|
manifest.minBufferTime = 1;
|
|
|
|
config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.rebufferingGoal = 1;
|
|
config.bufferingGoal = 1;
|
|
config.bufferBehind = 10;
|
|
});
|
|
|
|
it('evicts media to meet the max buffer tail limit', () => {
|
|
// Create StreamingEngine.
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine(config);
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
const originalRemove =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.test.FakeMediaSourceEngine.prototype.removeImpl
|
|
.bind(mediaSourceEngine);
|
|
|
|
mediaSourceEngine.remove.and.callFake((type, start, end) => {
|
|
expect(presentationTimeInSeconds).toBe(20);
|
|
expect(start).toBe(0);
|
|
expect(end).toBe(10);
|
|
|
|
if (mediaSourceEngine.remove.calls.count() == 3) {
|
|
mediaSourceEngine.remove.and.callFake((type, start, end) => {
|
|
expect(presentationTimeInSeconds).toBe(30);
|
|
expect(start).toBe(10);
|
|
expect(end).toBe(20);
|
|
return originalRemove(type, start, end);
|
|
});
|
|
}
|
|
|
|
return originalRemove(type, start, end);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
// Since StreamingEngine is free to peform audio, video, and text updates
|
|
// in any order, there are many valid ways in which StreamingEngine can
|
|
// evict segments. So, instead of verifying the exact, final buffer
|
|
// configuration, ensure the byte limit is never exceeded and at least
|
|
// one segment of each type is buffered at the end of the test.
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
|
|
expect(mediaSourceEngine.remove)
|
|
.toHaveBeenCalledWith(ContentType.AUDIO, 0, 10);
|
|
expect(mediaSourceEngine.remove)
|
|
.toHaveBeenCalledWith(ContentType.AUDIO, 10, 20);
|
|
|
|
expect(mediaSourceEngine.remove)
|
|
.toHaveBeenCalledWith(ContentType.VIDEO, 0, 10);
|
|
expect(mediaSourceEngine.remove)
|
|
.toHaveBeenCalledWith(ContentType.VIDEO, 10, 20);
|
|
|
|
expect(mediaSourceEngine.remove)
|
|
.toHaveBeenCalledWith(ContentType.TEXT, 0, 10);
|
|
expect(mediaSourceEngine.remove)
|
|
.toHaveBeenCalledWith(ContentType.TEXT, 10, 20);
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [false, false, true, true],
|
|
video: [false, false, true, true],
|
|
text: [false, false, true, true],
|
|
});
|
|
});
|
|
|
|
it('doesn\'t evict too much when bufferBehind is very low', () => {
|
|
// Set the bufferBehind to a value significantly below the segment size.
|
|
config.bufferBehind = 0.1;
|
|
|
|
// Create StreamingEngine.
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine(config);
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest(() => {
|
|
if (presentationTimeInSeconds == 8) {
|
|
// Run the test until a bit before the end of the first segment.
|
|
playing = false;
|
|
} else if (presentationTimeInSeconds == 6) {
|
|
// Soon before stopping the test, set the buffering goal up way
|
|
// higher to trigger more segment fetching, to (potentially) trigger
|
|
// an eviction.
|
|
config.bufferingGoal = 5;
|
|
streamingEngine.configure(config);
|
|
}
|
|
});
|
|
|
|
// It should not have removed any segments.
|
|
expect(mediaSourceEngine.remove).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('QuotaExceededError', () => {
|
|
it('does not fail immediately', () => {
|
|
setupVod();
|
|
|
|
manifest.minBufferTime = 1;
|
|
|
|
// Create StreamingEngine.
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.rebufferingGoal = 1;
|
|
config.bufferingGoal = 1;
|
|
config.bufferBehind = 10;
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine(config);
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
const originalAppendBuffer =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl;
|
|
const appendBufferSpy = jasmine.createSpy('appendBuffer');
|
|
mediaSourceEngine.appendBuffer = appendBufferSpy;
|
|
|
|
// Throw two QuotaExceededErrors at different times.
|
|
let numErrorsThrown = 0;
|
|
appendBufferSpy.and.callFake(
|
|
(type, data, startTime, endTime) => {
|
|
const throwError = (numErrorsThrown == 0 && startTime == 10) ||
|
|
(numErrorsThrown == 1 && startTime == 20);
|
|
if (throwError) {
|
|
numErrorsThrown++;
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
|
|
type);
|
|
} else {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
const p = originalAppendBuffer.call(
|
|
mediaSourceEngine, type, data, startTime, endTime);
|
|
return p;
|
|
}
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [false, false, true, true],
|
|
video: [false, false, true, true],
|
|
text: [false, false, true, true],
|
|
});
|
|
});
|
|
|
|
it('fails after multiple QuotaExceededError', () => {
|
|
setupVod();
|
|
|
|
manifest.minBufferTime = 1;
|
|
|
|
// Create StreamingEngine.
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.rebufferingGoal = 1;
|
|
config.bufferingGoal = 1;
|
|
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine(config);
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
const originalAppendBuffer =
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl;
|
|
const appendBufferSpy = jasmine.createSpy('appendBuffer');
|
|
mediaSourceEngine.appendBuffer = appendBufferSpy;
|
|
|
|
// Throw QuotaExceededError multiple times after at least one segment of
|
|
// each type has been appended.
|
|
appendBufferSpy.and.callFake(
|
|
(type, data, startTime, endTime) => {
|
|
if (startTime >= 10) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MEDIA,
|
|
shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
|
|
type);
|
|
} else {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
const p = originalAppendBuffer.call(
|
|
mediaSourceEngine, type, data, startTime, endTime);
|
|
return p;
|
|
}
|
|
});
|
|
|
|
onError.and.callFake((error) => {
|
|
expect(error.code).toBe(shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR);
|
|
expect(error.data[0] == ContentType.AUDIO ||
|
|
error.data[0] == ContentType.VIDEO ||
|
|
error.data[0] == ContentType.TEXT).toBe(true);
|
|
});
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
// Stop the playhead after 10 seconds since will not append any
|
|
// segments after this time.
|
|
const stopPlayhead = () => {
|
|
playing = presentationTimeInSeconds < 10;
|
|
};
|
|
|
|
runTest(stopPlayhead);
|
|
expect(onError).toHaveBeenCalled();
|
|
expect(mediaSourceEngine.endOfStream).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('VOD drift', () => {
|
|
beforeEach(() => {
|
|
setupVod();
|
|
});
|
|
|
|
/**
|
|
* @param {number} drift
|
|
*/
|
|
function testPositiveDrift(drift) {
|
|
mediaSourceEngine =
|
|
new shaka.test.FakeMediaSourceEngine(segmentData, drift);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(drift));
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {number} drift
|
|
*/
|
|
function testNegativeDrift(drift) {
|
|
mediaSourceEngine =
|
|
new shaka.test.FakeMediaSourceEngine(segmentData, drift);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest();
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
}
|
|
|
|
it('is handled for small + values', () => testPositiveDrift(3));
|
|
it('is handled for large + values', () => testPositiveDrift(12));
|
|
it('is handled for small - values', () => testNegativeDrift(-3));
|
|
});
|
|
|
|
describe('live drift', () => {
|
|
beforeEach(() => {
|
|
setupLive();
|
|
});
|
|
|
|
/**
|
|
* @param {number} drift
|
|
*/
|
|
function testNegativeDrift(drift) {
|
|
mediaSourceEngine =
|
|
new shaka.test.FakeMediaSourceEngine(segmentData, drift);
|
|
createStreamingEngine();
|
|
|
|
presentationTimeInSeconds = 100;
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(100));
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest(slideSegmentAvailabilityWindow);
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
|
|
// Verify buffers.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true],
|
|
text: [],
|
|
});
|
|
|
|
for (let i = 0; i <= 8; ++i) {
|
|
expect(mediaSourceEngine.segments['audio'][i]).toBeFalsy();
|
|
expect(mediaSourceEngine.segments['video'][i]).toBeFalsy();
|
|
expect(mediaSourceEngine.segments['text'][i]).toBeFalsy();
|
|
}
|
|
|
|
for (let i = 9; i <= 13; ++i) {
|
|
expect(mediaSourceEngine.segments['audio'][i]).toBeTruthy();
|
|
expect(mediaSourceEngine.segments['video'][i]).toBeTruthy();
|
|
expect(mediaSourceEngine.segments['text'][i]).toBeTruthy();
|
|
}
|
|
}
|
|
|
|
it('is handled for large - values', () => testNegativeDrift(-12));
|
|
});
|
|
|
|
describe('setTrickPlay', () => {
|
|
it('uses trick mode track when requested', () => {
|
|
setupVod(/* trickMode */ true);
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
// Only buffer ahead 1 second to make it easier to set segment
|
|
// expectations based on playheadTime.
|
|
config.rebufferingGoal = 1;
|
|
config.bufferingGoal = 1;
|
|
createStreamingEngine(config);
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
|
|
// Here we go!
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
streamingEngine.start();
|
|
|
|
runTest(() => {
|
|
if (presentationTimeInSeconds == 1) {
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [true, false],
|
|
video: [true, false],
|
|
trickvideo: [false, false],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, false, false, false],
|
|
video: [true, false, false, false],
|
|
trickvideo: [false, false, false, false],
|
|
text: [true, false, false, false],
|
|
});
|
|
|
|
// Engage trick play.
|
|
streamingEngine.setTrickPlay(true);
|
|
} else if (presentationTimeInSeconds == 11) {
|
|
// We're in the second segment, in trick play mode.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [true, false],
|
|
video: [true, false],
|
|
trickvideo: [true, false],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, false, false],
|
|
video: [true, false, false, false],
|
|
trickvideo: [false, true, false, false],
|
|
text: [true, true, false, false],
|
|
});
|
|
} else if (presentationTimeInSeconds == 21) {
|
|
// We've started the second period, still in trick play mode.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [true, false], // no init segment fetched for normal video
|
|
trickvideo: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, false],
|
|
video: [true, false, false, false],
|
|
trickvideo: [false, true, true, false],
|
|
text: [true, true, true, false],
|
|
});
|
|
} else if (presentationTimeInSeconds == 31) {
|
|
// We've started the final segment, still in trick play mode.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [true, false], // no init segment appended for normal video
|
|
trickvideo: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [true, false, false, false],
|
|
trickvideo: [false, true, true, true],
|
|
text: [true, true, true, true],
|
|
});
|
|
|
|
// Disengage trick play mode, which will clear the video buffer.
|
|
streamingEngine.setTrickPlay(false);
|
|
} else if (presentationTimeInSeconds == 39) {
|
|
// We're 1 second from the end of the stream now.
|
|
expect(mediaSourceEngine.initSegments).toEqual({
|
|
audio: [false, true],
|
|
video: [false, true], // init segment appended for normal video now
|
|
trickvideo: [false, true],
|
|
text: [],
|
|
});
|
|
expect(mediaSourceEngine.segments).toEqual({
|
|
audio: [true, true, true, true],
|
|
video: [false, false, true, true], // starts buffering one seg back
|
|
trickvideo: [false, false, false, false], // cleared
|
|
text: [true, true, true, true],
|
|
});
|
|
}
|
|
});
|
|
expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('embedded emsg boxes', () => {
|
|
const emsgSegment = Uint8ArrayUtils.fromHex(
|
|
'0000003b656d736700000000666f6f3a6261723a637573746f6d646174617363' +
|
|
'68656d6500310000000001000000080000ffff0000000174657374');
|
|
const emsgObj = {
|
|
startTime: 8,
|
|
endTime: 0xffff + 8,
|
|
schemeIdUri: 'foo:bar:customdatascheme',
|
|
value: '1',
|
|
timescale: 1,
|
|
presentationTimeDelta: 8,
|
|
eventDuration: 0xffff,
|
|
id: 1,
|
|
messageData: new Uint8Array([0x74, 0x65, 0x73, 0x74]),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
setupVod();
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
|
|
createStreamingEngine();
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p));
|
|
});
|
|
|
|
it('raises an event for registered embedded emsg boxes', () => {
|
|
segmentData[ContentType.VIDEO].segments[0] = emsgSegment.buffer;
|
|
videoStream1.emsgSchemeIdUris = [emsgObj.schemeIdUri];
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(onEvent).toHaveBeenCalledTimes(1);
|
|
|
|
const event = onEvent.calls.argsFor(0)[0];
|
|
expect(event.detail).toEqual(emsgObj);
|
|
});
|
|
|
|
it('raises multiple events', () => {
|
|
const dummyBox =
|
|
shaka.util.Uint8ArrayUtils.fromHex('0000000c6672656501020304');
|
|
segmentData[ContentType.VIDEO].segments[0] =
|
|
shaka.util.Uint8ArrayUtils.concat(emsgSegment, dummyBox, emsgSegment)
|
|
.buffer;
|
|
videoStream1.emsgSchemeIdUris = [emsgObj.schemeIdUri];
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('won\'t raise an event without stream field set', () => {
|
|
segmentData[ContentType.VIDEO].segments[0] = emsgSegment.buffer;
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(onEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('won\'t raise an event when no emsg boxes present', () => {
|
|
videoStream1.emsgSchemeIdUris = [emsgObj.schemeIdUri];
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(onEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('won\'t raise an event for an unregistered emsg box', () => {
|
|
segmentData[ContentType.VIDEO].segments[0] = emsgSegment.buffer;
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(onEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('triggers manifest updates', () => {
|
|
// This is an 'emsg' box that contains a scheme of
|
|
// urn:mpeg:dash:event:2012 to indicate a manifest update.
|
|
segmentData[ContentType.VIDEO].segments[0] =
|
|
Uint8ArrayUtils
|
|
.fromHex(
|
|
'0000003a656d73670000000075726e3a' +
|
|
'6d7065673a646173683a6576656e743a' +
|
|
'32303132000000000031000000080000' +
|
|
'00ff0000000c74657374')
|
|
.buffer;
|
|
videoStream1.emsgSchemeIdUris = ['urn:mpeg:dash:event:2012'];
|
|
|
|
// Here we go!
|
|
streamingEngine.start();
|
|
runTest();
|
|
|
|
expect(onEvent).not.toHaveBeenCalled();
|
|
expect(onManifestUpdate).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('network downgrading', () => {
|
|
/** @type {shaka.extern.Variant} */
|
|
let newVariant;
|
|
/** @type {!Array.<string>} */
|
|
let requestUris;
|
|
/** @type {shaka.util.PublicPromise} */
|
|
let delayedRequest;
|
|
/** @type {shaka.net.NetworkingEngine.PendingRequest} */
|
|
let lastResponse;
|
|
/** @type {boolean} */
|
|
let shouldDelayRequests;
|
|
|
|
beforeEach(() => {
|
|
/* eslint-disable indent */
|
|
manifest = new shaka.test.ManifestGenerator()
|
|
.setPresentationDuration(60)
|
|
.addPeriod(0)
|
|
.addVariant(0)
|
|
.bandwidth(500)
|
|
.addVideo(10).useSegmentTemplate('video-10-%d.mp4',
|
|
/* segmentDuration= */ 10,
|
|
/* segmentSize= */ 50)
|
|
.addVariant(1)
|
|
.bandwidth(100)
|
|
.addVideo(11).useSegmentTemplate('video-11-%d.mp4',
|
|
/* segmentDuration= */ 10,
|
|
/* segmentSize= */ 10)
|
|
.build();
|
|
/* eslint-enable indent */
|
|
|
|
const initialVariant = manifest.periods[0].variants[0];
|
|
newVariant = manifest.periods[0].variants[1];
|
|
requestUris = [];
|
|
delayedRequest = null;
|
|
lastResponse = null;
|
|
shouldDelayRequests = true;
|
|
playing = false;
|
|
presentationTimeInSeconds = 0;
|
|
|
|
// For these tests, we don't care about specific data appended.
|
|
// Just return any old ArrayBuffer for any requested segment.
|
|
netEngine = {
|
|
request: (requestType, request) => {
|
|
const buffer = new ArrayBuffer(0);
|
|
const response = {uri: request.uris[0], data: buffer, headers: {}};
|
|
const bytes = new shaka.net.NetworkingEngine.NumBytesRemainingClass();
|
|
bytes.setBytes(200);
|
|
|
|
delayedRequest = new shaka.util.PublicPromise();
|
|
const run = async () => {
|
|
if (shouldDelayRequests) {
|
|
await delayedRequest;
|
|
}
|
|
// Only add if the segment was appended; if it was aborted this
|
|
// won't be called.
|
|
requestUris.push(request.uris[0]);
|
|
return response;
|
|
};
|
|
const abort = () => {
|
|
delayedRequest.reject(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.PLAYER,
|
|
shaka.util.Error.Code.OPERATION_ABORTED));
|
|
return Promise.resolve();
|
|
};
|
|
const ret = new shaka.net.NetworkingEngine.PendingRequest(
|
|
run(), abort, bytes);
|
|
|
|
spyOn(ret, 'abort').and.callThrough();
|
|
lastResponse = ret;
|
|
return ret;
|
|
},
|
|
};
|
|
|
|
// For these tests, we also don't need FakeMediaSourceEngine to verify
|
|
// its input data.
|
|
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine({});
|
|
mediaSourceEngine.bufferStart.and.returnValue(0);
|
|
mediaSourceEngine.clear.and.returnValue(Promise.resolve());
|
|
mediaSourceEngine.remove.and.returnValue(Promise.resolve());
|
|
mediaSourceEngine.setStreamProperties.and.returnValue(Promise.resolve());
|
|
|
|
// Naive buffered range tracking that only tracks the buffer end.
|
|
const bufferEnd = {audio: 0, video: 0, text: 0};
|
|
mediaSourceEngine.appendBuffer.and.callFake((type, data, start, end) => {
|
|
bufferEnd[type] = end;
|
|
return Promise.resolve();
|
|
});
|
|
mediaSourceEngine.bufferEnd.and.callFake((type) => bufferEnd[type]);
|
|
mediaSourceEngine.bufferedAheadOf.and.callFake(
|
|
(type, start) => Math.max(0, bufferEnd[type] - start));
|
|
mediaSourceEngine.isBuffered.and.callFake(
|
|
(type, time) => time >= 0 && time < bufferEnd[type]);
|
|
|
|
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
|
|
config.bufferingGoal = 30;
|
|
config.retryParameters.maxAttempts = 1;
|
|
createStreamingEngine(config);
|
|
|
|
getBandwidthEstimate.and.returnValue(1); // very slow by default
|
|
|
|
onStartupComplete.and.callFake(() => setupFakeGetTime(0));
|
|
onChooseStreams.and.callFake(() => ({variant: initialVariant}));
|
|
});
|
|
|
|
it('aborts pending requests', () => {
|
|
streamingEngine.start().catch(fail);
|
|
Util.fakeEventLoop(1);
|
|
|
|
// Finish the first request.
|
|
delayedRequest.resolve();
|
|
Util.fakeEventLoop(1);
|
|
// We should have buffered the first segment and finished startup.
|
|
expect(onCanSwitch).toHaveBeenCalled();
|
|
expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')).toBe(10);
|
|
|
|
// This should abort the pending request for the second segment.
|
|
const oldResponse = lastResponse;
|
|
streamingEngine.switchVariant(
|
|
newVariant, /* clear_buffer= */ false, /* safe_margin= */ 0);
|
|
Util.fakeEventLoop(1);
|
|
expect(oldResponse.abort).toHaveBeenCalled();
|
|
|
|
// Finish the second request for the new stream.
|
|
delayedRequest.resolve();
|
|
Util.fakeEventLoop(1);
|
|
expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')).toBe(20);
|
|
expect(requestUris).toEqual(['video-10-0.mp4', 'video-11-1.mp4']);
|
|
});
|
|
|
|
it('still aborts if previous segment size unknown', () => {
|
|
// This should use the "bytes remaining" from the request instead of the
|
|
// previous stream's size.
|
|
const oldGet = manifest.periods[0].variants[0].video.getSegmentReference;
|
|
manifest.periods[0].variants[0].video.getSegmentReference = (idx) => {
|
|
const seg = oldGet(idx);
|
|
if (seg) {
|
|
// With endByte being null, we won't know the segment size.
|
|
return new shaka.media.SegmentReference(
|
|
seg.position, seg.startTime, seg.endTime, seg.getUris,
|
|
/* startByte= */ 0, /* endByte= */ null);
|
|
} else {
|
|
return seg;
|
|
}
|
|
};
|
|
|
|
prepareForAbort();
|
|
streamingEngine.switchVariant(
|
|
newVariant, /* clear_buffer= */ false, /* safe_margin= */ 0);
|
|
|
|
bufferAndCheck(/* didAbort= */ true);
|
|
});
|
|
|
|
it('doesn\'t abort if close to finished', () => {
|
|
prepareForAbort();
|
|
setBytesRemaining(3);
|
|
streamingEngine.switchVariant(
|
|
newVariant, /* clear_buffer= */ false, /* safe_margin= */ 0);
|
|
|
|
bufferAndCheck(/* didAbort= */ false);
|
|
});
|
|
|
|
it('doesn\'t abort if init segment is too large', () => {
|
|
newVariant.video.initSegmentReference =
|
|
new shaka.media.InitSegmentReference(() => ['init-11.mp4'], 0, 500);
|
|
|
|
prepareForAbort();
|
|
streamingEngine.switchVariant(
|
|
newVariant, /* clear_buffer= */ false, /* safe_margin= */ 0);
|
|
|
|
bufferAndCheck(/* didAbort= */ false, /* hasInit= */ true);
|
|
});
|
|
|
|
it('still aborts with small init segment', () => {
|
|
newVariant.video.initSegmentReference =
|
|
new shaka.media.InitSegmentReference(() => ['init-11.mp4'], 0, 5);
|
|
|
|
prepareForAbort();
|
|
streamingEngine.switchVariant(
|
|
newVariant, /* clear_buffer= */ false, /* safe_margin= */ 0);
|
|
|
|
bufferAndCheck(/* didAbort= */ true, /* hasInit= */ true);
|
|
});
|
|
|
|
it('aborts if we can finish the new one on time', () => {
|
|
// Very large init segment
|
|
newVariant.video.initSegmentReference =
|
|
new shaka.media.InitSegmentReference(() => ['init-11.mp4'], 0, 5e6);
|
|
|
|
prepareForAbort();
|
|
|
|
setBytesRemaining(3); // not much left
|
|
getBandwidthEstimate.and.returnValue(1e9); // insanely fast
|
|
|
|
streamingEngine.switchVariant(
|
|
newVariant, /* clear_buffer= */ false, /* safe_margin= */ 0);
|
|
|
|
bufferAndCheck(/* didAbort= */ true, /* hasInit= */ true);
|
|
});
|
|
|
|
/**
|
|
* Creates and starts the StreamingEngine instance. After this returns,
|
|
* it should be waiting for the second segment request to complete.
|
|
*/
|
|
function prepareForAbort() {
|
|
streamingEngine.start().catch(fail);
|
|
Util.fakeEventLoop(1);
|
|
|
|
// Finish the first segment request.
|
|
delayedRequest.resolve();
|
|
Util.fakeEventLoop(1);
|
|
expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')).toBe(10);
|
|
|
|
expect(onCanSwitch).toHaveBeenCalled();
|
|
}
|
|
|
|
/**
|
|
* Buffers to the buffering goal and checks the correct segment requests
|
|
* were made.
|
|
* @param {boolean} didAbort
|
|
* @param {boolean=} hasInit
|
|
*/
|
|
function bufferAndCheck(didAbort, hasInit) {
|
|
shouldDelayRequests = false;
|
|
delayedRequest.resolve();
|
|
Util.fakeEventLoop(3);
|
|
|
|
expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')).toBe(30);
|
|
const expected = ['video-10-0.mp4'];
|
|
if (!didAbort) {
|
|
expected.push('video-10-1.mp4');
|
|
}
|
|
if (hasInit) {
|
|
expected.push('init-11.mp4');
|
|
}
|
|
if (didAbort) {
|
|
expected.push('video-11-1.mp4');
|
|
}
|
|
expected.push('video-11-2.mp4');
|
|
expect(requestUris).toEqual(expected);
|
|
}
|
|
|
|
/**
|
|
* Sets the number of bytes remaining on the pending network request.
|
|
*
|
|
* @param {number} bytes
|
|
* @suppress {accessControls}
|
|
*/
|
|
function setBytesRemaining(bytes) {
|
|
lastResponse.bytesRemaining_.setBytes(bytes);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Verifies calls to NetworkingEngine.request(). Expects every segment
|
|
* in the given Period to have been requested.
|
|
*
|
|
* @param {number} period The Period number (one-based).
|
|
*/
|
|
function verifyNetworkingEngineRequestCalls(period) {
|
|
netEngine.expectRangeRequest(
|
|
period + '_audio_init',
|
|
initSegmentRanges[ContentType.AUDIO][0],
|
|
initSegmentRanges[ContentType.AUDIO][1]);
|
|
|
|
netEngine.expectRangeRequest(
|
|
period + '_video_init',
|
|
initSegmentRanges[ContentType.VIDEO][0],
|
|
initSegmentRanges[ContentType.VIDEO][1]);
|
|
|
|
const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
netEngine.expectRequest(period + '_audio_1', segmentType);
|
|
netEngine.expectRequest(period + '_video_1', segmentType);
|
|
netEngine.expectRequest(period + '_text_1', segmentType);
|
|
|
|
netEngine.expectRequest(period + '_audio_2', segmentType);
|
|
netEngine.expectRequest(period + '_video_2', segmentType);
|
|
netEngine.expectRequest(period + '_text_2', segmentType);
|
|
|
|
netEngine.request.calls.reset();
|
|
}
|
|
|
|
/**
|
|
* Choose streams for the given period.
|
|
*
|
|
* @param {shaka.extern.Period} period
|
|
* @return {!Object.<string, !shaka.extern.Stream>}
|
|
*/
|
|
function defaultOnChooseStreams(period) {
|
|
if (period == manifest.periods[0]) {
|
|
return {variant: variant1, text: textStream1};
|
|
} else if (period == manifest.periods[1]) {
|
|
return {variant: variant2, text: textStream2};
|
|
} else {
|
|
throw new Error();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Choose streams for the given period, used for testing unload text stream.
|
|
* The text stream of the second period is not choosen.
|
|
*
|
|
* @param {shaka.extern.Period} period
|
|
* @return {!Object.<string, !shaka.extern.Stream>}
|
|
*/
|
|
function onChooseStreamsWithUnloadedText(period) {
|
|
if (period == manifest.periods[0]) {
|
|
return {variant: variant1, text: textStream1};
|
|
} else if (period == manifest.periods[1]) {
|
|
expect(streamingEngine.unloadTextStream).toHaveBeenCalled();
|
|
return {variant: variant2};
|
|
} else {
|
|
throw new Error();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Makes the mock Playhead object behave as a fake Playhead object which
|
|
* begins playback at the given time.
|
|
*
|
|
* @param {number} startTime the playhead's starting time with respect to
|
|
* the presentation timeline.
|
|
*/
|
|
function setupFakeGetTime(startTime) {
|
|
presentationTimeInSeconds = startTime;
|
|
playing = true;
|
|
}
|
|
|
|
/**
|
|
* Slides the segment availability window forward by 1 second.
|
|
*/
|
|
function slideSegmentAvailabilityWindow() {
|
|
timeline.segmentAvailabilityStart++;
|
|
timeline.segmentAvailabilityEnd++;
|
|
}
|
|
|
|
/**
|
|
* @param {!Object} netEngine A NetworkingEngine look-alike from
|
|
* shaka.test.StreamingEngineUtil.createFakeNetworkingEngine()
|
|
* @param {string} targetUri
|
|
* @param {shaka.util.Error.Code} errorCode
|
|
*/
|
|
function failFirstRequestForTarget(netEngine, targetUri, errorCode) {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
const originalNetEngineRequest = netEngine.request.bind(netEngine);
|
|
|
|
netEngine.attempts = 0;
|
|
netEngine.request = jasmine.createSpy('request').and.callFake(
|
|
(requestType, request) => {
|
|
if (request.uris[0] == targetUri) {
|
|
if (++netEngine.attempts == 1) {
|
|
const data = [targetUri];
|
|
|
|
if (errorCode == shaka.util.Error.Code.BAD_HTTP_STATUS) {
|
|
data.push(404);
|
|
data.push('');
|
|
}
|
|
|
|
return shaka.util.AbortableOperation.failed(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.NETWORK,
|
|
errorCode, data));
|
|
}
|
|
}
|
|
return originalNetEngineRequest(requestType, request);
|
|
});
|
|
}
|
|
});
|