/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('DashParser SegmentBase', () => { const Dash = shaka.test.Dash; const indexSegmentUri = '/base/test/test/assets/index-segment.mp4'; /** @type {!shaka.test.FakeNetworkingEngine} */ let fakeNetEngine; /** @type {!shaka.dash.DashParser} */ let parser; /** @type {shaka.extern.ManifestParser.PlayerInterface} */ let playerInterface; /** @type {!ArrayBuffer} */ let indexSegment; beforeAll(async () => { indexSegment = await shaka.test.Util.fetch(indexSegmentUri); }); beforeEach(() => { fakeNetEngine = new shaka.test.FakeNetworkingEngine(); parser = shaka.test.Dash.makeDashParser(); const config = shaka.util.PlayerConfiguration.createDefault(); playerInterface = { networkingEngine: fakeNetEngine, modifyManifestRequest: (request, manifestInfo) => {}, modifySegmentRequest: (request, segmentInfo) => {}, filter: (manifest) => Promise.resolve(), makeTextStreamsForClosedCaptions: (manifest) => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, isLowLatencyMode: () => false, updateDuration: () => {}, newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, onMetadata: () => {}, disableStream: (stream) => {}, addFont: (name, url) => {}, getStreamingRetryParameters: () => config.streaming.retryParameters, onSegmentReceived: (deltaTimeMs, numBytes) => {}, }; }); afterEach(() => { // Dash parser stop is synchronous. parser.stop(); }); it('requests init data for WebM', async () => { const source = [ '', ' http://example.com', ' ', ' ', ' ', ' media-1.webm', ' ', ' ', ' ', ' ', ' ', ' media-2.webm', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseText('http://example.com/media-1.webm', '') .setResponseText('http://example.com/media-2.webm', '') .setResponseText('http://example.com/init-1.webm', '') .setResponseText('http://example.com/init-2.webm', ''); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); // Call createSegmentIndex() on each stream to make the requests, but expect // failure from the actual parsing, since the data is bogus. const stream1 = manifest.variants[0].video; await expectAsync(stream1.createSegmentIndex()).toBeRejected(); const stream2 = manifest.variants[1].video; await expectAsync(stream2.createSegmentIndex()).toBeRejected(); expect(fakeNetEngine.request).toHaveBeenCalledTimes(5); // Expect calls to fetch part of the media and init segments of each stream. fakeNetEngine.expectRangeRequest( 'http://example.com/media-1.webm', 100, 200, /* isInit= */ false); fakeNetEngine.expectRangeRequest( 'http://example.com/init-1.webm', 201, 300, /* isInit= */ true); fakeNetEngine.expectRangeRequest( 'http://example.com/media-2.webm', 1100, 1200, /* isInit= */ false); fakeNetEngine.expectRangeRequest( 'http://example.com/init-2.webm', 1201, 1300, /* isInit= */ true); }); it('inherits from Period', async () => { const source = [ '', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue('http://example.com', indexSegment); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const segmentReference = await Dash.getFirstVideoSegmentReference(manifest); const initSegmentReference = segmentReference.initSegmentReference; expect(initSegmentReference.getUris()).toEqual( ['http://example.com/init.mp4']); expect(initSegmentReference.getStartByte()).toBe(201); expect(initSegmentReference.getEndByte()).toBe(300); expect(fakeNetEngine.request).toHaveBeenCalledTimes(2); fakeNetEngine.expectRangeRequest( 'http://example.com', 100, 200, /* isInit= */ false); }); it('inherits from AdaptationSet', async () => { const source = [ '', ' ', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue('http://example.com', indexSegment); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const segmentReference = await Dash.getFirstVideoSegmentReference(manifest); const initSegmentReference = segmentReference.initSegmentReference; expect(initSegmentReference.getUris()).toEqual( ['http://example.com/init.mp4']); expect(initSegmentReference.getStartByte()).toBe(201); expect(initSegmentReference.getEndByte()).toBe(300); expect(fakeNetEngine.request).toHaveBeenCalledTimes(2); fakeNetEngine.expectRangeRequest( 'http://example.com', 100, 200, /* isInit= */ false); }); it('does not require sourceURL in Initialization', async () => { const source = [ '', ' ', ' ', ' ', ' http://example.com/stream.mp4', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue('http://example.com/stream.mp4', indexSegment); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const segmentReference = await Dash.getFirstVideoSegmentReference(manifest); const initSegmentReference = segmentReference.initSegmentReference; expect(initSegmentReference.getUris()).toEqual( ['http://example.com/stream.mp4']); expect(initSegmentReference.getStartByte()).toBe(201); expect(initSegmentReference.getEndByte()).toBe(300); expect(fakeNetEngine.request).toHaveBeenCalledTimes(2); fakeNetEngine.expectRangeRequest( 'http://example.com/stream.mp4', 100, 200, /* isInit= */ false); }); it('merges across levels', async () => { const source = [ '', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue('http://example.com/index.mp4', indexSegment); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const segmentReference = await Dash.getFirstVideoSegmentReference(manifest); const initSegmentReference = segmentReference.initSegmentReference; expect(initSegmentReference.getUris()).toEqual( ['http://example.com/init.mp4']); expect(initSegmentReference.getStartByte()).toBe(201); expect(initSegmentReference.getEndByte()).toBe(300); expect(segmentReference.timestampOffset).toBe(-10); expect(fakeNetEngine.request).toHaveBeenCalledTimes(2); fakeNetEngine.expectRangeRequest( 'http://example.com/index.mp4', 5, 2000, /* isInit= */ false); }); it('merges and overrides across levels', async () => { const source = [ '', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue('http://example.com', indexSegment); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const segmentReference = await Dash.getFirstVideoSegmentReference(manifest); const initSegmentReference = segmentReference.initSegmentReference; expect(initSegmentReference.getUris()).toEqual( ['http://example.com/special.mp4']); expect(initSegmentReference.getStartByte()).toBe(0); expect(initSegmentReference.getEndByte()).toBe(null); expect(segmentReference.timestampOffset).toBe(-20); expect(fakeNetEngine.request).toHaveBeenCalledTimes(2); fakeNetEngine.expectRangeRequest( 'http://example.com', 30, 900, /* isInit= */ false); }); it('does not assume the same timescale as media', async () => { const source = [ '', ' ', ' ', ' ', ' http://example.com/index.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue('http://example.com/index.mp4', indexSegment); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const video = manifest.variants[0].video; await video.createSegmentIndex(); // real data, should succeed goog.asserts.assert(video.segmentIndex != null, 'Null segmentIndex!'); const reference = Array.from(video.segmentIndex)[0]; expect(reference.startTime).toBe(-2); expect(reference.endTime).toBe(10); // would be 12 without PTO }); it('fetch RepresentationIndex from init, media from BaseURL', async () => { // Matches the pattern seen in the wild: Representation points to media, // but RepresentationIndex/Initialization live in a shared init file. const source = [ '', ' ', ' ', ' ', ' https://media.example.com/video_1.mp4', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); // Minimal SIDX with one ref starting at byte 0, size 1000. const makeSidx = () => { const size = 44; const buffer = new ArrayBuffer(size); const dv = new DataView(buffer); dv.setUint32(0, size); dv.setUint8(4, 's'.charCodeAt(0)); dv.setUint8(5, 'i'.charCodeAt(0)); dv.setUint8(6, 'd'.charCodeAt(0)); dv.setUint8(7, 'x'.charCodeAt(0)); dv.setUint32(8, 0); // version/flags dv.setUint32(12, 0); // reference_ID dv.setUint32(16, 24000);// timescale dv.setUint32(20, 0); // earliestPresentationTime dv.setUint32(24, 0); // firstOffset dv.setUint16(28, 0); // reserved dv.setUint16(30, 1); // referenceCount dv.setUint32(32, 1000); // referenceSize dv.setUint32(36, 24000);// subsegmentDuration (1s) dv.setUint32(40, 0); // SAP return buffer; }; fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue( 'https://media.example.com/video_init.mp4', makeSidx()); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const video = manifest.variants[0].video; await video.createSegmentIndex(); goog.asserts.assert(video.segmentIndex != null, 'Null segmentIndex!'); expect(fakeNetEngine.request).toHaveBeenCalledTimes(2); fakeNetEngine.expectRangeRequest( 'https://media.example.com/video_init.mp4', 0, 79, /* isInit= */ false); const reference = Array.from(video.segmentIndex)[0]; expect(reference.getUris()).toEqual( ['https://media.example.com/video_1.mp4']); expect(reference.startByte).toBe(0); expect(reference.endByte).toBe(999); }); // https://github.com/shaka-project/shaka-player/issues/3230 it('works with multi-Period with eviction', async () => { const source = [ '', ' ', ' ', ' ', ' http://example.com/index.mp4', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' http://example.com/index.mp4', ' ', ' ', ' ', ' ', '', ].join('\n'); fakeNetEngine .setResponseText('dummy://foo', source) .setResponseValue('http://example.com/index.mp4', indexSegment); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); const video = manifest.variants[0].video; await video.createSegmentIndex(); // real data, should succeed goog.asserts.assert(video.segmentIndex != null, 'Null segmentIndex!'); // There are originally 5 references, but the segment that spans the Period // boundary is duplicated. In the bug, we'd stop references at the Period // boundary and only have 3 references. const references = Array.from(video.segmentIndex); expect(references.length).toBe(6); }); describe('fails for', () => { it('unsupported container', async () => { const source = [ '', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); const error = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_UNSUPPORTED_CONTAINER); await Dash.testFails(source, error); }); it('missing init segment for WebM', async () => { const source = [ '', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); const error = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_WEBM_MISSING_INIT); await Dash.testFails(source, error); }); it('no @indexRange nor RepresentationIndex', async () => { const source = [ '', ' ', ' http://example.com', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); const error = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.DASH_NO_SEGMENT_INFO); await Dash.testFails(source, error); }); }); });