/*! @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; 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, filter: (manifest) => Promise.resolve(), 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.} startLines * @param {!Array.} endLines * @param {shaka.extern.Manifest} expected */ function makeTestsForEach(startLines, endLines, expected) { /** * Makes manifest text for testing. * * @param {!Array.} 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([ ' ', ' ', ' ', ]); await testDashParser(source); }); it('with SegmentList', async () => { const source = makeTestManifest([ ' ', ' ', ' ', ' ', ]); await testDashParser(source); }); it('with SegmentTemplate', async () => { const source = makeTestManifest([ ' ', ' ', ' ', ' ', ' ', ' ', ]); await testDashParser(source); }); } describe('parses and inherits attributes', () => { makeTestsForEach( [ '', ' ', ' http://example.com', ], [ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ], shaka.test.ManifestGenerator.generate((manifest) => { 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('rejects periods after one without duration', async () => { const periodContents = [ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ].join('\n'); const template = [ '', ' ', '%(periodContents)s', ' ', ' ', '%(periodContents)s', ' ', '', ].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 video = manifest.variants[0].video; await video.createSegmentIndex(); // The first period has a segment from 0-10. // With the second period skipping, we should fail to find a segment at 10. expect(video.segmentIndex.find(0)).not.toBe(null); expect(video.segmentIndex.find(10)).toBe(null); }); it('calculates Period times when missing', async () => { const periodContents = [ ' ', ' ', ' ', ' ', ' ', ].join('\n'); const template = [ '', ' ', '%(periodContents)s', ' ', ' ', '%(periodContents)s', ' ', ' ', '%(periodContents)s', ' ', '', ].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([ '', ' ', '', '', ' ', ' ', '', ]); 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([ '', ' ', ' ', '', '', ' ', ' ', ' ', ' ', '', ]); 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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' http://example.com/de.vtt', ' ', ' ', ' ', '', ].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 closed captions with channels and languages', async () => { const source = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); // First Representation should be dropped. const stream1 = manifest.variants[0].video; const stream2 = manifest.variants[1].video; const stream3 = manifest.variants[2].video; const expectedClosedCaptions = new Map( [['CC1', shaka.util.LanguageUtils.normalize('eng')], ['CC3', shaka.util.LanguageUtils.normalize('swe')]] ); expect(stream1.closedCaptions).toEqual(expectedClosedCaptions); expect(stream2.closedCaptions).toEqual(expectedClosedCaptions); expect(stream3.closedCaptions).toEqual(expectedClosedCaptions); }); it('Detects E-AC3 JOC content by SupplementalProperty', async () => { const idUri = 'tag:dolby.com,2018:dash:EC3_ExtensionType:2018'; const source = [ '', ' ', ' ', ' ', ' ', ' http://example.com', ' ', ' ', ' ', ' ', '', ].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.mimeType).toBe('audio/eac3-joc'); }); it('correctly parses closed captions without channel numbers', async () => { const source = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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', shaka.util.LanguageUtils.normalize('eng')], ['CC3', shaka.util.LanguageUtils.normalize('swe')]] ); expect(stream.closedCaptions).toEqual(expectedClosedCaptions); }); it('correctly parses closed captions with no channel and language info', async () => { const source = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' http://example.com', ' ', ' ', ' ', ' ', '', ].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.} lines * @return {string} */ function makeManifest(lines) { const template = [ '', ' %s', ' ', ' ', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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([ '', ]); fakeNetEngine.setResponseText('http://foo.bar/manifest', source); await runTest(25); }); it('does not produce errors', async () => { const source = makeManifest([ '', ]); fakeNetEngine.setResponseText('http://foo.bar/manifest', source); await runTest(5); }); it('tries multiple sources', async () => { const source = makeManifest([ '', '', ]); fakeNetEngine.setResponseText('http://foo.bar/manifest', source); await runTest(50); }); it('with HEAD', async () => { const source = makeManifest([ '', ]); fakeNetEngine.request.and.callFake((type, request) => { 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([ '', ]); 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([ '', ]); 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([ 'http://example.com', '', ]); 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([ '', ]); 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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = ' { const source = [ '', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', // Incorrect actuate ' ', ' ', ' ', ' ', '', ].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 = ''; 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 = [ '', ' ', ' ', ' ', '', ].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 = [ '', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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('skips unrecognized EssentialProperty elements', async () => { const manifestText = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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('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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].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 = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); // See https://bit.ly/2tx7f7A // 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.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); expect(manifest.variants.length).toBe(2); const variant1 = manifest.variants[0]; const variant2 = manifest.variants[1]; await variant1.video.createSegmentIndex(); await variant2.video.createSegmentIndex(); goog.asserts.assert(variant1.video.segmentIndex, 'Null segmentIndex!'); goog.asserts.assert(variant2.video.segmentIndex, 'Null segmentIndex!'); const variant1Ref = Array.from(variant1.video.segmentIndex)[0]; const variant2Ref = Array.from(variant2.video.segmentIndex)[0]; expect(variant1Ref.getUris()).toEqual(['dummy://foo/1.mp4']); expect(variant2Ref.getUris()).toEqual(['dummy://foo/2.mp4']); }); it('handles bandwidth of 0 or missing', async () => { // Regression test for https://github.com/google/shaka-player/issues/938 const source = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); expect(manifest.variants.length).toBe(2); const variant1 = manifest.variants[0]; expect(isNaN(variant1.bandwidth)).toBe(false); expect(variant1.bandwidth).toBeGreaterThan(0); const variant2 = manifest.variants[1]; expect(isNaN(variant2.bandwidth)).toBe(false); expect(variant2.bandwidth).toBeGreaterThan(0); }); describe('AudioChannelConfiguration', () => { /** * @param {?number} expectedNumChannels The expected number of channels * @param {!Object.} schemeMap A map where the map key is * the AudioChannelConfiguration's schemeIdUri attribute, and the map * value is the value attribute. * @return {!Promise} */ async function testAudioChannelConfiguration( expectedNumChannels, schemeMap) { const header = [ '', ' ', ' ', ' ', ].join('\n'); const configs = []; for (const scheme in schemeMap) { const value = schemeMap[scheme]; configs.push(''); } const footer = [ ' ', ' ', ' ', ' ', '', ].join('\n'); const 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.setResponseText('dummy://foo', source); /** @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.channelsCount).toBe(expectedNumChannels); } it('parses outputChannelPositionList scheme', async () => { // Parses the space-separated list and finds 8 channels. await testAudioChannelConfiguration(8, {'urn:mpeg:dash:outputChannelPositionList:2012': '2 0 1 4 5 3 17 1'}); // Does not get confused about extra spaces. await testAudioChannelConfiguration(7, {'urn:mpeg:dash:outputChannelPositionList:2012': ' 5 2 1 12 8 9 1 '}); }); it('parses 23003:3 scheme', async () => { // Parses a simple channel count. await testAudioChannelConfiguration(2, {'urn:mpeg:dash:23003:3:audio_channel_configuration:2011': '2'}); // This scheme seems to use the same format. await testAudioChannelConfiguration(6, {'urn:dts:dash:audio_channel_configuration:2012': '6'}); // Results in null if the value is not an integer. await testAudioChannelConfiguration(null, {'urn:mpeg:dash:23003:3:audio_channel_configuration:2011': 'foo'}); }); it('parses dolby scheme', async () => { // Parses a hex value in which each 1-bit is a channel. await testAudioChannelConfiguration(6, {'tag:dolby.com,2014:dash:audio_channel_configuration:2011': 'F801'}); // This scheme seems to use the same format. await testAudioChannelConfiguration(8, {'urn:dolby:dash:audio_channel_configuration:2011': '7037'}); // Results in null if the value is not a valid hex number. await testAudioChannelConfiguration(null, {'urn:dolby:dash:audio_channel_configuration:2011': 'x'}); }); it('ignores unrecognized schemes', async () => { await testAudioChannelConfiguration(null, {'foo': 'bar'}); await testAudioChannelConfiguration(2, { 'foo': 'bar', 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011': '2', }); }); }); it('does not fail on AdaptationSets without segment info', async () => { const manifestText = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); await parser.start('dummy://foo', playerInterface); }); it('exposes Representation IDs', async () => { const manifestText = [ '', ' ', ' ', ' ', ' t-en.vtt', ' ', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', ' ', ' a-en.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const variant = manifest.variants[0]; const textStream = manifest.textStreams[0]; expect(variant.audio.originalId).toBe('audio-en'); expect(variant.video.originalId).toBe('video-sd'); expect(textStream.originalId).toBe('text-en'); }); it('Disable audio does not create audio streams', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', ' ', ' a-en.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.disableAudio = true; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const variant = manifest.variants[0]; expect(variant.audio).toBe(null); expect(variant.video).toBeTruthy(); }); it('Disable video does not create video streams', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', ' ', ' a-en.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.disableVideo = true; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const variant = manifest.variants[0]; expect(variant.audio).toBeTruthy(); expect(variant.video).toBe(null); }); it('Disable text does not create text streams', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', ' ', ' a-en.mp4', ' ', ' ', ' ', ' ', ' ', ' http://example.com/de.vtt', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.disableText = true; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const stream = manifest.textStreams[0]; expect(stream).toBeUndefined(); }); it('override manifest value if ignoreMinBufferTime is true', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.ignoreMinBufferTime = true; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const minBufferTime = manifest.minBufferTime; expect(minBufferTime).toBe(0); }); it('get manifest value if ignoreMinBufferTime is false', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.ignoreMinBufferTime = false; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const minBufferTime = manifest.minBufferTime; expect(minBufferTime).toBe(75); }); it('does not set presentationDelay to NaN', async () => { // NOTE: This is a regression test for #2015. It ensures that, if // ignoreMinBufferTime is true and there is no suggestedPresentationDelay, // we do not erroneously set presentationDelay to NaN. const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.ignoreMinBufferTime = true; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const presentationTimeline = manifest.presentationTimeline; const presentationDelay = presentationTimeline.getDelay(); expect(presentationDelay).not.toBeNaN(); expect(presentationDelay).toBe(config.defaultPresentationDelay); }); it('Honors the ignoreSuggestedPresentationDelay config', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.ignoreSuggestedPresentationDelay = true; config.defaultPresentationDelay = 10; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const presentationTimeline = manifest.presentationTimeline; const presentationDelay = presentationTimeline.getDelay(); expect(presentationDelay).toBe(config.defaultPresentationDelay); }); it('Uses 1.5 times minBufferTime as default presentation delay', async () => { // When sugguestedPresentDelay should be ignored, and // config.defaultpresentdelay is not set other than 0, use 1.5*minBufferTime // as the presentationDelay. const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.ignoreSuggestedPresentationDelay = true; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const presentationTimeline = manifest.presentationTimeline; const presentationDelay = presentationTimeline.getDelay(); expect(presentationDelay).toBe(1.5*manifest.minBufferTime); }); it('Honors the ignoreEmptyAdaptationSet config', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.ignoreEmptyAdaptationSet = true; parser.configure(config); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); expect(manifest.presentationTimeline).toBeTruthy(); }); it('converts Accessibility element to "kind"', async () => { const manifestText = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' t-en.vtt', ' ', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const textStream = manifest.textStreams[0]; expect(textStream.roles).toEqual(['captions', 'foo']); expect(textStream.kind).toBe('caption'); }); it('Does not error when image adaptation sets are present', async () => { const manifestText = [ '', ' ', ' ', ' ', ' v-sd.mp4', ' ', ' ', ' ', ' ', ' ', ' a-en.mp4', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const variant = manifest.variants[0]; expect(variant.audio).toBeTruthy(); expect(variant.video).toBeTruthy(); }); // Regression #2650 in v3.0.0 // A later BaseURL was being applied to earlier Representations, specifically // in the context of SegmentTimeline. it('uses the correct BaseURL for SegmentTimeline', async () => { const manifestText = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' http://example.com/r0/', ' ', ' ', ' http://example.com/r1/', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const video0 = manifest.variants[0].video; await video0.createSegmentIndex(); goog.asserts.assert(video0.segmentIndex, 'Null segmentIndex!'); const segment0 = Array.from(video0.segmentIndex)[0]; const uri0 = segment0.getUris()[0]; const video1 = manifest.variants[1].video; await video1.createSegmentIndex(); goog.asserts.assert(video1.segmentIndex, 'Null segmentIndex!'); const segment1 = Array.from(video1.segmentIndex)[0]; const uri1 = segment1.getUris()[0]; expect(uri0).toBe('http://example.com/r0/1.mp4'); expect(uri1).toBe('http://example.com/r1/1.mp4'); }); });