From e23dc04e4cab6fe1f22aca980b3d565562525ec9 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Mon, 24 Aug 2026 22:04:23 +0200 Subject: [PATCH] dash: Do not request undeclared initialization segments --- .../manifest/dash/common/indexes/base.ts | 32 +++++---- .../dash/common/indexes/get_init_segment.ts | 40 ++++++++--- .../manifest/dash/common/indexes/list.ts | 37 +++++----- .../manifest/dash/common/indexes/template.ts | 32 +++++---- .../timeline/timeline_representation_index.ts | 34 +++++----- .../DASH_static_SelfInitializing/index.js | 11 +++ .../media/manifest.mpd | 65 ++++++++++++++++++ .../media/text-list.vtt | 4 ++ .../media/text-template-1.vtt | 4 ++ .../media/text-timeline-0.vtt | 4 ++ .../DASH_static_SelfInitializing/urls.mjs | 30 ++++++++ tests/contents/static/urls.mjs | 2 + .../dash_self_initializing_segments.test.js | 68 +++++++++++++++++++ .../common/indexes/get_init_segment.test.ts | 47 +++++++++++++ 14 files changed, 340 insertions(+), 70 deletions(-) create mode 100644 tests/contents/static/DASH_static_SelfInitializing/index.js create mode 100644 tests/contents/static/DASH_static_SelfInitializing/media/manifest.mpd create mode 100644 tests/contents/static/DASH_static_SelfInitializing/media/text-list.vtt create mode 100644 tests/contents/static/DASH_static_SelfInitializing/media/text-template-1.vtt create mode 100644 tests/contents/static/DASH_static_SelfInitializing/media/text-timeline-0.vtt create mode 100644 tests/contents/static/DASH_static_SelfInitializing/urls.mjs create mode 100644 tests/integration/scenarios/dash_self_initializing_segments.test.js create mode 100644 tests/unit/src/parsers/manifest/dash/common/indexes/get_init_segment.test.ts diff --git a/src/parsers/manifest/dash/common/indexes/base.ts b/src/parsers/manifest/dash/common/indexes/base.ts index c7342d8b88..56c92a2e62 100644 --- a/src/parsers/manifest/dash/common/indexes/base.ts +++ b/src/parsers/manifest/dash/common/indexes/base.ts @@ -51,18 +51,19 @@ export interface IBaseIndex { * ``` */ indexTimeOffset: number; - /** Information on the initialization segment. */ - initialization: - | { - /** - * URL path, to add to the wanted CDN, to access the initialization segment. - * `null` if no URL exists. - */ - url: string | null; - /** possible byte range to request it. */ - range?: [number, number] | undefined; - } - | undefined; + /** + * Information on the initialization segment. + * `null` if this index has no initialization segment. + */ + initialization: { + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + * `null` if no URL exists. + */ + url: string | null; + /** possible byte range to request it. */ + range?: [number, number] | undefined; + } | null; /** * URL base to access any segment. * Can contain token to replace to convert it to real URLs. @@ -257,10 +258,11 @@ export default class BaseRepresentationIndex implements IRepresentationIndex { } /** - * Construct init Segment. - * @returns {Object} + * Construct the metadata object for the init Segment linked to that index. + * Returns `null` if no initialization segment appears to be linked to that index. + * @returns {Object|null} */ - getInitSegment(): ISegment { + getInitSegment(): ISegment | null { return getInitSegment(this._index, this._isEMSGWhitelisted); } diff --git a/src/parsers/manifest/dash/common/indexes/get_init_segment.ts b/src/parsers/manifest/dash/common/indexes/get_init_segment.ts index 8f35b6f054..fb301ac9e8 100644 --- a/src/parsers/manifest/dash/common/indexes/get_init_segment.ts +++ b/src/parsers/manifest/dash/common/indexes/get_init_segment.ts @@ -15,26 +15,50 @@ */ import type { IPrivateInfos, ISegment } from "../../../../../manifest/index.ts"; -import isNullOrUndefined from "../../../../../utils/is_null_or_undefined.ts"; import type { IEMSG } from "../../../../containers/isobmff/index.ts"; /** - * Construct init segment for the given index. + * Construct the metadata object for the init Segment linked to that index. + * Returns `null` if no initialization segment appears to be linked to that index. * @param {Object} index * @param {function} isEMSGWhitelisted - * @returns {Object} + * @returns {Object|null} */ export default function getInitSegment( index: { + /** Convert time numbers into seconds (`ticks / timescale == seconds`). */ timescale: number; - initialization?: - { url: string | null; range?: [number, number] | undefined } | undefined; + /** + * Information on the initialization segment present on the index. + * `null` if there's no such information. + */ + initialization: { url: string | null; range?: [number, number] | undefined } | null; + /** Optional range for the index segment (e.g. ISOBMFF's sidx). */ indexRange?: [number, number] | undefined; + /** + * Temporal offset, in the current timescale (see timescale), to add to the + * presentation time (time a segment has at decoding time) to obtain the + * corresponding media time (original time of the media segment in the index + * and on the media file). + * For example, to look for a segment beginning at a second `T` on a + * HTMLMediaElement, we actually will look for a segment in the index + * beginning at: + * ``` + * T * timescale + indexTimeOffset + * ``` + */ indexTimeOffset: number; }, + /** + * Callback returning `true` if the corresponding inband event is supposed to + * be listened to. + */ isEMSGWhitelisted?: (inbandEvent: IEMSG) => boolean, -): ISegment { +): ISegment | null { const { initialization } = index; + if (initialization === null) { + return null; + } const privateInfos: IPrivateInfos = {}; if (isEMSGWhitelisted !== undefined) { privateInfos.isEMSGWhitelisted = isEMSGWhitelisted; @@ -47,9 +71,9 @@ export default function getInitSegment( end: 0, duration: 0, timescale: 1, - range: !isNullOrUndefined(initialization) ? initialization.range : undefined, + range: initialization.range, indexRange: index.indexRange, - url: initialization?.url ?? null, + url: initialization.url, complete: true, privateInfos, timestampOffset: -(index.indexTimeOffset / index.timescale), diff --git a/src/parsers/manifest/dash/common/indexes/list.ts b/src/parsers/manifest/dash/common/indexes/list.ts index c4e5b900df..ad7c26df2b 100644 --- a/src/parsers/manifest/dash/common/indexes/list.ts +++ b/src/parsers/manifest/dash/common/indexes/list.ts @@ -48,18 +48,19 @@ export interface IListIndex { * ``` */ indexTimeOffset: number; - /** Information on the initialization segment. */ - initialization?: - | { - /** - * URL path, to add to the wanted CDN, to access the initialization segment. - * `null` if no URL exists. - */ - url: string | null; - /** possible byte range to request it. */ - range?: [number, number] | undefined; - } - | undefined; + /** + * Information on the initialization segment. + * `null` if this index has no initialization segment. + */ + initialization: { + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + * `null` if no URL exists. + */ + url: string | null; + /** possible byte range to request it. */ + range?: [number, number] | undefined; + } | null; /** Information on the list of segments for this index. */ list: Array<{ /** @@ -186,17 +187,21 @@ export default class ListRepresentationIndex implements IRepresentationIndex { indexTimeOffset, indexRange: index.indexRange, initialization: isNullOrUndefined(index.initialization) - ? undefined + ? null : { url: initializationUrl, range: index.initialization.range }, }; } /** - * Construct init Segment. - * @returns {Object} + * Construct the metadata object for the init Segment linked to that index. + * Returns `null` if no initialization segment appears to be linked to that index. + * @returns {Object|null} */ - getInitSegment(): ISegment { + getInitSegment(): ISegment | null { const initSegment = getInitSegment(this._index); + if (initSegment === null) { + return null; + } if (initSegment.privateInfos === undefined) { initSegment.privateInfos = {}; } diff --git a/src/parsers/manifest/dash/common/indexes/template.ts b/src/parsers/manifest/dash/common/indexes/template.ts index f35e0bce89..074103edda 100644 --- a/src/parsers/manifest/dash/common/indexes/template.ts +++ b/src/parsers/manifest/dash/common/indexes/template.ts @@ -44,17 +44,18 @@ export interface ITemplateIndex { timescale: number; /** Byte range for a possible index of segments in the server. */ indexRange?: [number, number] | undefined; - /** Information on the initialization segment. */ - initialization?: - | { - /** - * URL path, to add to the wanted CDN, to access the initialization segment. - */ - url: string | null; - /** possible byte range to request it. */ - range?: [number, number] | undefined; - } - | undefined; + /** + * Information on the initialization segment. + * `null` if this index has no initialization segment. + */ + initialization: { + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + */ + url: string | null; + /** possible byte range to request it. */ + range?: [number, number] | undefined; + } | null; /** * URL base to access any segment. * Can contain token to replace to convert it to real URLs. @@ -214,7 +215,7 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex indexRange: index.indexRange, indexTimeOffset, initialization: isNullOrUndefined(index.initialization) - ? undefined + ? null : { url: initializationUrl, range: index.initialization.range }, url: segmentUrlTemplate, presentationTimeOffset, @@ -229,10 +230,11 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex } /** - * Construct init Segment. - * @returns {Object} + * Construct the metadata object for the init Segment linked to that index. + * Returns `null` if no initialization segment appears to be linked to that index. + * @returns {Object|null} */ - getInitSegment(): ISegment { + getInitSegment(): ISegment | null { return getInitSegment(this._index, this._isEMSGWhitelisted); } diff --git a/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts b/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts index 43ebb2f1fe..79069dbc99 100644 --- a/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts +++ b/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts @@ -71,18 +71,19 @@ export interface ITimelineIndex { * ``` */ indexTimeOffset: number; - /** Information on the initialization segment. */ - initialization?: - | { - /** - * URL path, to add to the wanted CDN, to access the initialization segment. - * `null` if no URL exists. - */ - url: string | null; - /** possible byte range to request it. */ - range?: [number, number] | undefined; - } - | undefined; + /** + * Information on the initialization segment. + * `null` if this index has no initialization segment. + */ + initialization: { + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + * `null` if no URL exists. + */ + url: string | null; + /** possible byte range to request it. */ + range?: [number, number] | undefined; + } | null; /** * Template for the URL suffix (to concatenate to the wanted CDN), to access any * media segment. @@ -358,7 +359,7 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex indexRange: index.indexRange, indexTimeOffset, initialization: isNullOrUndefined(index.initialization) - ? undefined + ? null : { url: initializationUrl, range: index.initialization.range, @@ -383,10 +384,11 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex } /** - * Construct init Segment. - * @returns {Object} + * Construct the metadata object for the init Segment linked to that index. + * Returns `null` if no initialization segment appears to be linked to that index. + * @returns {Object|null} */ - getInitSegment(): ISegment { + getInitSegment(): ISegment | null { return getInitSegment(this._index, this._isEMSGWhitelisted); } diff --git a/tests/contents/static/DASH_static_SelfInitializing/index.js b/tests/contents/static/DASH_static_SelfInitializing/index.js new file mode 100644 index 0000000000..21ed2a1bf6 --- /dev/null +++ b/tests/contents/static/DASH_static_SelfInitializing/index.js @@ -0,0 +1,11 @@ +const BASE_URL = + "http://" + + __TEST_CONTENT_SERVER__.URL + + ":" + + __TEST_CONTENT_SERVER__.PORT + + "/DASH_static_SelfInitializing/media/"; + +export default { + url: BASE_URL + "manifest.mpd", + transport: "dash", +}; diff --git a/tests/contents/static/DASH_static_SelfInitializing/media/manifest.mpd b/tests/contents/static/DASH_static_SelfInitializing/media/manifest.mpd new file mode 100644 index 0000000000..4d7807e301 --- /dev/null +++ b/tests/contents/static/DASH_static_SelfInitializing/media/manifest.mpd @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/contents/static/DASH_static_SelfInitializing/media/text-list.vtt b/tests/contents/static/DASH_static_SelfInitializing/media/text-list.vtt new file mode 100644 index 0000000000..250c785166 --- /dev/null +++ b/tests/contents/static/DASH_static_SelfInitializing/media/text-list.vtt @@ -0,0 +1,4 @@ +WEBVTT + +00:00.000 --> 00:10.000 +The SegmentList was loaded. diff --git a/tests/contents/static/DASH_static_SelfInitializing/media/text-template-1.vtt b/tests/contents/static/DASH_static_SelfInitializing/media/text-template-1.vtt new file mode 100644 index 0000000000..97f1465023 --- /dev/null +++ b/tests/contents/static/DASH_static_SelfInitializing/media/text-template-1.vtt @@ -0,0 +1,4 @@ +WEBVTT + +00:00.000 --> 00:10.000 +The duration-based SegmentTemplate was loaded. diff --git a/tests/contents/static/DASH_static_SelfInitializing/media/text-timeline-0.vtt b/tests/contents/static/DASH_static_SelfInitializing/media/text-timeline-0.vtt new file mode 100644 index 0000000000..1acb983d81 --- /dev/null +++ b/tests/contents/static/DASH_static_SelfInitializing/media/text-timeline-0.vtt @@ -0,0 +1,4 @@ +WEBVTT + +00:00.000 --> 00:10.000 +The SegmentTimeline was loaded. diff --git a/tests/contents/static/DASH_static_SelfInitializing/urls.mjs b/tests/contents/static/DASH_static_SelfInitializing/urls.mjs new file mode 100644 index 0000000000..ce5ff9de36 --- /dev/null +++ b/tests/contents/static/DASH_static_SelfInitializing/urls.mjs @@ -0,0 +1,30 @@ +/* eslint-env node */ + +import * as path from "path"; +import { fileURLToPath } from "url"; + +const currentDirectory = path.dirname(fileURLToPath(import.meta.url)); +const BASE_URL = "/DASH_static_SelfInitializing/media/"; + +export default [ + { + url: BASE_URL + "manifest.mpd", + path: path.join(currentDirectory, "media", "manifest.mpd"), + contentType: "application/dash+xml", + }, + { + url: BASE_URL + "text-template-1.vtt", + path: path.join(currentDirectory, "media", "text-template-1.vtt"), + contentType: "text/vtt", + }, + { + url: BASE_URL + "text-timeline-0.vtt", + path: path.join(currentDirectory, "media", "text-timeline-0.vtt"), + contentType: "text/vtt", + }, + { + url: BASE_URL + "text-list.vtt", + path: path.join(currentDirectory, "media", "text-list.vtt"), + contentType: "text/vtt", + }, +]; diff --git a/tests/contents/static/urls.mjs b/tests/contents/static/urls.mjs index 7527fbefdf..e34cac7b88 100644 --- a/tests/contents/static/urls.mjs +++ b/tests/contents/static/urls.mjs @@ -16,6 +16,7 @@ import urls13 from "./DASH_dynamic_SegmentTemplate_UnsupportedAudio/urls.mjs"; import urls14 from "./imagetracks/urls.mjs"; import urls15 from "./DASH_static_audio_tag/urls.mjs"; import urls16 from "./DASH_static_Large_MultiPeriod/urls.mjs"; +import urls17 from "./DASH_static_SelfInitializing/urls.mjs"; export default [ ...urls1, @@ -34,4 +35,5 @@ export default [ ...urls14, ...urls15, ...urls16, + ...urls17, ]; diff --git a/tests/integration/scenarios/dash_self_initializing_segments.test.js b/tests/integration/scenarios/dash_self_initializing_segments.test.js new file mode 100644 index 0000000000..a5ff32736a --- /dev/null +++ b/tests/integration/scenarios/dash_self_initializing_segments.test.js @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import RxPlayer from "../../../dist/es2017"; +import { EMBEDDED_DASH_WASM } from "../../../dist/es2017/__GENERATED_CODE/index.js"; +import { MULTI_THREAD } from "../../../dist/es2017/experimental/features/index.js"; +import selfInitializingContent from "../../contents/static/DASH_static_SelfInitializing"; +import TestWorkerEmbed from "../../embedded_worker_bundle"; +import { checkAfterSleepWithBackoff } from "../../utils/checkAfterSleepWithBackoff.js"; +import { waitForLoadedStateAfterLoadVideo } from "../../utils/waitForPlayerState"; + +runSelfInitializingSegmentTests(); +runSelfInitializingSegmentTests({ multithread: true }); + +function runSelfInitializingSegmentTests({ multithread } = {}) { + let title = "DASH self-initializing segments"; + if (multithread === true) { + RxPlayer.addFeatures([MULTI_THREAD]); + title += " with worker"; + } + + describe(title, () => { + let player; + let textTrackElement; + + beforeEach(() => { + player = new RxPlayer(); + textTrackElement = document.createElement("div"); + document.body.appendChild(textTrackElement); + if (multithread === true) { + player.attachWorker({ + workerUrl: TestWorkerEmbed, + dashWasmUrl: EMBEDDED_DASH_WASM, + }); + } + }); + + afterEach(() => { + player.dispose(); + textTrackElement.remove(); + }); + + it("should load plain text segments without initialization", async () => { + player.loadVideo({ + url: selfInitializingContent.url, + transport: selfInitializingContent.transport, + mode: multithread === true ? "multithread" : "main", + textTrackMode: "html", + textTrackElement, + }); + await waitForLoadedStateAfterLoadVideo(player); + + const textTracks = player.getAvailableTextTracks(); + expect(textTracks).toHaveLength(3); + + await checkTextTrack("en", "The duration-based SegmentTemplate was loaded."); + await checkTextTrack("fr", "The SegmentTimeline was loaded."); + await checkTextTrack("de", "The SegmentList was loaded."); + + async function checkTextTrack(language, expectedText) { + const track = textTracks.find((textTrack) => textTrack.language === language); + expect(track).not.toBeUndefined(); + player.setTextTrack(track.id); + await checkAfterSleepWithBackoff({ maxTimeMs: 5000, stepMs: 100 }, () => { + expect(textTrackElement.textContent).toContain(expectedText); + }); + } + }); + }); +} diff --git a/tests/unit/src/parsers/manifest/dash/common/indexes/get_init_segment.test.ts b/tests/unit/src/parsers/manifest/dash/common/indexes/get_init_segment.test.ts new file mode 100644 index 0000000000..912ad43128 --- /dev/null +++ b/tests/unit/src/parsers/manifest/dash/common/indexes/get_init_segment.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import getInitSegment from "../../../../../../../../src/parsers/manifest/dash/common/indexes/get_init_segment.ts"; + +describe("getInitSegment", () => { + it("returns null when no initialization segment is declared", () => { + expect( + getInitSegment({ + timescale: 1, + initialization: null, + indexRange: [0, 10], + indexTimeOffset: 0, + }), + ).toBeNull(); + }); + + it("returns an initialization segment when its URL is declared", () => { + expect( + getInitSegment({ + timescale: 10, + initialization: { url: "init.mp4" }, + indexTimeOffset: 5, + }), + ).toMatchObject({ + id: "init", + isInit: true, + range: undefined, + url: "init.mp4", + timestampOffset: -0.5, + }); + }); + + it("keeps an initialization segment declared through a byte range", () => { + expect( + getInitSegment({ + timescale: 1, + initialization: { url: null, range: [0, 99] }, + indexRange: [100, 199], + indexTimeOffset: 0, + }), + ).toMatchObject({ + isInit: true, + range: [0, 99], + indexRange: [100, 199], + url: null, + }); + }); +});