diff --git a/lib/dash/mp4_segment_index_parser.js b/lib/dash/mp4_segment_index_parser.js index 880359fe8..ae7b372f1 100644 --- a/lib/dash/mp4_segment_index_parser.js +++ b/lib/dash/mp4_segment_index_parser.js @@ -26,13 +26,15 @@ shaka.dash.Mp4SegmentIndexParser = class { * @param {number} timestampOffset * @param {number} appendWindowStart * @param {number} appendWindowEnd + * @param {boolean=} indexIsExternal sidx files are external to the media * @return {!Array} */ static parse( sidxData, sidxOffset, uris, initSegmentReference, timestampOffset, - appendWindowStart, appendWindowEnd) { + appendWindowStart, appendWindowEnd, indexIsExternal) { const Mp4SegmentIndexParser = shaka.dash.Mp4SegmentIndexParser; + /** @type {!Array} */ let references; const parser = new shaka.util.Mp4Parser() @@ -44,7 +46,8 @@ shaka.dash.Mp4SegmentIndexParser = class { appendWindowStart, appendWindowEnd, uris, - box); + box, + indexIsExternal); }); if (sidxData) { @@ -74,12 +77,13 @@ shaka.dash.Mp4SegmentIndexParser = class { * @param {!Array} uris The possible locations of the MP4 file that * contains the segments. * @param {!shaka.extern.ParsedBox} box + * @param {boolean=} indexIsExternal sidx file is external to the media * @return {!Array} * @private */ static parseSIDX_( sidxOffset, initSegmentReference, timestampOffset, appendWindowStart, - appendWindowEnd, uris, box) { + appendWindowEnd, uris, box, indexIsExternal = false) { goog.asserts.assert( box.version != null, 'SIDX is a full box and should have a valid version.'); @@ -119,7 +123,11 @@ shaka.dash.Mp4SegmentIndexParser = class { // Subtract the presentation time offset let unscaledStartTime = earliestPresentationTime; - let startByte = sidxOffset + box.size + firstOffset; + // For external SIDX (index fetched from a different file), the spec + // anchors first_offset at the start of the media segment. Re-anchor by + // subtracting the box size so the initial start byte is |firstOffset|. + const anchorOffset = indexIsExternal ? -box.size : sidxOffset; + let startByte = anchorOffset + box.size + firstOffset; for (let i = 0; i < referenceCount; i++) { // |chunk| is 1 bit for |referenceType|, and 31 bits for |referenceSize|. diff --git a/lib/dash/segment_base.js b/lib/dash/segment_base.js index c3580d4cd..cbcc2f560 100644 --- a/lib/dash/segment_base.js +++ b/lib/dash/segment_base.js @@ -189,14 +189,20 @@ shaka.dash.SegmentBase = class { const appendWindowEnd = periodDuration ? periodStart + periodDuration : Infinity; + // Segment references should resolve against the Representation's base URIs + const segUris = context.representation.getBaseUris(); + // Check if the index URIs are different from the base URIs (i.e. external). + const indexIsExternal = segUris.length != uris.length || + segUris.some((uri, i) => uri != uris[i]); + if (containerType == 'mp4') { references = shaka.dash.Mp4SegmentIndexParser.parse( - indexData, startByte, uris, initSegmentReference, timestampOffset, - appendWindowStart, appendWindowEnd); + indexData, startByte, segUris, initSegmentReference, timestampOffset, + appendWindowStart, appendWindowEnd, indexIsExternal); } else { goog.asserts.assert(initData, 'WebM requires init data'); references = shaka.dash.WebmSegmentIndexParser.parse( - indexData, initData, uris, initSegmentReference, timestampOffset, + indexData, initData, segUris, initSegmentReference, timestampOffset, appendWindowStart, appendWindowEnd); } for (const ref of references) { diff --git a/test/dash/dash_parser_segment_base_unit.js b/test/dash/dash_parser_segment_base_unit.js index 116f8f189..84b83950a 100644 --- a/test/dash/dash_parser_segment_base_unit.js +++ b/test/dash/dash_parser_segment_base_unit.js @@ -316,6 +316,72 @@ describe('DashParser SegmentBase', () => { 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 = [