shaka-player
Version:
DASH/EME video player library
1,319 lines (1,195 loc) • 103 kB
JavaScript
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// Test basic manifest parsing functionality.
describe('DashParser Manifest', () => {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const Dash = shaka.test.Dash;
const mp4IndexSegmentUri = '/base/test/test/assets/index-segment.mp4';
/** @type {!shaka.test.FakeNetworkingEngine} */
let fakeNetEngine;
/** @type {!shaka.dash.DashParser} */
let parser;
/** @type {!jasmine.Spy} */
let onEventSpy;
/** @type {shaka.extern.ManifestParser.PlayerInterface} */
let playerInterface;
/** @type {!ArrayBuffer} */
let mp4Index;
/** @type {string} */
const thumbnailScheme = 'http://dashif.org/guidelines/thumbnail_tile';
/**
* CICP scheme. parameter must be one of the following: "ColourPrimaries",
* "TransferCharacteristics", or "MatrixCoefficients".
*
* @param {string} parameter
* @return {string}
*/
const cicpScheme = (parameter) => `urn:mpeg:mpegB:cicp:${parameter}`;
beforeAll(async () => {
mp4Index = await shaka.test.Util.fetch(mp4IndexSegmentUri);
});
beforeEach(() => {
fakeNetEngine = new shaka.test.FakeNetworkingEngine();
parser = shaka.test.Dash.makeDashParser();
onEventSpy = jasmine.createSpy('onEvent');
playerInterface = {
networkingEngine: fakeNetEngine,
modifyManifestRequest: (request, manifestInfo) => {},
modifySegmentRequest: (request, segmentInfo) => {},
filter: (manifest) => Promise.resolve(),
makeTextStreamsForClosedCaptions: (manifest) => {},
onTimelineRegionAdded: fail, // Should not have any EventStream elements.
onEvent: shaka.test.Util.spyFunc(onEventSpy),
onError: fail,
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
};
});
afterEach(() => {
// Dash parser stop is synchronous.
parser.stop();
});
/**
* Makes a series of tests for the given manifest type.
*
* @param {!Array.<string>} startLines
* @param {!Array.<string>} endLines
* @param {shaka.extern.Manifest} expected
*/
function makeTestsForEach(startLines, endLines, expected) {
/**
* Makes manifest text for testing.
*
* @param {!Array.<string>} lines
* @return {string}
*/
function makeTestManifest(lines) {
return startLines.concat(lines, endLines).join('\n');
}
/**
* Tests that the parser produces the correct results.
*
* @param {string} manifestText
* @return {!Promise}
*/
async function testDashParser(manifestText) {
fakeNetEngine.setResponseText('dummy://foo', manifestText);
const actual = await parser.start('dummy://foo', playerInterface);
expect(actual).toEqual(expected);
}
it('with SegmentBase', async () => {
const source = makeTestManifest([
' <SegmentBase indexRange="100-200" timescale="9000">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' </SegmentBase>',
]);
await testDashParser(source);
});
it('with SegmentList', async () => {
const source = makeTestManifest([
' <SegmentList startNumber="1" duration="10">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' <SegmentURL media="s1.mp4" />',
' </SegmentList>',
]);
await testDashParser(source);
});
it('with SegmentTemplate', async () => {
const source = makeTestManifest([
' <SegmentTemplate startNumber="1" media="l-$Number$.mp4"',
' initialization="init.mp4">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' <SegmentTimeline>',
' <S t="0" d="30" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
]);
await testDashParser(source);
});
}
describe('parses and inherits attributes with sequenceMode', () => {
beforeEach(() => {
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.dash.sequenceMode = true;
parser.configure(config);
});
makeTestsForEach(
[
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <BaseURL>http://example.com</BaseURL>',
],
[
' <AdaptationSet contentType="video" mimeType="video/mp4"',
' codecs="avc1.4d401f" frameRate="1000000/42000">',
' <Representation bandwidth="100" width="768" height="576" />',
' <Representation bandwidth="50" width="576" height="432" />',
' </AdaptationSet>',
' <AdaptationSet mimeType="text/vtt"',
' lang="spa" label="spanish">',
' <Role value="caption" />',
' <Role value="main" />',
' <Representation bandwidth="100" />',
' </AdaptationSet>',
' <AdaptationSet mimeType="audio/mp4" lang="en" ',
' codecs="mp4a.40.29">',
' <Role value="main" />',
' <Representation bandwidth="100" />',
' </AdaptationSet>',
' </Period>',
'</MPD>',
],
shaka.test.ManifestGenerator.generate((manifest) => {
manifest.sequenceMode = true;
manifest.type = shaka.media.ManifestParser.DASH;
manifest.anyTimeline();
manifest.minBufferTime = 75;
manifest.addPartialVariant((variant) => {
variant.language = 'en';
variant.bandwidth = 200;
variant.primary = true;
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.bandwidth = 100;
stream.frameRate = 1000000 / 42000;
stream.size(768, 576);
stream.mime('video/mp4', 'avc1.4d401f');
});
variant.addPartialStream(ContentType.AUDIO, (stream) => {
stream.bandwidth = 100;
stream.primary = true;
stream.roles = ['main'];
stream.mime('audio/mp4', 'mp4a.40.29');
});
});
manifest.addPartialVariant((variant) => {
variant.language = 'en';
variant.bandwidth = 150;
variant.primary = true;
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.bandwidth = 50;
stream.frameRate = 1000000 / 42000;
stream.size(576, 432);
stream.mime('video/mp4', 'avc1.4d401f');
});
variant.addPartialStream(ContentType.AUDIO, (stream) => {
stream.bandwidth = 100;
stream.primary = true;
stream.roles = ['main'];
stream.mime('audio/mp4', 'mp4a.40.29');
});
});
manifest.addPartialTextStream((stream) => {
stream.language = 'es';
stream.originalLanguage = 'spa';
stream.label = 'spanish';
stream.primary = true;
stream.mimeType = 'text/vtt';
stream.bandwidth = 100;
stream.kind = 'caption';
stream.roles = ['caption', 'main'];
});
}));
});
describe('parses and inherits attributes without sequenceMode', () => {
beforeEach(() => {
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.dash.sequenceMode = false;
parser.configure(config);
});
makeTestsForEach(
[
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <BaseURL>http://example.com</BaseURL>',
],
[
' <AdaptationSet contentType="video" mimeType="video/mp4"',
' codecs="avc1.4d401f" frameRate="1000000/42000">',
' <Representation bandwidth="100" width="768" height="576" />',
' <Representation bandwidth="50" width="576" height="432" />',
' </AdaptationSet>',
' <AdaptationSet mimeType="text/vtt"',
' lang="es" label="spanish">',
' <Role value="caption" />',
' <Role value="main" />',
' <Representation bandwidth="100" />',
' </AdaptationSet>',
' <AdaptationSet mimeType="audio/mp4" lang="en" ',
' codecs="mp4a.40.29">',
' <Role value="main" />',
' <Representation bandwidth="100" />',
' </AdaptationSet>',
' </Period>',
'</MPD>',
],
shaka.test.ManifestGenerator.generate((manifest) => {
manifest.sequenceMode = false;
manifest.type = shaka.media.ManifestParser.DASH;
manifest.anyTimeline();
manifest.minBufferTime = 75;
manifest.addPartialVariant((variant) => {
variant.language = 'en';
variant.bandwidth = 200;
variant.primary = true;
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.bandwidth = 100;
stream.frameRate = 1000000 / 42000;
stream.size(768, 576);
stream.mime('video/mp4', 'avc1.4d401f');
});
variant.addPartialStream(ContentType.AUDIO, (stream) => {
stream.bandwidth = 100;
stream.primary = true;
stream.roles = ['main'];
stream.mime('audio/mp4', 'mp4a.40.29');
});
});
manifest.addPartialVariant((variant) => {
variant.language = 'en';
variant.bandwidth = 150;
variant.primary = true;
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.bandwidth = 50;
stream.frameRate = 1000000 / 42000;
stream.size(576, 432);
stream.mime('video/mp4', 'avc1.4d401f');
});
variant.addPartialStream(ContentType.AUDIO, (stream) => {
stream.bandwidth = 100;
stream.primary = true;
stream.roles = ['main'];
stream.mime('audio/mp4', 'mp4a.40.29');
});
});
manifest.addPartialTextStream((stream) => {
stream.language = 'es';
stream.label = 'spanish';
stream.primary = true;
stream.mimeType = 'text/vtt';
stream.bandwidth = 100;
stream.kind = 'caption';
stream.roles = ['caption', 'main'];
});
}));
});
it('calculates Period times when missing', async () => {
const periodContents = [
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Representation bandwidth="100">',
' <SegmentTemplate duration="2" media="s$Number$.mp4" />',
' </Representation>',
' </AdaptationSet>',
].join('\n');
const template = [
'<MPD>',
' <Period id="1" start="PT10S">',
'%(periodContents)s',
' </Period>',
' <Period id="2" start="PT20S" duration="PT10S">',
'%(periodContents)s',
' </Period>',
' <Period id="3" duration="PT10S">',
'%(periodContents)s',
' </Period>',
'</MPD>',
].join('\n');
const source = sprintf(template, {periodContents: periodContents});
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const timeline = manifest.presentationTimeline;
expect(timeline.getDuration()).toBe(40);
});
it('defaults to SegmentBase with multiple Segment*', async () => {
const source = Dash.makeSimpleManifestText([
'<SegmentBase presentationTimeOffset="1" indexRange="100-200">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
'</SegmentBase>',
'<SegmentList presentationTimeOffset="2" duration="10">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' <SegmentURL media="s1.mp4" />',
'</SegmentList>',
]);
fakeNetEngine.setResponseText('dummy://foo', source);
fakeNetEngine.setResponseValue('http://example.com', mp4Index);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
await stream.createSegmentIndex();
goog.asserts.assert(stream.segmentIndex != null, 'Null segmentIndex!');
const ref = Array.from(stream.segmentIndex)[0];
expect(ref.timestampOffset).toBe(-1);
});
it('defaults to SegmentList with SegmentTemplate', async () => {
const source = Dash.makeSimpleManifestText([
'<SegmentList presentationTimeOffset="2" duration="10">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' <SegmentURL media="s1.mp4" />',
'</SegmentList>',
'<SegmentTemplate startNumber="1" media="l-$Number$.mp4"',
' presentationTimeOffset="3" initialization="init.mp4">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' <SegmentTimeline>',
' <S t="0" d="30" />',
' </SegmentTimeline>',
'</SegmentTemplate>',
]);
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
await stream.createSegmentIndex();
goog.asserts.assert(stream.segmentIndex != null, 'Null segmentIndex!');
const ref = Array.from(stream.segmentIndex)[0];
expect(ref.timestampOffset).toBe(-2);
});
it('generates a correct index for non-segmented text', async () => {
const source = [
'<MPD mediaPresentationDuration="PT30S">',
' <Period>',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet mimeType="text/vtt" lang="de">',
' <Representation>',
' <BaseURL>http://example.com/de.vtt</BaseURL>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.textStreams[0];
await stream.createSegmentIndex();
goog.asserts.assert(stream.segmentIndex != null, 'Null segmentIndex!');
const ref = Array.from(stream.segmentIndex)[0];
expect(ref).toEqual(new shaka.media.SegmentReference(
/* startTime= */ 0,
/* endTime= */ 30,
/* getUris= */ () => ['http://example.com/de.vtt'],
/* startByte= */ 0,
/* endBytes= */ null,
/* initSegmentReference= */ null,
/* timestampOffset= */ 0,
/* appendWindowStart= */ 0,
/* appendWindowEnd= */ 30));
});
it('correctly parses mixed captions with channels, services, and languages',
async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Accessibility schemeIdUri="urn:scte:dash:cc:cea-608:2015"',
' value="CC1=eng;CC3=swe"/>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet mimeType="video/mp4" lang="ru" group="1">',
' <Accessibility schemeIdUri="urn:scte:dash:cc:cea-708:2015"',
' value="1=lang:bos;3=lang:cze,war:1,er:1"/>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream1 = manifest.variants[0].video;
const stream2 = manifest.variants[1].video;
const expectedClosedCaptions1 = new Map([
['CC1', shaka.util.LanguageUtils.normalize('eng')],
['CC3', shaka.util.LanguageUtils.normalize('swe')],
]);
const expectedClosedCaptions2 = new Map([
['svc1', shaka.util.LanguageUtils.normalize('bos')],
['svc3', shaka.util.LanguageUtils.normalize('cze')],
]);
expect(stream1.closedCaptions).toEqual(expectedClosedCaptions1);
expect(stream2.closedCaptions).toEqual(expectedClosedCaptions2);
});
it('correctly parses CEA-708 caption tags with service numbers and languages',
async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Accessibility schemeIdUri="urn:scte:dash:cc:cea-708:2015"',
' value="1=lang:eng;3=lang:swe,er"/>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
const expectedClosedCaptions = new Map([
['svc1', shaka.util.LanguageUtils.normalize('eng')],
['svc3', shaka.util.LanguageUtils.normalize('swe')],
]);
expect(stream.closedCaptions).toEqual(expectedClosedCaptions);
});
it('correctly parses CEA-708 caption tags without service #s and languages',
async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Accessibility schemeIdUri="urn:scte:dash:cc:cea-708:2015"',
' value="eng;swe"/>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
const expectedClosedCaptions = new Map([
['svc1', shaka.util.LanguageUtils.normalize('eng')],
['svc2', shaka.util.LanguageUtils.normalize('swe')],
]);
expect(stream.closedCaptions).toEqual(expectedClosedCaptions);
});
it('Detects spatial audio', async () => {
const idUri = 'tag:dolby.com,2018:dash:EC3_ExtensionType:2018';
const source = [
'<MPD>',
' <Period duration="PT30M">',
' <AdaptationSet mimeType="audio/mp4" lang="\u2603">',
' <Representation bandwidth="500">',
' <SupplementalProperty schemeIdUri="' + idUri + '" value="JOC"/>',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].audio;
expect(stream.spatialAudio).toBe(true);
});
it('correctly parses CEA-608 closed caption tags without channel numbers',
async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Accessibility schemeIdUri="urn:scte:dash:cc:cea-608:2015"',
' value="eng;swe"/>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Accessibility schemeIdUri="urn:scte:dash:cc:cea-608:2015"',
' value="eng;swe;fre;pol"/>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream1 = manifest.variants[0].video;
const stream2 = manifest.variants[1].video;
const expectedClosedCaptions1 = new Map([
['CC1', shaka.util.LanguageUtils.normalize('eng')],
['CC3', shaka.util.LanguageUtils.normalize('swe')],
]);
const expectedClosedCaptions2 = new Map([
['CC1', shaka.util.LanguageUtils.normalize('eng')],
['CC2', shaka.util.LanguageUtils.normalize('swe')],
['CC3', shaka.util.LanguageUtils.normalize('fre')],
['CC4', shaka.util.LanguageUtils.normalize('pol')],
]);
expect(stream1.closedCaptions).toEqual(expectedClosedCaptions1);
expect(stream2.closedCaptions).toEqual(expectedClosedCaptions2);
});
it('correctly parses CEA-608 caption tags with no channel and language info',
async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Accessibility schemeIdUri="urn:scte:dash:cc:cea-608:2015"/>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
const expectedClosedCaptions = new Map([['CC1', 'und']]);
expect(stream.closedCaptions).toEqual(expectedClosedCaptions);
});
it('correctly parses UTF-8', async () => {
const source = [
'<MPD>',
' <Period duration="PT30M">',
' <AdaptationSet mimeType="audio/mp4" lang="\u2603">',
' <Representation bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentTemplate media="2.mp4" duration="1"',
' initialization="\u0227.mp4" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
const variant = manifest.variants[0];
const stream = variant.audio;
await stream.createSegmentIndex();
goog.asserts.assert(stream.segmentIndex != null, 'Null segmentIndex!');
const segment = Array.from(stream.segmentIndex)[0];
expect(segment.initSegmentReference.getUris()[0])
.toBe('http://example.com/%C8%A7.mp4');
expect(variant.language).toBe('\u2603');
});
describe('UTCTiming', () => {
const originalNow = Date.now;
const dateRequestType = shaka.net.NetworkingEngine.RequestType.TIMING;
beforeAll(() => {
Date.now = () => 10 * 1000;
});
beforeEach(() => {
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.dash.autoCorrectDrift = false;
parser.configure(config);
});
afterAll(() => {
Date.now = originalNow;
});
/**
* @param {!Array.<string>} lines
* @return {string}
*/
function makeManifest(lines) {
const template = [
'<MPD type="dynamic"',
' availabilityStartTime="1970-01-01T00:00:00Z"',
' timeShiftBufferDepth="PT60S"',
' maxSegmentDuration="PT5S"',
' suggestedPresentationDelay="PT0S">',
' %s',
' <Period>',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
' <SegmentList>',
' <SegmentURL media="s1.mp4" />',
' <SegmentTimeline>',
' <S d="5" t="0" />',
' </SegmentTimeline>',
' </SegmentList>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
return sprintf(template, lines.join('\n'));
}
/**
* @param {number} expectedTime
* @return {!Promise}
*/
async function runTest(expectedTime) {
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start(
'http://foo.bar/manifest', playerInterface);
expect(manifest.presentationTimeline).toBeTruthy();
expect(manifest.presentationTimeline.getSegmentAvailabilityEnd())
.toBe(expectedTime);
}
it('with direct', async () => {
const source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:direct:2014"',
' value="1970-01-01T00:00:30Z" />',
]);
fakeNetEngine.setResponseText('http://foo.bar/manifest', source);
await runTest(25);
});
it('does not produce errors', async () => {
const source = makeManifest([
'<UTCTiming schemeIdUri="unknown scheme" value="foobar" />',
]);
fakeNetEngine.setResponseText('http://foo.bar/manifest', source);
await runTest(5);
});
it('tries multiple sources', async () => {
const source = makeManifest([
'<UTCTiming schemeIdUri="unknown scheme" value="foobar" />',
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:direct:2014"',
' value="1970-01-01T00:00:55Z" />',
]);
fakeNetEngine.setResponseText('http://foo.bar/manifest', source);
await runTest(50);
});
it('with HEAD', async () => {
const source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-head:2014"',
' value="http://foo.bar/date" />',
]);
fakeNetEngine.request.and.callFake((type, request, context) => {
if (request.uris[0] == 'http://foo.bar/manifest') {
const data = shaka.util.StringUtils.toUTF8(source);
return shaka.util.AbortableOperation.completed({
data: data,
headers: {},
uri: '',
});
} else {
expect(request.uris[0]).toBe('http://foo.bar/date');
return shaka.util.AbortableOperation.completed({
data: new ArrayBuffer(0),
headers: {'date': '1970-01-01T00:00:40Z'},
uri: '',
});
}
});
await runTest(35);
fakeNetEngine.expectRequest('http://foo.bar/date', dateRequestType);
});
it('with xsdate', async () => {
const source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="http://foo.bar/date" />',
]);
fakeNetEngine
.setResponseText('http://foo.bar/manifest', source)
.setResponseText('http://foo.bar/date', '1970-01-01T00:00:50Z');
await runTest(45);
fakeNetEngine.expectRequest('http://foo.bar/date', dateRequestType);
});
it('with relative paths', async () => {
const source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="/date" />',
]);
fakeNetEngine
.setResponseText('http://foo.bar/manifest', source)
.setResponseText('http://foo.bar/date', '1970-01-01T00:00:50Z');
await runTest(45);
fakeNetEngine.expectRequest('http://foo.bar/date', dateRequestType);
});
it('with paths relative to BaseURLs', async () => {
const source = makeManifest([
'<BaseURL>http://example.com</BaseURL>',
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="/date" />',
]);
fakeNetEngine
.setResponseText('http://foo.bar/manifest', source)
.setResponseText('http://example.com/date', '1970-01-01T00:00:50Z');
await runTest(45);
fakeNetEngine.expectRequest('http://example.com/date', dateRequestType);
});
it('ignored with autoCorrectDrift', async () => {
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.dash.autoCorrectDrift = true;
parser.configure(config);
const source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="http://foo.bar/date" />',
]);
fakeNetEngine
.setResponseText('http://foo.bar/manifest', source)
.setResponseText('http://foo.bar/date', '1970-01-01T00:00:50Z');
// Expect the presentation timeline to end at 5 based on the segments
// instead of 45 based on the UTCTiming element.
await runTest(5);
});
});
it('handles missing Segment* elements', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Representation bandwidth="100" />',
' <Representation bandwidth="200">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
// First Representation should be dropped.
expect(manifest.variants.length).toBe(1);
expect(manifest.variants[0].bandwidth).toBe(200);
});
describe('allows missing Segment* elements for text', () => {
it('specified via AdaptationSet@contentType', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet contentType="text" lang="en" group="1">',
' <Representation />',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.textStreams.length).toBe(1);
});
it('specified via AdaptationSet@mimeType', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet mimeType="text/vtt" lang="en" group="1">',
' <Representation />',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.textStreams.length).toBe(1);
});
it('specified via Representation@mimeType', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet>',
' <Representation mimeType="text/vtt" />',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', source);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.textStreams.length).toBe(1);
});
});
describe('fails for', () => {
it('invalid XML', async () => {
const source = '<not XML';
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
await Dash.testFails(source, error);
});
it('XML with inner errors', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentBase indexRange="100-200" />',
' </Representation', // Missing a close bracket.
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
await Dash.testFails(source, error);
});
it('xlink problems when xlinkFailGracefully is false', async () => {
const source = [
'<MPD minBufferTime="PT75S" xmlns="urn:mpeg:dash:schema:mpd:2011" ' +
'xmlns:xlink="http://www.w3.org/1999/xlink">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1" xlink:href="https://xlink1" ' +
'xlink:actuate="onInvalid">', // Incorrect actuate
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_UNSUPPORTED_XLINK_ACTUATE);
await Dash.testFails(source, error);
});
it('failed network requests', async () => {
const expectedError = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.BAD_HTTP_STATUS);
fakeNetEngine.request.and.returnValue(
shaka.util.AbortableOperation.failed(expectedError));
await expectAsync(parser.start('', playerInterface))
.toBeRejectedWith(shaka.test.Util.jasmineError(expectedError));
});
it('missing MPD element', async () => {
const source = '<XML></XML>';
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
await Dash.testFails(source, error);
});
it('empty AdaptationSet', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1" />',
' </Period>',
'</MPD>',
].join('\n');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_ADAPTATION_SET);
await Dash.testFails(source, error);
});
it('empty Period', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S" />',
'</MPD>',
].join('\n');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_PERIOD);
await Dash.testFails(source, error);
});
it('duplicate Representation ids with live', async () => {
const source = [
'<MPD minBufferTime="PT75S" type="dynamic"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="1">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="1">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_DUPLICATE_REPRESENTATION_ID);
await Dash.testFails(source, error);
});
});
it('parses trickmode tracks', async () => {
const manifestText = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet id="1" mimeType="video/mp4">',
' <Representation bandwidth="1" codecs="avc1.4d401f">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <EssentialProperty value="1" ',
' schemeIdUri="http://dashif.org/guidelines/trickmode" />',
' <Representation bandwidth="1" codecs="avc1.4d401f">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.variants.length).toBe(1);
expect(manifest.textStreams.length).toBe(0);
const variant = manifest.variants[0];
const trickModeVideo = variant && variant.video &&
variant.video.trickModeVideo;
expect(trickModeVideo).toEqual(jasmine.objectContaining({
id: 2,
type: shaka.util.ManifestParserUtils.ContentType.VIDEO,
}));
});
it('trick-mode track with multiple AdaptationSet elements', async () => {
const manifestText = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet id="1" mimeType="video/mp4">',
' <Representation bandwidth="1" codecs="avc1.4d401f">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation bandwidth="2" codecs="avc1.4d401f">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="3" mimeType="video/mp4">',
' <EssentialProperty value="1 2" ',
' schemeIdUri="http://dashif.org/guidelines/trickmode" />',
' <Representation bandwidth="1" codecs="avc1.4d401f">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.variants.length).toBe(2);
expect(manifest.textStreams.length).toBe(0);
const variant = manifest.variants[0];
const trickModeVideo = variant && variant.video &&
variant.video.trickModeVideo;
expect(trickModeVideo).toEqual(jasmine.objectContaining({
id: 3,
type: shaka.util.ManifestParserUtils.ContentType.VIDEO,
}));
const variant2 = manifest.variants[1];
const trickModeVideo2 = variant2 && variant2.video &&
variant2.video.trickModeVideo;
expect(trickModeVideo2).toEqual(jasmine.objectContaining({
id: 3,
type: shaka.util.ManifestParserUtils.ContentType.VIDEO,
}));
});
it('ignore incompatible trickmode tracks', async () => {
const manifestText = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet id="1" mimeType="video/mp4">',
' <Representation bandwidth="1" codecs="avc1.4d401f">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <EssentialProperty value="1" ',
' schemeIdUri="http://dashif.org/guidelines/trickmode" />',
' <Representation bandwidth="1" codecs="foo">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.variants.length).toBe(1);
expect(manifest.textStreams.length).toBe(0);
expect(manifest.variants[0].video.trickModeVideo).toBeUndefined();
});
it('skips unrecognized EssentialProperty elements', async () => {
const manifestText = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet id="1" mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <EssentialProperty schemeIdUri="http://foo.bar/" />',
' <Representation bandwidth="1">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
// The bogus EssentialProperty did not result in a variant.
expect(manifest.variants.length).toBe(1);
expect(manifest.textStreams.length).toBe(0);
// The bogus EssentialProperty did not result in a trick mode track.
const variant = manifest.variants[0];
const trickModeVideo = variant && variant.video &&
variant.video.trickModeVideo;
expect(trickModeVideo).toBe(null);
});
it('populates groupId if configuration enabled', async () => {
const manifestText = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet id="1" contentType="text">',
' <Representation id="text-en" mimeType="text/webvtt">',
' <BaseURL>t-en.vtt</BaseURL>',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation id="video-sd" width="640" height="480">',
' <BaseURL>v-sd.mp4</BaseURL>',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="3" mimeType="audio/mp4">',
' <Representation id="audio-en">',
' <BaseURL>a-en.mp4</BaseURL>',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.dash.enableAudioGroups = true;
parser.configure(config);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.variants.length).toBe(1);
const variant = manifest.variants[0];
expect(variant.audio.groupId).toBe('3');
});
it('sets contentType to text for embedded text mime types', async () => {
// One MIME type for embedded TTML, one for embedded WebVTT.
// One MIME type specified on AdaptationSet, on one Representation.
const manifestText = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet',
' id="1"',
' mimeType="application/mp4"',
' codecs="stpp"',
' lang="en"',
' >',
' <Representation>',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="2" lang="fr">',
' <Representation mimeType="application/mp4" codecs="wvtt">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
expect(manifest.textStreams.length).toBe(2);
// At one time, these came out as 'application' rather than 'text'.
const ContentType = shaka.util.ManifestParserUtils.ContentType;
expect(manifest.textStreams[0].type).toBe(ContentType.TEXT);
expect(manifest.textStreams[1].type).toBe(ContentType.TEXT);
});
it('handles text with mime and codecs on different levels', async () => {
// Regression test for #875
const manifestText = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation bandwidth="1">',
' <SegmentBase indexRange="100-200" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="1" mimeType="application/mp4">',
' <Representation codecs="stpp">',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>',
].join('\n');
fakeNetEngine.setResponseText('dummy://foo', manifestText);
/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);
// In #875, this was an empty list.
expect(manifest.textStreams.length).toBe(1);
if (manifest.textStreams.length) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
expect(manifest.textStreams[0].type).toBe(ContentType.TEXT);
}
});
it('ignores duplicate Representation IDs for VOD', async () => {
const source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="1">',
' <SegmentTemplate media="1.mp4">',
' <SegmentTimeline>',
' <S t="0" d="30" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="2">',
' <SegmentTemplate media="2.mp4">',
' <SegmentTimeline>',
' <S t="0" d="30" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</M