shaka-player
Version:
DASH/EME video player library
1,327 lines (1,178 loc) • 59.8 kB
JavaScript
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('DashParser Live', () => {
const Util = shaka.test.Util;
const ManifestParser = shaka.test.ManifestParser;
const oldNow = Date.now;
const updateTime = 5;
const originalUri = 'http://example.com/';
/** @type {!shaka.test.FakeNetworkingEngine} */
let fakeNetEngine;
/** @type {!shaka.dash.DashParser} */
let parser;
/** @type {shaka.extern.ManifestParser.PlayerInterface} */
let playerInterface;
beforeEach(() => {
fakeNetEngine = new shaka.test.FakeNetworkingEngine();
parser = new shaka.dash.DashParser();
parser.configure(shaka.util.PlayerConfiguration.createDefault().manifest);
playerInterface = {
networkingEngine: fakeNetEngine,
filter: (manifest) => Promise.resolve(),
makeTextStreamsForClosedCaptions: (manifest) => {},
onTimelineRegionAdded: fail, // Should not have any EventStream elements.
onEvent: fail,
onError: fail,
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
};
});
afterEach(() => {
// Dash parser stop is synchronous.
parser.stop();
Date.now = oldNow;
});
/**
* Trigger a manifest update.
* @suppress {accessControls}
*/
async function updateManifest() {
if (parser.updateTimer_) {
parser.updateTimer_.tickNow();
}
await Util.shortDelay(); // Allow update to complete.
}
/**
* Gets a spy on the function that sets the update period.
* @return {!jasmine.Spy}
* @suppress {accessControls}
*/
function updateTickSpy() {
return spyOn(parser.updateTimer_, 'tickAfter');
}
/**
* Makes a simple live manifest with the given representation contents.
*
* @param {!Array.<string>} lines
* @param {number?} updateTime
* @param {number=} duration
* @return {string}
*/
function makeSimpleLiveManifestText(lines, updateTime, duration) {
const updateAttr = updateTime != null ?
'minimumUpdatePeriod="PT' + updateTime + 'S"' : '';
const durationAttr = duration != undefined ?
'duration="PT' + duration + 'S"' : '';
const template = [
'<MPD type="dynamic" %(updateAttr)s',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1" %(durationAttr)s>',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const text = sprintf(template, {
updateAttr: updateAttr,
durationAttr: durationAttr,
contents: lines.join('\n'),
updateTime: updateTime,
});
return text;
}
/**
* Make clones of a list of references so that they can be modified without
* affecting the originals.
*
* @param {!Array.<!shaka.media.SegmentReference>} references
* @return {!Array.<!shaka.media.SegmentReference>}
*/
function cloneRefs(references) {
return references.map((ref) => {
return new shaka.media.SegmentReference(
ref.startTime,
ref.endTime,
ref.getUrisInner,
ref.startByte,
ref.endByte,
ref.initSegmentReference,
ref.timestampOffset,
ref.appendWindowStart,
ref.appendWindowEnd);
});
}
/**
* Creates tests that test the behavior common between SegmentList and
* SegmentTemplate.
*
* @param {!Array.<string>} basicLines
* @param {!Array.<!shaka.media.SegmentReference>} basicRefs
* @param {!Array.<string>} updateLines
* @param {!Array.<!shaka.media.SegmentReference>} updateRefs
* @param {!Array.<string>} partialUpdateLines
*/
function testCommonBehaviors(
basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines) {
/**
* Tests that an update will show the given references.
*
* @param {!Array.<string>} firstLines The Representation contents for the
* first manifest.
* @param {!Array.<!shaka.media.SegmentReference>} firstReferences The media
* references for the first parse.
* @param {!Array.<string>} secondLines The Representation contents for the
* updated manifest.
* @param {!Array.<!shaka.media.SegmentReference>} secondReferences The
* media references for the updated manifest.
*/
async function testBasicUpdate(
firstLines, firstReferences, secondLines, secondReferences) {
const firstManifest = makeSimpleLiveManifestText(firstLines, updateTime);
const secondManifest =
makeSimpleLiveManifestText(secondLines, updateTime);
fakeNetEngine.setResponseText('dummy://foo', firstManifest);
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
await stream.createSegmentIndex();
ManifestParser.verifySegmentIndex(stream, firstReferences);
fakeNetEngine.setResponseText('dummy://foo', secondManifest);
await updateManifest();
ManifestParser.verifySegmentIndex(stream, secondReferences);
}
it('basic support', async () => {
await testBasicUpdate(basicLines, basicRefs, updateLines, updateRefs);
});
it('new manifests don\'t need to include old references', async () => {
await testBasicUpdate(
basicLines, basicRefs, partialUpdateLines, updateRefs);
});
it('evicts old references for single-period live stream', async () => {
const template = [
'<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"',
' timeShiftBufferDepth="PT30S"',
' suggestedPresentationDelay="PT5S"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const text = sprintf(
template, {updateTime: updateTime, contents: basicLines.join('\n')});
fakeNetEngine.setResponseText('dummy://foo', text);
const baseTime = new Date(2015, 11, 30);
Date.now = () => baseTime.getTime();
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest).toBeTruthy();
const stream = manifest.variants[0].video;
expect(stream).toBeTruthy();
await stream.createSegmentIndex();
expect(stream.segmentIndex).toBeTruthy();
const firstPosition = stream.segmentIndex.find(0);
ManifestParser.verifySegmentIndex(stream, basicRefs);
// The 30 second availability window is initially full in all cases
// (SegmentTemplate+Timeline, etc.) The first segment is always 10
// seconds long in all of these cases. So 11 seconds after the
// manifest was parsed, the first segment should have fallen out of
// the availability window.
Date.now = () => baseTime.getTime() + (11 * 1000);
await updateManifest();
// The first reference should have been evicted.
expect(stream.segmentIndex.find(0)).toBe(firstPosition + 1);
ManifestParser.verifySegmentIndex(stream, basicRefs.slice(1));
});
it('evicts old references for multi-period live stream', async () => {
const template = [
'<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"',
' timeShiftBufferDepth="PT60S"',
' suggestedPresentationDelay="PT5S"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
' <Period id="2" start="PT%(pStart)dS">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="4" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
// Set the period start to the sum of the durations of the references
// in the previous period.
const durs = basicRefs.map((r) => {
return r.endTime - r.startTime;
});
const pStart = durs.reduce((p, d) => p + d, 0);
const args = {
updateTime: updateTime,
pStart: pStart,
contents: basicLines.join('\n'),
};
const text = sprintf(template, args);
fakeNetEngine.setResponseText('dummy://foo', text);
Date.now = () => 0;
const manifest = await parser.start('dummy://foo', playerInterface);
/** @const {!Array.<!shaka.media.SegmentReference>} */
const period1Refs = cloneRefs(basicRefs);
/** @const {!Array.<!shaka.media.SegmentReference>} */
const period2Refs = cloneRefs(basicRefs);
for (const ref of period2Refs) {
ref.timestampOffset = pStart;
ref.startTime += pStart;
ref.endTime += pStart;
ref.trueEndTime += pStart;
}
/** @const {!Array.<!shaka.media.SegmentReference>} */
const allRefs = period1Refs.concat(period2Refs);
const stream1 = manifest.variants[0].video;
await stream1.createSegmentIndex();
ManifestParser.verifySegmentIndex(stream1, allRefs);
// The 60 second availability window is initially full in all cases
// (SegmentTemplate+Timeline, etc.) The first segment is always 10
// seconds long in all of these cases. So 11 seconds after the
// manifest was parsed, the first segment should have fallen out of
// the availability window.
Date.now = () => 11 * 1000;
await updateManifest();
// The first reference should have been evicted.
ManifestParser.verifySegmentIndex(stream1, allRefs.slice(1));
// Same as above, but 1 period length later
Date.now = () => (11 + pStart) * 1000;
await updateManifest();
ManifestParser.verifySegmentIndex(stream1, period2Refs.slice(1));
});
it('sets infinite duration for single-period live streams', async () => {
const template = [
'<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"',
' timeShiftBufferDepth="PT1S"',
' suggestedPresentationDelay="PT5S"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const text = sprintf(
template, {updateTime: updateTime, contents: basicLines.join('\n')});
fakeNetEngine.setResponseText('dummy://foo', text);
Date.now = () => 0;
const manifest = await parser.start('dummy://foo', playerInterface);
const timeline = manifest.presentationTimeline;
expect(timeline.getDuration()).toBe(Infinity);
});
it('sets infinite duration for multi-period live streams', async () => {
const template = [
'<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"',
' timeShiftBufferDepth="PT1S"',
' suggestedPresentationDelay="PT5S"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
' <Period id="2" start="PT60S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="4" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const text = sprintf(
template, {updateTime: updateTime, contents: basicLines.join('\n')});
fakeNetEngine.setResponseText('dummy://foo', text);
Date.now = () => 0;
const manifest = await parser.start('dummy://foo', playerInterface);
const timeline = manifest.presentationTimeline;
expect(timeline.getDuration()).toBe(Infinity);
});
}
it('can add Periods with SegmentTemplate', async () => {
const template1 = [
'<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
' suggestedPresentationDelay="PT5S"',
' minimumUpdatePeriod="PT%(updateTime)dS">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentTemplate media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S t="0" d="2" r="1" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const template2 = [
'<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
' suggestedPresentationDelay="PT5S"',
' minimumUpdatePeriod="PT%(updateTime)dS">',
' <Period id="1" duration="PT10S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentTemplate media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S t="0" d="2" r="4" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
' <Period id="2">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="2" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentTemplate media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S t="0" d="2" r="1" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const firstManifest = sprintf(template1, {updateTime: updateTime});
const secondManifest = sprintf(template2, {updateTime: updateTime});
fakeNetEngine.setResponseText('dummy://foo', firstManifest);
// First two segments should exist
Date.now = () => 5;
const manifest = await parser.start('dummy://foo', playerInterface);
const variant = manifest.variants[0];
const stream = variant.video;
await stream.createSegmentIndex();
// First two segments exist, but not the third.
expect(stream.segmentIndex.find(3)).not.toBe(null);
expect(stream.segmentIndex.find(5)).toBe(null);
fakeNetEngine.setResponseText('dummy://foo', secondManifest);
// First period (10s) is complete, plus first two segments of next period
Date.now = () => 15;
await updateManifest();
// The update should have affected the same variant object we captured
// before. Now the entire first period should exist (0-10s), plus the next
// two segments (10-14s).
expect(stream.segmentIndex.find(9)).not.toBe(null);
expect(stream.segmentIndex.find(13)).not.toBe(null);
stream.closeSegmentIndex();
await stream.createSegmentIndex();
expect(stream.segmentIndex.find(9)).not.toBe(null);
expect(stream.segmentIndex.find(13)).not.toBe(null);
});
it('can add Periods with SegmentList', async () => {
const list1 = [
'<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
' suggestedPresentationDelay="PT5S"',
' minimumUpdatePeriod="PT%(updateTime)dS">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentList>',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
' <SegmentURL media="s3.mp4" />',
' <SegmentTimeline>',
' <S d="10" t="0" r="2"/>',
' </SegmentTimeline>',
' </SegmentList>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const list2 = [
'<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
' suggestedPresentationDelay="PT5S"',
' minimumUpdatePeriod="PT%(updateTime)dS">',
' <Period id="1" duration="PT40S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentList>',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
' <SegmentURL media="s3.mp4" />',
' <SegmentURL media="s4.mp4" />',
' <SegmentTimeline>',
' <S d="10" t="0" r="3"/>',
' </SegmentTimeline>',
' </SegmentList>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
' <Period id="2">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="2" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentList>',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
' <SegmentTimeline>',
' <S d="10" t="0" r="1"/>',
' </SegmentTimeline>',
' </SegmentList>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const firstManifest = sprintf(list1, {updateTime: updateTime});
const secondManifest = sprintf(list2, {updateTime: updateTime});
fakeNetEngine.setResponseText('dummy://foo', firstManifest);
// First three segments should exist.
Date.now = () => 5;
const manifest = await parser.start('dummy://foo', playerInterface);
const variant = manifest.variants[0];
const stream = variant.video;
await stream.createSegmentIndex();
// First three segments exist, but not the fourth.
expect(stream.segmentIndex.find(25)).not.toBe(null);
expect(stream.segmentIndex.find(45)).toBe(null);
fakeNetEngine.setResponseText('dummy://foo', secondManifest);
Date.now = () => 25;
await updateManifest();
// The update should have affected the same variant object we captured
// before. Now the entire first period should exist (0-40s), plus the next
// two segments of the second period(40-60s).
expect(stream.segmentIndex.find(25)).not.toBe(null);
expect(stream.segmentIndex.find(45)).not.toBe(null);
stream.closeSegmentIndex();
await stream.createSegmentIndex();
expect(stream.segmentIndex.find(25)).not.toBe(null);
expect(stream.segmentIndex.find(45)).not.toBe(null);
});
it('uses redirect URL for manifest BaseURL and updates', async () => {
const template = [
'<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
' suggestedPresentationDelay="PT5S"',
' minimumUpdatePeriod="PT%(updateTime)dS">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <SegmentTemplate startNumber="1" media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S d="10" t="0" />',
' <S d="5" />',
' <S d="15" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const manifestText = sprintf(template, {updateTime: updateTime});
const manifestData = shaka.util.StringUtils.toUTF8(manifestText);
const redirectedUri = 'http://redirected.com/';
// The initial manifest request will be redirected.
fakeNetEngine.request.and.returnValue(
shaka.util.AbortableOperation.completed({
uri: redirectedUri,
data: manifestData,
}));
const manifest = await parser.start(originalUri, playerInterface);
// The manifest request was made to the original URL.
// But includes a redirect
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
const netRequest = fakeNetEngine.request.calls.argsFor(0)[1];
expect(netRequest.uris).toEqual([redirectedUri, originalUri]);
// Since the manifest request was redirected, the segment refers to
// the redirected base.
const stream = manifest.variants[0].video;
await stream.createSegmentIndex();
goog.asserts.assert(stream.segmentIndex != null, 'Null segmentIndex!');
const ref = Array.from(stream.segmentIndex)[0];
const segmentUri = ref.getUris()[0];
expect(segmentUri).toBe(redirectedUri + 's1.mp4');
});
it('calls the error callback if an update fails', async () => {
const lines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
];
const manifestText = makeSimpleLiveManifestText(lines, updateTime);
/** @type {!jasmine.Spy} */
const onError = jasmine.createSpy('onError');
playerInterface.onError = Util.spyFunc(onError);
const updateTick = updateTickSpy();
fakeNetEngine.setResponseText('dummy://foo', manifestText);
await parser.start('dummy://foo', playerInterface);
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.BAD_HTTP_STATUS);
const operation = shaka.util.AbortableOperation.failed(error);
fakeNetEngine.request.and.returnValue(operation);
await updateManifest();
expect(onError).toHaveBeenCalledTimes(1);
expect(updateTick).toHaveBeenCalledTimes(2);
});
it('fatal error on manifest update request failure when ' +
'raiseFatalErrorOnManifestUpdateRequestFailure is true', async () => {
const manifestConfig =
shaka.util.PlayerConfiguration.createDefault().manifest;
manifestConfig.raiseFatalErrorOnManifestUpdateRequestFailure = true;
parser.configure(manifestConfig);
const updateTick = updateTickSpy();
const lines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2"/>',
];
const manifestText = makeSimpleLiveManifestText(lines, updateTime);
/** @type {!jasmine.Spy} */
const onError = jasmine.createSpy('onError');
playerInterface.onError = Util.spyFunc(onError);
fakeNetEngine.setResponseText('dummy://foo', manifestText);
await parser.start('dummy://foo', playerInterface);
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.BAD_HTTP_STATUS);
const operation = shaka.util.AbortableOperation.failed(error);
fakeNetEngine.request.and.returnValue(operation);
await updateManifest();
expect(onError).toHaveBeenCalledWith(error);
expect(updateTick).toHaveBeenCalledTimes(1);
});
it('uses @minimumUpdatePeriod', async () => {
const lines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
];
// updateTime parameter sets @minimumUpdatePeriod in the manifest.
const manifestText = makeSimpleLiveManifestText(lines, updateTime);
/** @type {!jasmine.Spy} */
const tickAfter = updateTickSpy();
Date.now = () => 0;
fakeNetEngine.setResponseText('dummy://foo', manifestText);
await parser.start('dummy://foo', playerInterface);
expect(tickAfter).toHaveBeenCalledTimes(1);
const delay = tickAfter.calls.mostRecent().args[0];
expect(delay).toBe(updateTime);
});
it('still updates when @minimumUpdatePeriod is zero', async () => {
const lines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
];
// updateTime parameter sets @minimumUpdatePeriod in the manifest.
const manifestText = makeSimpleLiveManifestText(lines, /* updateTime= */ 0);
/** @type {!jasmine.Spy} */
const tickAfter = updateTickSpy();
Date.now = () => 0;
fakeNetEngine.setResponseText('dummy://foo', manifestText);
await parser.start('dummy://foo', playerInterface);
expect(tickAfter).toHaveBeenCalledTimes(1);
const delay = tickAfter.calls.mostRecent().args[0];
expect(delay).toBe(0);
});
it('does not update when @minimumUpdatePeriod is missing', async () => {
const lines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
];
// updateTime parameter sets @minimumUpdatePeriod in the manifest.
const manifestText =
makeSimpleLiveManifestText(lines, /* updateTime= */ null);
/** @type {!jasmine.Spy} */
const tickAfter = updateTickSpy();
fakeNetEngine.setResponseText('dummy://foo', manifestText);
await parser.start('dummy://foo', playerInterface);
expect(tickAfter).not.toHaveBeenCalled();
});
it('delays subsequent updates when an update is slow', async () => {
const lines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
];
const extraWaitTime = 15.0;
const manifestText = makeSimpleLiveManifestText(lines, 3);
let now = 0;
Date.now = () => now;
/** @type {!jasmine.Spy} */
const tickAfter = updateTickSpy();
fakeNetEngine.setResponseText('dummy://foo', manifestText);
/** @type {!shaka.util.PublicPromise} */
const delay = fakeNetEngine.delayNextRequest();
const p = parser.start('dummy://foo', playerInterface);
now += extraWaitTime * 1000; // Make the update appear to take longer.
delay.resolve();
await p;
// Check the last update was scheduled close to how long the update took.
const realDelay = tickAfter.calls.mostRecent().args[0];
expect(realDelay).toBeCloseTo(extraWaitTime, 0);
});
it('uses Mpd.Location', async () => {
const manifestText = [
'<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
' suggestedPresentationDelay="PT5S"',
' minimumUpdatePeriod="PT' + updateTime + 'S">',
' <Location>http://foobar</Location>',
' <Location>http://foobar2</Location>',
' <Location>foobar3</Location>',
' <Period id="1" duration="PT10S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
const manifestRequest = shaka.net.NetworkingEngine.RequestType.MANIFEST;
const manifestContext = {
type: shaka.net.NetworkingEngine.AdvancedRequestType.MPD,
};
await parser.start('dummy://foo', playerInterface);
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
fakeNetEngine.expectRequest('dummy://foo', manifestRequest, manifestContext);
fakeNetEngine.request.calls.reset();
// Create a mock so we can verify it gives two URIs.
// The third location is a relative url, and should be resolved as an
// absolute url.
fakeNetEngine.request.and.callFake((type, request, context) => {
expect(type).toBe(manifestRequest);
expect(context).toEqual(manifestContext);
expect(request.uris).toEqual(
['http://foobar', 'http://foobar2', 'dummy://foo/foobar3']);
const data = shaka.util.StringUtils.toUTF8(manifestText);
return shaka.util.AbortableOperation.completed(
{uri: request.uris[0], data: data, headers: {}});
});
await updateManifest();
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
});
it('uses @suggestedPresentationDelay', async () => {
const manifestText = [
'<MPD type="dynamic" suggestedPresentationDelay="PT60S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' maxSegmentDuration="PT10S"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
Date.now = () => 600000; /* 10 minutes */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest).toBeTruthy();
const timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
// We are 5 minutes into the presentation, with a
// @timeShiftBufferDepth of 120 seconds and a @maxSegmentDuration of
// 10 seconds, the start will be 2:50.
expect(timeline.getSegmentAvailabilityStart()).toBe(170);
// Normally the end should be 4:50; but with a 60 second
// @suggestedPresentationDelay it will be 3:50 minutes.
expect(timeline.getSegmentAvailabilityEnd()).toBe(290);
expect(timeline.getSeekRangeEnd()).toBe(230);
});
it('parses availabilityTimeOffset', async () => {
const manifestText = [
'<MPD type="dynamic" suggestedPresentationDelay="PT0S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' maxSegmentDuration="PT10S"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL availabilityTimeOffset="6">http://example.com',
' </BaseURL>',
' <SegmentTemplate availabilityTimeOffset="6.00" ',
' startNumber="1" media="s$Number$.mp4" duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
playerInterface.isLowLatencyMode = () => true;
Date.now = () => 600000; /* 10 minutes */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest).toBeTruthy();
const timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
// We are 5 minutes into the presentation, with a @timeShiftBufferDepth of
// 120 seconds and a @maxSegmentDuration of 10 seconds, the start will be
// 2:50. With the availabilityTimeOffset of 6+6 seconds, it will be 3:02.
expect(timeline.getSegmentAvailabilityStart()).toBe(182);
// Normally the end should be 4:50; with the availabilityTimeOffset of
// 12 seconds, it will be 5:02.
expect(timeline.getSegmentAvailabilityEnd()).toBe(302);
expect(timeline.getSeekRangeEnd()).toBe(302);
});
describe('availabilityWindowOverride', () => {
it('overrides @timeShiftBufferDepth', async () => {
const manifestText = [
'<MPD type="dynamic" suggestedPresentationDelay="PT60S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' maxSegmentDuration="PT10S"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentTemplate media="s$Number$.mp4" duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.availabilityWindowOverride = 4 * 60;
parser.configure(config);
Date.now = () => 600000; /* 10 minutes */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest).toBeTruthy();
const timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
// The parser was configured to have a manifest availability window
// of 4 minutes.
const end = timeline.getSegmentAvailabilityEnd();
const start = timeline.getSegmentAvailabilityStart();
expect(end - start).toBe(4 * 60);
});
});
describe('maxSegmentDuration', () => {
it('uses @maxSegmentDuration', async () => {
const manifestText = [
'<MPD type="dynamic" suggestedPresentationDelay="PT0S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' maxSegmentDuration="PT15S"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'<SegmentTemplate media="s$Number$.mp4" duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
Date.now = () => 600000; /* 10 minutes */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest).toBeTruthy();
const timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
expect(timeline.getMaxSegmentDuration()).toBe(15);
});
it('derived from SegmentTemplate w/ SegmentTimeline', async () => {
const lines = [
'<SegmentTemplate media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S t="0" d="7" />',
' <S d="8" />',
' <S d="6" />',
' </SegmentTimeline>',
'</SegmentTemplate>',
];
await testDerived(lines);
});
it('derived from SegmentTemplate w/ @duration', async () => {
const lines = [
'<SegmentTemplate media="s$Number$.mp4" duration="8" />',
];
await testDerived(lines);
});
it('derived from SegmentList', async () => {
const lines = [
'<SegmentList duration="8">',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
'</SegmentList>',
];
await testDerived(lines);
});
it('derived from SegmentList w/ SegmentTimeline', async () => {
const lines = [
'<SegmentList duration="8">',
' <SegmentTimeline>',
' <S t="0" d="5" />',
' <S d="4" />',
' <S d="8" />',
' </SegmentTimeline>',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
'</SegmentList>',
];
await testDerived(lines);
});
async function testDerived(lines) {
const template = [
'<MPD type="dynamic" suggestedPresentationDelay="PT0S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const manifestText = sprintf(template, {contents: lines.join('\n')});
fakeNetEngine.setResponseText('dummy://foo', manifestText);
Date.now = () => 600000; /* 10 minutes */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest).toBeTruthy();
const timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
// NOTE: the largest segment is 8 seconds long in each test.
expect(timeline.getMaxSegmentDuration()).toBe(8);
}
}); // describe('maxSegmentDuration')
describe('stop', () => {
const manifestRequestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
const manifestContext = {
type: shaka.net.NetworkingEngine.AdvancedRequestType.MPD,
};
const dateRequestType = shaka.net.NetworkingEngine.RequestType.TIMING;
const manifestUri = 'dummy://foo';
const dateUri = 'http://foo.bar/date';
beforeEach(() => {
const manifest = [
'<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
' minimumUpdatePeriod="PT' + updateTime + 'S">',
' <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="http://foo.bar/date" />',
' <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="http://foo.bar/date" />',
' <Period id="1">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentTemplate startNumber="1" media="s$Number$.mp4"',
' duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine
.setResponseText('http://foo.bar/date', '1970-01-01T00:00:30Z')
.setResponseText('dummy://foo', manifest);
});
it('stops updates', async () => {
await parser.start(manifestUri, playerInterface);
fakeNetEngine.expectRequest(
manifestUri, manifestRequestType, manifestContext);
fakeNetEngine.request.calls.reset();
parser.stop();
await updateManifest();
expect(fakeNetEngine.request).not.toHaveBeenCalled();
});
it('stops initial parsing', async () => {
const expectation =
expectAsync(parser.start('dummy://foo', playerInterface))
.toBeRejected();
// start will only begin the network request, calling stop here will be
// after the request has started but before any parsing has been done.
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
parser.stop();
await expectation;
fakeNetEngine.expectRequest(
manifestUri, manifestRequestType, manifestContext);
fakeNetEngine.request.calls.reset();
await updateManifest();
// An update should not occur.
expect(fakeNetEngine.request).not.toHaveBeenCalled();
});
it('interrupts manifest updates', async () => {
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest).toBeTruthy();
fakeNetEngine.expectRequest(
manifestUri, manifestRequestType, manifestContext);
fakeNetEngine.request.calls.reset();
/** @type {!shaka.util.PublicPromise} */
const delay = fakeNetEngine.delayNextRequest();
await updateManifest();
// The request was made but should not be resolved yet.
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
fakeNetEngine.expectRequest(
manifestUri, manifestRequestType, manifestContext);
fakeNetEngine.request.calls.reset();
parser.stop();
delay.resolve();
// Wait for another update period.
await updateManifest();
// A second update should not occur.
expect(fakeNetEngine.request).not.toHaveBeenCalled();
});
it('interrupts UTCTiming requests', async () => {
/** @type {!shaka.util.PublicPromise} */
let delay = fakeNetEngine.delayNextRequest();
const expectation =
expectAsync(parser.start('dummy://foo', playerInterface))
.toBeRejected();
await Util.shortDelay();
// This is the initial manifest request.
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
fakeNetEngine.expectRequest(
manifestUri, manifestRequestType, manifestContext);
fakeNetEngine.request.calls.reset();
// Resolve the manifest request and wait on the UTCTiming request.
delay.resolve();
delay = fakeNetEngine.delayNextRequest();
await Util.shortDelay();
// This is the first UTCTiming request.
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
fakeNetEngine.expectRequest(dateUri, dateRequestType);
fakeNetEngine.request.calls.reset();
// Interrupt the parser, then fail the request.
parser.stop();
delay.reject();
await expectation;
// Wait for another update period.
await updateManifest();
// No more updates should occur.
expect(fakeNetEngine.request).not.toHaveBeenCalled();
});
});
describe('SegmentTemplate w/ SegmentTimeline', () => {
const basicLines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S d="10" t="0" />',
' <S d="5" />',
' <S d="15" />',
' </SegmentTimeline>',
'</SegmentTemplate>',
];
const basicRefs = [
shaka.test.ManifestParser.makeReference('s1.mp4', 0, 10, originalUri),
shaka.test.ManifestParser.makeReference('s2.mp4', 10, 15, originalUri),
shaka.test.ManifestParser.makeReference('s3.mp4', 15, 30, originalUri),
];
const updateLines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S d="10" t="0" />',
' <S d="5" />',
' <S d="15" />',
' <S d="10" />',
' </SegmentTimeline>',
'</SegmentTemplate>',
];
const updateRefs = [
shaka.test.ManifestParser.makeReference('s1.mp4', 0, 10, originalUri),
shaka.test.ManifestParser.makeReference('s2.mp4', 10, 15, originalUri),
shaka.test.ManifestParser.makeReference('s3.mp4', 15, 30, originalUri),
shaka.test.ManifestParser.makeReference('s4.mp4', 30, 40, originalUri),
];
const partialUpdateLines = [
'<SegmentTemplate startNumber="3" media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S d="15" t="15" />',
' <S d="10" />',
' </SegmentTimeline>',
'</SegmentTemplate>',
];
testCommonBehaviors(
basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines);
});
describe('SegmentList w/ SegmentTimeline', () => {
const basicLines = [
'<SegmentList>',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
' <SegmentURL media="s3.mp4" />',
' <SegmentTimeline>',
' <S d="10" t="0" />',
' <S d="5" />',
' <S d="15" />',
' </SegmentTimeline>',
'</SegmentList>',
];
const basicRefs = [
shaka.test.ManifestParser.makeReference('s1.mp4', 0, 10, originalUri),
shaka.test.ManifestParser.makeReference('s2.mp4', 10, 15, originalUri),
shaka.test.ManifestParser.makeReference('s3.mp4', 15, 30, originalUri),
];
const updateLines = [
'<SegmentList>',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
' <SegmentURL media="s3.mp4" />',
' <SegmentURL media="s4.mp4" />',
' <SegmentTimeline>',
' <S d="10" t="0" />',
' <S d="5" />',
' <S d="15" />',
' <S d="10" />',
' </SegmentTimeline>',
'</SegmentList>',
];
const updateRefs = [
shaka.test.ManifestParser.makeReference('s1.mp4', 0, 10, originalUri),
shaka.test.ManifestParser.makeReference('s2.mp4', 10, 15, originalUri),
shaka.test.ManifestParser.makeReference('s3.mp4', 15, 30, originalUri),
shaka.test.ManifestParser.makeReference('s4.mp4', 30, 40, originalUri),
];
const partialUpdateLines = [
'<SegmentList startNumber="3">',
' <SegmentURL media="s3.mp4" />',
' <SegmentURL media="s4.mp4" />',
' <SegmentTimeline>',
' <S d="15" t="15" />',
' <S d="10" />',
' </SegmentTimeline>',
'</SegmentList>',
];
testCommonBehaviors(
basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines);
});
describe('SegmentList w/ @duration', () => {
const basicLines = [
'<SegmentList duration="10">',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
' <SegmentURL media="s3.mp4" />',
'</SegmentList>',
];
const basicRefs = [
shaka.test.ManifestParser.makeReference('s1.mp4', 0, 10, originalUri),
shaka.test.ManifestParser.makeReference('s2.mp4', 10, 20, originalUri),
shaka.test.ManifestParser.makeReference('s3.mp4', 20, 30, originalUri),
];
const updateLines = [
'<SegmentList duration="10">',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
' <SegmentURL media="s3.mp4" />',
' <SegmentURL media="s4.mp4" />',
'</SegmentList>',
];
const updateRefs = [
shaka.test.ManifestParser.makeReference('s1.mp4', 0, 10, originalUri),
shaka.test.ManifestParser.makeReference('s2.mp4', 10, 20, originalUri),
shaka.test.ManifestParser.makeReference('s3.mp4', 20, 30, originalUri),
shaka.test.ManifestParser.makeReference('s4.mp4', 30, 40, originalUri),
];
const partialUpdateLines = [
'<SegmentList startNumber="3" duration="10">',
' <SegmentURL media="s3.mp4" />',
' <SegmentURL media="s4.mp4" />',
'</SegmentList>',
];
testCommonBehaviors(
basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines);
});
describe('SegmentTemplate w/ duration', () => {
const templateLines = [
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
];
it('produces sane references without assertions', async () => {
const manifestText =
makeSimpleLiveManifestText(templateLines, updateTime);
fakeNetEngine.setResponseText('dummy://foo', manifestText);
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
await stream.createSegmentIndex();
const liveEdge =
manifest.presentationTimeline.getSegmentAvailabilityEnd();
// In https://github.com/shaka-project/shaka-player/issues/1204, a get on
// the final segment failed an assertion and returned endTime == 0.
// Find the last segment by looking just before the live edge. Looking
// right on the live edge creates test flake, and the segments are 2
// seconds in duration.
const idx = stream.segmentIndex.find(liveEdge - 0.5);
goog.asserts.assert(idx != null, 'Live edge not found!');
// This should not throw an assertion.
const ref = stream.segmentIndex.get(idx);
// The segment's endTime should definitely not be 0.
expect(ref.endTime).toBeGreaterThan(0);
});
});
describe('EventStream', () => {
const originalManifest = [
'<MPD type="dynamic" minimumUpdatePeriod="PT' + updateTime + 'S"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1" duration="PT60S" start="PT10S">',
' <EventStream schemeIdUri="http://example.com" value="foobar"',
' timescale="100">',
' <Event duration="5000" />',
' <Event id="abc" presentationTime="300" duration="1000" />',
' </EventStream>',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentTemplate startNumber="1" media="s$Number$.mp4"',
' duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
/** @type {!jasmine.Spy} */
let onTimelineRegionAddedSpy;
beforeEach(() => {
onTimelineRegionAddedSpy = jasmine.createSpy('onTimelineRegionAdded');
playerInterface.onTimelineRegionAdded =
shaka.test.Util.spyFunc(onTimelineRegionAddedSpy);
});
it('will parse EventStream nodes', async () => {
fakeNetEngine.setResponseText('dummy://foo', originalManifest);
await parser.start('dummy://foo', playerInterface);
expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(2);
expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith({
schemeIdUri: 'http://example.com',
value: 'foobar',
startTime: 10,
endTime: 60,
id: '',
eventElement: jasmine.any(Element),
});
expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith({
schemeIdUri: 'http://example.com',
value: 'foobar',