mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
42df30a84e
This pull request improves support for external SIDX (Segment Index) files in DASH manifests, particularly when the `RepresentationIndex` uses a different `BaseURL` or `sourceURL` than the media itself. It also enhances base64 decoding robustness and adds a new unit test to verify correct behavior. **DASH SIDX and Segment Reference Handling:** * Enhanced `Mp4SegmentIndexParser.parse` to accept an `indexIsExternal` parameter, enabling correct parsing of SIDX files that are external to the media and may have different base URIs. The parser now adjusts the offset logic for external indices. [[1]](diffhunk://#diff-6435d27cfd56024b0920175aa9a6992242d18900d27f7edfaa77d89673a8dd0aR29-R37) [[2]](diffhunk://#diff-6435d27cfd56024b0920175aa9a6992242d18900d27f7edfaa77d89673a8dd0aR54-L63) * Addresses #6091: Updated `SegmentBase.generateSegmentIndexFromUris` to detect when the index URI is external by comparing the base URIs, and to pass this information to the parser. This ensures that segment references are resolved against the correct URIs. **Robustness Improvements:** * Improved base64 decoding in `Uint8ArrayUtils.fromBase64` by normalizing padding, handling cases where the input string omits trailing `=` characters. **Testing Enhancements:** * Added a unit test to verify that `RepresentationIndex` with a different `BaseURL` or `sourceURL` is correctly honored, ensuring that segment index requests use the proper URI and range. --------- Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
489 lines
19 KiB
JavaScript
489 lines
19 KiB
JavaScript
/*! @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();
|
|
|
|
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) => {},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Dash parser stop is synchronous.
|
|
parser.stop();
|
|
});
|
|
|
|
it('requests init data for WebM', async () => {
|
|
const source = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <Period>',
|
|
' <AdaptationSet mimeType="video/webm">',
|
|
' <Representation id="1" bandwidth="1" frameRate="3000/3001">',
|
|
' <BaseURL>media-1.webm</BaseURL>',
|
|
' <SegmentBase indexRange="100-200" timescale="9000">',
|
|
' <Initialization sourceURL="init-1.webm" range="201-300" />',
|
|
' </SegmentBase>',
|
|
' </Representation>',
|
|
' <Representation id="2" bandwidth="1" frameRate="1500/1501">',
|
|
' <BaseURL>media-2.webm</BaseURL>',
|
|
' <SegmentBase indexRange="1100-1200" timescale="9000">',
|
|
' <Initialization sourceURL="init-2.webm" range="1201-1300" />',
|
|
' </SegmentBase>',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <SegmentBase indexRange="100-200" timescale="9000">',
|
|
' <Initialization sourceURL="init.mp4" range="201-300" />',
|
|
' </SegmentBase>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <Representation bandwidth="1" />',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <SegmentBase indexRange="100-200" timescale="9000">',
|
|
' <Initialization sourceURL="init.mp4" range="201-300" />',
|
|
' </SegmentBase>',
|
|
' <Representation bandwidth="1" />',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <Representation bandwidth="1">',
|
|
' <BaseURL>http://example.com/stream.mp4</BaseURL>',
|
|
' <SegmentBase indexRange="100-200" timescale="9000">',
|
|
' <Initialization range="201-300" />',
|
|
' </SegmentBase>',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <SegmentBase timescale="9000">',
|
|
' <Initialization sourceURL="init.mp4" range="201-300" />',
|
|
' </SegmentBase>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <SegmentBase presentationTimeOffset="90000">',
|
|
' <Initialization sourceURL="init.mp4" range="201-300" />',
|
|
' </SegmentBase>',
|
|
' <Representation bandwidth="1">',
|
|
' <SegmentBase>',
|
|
' <RepresentationIndex sourceURL="index.mp4" range="5-2000" />',
|
|
' </SegmentBase>',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <SegmentBase indexRange="0-10" timescale="9000">',
|
|
' <Initialization sourceURL="init.mp4" range="201-300" />',
|
|
' </SegmentBase>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <SegmentBase timescale="10" presentationTimeOffset="10">',
|
|
' <Initialization sourceURL="special.mp4" />',
|
|
' </SegmentBase>',
|
|
' <Representation bandwidth="1">',
|
|
' <SegmentBase indexRange="30-900" ',
|
|
' presentationTimeOffset="200" />',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <Representation bandwidth="1">',
|
|
' <BaseURL>http://example.com/index.mp4</BaseURL>',
|
|
' <SegmentBase indexRange="30-900" ',
|
|
' timescale="1000"',
|
|
' presentationTimeOffset="2000" />',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT10S">',
|
|
' <Period>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <Representation bandwidth="1">',
|
|
' <BaseURL>https://media.example.com/video_1.mp4</BaseURL>',
|
|
' <SegmentBase timescale="24000">',
|
|
' <Initialization range="80-751" sourceURL="video_init.mp4"/>',
|
|
' <RepresentationIndex range="0-79"' +
|
|
' sourceURL="video_init.mp4"/>',
|
|
' </SegmentBase>',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period duration="PT30S">',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <Representation bandwidth="1">',
|
|
' <BaseURL>http://example.com/index.mp4</BaseURL>',
|
|
' <SegmentBase indexRange="30-900" />',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
' <Period>',
|
|
' <AdaptationSet mimeType="video/mp4">',
|
|
' <Representation bandwidth="1">',
|
|
' <BaseURL>http://example.com/index.mp4</BaseURL>',
|
|
' <SegmentBase indexRange="30-900" presentationTimeOffset="30" />',
|
|
' </Representation>',
|
|
' </AdaptationSet>',
|
|
' </Period>',
|
|
'</MPD>',
|
|
].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 = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <AdaptationSet mimeType="video/cat">',
|
|
' <Representation bandwidth="1">',
|
|
' <SegmentBase indexRange="30-900" />',
|
|
' </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_CONTAINER);
|
|
await Dash.testFails(source, error);
|
|
});
|
|
|
|
it('missing init segment for WebM', async () => {
|
|
const source = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <AdaptationSet mimeType="video/webm">',
|
|
' <Representation bandwidth="1">',
|
|
' <SegmentBase indexRange="30-900" />',
|
|
' </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_WEBM_MISSING_INIT);
|
|
await Dash.testFails(source, error);
|
|
});
|
|
|
|
it('no @indexRange nor RepresentationIndex', async () => {
|
|
const source = [
|
|
'<MPD mediaPresentationDuration="PT75S">',
|
|
' <Period>',
|
|
' <BaseURL>http://example.com</BaseURL>',
|
|
' <AdaptationSet mimeType="video/webm">',
|
|
' <Representation bandwidth="1">',
|
|
' <SegmentBase>',
|
|
' <Initialization sourceURL="test.webm" />',
|
|
' </SegmentBase>',
|
|
' </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_NO_SEGMENT_INFO);
|
|
await Dash.testFails(source, error);
|
|
});
|
|
});
|
|
});
|