Files
shaka-player/test/dash/dash_parser_manifest_unit.js
T
Michelle Zhuo a09ae24b85 Add audio channels count configuration
Added a preferred audio channel count to configuration, default set
to 2.
Choose the codec with the largest number of audio channels less than
or equal to the configured number of output channels. If this is not
possible, choose the smallest number of channels.

Closes #1013.
Issue #1071.

Change-Id: I6c6a956e33637cf34bd4bd79af563dc10a595e94
2018-04-06 20:51:28 +00:00

1137 lines
40 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.
*/
// Test basic manifest parsing functionality.
describe('DashParser Manifest', function() {
const Dash = shaka.test.Dash;
/** @type {!shaka.test.FakeNetworkingEngine} */
let fakeNetEngine;
/** @type {!shaka.dash.DashParser} */
let parser;
/** @type {!jasmine.Spy} */
let onEventSpy;
/** @type {shakaExtern.ManifestParser.PlayerInterface} */
let playerInterface;
beforeEach(function() {
fakeNetEngine = new shaka.test.FakeNetworkingEngine();
parser = shaka.test.Dash.makeDashParser();
onEventSpy = jasmine.createSpy('onEvent');
playerInterface = {
networkingEngine: fakeNetEngine,
filterNewPeriod: function() {},
filterAllPeriods: function() {},
onTimelineRegionAdded: fail, // Should not have any EventStream elements.
onEvent: shaka.test.Util.spyFunc(onEventSpy),
onError: fail
};
});
/**
* Makes a series of tests for the given manifest type.
*
* @param {!Array.<string>} startLines
* @param {!Array.<string>} endLines
* @param {shakaExtern.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 {function()} done
* @param {string} manifestText
*/
function testDashParser(done, manifestText) {
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifestText});
parser.start('dummy://foo', playerInterface)
.then(function(actual) { expect(actual).toEqual(expected); })
.catch(fail)
.then(done);
}
it('with SegmentBase', function(done) {
let source = makeTestManifest([
' <SegmentBase indexRange="100-200" timescale="9000">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' </SegmentBase>'
]);
testDashParser(done, source);
});
it('with SegmentList', function(done) {
let source = makeTestManifest([
' <SegmentList startNumber="1" duration="10">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' <SegmentURL media="s1.mp4" />',
' </SegmentList>'
]);
testDashParser(done, source);
});
it('with SegmentTemplate', function(done) {
let 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>'
]);
testDashParser(done, source);
});
}
describe('parses and inherits attributes', function() {
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>'
],
new shaka.test.ManifestGenerator()
.anyTimeline()
.minBufferTime(75)
.addPeriod(jasmine.any(Number))
.addVariant(jasmine.any(Number))
.language('en')
.bandwidth(200)
.primary()
.addVideo(jasmine.any(Number))
.anySegmentFunctions()
.anyInitSegment()
.presentationTimeOffset(0)
.mime('video/mp4', 'avc1.4d401f')
.bandwidth(100)
.frameRate(1000000 / 42000)
.size(768, 576)
.addAudio(jasmine.any(Number))
.anySegmentFunctions()
.anyInitSegment()
.bandwidth(100)
.presentationTimeOffset(0)
.mime('audio/mp4', 'mp4a.40.29')
.primary()
.roles(['main'])
.addVariant(jasmine.any(Number))
.language('en')
.bandwidth(150)
.primary()
.addVideo(jasmine.any(Number))
.anySegmentFunctions()
.anyInitSegment()
.presentationTimeOffset(0)
.mime('video/mp4', 'avc1.4d401f')
.bandwidth(50)
.frameRate(1000000 / 42000)
.size(576, 432)
.addAudio(jasmine.any(Number))
.anySegmentFunctions()
.anyInitSegment()
.bandwidth(100)
.presentationTimeOffset(0)
.mime('audio/mp4', 'mp4a.40.29')
.primary()
.roles(['main'])
.addTextStream(jasmine.any(Number))
.language('es')
.label('spanish')
.primary()
.anySegmentFunctions()
.anyInitSegment()
.presentationTimeOffset(0)
.mime('text/vtt')
.bandwidth(100)
.kind('caption')
.roles(['caption', 'main'])
.build());
});
it('skips any periods after one without duration', function(done) {
let periodContents = [
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Representation bandwidth="100">',
' <SegmentBase presentationTimeOffset="1" indexRange="100-200">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' </SegmentBase>',
' </Representation>',
' </AdaptationSet>'
].join('\n');
let template = [
'<MPD mediaPresentationDuration="PT75S">',
' <Period id="1">',
'%(periodContents)s',
' </Period>',
' <Period id="2">',
'%(periodContents)s',
' </Period>',
'</MPD>'
].join('\n');
let source = sprintf(template, {periodContents: periodContents});
fakeNetEngine.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
})
.catch(fail)
.then(done);
});
it('calculates Period times when missing', function(done) {
let periodContents = [
' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
' <Representation bandwidth="100">',
' <SegmentBase presentationTimeOffset="1" indexRange="100-200">',
' <Initialization sourceURL="init.mp4" range="201-300" />',
' </SegmentBase>',
' </Representation>',
' </AdaptationSet>'
].join('\n');
let template = [
'<MPD mediaPresentationDuration="PT75S">',
' <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');
let source = sprintf(template, {periodContents: periodContents});
fakeNetEngine.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(3);
expect(manifest.periods[0].startTime).toBe(10);
expect(manifest.periods[1].startTime).toBe(20);
expect(manifest.periods[2].startTime).toBe(30);
})
.catch(fail)
.then(done);
});
it('defaults to SegmentBase with multiple Segment*', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
let stream = manifest.periods[0].variants[0].video;
expect(stream.presentationTimeOffset).toBe(1);
})
.catch(fail)
.then(done);
});
it('defaults to SegmentList with SegmentTemplate', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
let stream = manifest.periods[0].variants[0].video;
expect(stream.presentationTimeOffset).toBe(2);
})
.catch(fail)
.then(done);
});
it('generates a correct index for non-segmented text', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
let stream;
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
stream = manifest.periods[0].textStreams[0];
return stream.createSegmentIndex();
})
.then(function() {
expect(stream.initSegmentReference).toBe(null);
expect(stream.findSegmentPosition(0)).toBe(1);
expect(stream.getSegmentReference(1))
.toEqual(new shaka.media.SegmentReference(1, 0, 30, function() {
return ['http://example.com/de.vtt'];
}, 0, null));
})
.catch(fail)
.then(done);
});
it('correctly parses UTF-8', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
let variant = manifest.periods[0].variants[0];
let stream = manifest.periods[0].variants[0].audio;
expect(stream.initSegmentReference.getUris()[0])
.toBe('http://example.com/%C8%A7.mp4');
expect(variant.language).toBe('\u2603');
})
.catch(fail)
.then(done);
});
describe('supports UTCTiming', function() {
const originalNow = Date.now;
beforeAll(function() {
Date.now = function() { return 10 * 1000; };
});
afterAll(function() {
Date.now = originalNow;
});
/**
* @param {!Array.<string>} lines
* @return {string}
*/
function makeManifest(lines) {
let 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>',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
return sprintf(template, lines.join('\n'));
}
/**
* @param {function()} done
* @param {number} expectedTime
*/
function runTest(done, expectedTime) {
parser.start('http://foo.bar/manifest', playerInterface)
.then(function(manifest) {
expect(manifest.presentationTimeline).toBeTruthy();
expect(manifest.presentationTimeline.getSegmentAvailabilityEnd())
.toBe(expectedTime);
})
.catch(fail)
.then(done);
}
it('with direct', function(done) {
let source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:direct:2014"',
' value="1970-01-01T00:00:30Z" />'
]);
fakeNetEngine.setResponseMapAsText({'http://foo.bar/manifest': source});
runTest(done, 25);
});
it('does not produce errors', function(done) {
let source = makeManifest([
'<UTCTiming schemeIdUri="unknown scheme" value="foobar" />'
]);
fakeNetEngine.setResponseMapAsText({'http://foo.bar/manifest': source});
runTest(done, 5);
});
it('tries multiple sources', function(done) {
let source = makeManifest([
'<UTCTiming schemeIdUri="unknown scheme" value="foobar" />',
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:direct:2014"',
' value="1970-01-01T00:00:55Z" />'
]);
fakeNetEngine.setResponseMapAsText({'http://foo.bar/manifest': source});
runTest(done, 50);
});
it('with HEAD', function(done) {
let source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-head:2014"',
' value="http://foo.bar/date" />'
]);
fakeNetEngine.request.and.callFake(function(type, request) {
if (request.uris[0] == 'http://foo.bar/manifest') {
let 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: '',
});
}
});
runTest(done, 35);
});
it('with xsdate', function(done) {
let source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="http://foo.bar/date" />'
]);
fakeNetEngine.setResponseMapAsText({
'http://foo.bar/manifest': source,
'http://foo.bar/date': '1970-01-01T00:00:50Z'
});
runTest(done, 45);
});
it('with relative paths', function(done) {
let source = makeManifest([
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="/date" />'
]);
fakeNetEngine.setResponseMapAsText({
'http://foo.bar/manifest': source,
'http://foo.bar/date': '1970-01-01T00:00:50Z'
});
runTest(done, 45);
});
it('with paths relative to BaseURLs', function(done) {
let source = makeManifest([
'<BaseURL>http://example.com</BaseURL>',
'<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"',
' value="/date" />'
]);
fakeNetEngine.setResponseMapAsText({
'http://foo.bar/manifest': source,
'http://example.com/date': '1970-01-01T00:00:50Z'
});
runTest(done, 45);
});
});
it('handles missing Segment* elements', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
// First Representation should be dropped.
let period = manifest.periods[0];
expect(period.variants.length).toBe(1);
expect(period.variants[0].bandwidth).toBe(200);
})
.catch(fail)
.then(done);
});
describe('allows missing Segment* elements for text', function() {
it('specified via AdaptationSet@contentType', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods[0].textStreams.length).toBe(1);
})
.catch(fail)
.then(done);
});
it('specified via AdaptationSet@mimeType', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods[0].textStreams.length).toBe(1);
})
.catch(fail)
.then(done);
});
it('specified via Representation@mimeType', function(done) {
let 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.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods[0].textStreams.length).toBe(1);
})
.catch(fail)
.then(done);
});
});
describe('fails for', function() {
it('invalid XML', function(done) {
let source = '<not XML';
let error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
Dash.testFails(done, source, error);
});
it('XML with inner errors', function(done) {
let 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');
let error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
Dash.testFails(done, source, error);
});
it('xlink problems when xlinkFailGracefully is false', function(done) {
let 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');
let error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_UNSUPPORTED_XLINK_ACTUATE);
Dash.testFails(done, source, error);
});
it('failed network requests', function(done) {
let 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));
parser.start('', playerInterface)
.then(fail)
.catch(function(error) { expect(error).toEqual(expectedError); })
.then(done);
});
it('missing MPD element', function(done) {
let source = '<XML></XML>';
let error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
Dash.testFails(done, source, error);
});
it('empty AdaptationSet', function(done) {
let source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4" lang="en" group="1" />',
' </Period>',
'</MPD>'
].join('\n');
let error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_ADAPTATION_SET);
Dash.testFails(done, source, error);
});
it('empty Period', function(done) {
let source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S" />',
'</MPD>'
].join('\n');
let error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_PERIOD);
Dash.testFails(done, source, error);
});
it('duplicate Representation ids with live', function(done) {
let source = [
'<MPD minBufferTime="PT75S" type="dynamic">',
' <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');
let error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_DUPLICATE_REPRESENTATION_ID);
Dash.testFails(done, source, error);
});
});
it('parses trickmode tracks', function(done) {
let 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 value="1" ',
' schemeIdUri="http://dashif.org/guidelines/trickmode" />',
' <Representation bandwidth="1">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifestText});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
expect(manifest.periods[0].variants.length).toBe(1);
expect(manifest.periods[0].textStreams.length).toBe(0);
let variant = manifest.periods[0].variants[0];
let trickModeVideo = variant && variant.video &&
variant.video.trickModeVideo;
expect(trickModeVideo).toEqual(jasmine.objectContaining({
id: 2,
type: shaka.util.ManifestParserUtils.ContentType.VIDEO
}));
})
.catch(fail)
.then(done);
});
it('skips unrecognized EssentialProperty elements', function(done) {
let 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.setResponseMapAsText({'dummy://foo': manifestText});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
// The bogus EssentialProperty did not result in a variant.
expect(manifest.periods[0].variants.length).toBe(1);
expect(manifest.periods[0].textStreams.length).toBe(0);
// The bogus EssentialProperty did not result in a trick mode track.
let variant = manifest.periods[0].variants[0];
let trickModeVideo = variant && variant.video &&
variant.video.trickModeVideo;
expect(trickModeVideo).toBe(null);
})
.catch(fail)
.then(done);
});
it('sets contentType to text for embedded text mime types', function(done) {
// One MIME type for embedded TTML, one for embedded WebVTT.
// One MIME type specified on AdaptationSet, on one Representation.
let 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">',
' <Representation>',
' <SegmentTemplate media="1.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet id="2">',
' <Representation mimeType="application/mp4" codecs="wvtt">',
' <SegmentTemplate media="2.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifestText});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
expect(manifest.periods[0].textStreams.length).toBe(2);
// At one time, these came out as 'application' rather than 'text'.
const ContentType = shaka.util.ManifestParserUtils.ContentType;
expect(manifest.periods[0].textStreams[0].type)
.toBe(ContentType.TEXT);
expect(manifest.periods[0].textStreams[1].type)
.toBe(ContentType.TEXT);
})
.catch(fail)
.then(done);
});
it('handles text with mime and codecs on different levels', function(done) {
// Regression test for #875
let 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.setResponseMapAsText({'dummy://foo': manifestText});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
// In #875, this was an empty list.
expect(manifest.periods[0].textStreams.length).toBe(1);
if (manifest.periods[0].textStreams.length) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
expect(manifest.periods[0].textStreams[0].type)
.toBe(ContentType.TEXT);
}
})
.catch(fail)
.then(done);
});
it('ignores duplicate Representation IDs for VOD', function(done) {
let 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="1">',
' <SegmentTemplate media="2.mp4">',
' <SegmentTimeline>',
' <S t="0" d="30" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
// See https://goo.gl/BAM3mi
// The old error was that with SegmentTimeline, duplicate Representation IDs
// would use the same segment index, so they would have the same references.
// This test proves that duplicate Representation IDs are allowed for VOD
// and that error doesn't occur.
fakeNetEngine.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
expect(manifest.periods[0].variants.length).toBe(2);
let variant1 = manifest.periods[0].variants[0];
let variant2 = manifest.periods[0].variants[1];
expect(variant1.video).toBeTruthy();
expect(variant2.video).toBeTruthy();
expect(variant1.video.getSegmentReference(1).getUris())
.toEqual(['dummy://foo/1.mp4']);
expect(variant2.video.getSegmentReference(1).getUris())
.toEqual(['dummy://foo/2.mp4']);
})
.catch(fail)
.then(done);
});
it('handles bandwidth of 0 or missing', function(done) {
// Regression test for https://github.com/google/shaka-player/issues/938
let source = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="video/mp4">',
' <Representation id="1" bandwidth="1">',
' <SegmentTemplate media="1-$Number$.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' <AdaptationSet mimeType="audio/mp4">',
' <Representation id="2" bandwidth="0">',
' <SegmentTemplate media="2-$Number$.mp4" duration="1" />',
' </Representation>',
' <Representation id="3">',
' <SegmentTemplate media="3-$Number$.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
expect(manifest.periods[0].variants.length).toBe(2);
let variant1 = manifest.periods[0].variants[0];
expect(isNaN(variant1.bandwidth)).toBe(false);
expect(variant1.bandwidth).toBeGreaterThan(0);
let variant2 = manifest.periods[0].variants[1];
expect(isNaN(variant2.bandwidth)).toBe(false);
expect(variant2.bandwidth).toBeGreaterThan(0);
})
.catch(fail)
.then(done);
});
describe('AudioChannelConfiguration', function() {
/**
* @param {?number} expectedNumChannels The expected number of channels
* @param {!Object.<string, string>} schemeMap A map where the map key is
* the AudioChannelConfiguration's schemeIdUri attribute, and the map
* value is the value attribute.
* @return {!Promise}
*/
function testAudioChannelConfiguration(expectedNumChannels, schemeMap) {
let header = [
'<MPD minBufferTime="PT75S">',
' <Period id="1" duration="PT30S">',
' <AdaptationSet mimeType="audio/mp4">',
' <Representation id="1" bandwidth="1">'
].join('\n');
let configs = [];
for (let scheme in schemeMap) {
let value = schemeMap[scheme];
configs.push('<AudioChannelConfiguration schemeIdUri="' + scheme +
'" value="' + value + '" />');
}
let footer = [
' <SegmentTemplate media="1-$Number$.mp4" duration="1" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
let source = header + configs.join('\n') + footer;
// Create a fresh parser, to avoid issues when we chain multiple tests
// together.
parser = shaka.test.Dash.makeDashParser();
fakeNetEngine.setResponseMapAsText({'dummy://foo': source});
return parser.start('dummy://foo', playerInterface)
.then(function(manifest) {
expect(manifest.periods.length).toBe(1);
expect(manifest.periods[0].variants.length).toBe(1);
let variant = manifest.periods[0].variants[0];
expect(variant.audio.channelCount).toEqual(expectedNumChannels);
}).catch(fail);
}
it('parses outputChannelPositionList scheme', function(done) {
Promise.resolve().then(function() {
// Parses the space-separated list and finds 8 channels.
return testAudioChannelConfiguration(8,
{'urn:mpeg:dash:outputChannelPositionList:2012':
'2 0 1 4 5 3 17 1'});
}).then(function() {
// Does not get confused about extra spaces.
return testAudioChannelConfiguration(7,
{'urn:mpeg:dash:outputChannelPositionList:2012':
' 5 2 1 12 8 9 1 '});
}).then(done);
});
it('parses 23003:3 scheme', function(done) {
return Promise.resolve().then(function() {
// Parses a simple channel count.
return testAudioChannelConfiguration(2,
{'urn:mpeg:dash:23003:3:audio_channel_configuration:2011': '2'});
}).then(function() {
// This scheme seems to use the same format.
return testAudioChannelConfiguration(6,
{'urn:dts:dash:audio_channel_configuration:2012': '6'});
}).then(function() {
// Results in null if the value is not an integer.
return testAudioChannelConfiguration(null,
{'urn:mpeg:dash:23003:3:audio_channel_configuration:2011':
'foo'});
}).then(done);
});
it('parses dolby scheme', function(done) {
return Promise.resolve().then(function() {
// Parses a hex value in which each 1-bit is a channel.
return testAudioChannelConfiguration(6,
{'tag:dolby.com,2014:dash:audio_channel_configuration:2011':
'F801'});
}).then(function() {
// This scheme seems to use the same format.
return testAudioChannelConfiguration(8,
{'urn:dolby:dash:audio_channel_configuration:2011': '7037'});
}).then(function() {
// Results in null if the value is not a valid hex number.
return testAudioChannelConfiguration(null,
{'urn:dolby:dash:audio_channel_configuration:2011': 'x'});
}).then(done);
});
it('ignores unrecognized schemes', function(done) {
return Promise.resolve().then(function() {
return testAudioChannelConfiguration(null,
{'foo': 'bar'});
}).then(function() {
return testAudioChannelConfiguration(2,
{
'foo': 'bar',
'urn:mpeg:dash:23003:3:audio_channel_configuration:2011': '2'
});
}).then(done);
});
});
/**
* @param {string} manifestText
* @param {Uint8Array} emsgUpdate
* @param {Function} done
*/
function emsgBoxPresenceHelper(manifestText, emsgUpdate, done) {
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifestText});
parser.start('dummy://foo', playerInterface)
.then(function() {
expect(fakeNetEngine.registerResponseFilter).toHaveBeenCalled();
let filter = /** @type {!Function} */ (
fakeNetEngine.registerResponseFilter.calls.mostRecent().args[0]);
const type = shaka.net.NetworkingEngine.RequestType.SEGMENT;
let response = {data: emsgUpdate.buffer};
fakeNetEngine.request.calls.reset();
filter(type, response);
expect(fakeNetEngine.request).toHaveBeenCalled();
})
.catch(fail)
.then(done);
}
});