diff --git a/src/core/fetchers/manifest/manifest_fetcher.ts b/src/core/fetchers/manifest/manifest_fetcher.ts index 1ad0cbfea6..962709cae1 100644 --- a/src/core/fetchers/manifest/manifest_fetcher.ts +++ b/src/core/fetchers/manifest/manifest_fetcher.ts @@ -625,7 +625,7 @@ export default class ManifestFetcher extends EventEmitter // updates (e.g. redirects or manifest-provided alternatives) to stay the // source of truth after the refresh completed. fullRefresh = !enablePartialRefresh || manifestUpdateUrl === undefined; - refreshURL = fullRefresh ? manifest.getUrls()[0] : manifestUpdateUrl; + refreshURL = fullRefresh ? manifest.getRefreshUrls()[0] : manifestUpdateUrl; } const externalClockOffset = manifest.clockOffset; diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index 23fd021a9c..b3bf35f264 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -1749,7 +1749,7 @@ class Player extends EventEmitter { return originalUrl === undefined ? undefined : [originalUrl]; } if (manifest !== null) { - return manifest.uris; + return manifest.refreshUrls; } return undefined; } diff --git a/src/manifest/classes/manifest.ts b/src/manifest/classes/manifest.ts index 32cf5d6016..f414787b30 100644 --- a/src/manifest/classes/manifest.ts +++ b/src/manifest/classes/manifest.ts @@ -172,7 +172,7 @@ export default class Manifest * They can be used for refreshing the Manifest. * Listed from the most important to the least important. */ - public uris: string[]; + public refreshUrls: string[]; /** Optional URL that points to a shorter version of the Manifest used * for updates only. */ @@ -352,7 +352,7 @@ export default class Manifest this.isDynamic = parsedManifest.isDynamic; this.isLive = parsedManifest.isLive; this.isLastPeriodKnown = parsedManifest.isLastPeriodKnown; - this.uris = parsedManifest.uris === undefined ? [] : parsedManifest.uris; + this.refreshUrls = parsedManifest.refreshUrls.map((r) => r.baseUrl); this.updateUrl = manifestUpdateUrl; this.lifetime = parsedManifest.lifetime; @@ -447,8 +447,8 @@ export default class Manifest * `undefined` if no URL is found. * @returns {Array.} */ - public getUrls(): string[] { - return this.uris; + public getRefreshUrls(): string[] { + return this.refreshUrls; } /** @@ -650,7 +650,7 @@ export default class Manifest isLastPeriodKnown: this.isLastPeriodKnown, suggestedPresentationDelay: this.suggestedPresentationDelay, clockOffset: this.clockOffset, - uris: this.uris, + refreshUrls: this.refreshUrls, availabilityStartTime: this.availabilityStartTime, timeBounds: this.timeBounds, }; @@ -689,11 +689,11 @@ export default class Manifest let updatedPeriodsResult; if (updateType === MANIFEST_UPDATE_TYPE.Full) { this.timeBounds = newManifest.timeBounds; - this.uris = newManifest.uris; + this.refreshUrls = newManifest.refreshUrls; updatedPeriodsResult = replacePeriods(this.periods, newManifest.periods); } else { this.timeBounds.maximumTimeData = newManifest.timeBounds.maximumTimeData; - this.updateUrl = newManifest.uris[0]; + this.updateUrl = newManifest.refreshUrls[0]; updatedPeriodsResult = updatePeriods(this.periods, newManifest.periods); // Partial updates do not remove old Periods. diff --git a/src/manifest/types.ts b/src/manifest/types.ts index 4793d681d0..c1b977f3d4 100644 --- a/src/manifest/types.ts +++ b/src/manifest/types.ts @@ -91,12 +91,8 @@ export interface IManifestMetadata { */ isLastPeriodKnown: boolean; - /* - * Every URI linking to that Manifest. - * They can be used for refreshing the Manifest. - * Listed from the most important to the least important. - */ - uris: string[]; + /** URLs through which the Manifest can be refreshed, by order of importance. */ + refreshUrls: string[]; /** * Minimum time, in seconds, at which a segment defined in the Manifest diff --git a/src/parsers/manifest/dash/common/parse_mpd.ts b/src/parsers/manifest/dash/common/parse_mpd.ts index 7b7763a8d6..19d717cea6 100644 --- a/src/parsers/manifest/dash/common/parse_mpd.ts +++ b/src/parsers/manifest/dash/common/parse_mpd.ts @@ -20,7 +20,11 @@ import type { IManifest } from "../../../../manifest/index.ts"; import arrayFind from "../../../../utils/array_find.ts"; import isNullOrUndefined from "../../../../utils/is_null_or_undefined.ts"; import getMonotonicTimeStamp from "../../../../utils/monotonic_timestamp.ts"; -import { getFilenameIndexInUrl } from "../../../../utils/url-utils.ts"; +import { + getFilenameIndexInUrl, + isAbsoluteURL, + resolveURL, +} from "../../../../utils/url-utils.ts"; import type { IParsedManifest } from "../../types.ts"; import type { IMPDIntermediateRepresentation, @@ -418,9 +422,33 @@ function parseCompleteIntermediateRepresentation( (parsedPeriods[parsedPeriods.length - 1]?.end !== undefined || mpdIR.attributes.mediaPresentationDuration !== undefined)); + const refreshUrls = []; + const manifestUrl = + args.url !== undefined && args.url.length > 0 ? args.url : undefined; + for (const location of rootChildren.Location) { + if (manifestUrl === undefined && !isAbsoluteURL(location.value)) { + warnings.push( + new Error( + `DASH Parser: Cannot resolve relative URL without a manifest URL: "${location.value}"`, + ), + ); + continue; + } + refreshUrls.push({ + baseUrl: + manifestUrl === undefined + ? location.value + : resolveURL(manifestUrl, location.value), + id: location.attributes.serviceLocation, + }); + } + if (refreshUrls.length === 0 && manifestUrl !== undefined) { + refreshUrls.push({ baseUrl: manifestUrl, id: undefined }); + } const parsedMPD: IParsedManifest = { availabilityStartTime, clockOffset: args.externalClockOffset, + refreshUrls, isDynamic, isLive: isDynamic, isLastPeriodKnown, @@ -434,9 +462,6 @@ function parseCompleteIntermediateRepresentation( maximumTimeData, }, lifetime, - uris: isNullOrUndefined(args.url) - ? rootChildren.Location.map((l) => l.value) - : [args.url, ...rootChildren.Location.map((l) => l.value)], }; return { type: "done", value: { parsed: parsedMPD, warnings } }; diff --git a/src/parsers/manifest/dash/common/resolve_base_urls.ts b/src/parsers/manifest/dash/common/resolve_base_urls.ts index 3c647a89af..f815afc880 100644 --- a/src/parsers/manifest/dash/common/resolve_base_urls.ts +++ b/src/parsers/manifest/dash/common/resolve_base_urls.ts @@ -36,7 +36,7 @@ export default function resolveBaseURLs( } const newBaseUrls: IResolvedBaseUrl[] = newBaseUrlsIR.map((ir) => { - return { url: ir.value }; + return { url: ir.value, serviceLocation: ir.attributes.serviceLocation }; }); if (currentBaseURLs.length === 0) { return newBaseUrls; diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts b/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts index 304796d976..140145e4da 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import isNullOrUndefined from "../../../../../utils/is_null_or_undefined.ts"; import type { ITNode } from "../../../../../utils/xml-parser.ts"; import type { IBaseUrlIntermediateRepresentation } from "../../node_parser_types.ts"; import { textContent } from "./utils.ts"; @@ -25,12 +26,20 @@ import { textContent } from "./utils.ts"; * @returns {Array.} */ export default function parseBaseURL( - root: ITNode | string, + root: ITNode, ): [IBaseUrlIntermediateRepresentation | undefined, Error[]] { + const attributes: { serviceLocation?: string } = {}; const value = typeof root === "string" ? root : textContent(root.children); const warnings: Error[] = []; - if (value === null || value.length === 0) { - return [undefined, warnings]; + + for (const attributeName of Object.keys(root.attributes)) { + const attributeVal = root.attributes[attributeName]; + if (isNullOrUndefined(attributeVal)) { + continue; + } + if (attributeName === "serviceLocation") { + attributes.serviceLocation = attributeVal; + } } - return [{ value }, warnings]; + return [{ value, attributes }, warnings]; } diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts b/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts index 5aa127ba08..e1c0421d0c 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts @@ -21,6 +21,7 @@ import type { IMPDAttributes, IMPDChildren, IMPDIntermediateRepresentation, + ILocationIntermediateRepresentation, } from "../../node_parser_types.ts"; import parseBaseURL from "./BaseURL.ts"; import parseContentProtection from "./ContentProtection.ts"; @@ -66,9 +67,10 @@ function parseMPDChildren( break; } - case "Location": - ret.Location.push({ value: textContent(currentNode.children) }); + case "Location": { + ret.Location.push(parseLocation(currentNode)); break; + } case "Period": { const [period, periodWarnings] = createPeriodIntermediateRepresentation( @@ -204,6 +206,18 @@ function parseMPDAttributes(root: ITNode): [IMPDAttributes, Error[]] { return [res, warnings]; } +/** Parse a root-level `` element. */ +export default function parseLocation(root: ITNode): ILocationIntermediateRepresentation { + const location: ILocationIntermediateRepresentation = { + value: textContent(root.children), + attributes: {}, + }; + if (typeof root.attributes.serviceLocation === "string") { + location.attributes.serviceLocation = root.attributes.serviceLocation; + } + return location; +} + /** * @param {Object} root * @param {string} fullMpd diff --git a/src/parsers/manifest/dash/node_parser_types.ts b/src/parsers/manifest/dash/node_parser_types.ts index c3e3448ee9..1771220063 100644 --- a/src/parsers/manifest/dash/node_parser_types.ts +++ b/src/parsers/manifest/dash/node_parser_types.ts @@ -50,7 +50,7 @@ export interface IMPDChildren { * node, * from the first encountered to the last encountered. */ - Location: Array<{ value: string }>; + Location: ILocationIntermediateRepresentation[]; /** * Temporal subdivisions in that Manifest. * @@ -410,6 +410,27 @@ export interface IBaseUrlIntermediateRepresentation { * This is the inner content of a BaseURL node. */ value: string; + + /** Attributes assiociated to the BaseURL node. */ + attributes: { + /** + * Potential value for a `serviceLocation` attribute, used in content + * steering mechanisms. + */ + serviceLocation?: string; + }; +} + +/** Intermediate representation for a Location node. */ +export interface ILocationIntermediateRepresentation { + /** The URL contained in the Location node. */ + value: string; + + /** Attributes associated to the Location node. */ + attributes: { + /** Value of the `serviceLocation` attribute. */ + serviceLocation?: string | undefined; + }; } /** Intermediate representation for a Node following a "scheme" format. */ diff --git a/src/parsers/manifest/dash/wasm-parser/rs/events.rs b/src/parsers/manifest/dash/wasm-parser/rs/events.rs index c5ae711c94..acea163773 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/events.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/events.rs @@ -92,6 +92,9 @@ pub enum TagName { /// Indicate an node Initialization = 22, + + /// Indicate a `` node. + Location = 23, } #[derive(PartialEq, Clone, Copy)] @@ -232,8 +235,6 @@ pub enum AttributeName { Text = 64, QualityRanking = 65, - Location = 66, - InitializationMedia = 67, /// Describes an encountered "mediaPresentationDuration" attribute, as found diff --git a/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs b/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs index 5ea5cf67ca..61db2e9ba1 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs @@ -284,6 +284,18 @@ pub fn report_segment_url_attrs(tag_bs: &quick_xml::events::BytesStart) { } } +pub fn report_location_attrs(tag_bs: &quick_xml::events::BytesStart) { + for res_attr in tag_bs.attributes() { + match res_attr { + Ok(attr) => match attr.key.as_ref() { + b"serviceLocation" => ServiceLocation.try_report_as_string(&attr), + _ => {} + }, + Err(err) => ParsingError::from(err).report_err(), + }; + } +} + pub fn report_event_stream_attrs(tag_bs: &quick_xml::events::BytesStart) { for res_attr in tag_bs.attributes() { match res_attr { diff --git a/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs b/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs index 68c58c8e5e..167c456e22 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs @@ -116,7 +116,11 @@ impl MPDProcessor { self.process_base_url_element(); } b"cenc:pssh" => self.process_cenc_element(), - b"Location" => self.process_location_element(), + b"Location" => { + TagName::Location.report_tag_open(); + attributes::report_location_attrs(&tag); + self.process_location_element(); + } b"Label" => { TagName::Label.report_tag_open(); self.process_label_element(); @@ -243,7 +247,7 @@ impl MPDProcessor { Ok(Event::Text(t)) => { if t.len() > 0 { match t.unescape() { - Ok(unescaped) => AttributeName::Location.report(unescaped), + Ok(unescaped) => AttributeName::Text.report(unescaped), Err(err) => ParsingError::from(err).report_err(), } } @@ -253,6 +257,7 @@ impl MPDProcessor { if inner_tag > 0 { inner_tag -= 1; } else { + TagName::Location.report_tag_close(); break; } } diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts index 767fcd1294..6dad5874fb 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts @@ -31,8 +31,20 @@ export function generateBaseUrlAttrParser( ): IAttributeParser { const textDecoder = new TextDecoder(); return function onMPDAttribute(attr: AttributeName, ptr: number, len: number) { - if (attr === AttributeName.Text) { - baseUrlAttrs.value = parseString(textDecoder, linearMemory.buffer, ptr, len); + switch (attr) { + case AttributeName.Text: + baseUrlAttrs.value = parseString(textDecoder, linearMemory.buffer, ptr, len); + break; + + case AttributeName.ServiceLocation: { + baseUrlAttrs.attributes.serviceLocation = parseString( + textDecoder, + linearMemory.buffer, + ptr, + len, + ); + break; + } } }; } diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/Location.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/Location.ts new file mode 100644 index 0000000000..58980b1f53 --- /dev/null +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/Location.ts @@ -0,0 +1,27 @@ +import type { ILocationIntermediateRepresentation } from "../../../node_parser_types.ts"; +import type { IAttributeParser } from "../parsers_stack.ts"; +import { AttributeName } from "../types.ts"; +import { parseString } from "../utils.ts"; + +/** Generate the attribute parser for a root-level `` element. */ +export function generateLocationAttrParser( + location: ILocationIntermediateRepresentation, + linearMemory: WebAssembly.Memory, +): IAttributeParser { + const textDecoder = new TextDecoder(); + return (attribute, ptr, len) => { + switch (attribute) { + case AttributeName.Text: + location.value = parseString(textDecoder, linearMemory.buffer, ptr, len); + break; + case AttributeName.ServiceLocation: + location.attributes.serviceLocation = parseString( + textDecoder, + linearMemory.buffer, + ptr, + len, + ); + break; + } + }; +} diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts index 7d570d56fb..aa3822fd71 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts @@ -17,6 +17,7 @@ import noop from "../../../../../../utils/noop.ts"; import type { IContentProtectionIntermediateRepresentation, + ILocationIntermediateRepresentation, IMPDAttributes, IMPDChildren, IPeriodIntermediateRepresentation, @@ -27,6 +28,7 @@ import { AttributeName, TagName } from "../types.ts"; import { parseString } from "../utils.ts"; import { generateBaseUrlAttrParser } from "./BaseURL.ts"; import { generateContentProtectionAttrParser } from "./ContentProtection.ts"; +import { generateLocationAttrParser } from "./Location.ts"; import { generatePeriodAttrParser, generatePeriodChildrenParser } from "./Period.ts"; import { generateSchemeAttrParser } from "./Scheme.ts"; @@ -56,6 +58,20 @@ export function generateMPDChildrenParser( break; } + case TagName.Location: { + const location: ILocationIntermediateRepresentation = { + value: "", + attributes: {}, + }; + mpdChildren.Location.push(location); + parsersStack.pushParsers( + nodeId, + noop, + generateLocationAttrParser(location, linearMemory), + ); + break; + } + case TagName.Period: { const period: IPeriodIntermediateRepresentation = { children: { @@ -116,7 +132,6 @@ export function generateMPDChildrenParser( } export function generateMPDAttrParser( - mpdChildren: IMPDChildren, mpdAttrs: IMPDAttributes, linearMemory: WebAssembly.Memory, ): IAttributeParser { @@ -176,11 +191,7 @@ export function generateMPDAttrParser( dataView = new DataView(linearMemory.buffer); mpdAttrs.maxSubsegmentDuration = dataView.getFloat64(ptr, true); break; - case AttributeName.Location: { - const location = parseString(textDecoder, linearMemory.buffer, ptr, len); - mpdChildren.Location.push({ value: location }); - break; - } + case AttributeName.Namespace: { const xmlNs = { key: "", value: "" }; dataView = new DataView(linearMemory.buffer); diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/root.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/root.ts index 6251efe706..e9498e9bed 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/root.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/root.ts @@ -53,7 +53,6 @@ export function generateRootChildrenParser( fullMpd, ); const attributeParser = generateMPDAttrParser( - rootObj.mpd.children, rootObj.mpd.attributes, linearMemory, ); diff --git a/src/parsers/manifest/dash/wasm-parser/ts/types.ts b/src/parsers/manifest/dash/wasm-parser/ts/types.ts index 4362ca3af8..fab2a106e3 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/types.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/types.ts @@ -114,6 +114,9 @@ export const enum TagName { /// Indicate an node Initialization = 22, + + /// Indicate a `` node. + Location = 23, } /** @@ -238,8 +241,6 @@ export const enum AttributeName { Text = 64, QualityRanking = 65, - Location = 66, - InitializationMedia = 67, /// Describes an encountered "mediaPresentationDuration" attribute, as found diff --git a/src/parsers/manifest/local/parse_local_manifest.ts b/src/parsers/manifest/local/parse_local_manifest.ts index 55fa97a8e0..b7214cb6db 100644 --- a/src/parsers/manifest/local/parse_local_manifest.ts +++ b/src/parsers/manifest/local/parse_local_manifest.ts @@ -62,7 +62,7 @@ export default function parseLocalManifest( isDynamic: !isFinished, isLastPeriodKnown: true, isLive: false, - uris: [], + refreshUrls: [], timeBounds: { minimumSafePosition: minimumPosition ?? 0, timeshiftDepth: null, diff --git a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts index 34625d26c1..e121a74148 100644 --- a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts +++ b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts @@ -345,7 +345,7 @@ function createManifest( isLive: isDynamic, isDynamic, isLastPeriodKnown, - uris: isNullOrUndefined(url) ? [] : [url], + refreshUrls: isNullOrUndefined(url) ? [] : [{ baseUrl: url }], // TODO more precize time bounds? timeBounds: { diff --git a/src/parsers/manifest/smooth/create_parser.ts b/src/parsers/manifest/smooth/create_parser.ts index 4a7aecda2a..491e2010f2 100644 --- a/src/parsers/manifest/smooth/create_parser.ts +++ b/src/parsers/manifest/smooth/create_parser.ts @@ -694,7 +694,7 @@ function createSmoothStreamingParser( ], suggestedPresentationDelay, transportType: "smooth", - uris: isNullOrUndefined(url) ? [] : [url], + refreshUrls: isNullOrUndefined(url) ? [] : [{ baseUrl: url }], }; checkManifestIDs(manifest); return manifest; diff --git a/src/parsers/manifest/types.ts b/src/parsers/manifest/types.ts index 8f2aebd6c1..cf8e0d25bb 100644 --- a/src/parsers/manifest/types.ts +++ b/src/parsers/manifest/types.ts @@ -454,6 +454,9 @@ export interface IParsedManifest { * default. */ suggestedPresentationDelay?: number | undefined; - /** URIs where the manifest can be refreshed by order of importance. */ - uris?: string[] | undefined; + /** + * URLs through which the Manifest can be refreshed, by order of importance. + * Here only the `baseUrl` allows to construct the URL. + */ + refreshUrls: ICdnMetadata[]; } diff --git a/src/utils/url-utils.ts b/src/utils/url-utils.ts index f1aa7c3562..83a0b7b2df 100644 --- a/src/utils/url-utils.ts +++ b/src/utils/url-utils.ts @@ -275,6 +275,11 @@ function formatURL(parts: IParsedURL): string { return url; } +/** Returns `true` when the given URL contains an RFC 3986 scheme. */ +function isAbsoluteURL(url: string): boolean { + return parseURL(url).scheme.length > 0; +} + /** * Removes "." and ".." from the URL path, as described by the algorithm * in RFC 3986 Section 5.2.4. Remove Dot Segments @@ -356,4 +361,4 @@ function resolveURL(...args: Array): string { } } -export { getFilenameIndexInUrl, getRelativeUrl, resolveURL }; +export { getFilenameIndexInUrl, getRelativeUrl, isAbsoluteURL, resolveURL }; diff --git a/tests/unit/mocks/manifest.ts b/tests/unit/mocks/manifest.ts index 86d98ccce5..b283d83618 100644 --- a/tests/unit/mocks/manifest.ts +++ b/tests/unit/mocks/manifest.ts @@ -27,7 +27,7 @@ export const DummyManifest = makeMockedClass( getPeriodForTime: notImplemented("getPeriodForTime"), getNextPeriod: notImplemented("getNextPeriod"), getPeriodAfter: notImplemented("getPeriodAfter"), - getUrls: notImplemented("getUrls"), + getRefreshUrls: notImplemented("getRefreshUrls"), replace: notImplemented("replace"), update: notImplemented("update"), getMinimumSafePosition: notImplemented("getMinimumSafePosition"), @@ -54,7 +54,7 @@ export const DummyManifest = makeMockedClass( isDynamic: false, isLive: false, isLastPeriodKnown: true, - uris: [], + refreshUrls: [], updateUrl: undefined, suggestedPresentationDelay: undefined, lifetime: undefined, diff --git a/tests/unit/src/core/fetchers/manifest/manifest_fetcher.test.ts b/tests/unit/src/core/fetchers/manifest/manifest_fetcher.test.ts index 1003490418..2231977525 100644 --- a/tests/unit/src/core/fetchers/manifest/manifest_fetcher.test.ts +++ b/tests/unit/src/core/fetchers/manifest/manifest_fetcher.test.ts @@ -26,7 +26,7 @@ const { updateUrl: string | undefined = undefined; replace = vi.fn(); update = vi.fn(); - getUrls = vi.fn(() => ["http://example.com/manifest"]); + getRefreshUrls = vi.fn(() => ["http://example.com/manifest"]); } return { mockConfigGetCurrent: vi.fn(), diff --git a/tests/unit/src/main_thread/init/utils/update_manifest_codec_support.test.ts b/tests/unit/src/main_thread/init/utils/update_manifest_codec_support.test.ts index 847a64c731..7d232e6f4c 100644 --- a/tests/unit/src/main_thread/init/utils/update_manifest_codec_support.test.ts +++ b/tests/unit/src/main_thread/init/utils/update_manifest_codec_support.test.ts @@ -83,7 +83,7 @@ function generateFakeManifestWithRepresentations( availabilityStartTime: 0, isLastPeriodKnown: true, manifestFormat: ManifestMetadataFormat.MetadataObject, - uris: [], + refreshUrls: [], }; return manifest; diff --git a/tests/unit/src/manifest/classes/manifest.test.ts b/tests/unit/src/manifest/classes/manifest.test.ts index 8fa9abca87..333ffd3614 100644 --- a/tests/unit/src/manifest/classes/manifest.test.ts +++ b/tests/unit/src/manifest/classes/manifest.test.ts @@ -107,6 +107,7 @@ describe("Manifest - Manifest", () => { }, }, periods: [], + refreshUrls: [], }; const manifest = new Manifest(simpleFakeManifest, {}); @@ -121,7 +122,7 @@ describe("Manifest - Manifest", () => { expect(manifest.getMinimumSafePosition()).toEqual(0); expect(manifest.periods).toEqual([]); expect(manifest.suggestedPresentationDelay).toEqual(undefined); - expect(manifest.uris).toEqual([]); + expect(manifest.refreshUrls).toEqual([]); expect(mocks.fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(mocks.fakeLogger.info).not.toHaveBeenCalled(); @@ -149,6 +150,7 @@ describe("Manifest - Manifest", () => { }, }, periods: [period1, period2], + refreshUrls: [], }; mocks.fakePeriod.mockImplementation(function (period: IPeriod) { @@ -200,6 +202,7 @@ describe("Manifest - Manifest", () => { }, }, periods: [period1, period2], + refreshUrls: [], }; const representationFilter = function () { @@ -254,6 +257,7 @@ describe("Manifest - Manifest", () => { }, }, periods: [period1, period2], + refreshUrls: [], }; mocks.fakePeriod.mockImplementation(function (period: IParsedPeriod): IPeriod { @@ -309,7 +313,7 @@ describe("Manifest - Manifest", () => { }, }, suggestedPresentationDelay: 99, - uris: ["url1", "url2"], + refreshUrls: [{ baseUrl: "url1" }, { baseUrl: "url2" }], }; mocks.fakePeriod.mockImplementation(function (period: IParsedPeriod): IPeriod { @@ -343,13 +347,13 @@ describe("Manifest - Manifest", () => { }, ]); expect(manifest.suggestedPresentationDelay).toEqual(99); - expect(manifest.uris).toEqual(["url1", "url2"]); + expect(manifest.refreshUrls).toEqual(["url1", "url2"]); expect(mocks.fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(mocks.fakeLogger.info).not.toHaveBeenCalled(); expect(mocks.fakeLogger.warn).not.toHaveBeenCalled(); }); - it("should return all URLs given with `getContentUrls`", async () => { + it("should return all URLs given with `getRefreshUrls`", async () => { mocks.fakePeriod.mockImplementation(function (period: IParsedPeriod): IPeriod { return { ...period, id: `foo${period.id}` } as unknown as IPeriod; }); @@ -377,11 +381,11 @@ describe("Manifest - Manifest", () => { }, periods: [oldPeriod1, oldPeriod2], suggestedPresentationDelay: 99, - uris: ["url1", "url2"], + refreshUrls: [{ baseUrl: "url1" }, { baseUrl: "url2" }], }; const manifest1 = new Manifest(oldManifestArgs1, {}); - expect(manifest1.getUrls()).toEqual(["url1", "url2"]); + expect(manifest1.getRefreshUrls()).toEqual(["url1", "url2"]); const oldManifestArgs2 = { availabilityStartTime: 5, @@ -407,10 +411,10 @@ describe("Manifest - Manifest", () => { time: 10, }, }, - uris: [], + refreshUrls: [], }; const manifest2 = new Manifest(oldManifestArgs2, {}); - expect(manifest2.getUrls()).toEqual([]); + expect(manifest2.getRefreshUrls()).toEqual([]); }); it("should replace with a new Manifest when calling `replace`", async () => { @@ -446,7 +450,7 @@ describe("Manifest - Manifest", () => { }, }, suggestedPresentationDelay: 99, - uris: ["url1", "url2"], + refreshUrls: [{ baseUrl: "url1" }, { baseUrl: "url2" }], }; const manifest = new Manifest(oldManifestArgs, {}); @@ -490,7 +494,7 @@ describe("Manifest - Manifest", () => { }, }, periods: [newPeriod1, newPeriod2], - uris: ["url3", "url4"], + refreshUrls: [{ baseUrl: "url3" }, { baseUrl: "url4" }], } as unknown as Manifest; manifest.replace(newManifest); diff --git a/tests/unit/src/parsers/manifest/dash/common/resolve_base_urls.test.ts b/tests/unit/src/parsers/manifest/dash/common/resolve_base_urls.test.ts new file mode 100644 index 0000000000..c5f5660982 --- /dev/null +++ b/tests/unit/src/parsers/manifest/dash/common/resolve_base_urls.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import resolveBaseURLs from "../../../../../../../src/parsers/manifest/dash/common/resolve_base_urls.ts"; + +describe("resolveBaseURLs", () => { + it("resolves an empty BaseURL to its parent and preserves its serviceLocation", () => { + expect( + resolveBaseURLs( + [{ url: "https://example.com/path/", serviceLocation: "parent-cdn" }], + [{ value: "", attributes: { serviceLocation: "child-cdn" } }], + ), + ).toEqual([{ url: "https://example.com/path/", serviceLocation: "child-cdn" }]); + }); + + it("keeps the serviceLocation of newly encountered BaseURLs", () => { + expect( + resolveBaseURLs( + [], + [{ value: "https://cdn.example/", attributes: { serviceLocation: "cdn-a" } }], + ), + ).toEqual([{ url: "https://cdn.example/", serviceLocation: "cdn-a" }]); + }); + + it("inherits the parent serviceLocation when the child has none", () => { + expect( + resolveBaseURLs( + [{ url: "https://example.com/path/", serviceLocation: "parent-cdn" }], + [{ value: "video/", attributes: {} }], + ), + ).toEqual([ + { url: "https://example.com/path/video/", serviceLocation: "parent-cdn" }, + ]); + }); + + it("gives precedence to the child serviceLocation", () => { + expect( + resolveBaseURLs( + [{ url: "https://example.com/path/", serviceLocation: "parent-cdn" }], + [{ value: "video/", attributes: { serviceLocation: "child-cdn" } }], + ), + ).toEqual([{ url: "https://example.com/path/video/", serviceLocation: "child-cdn" }]); + }); +}); diff --git a/tests/unit/src/parsers/manifest/dash/js-parser/node_parsers/AdaptationSet.test.ts b/tests/unit/src/parsers/manifest/dash/js-parser/node_parsers/AdaptationSet.test.ts index aabc93f2d4..5a8a44cf8b 100644 --- a/tests/unit/src/parsers/manifest/dash/js-parser/node_parsers/AdaptationSet.test.ts +++ b/tests/unit/src/parsers/manifest/dash/js-parser/node_parsers/AdaptationSet.test.ts @@ -1050,7 +1050,7 @@ describe("DASH Node Parsers - AdaptationSet", () => { attributes: {}, children: { Accessibility: [], - BaseURL: [], + BaseURL: [{ value: "", attributes: {} }], Representation: [], ContentComponent: [], ContentProtection: [], @@ -1068,14 +1068,14 @@ describe("DASH Node Parsers - AdaptationSet", () => { ]); const element2 = parseXml( - "", + "", )[0] as ITNode; expect(createAdaptationSetIntermediateRepresentation(element2)).toEqual([ { attributes: {}, children: { Accessibility: [], - BaseURL: [], + BaseURL: [{ value: "", attributes: {} }], Representation: [], ContentComponent: [], ContentProtection: [], @@ -1102,7 +1102,7 @@ describe("DASH Node Parsers - AdaptationSet", () => { attributes: {}, children: { Accessibility: [], - BaseURL: [{ value: "a" }], + BaseURL: [{ value: "a", attributes: { serviceLocation: "foo" } }], ContentComponent: [], ContentProtection: [], EssentialProperty: [], @@ -1127,7 +1127,7 @@ describe("DASH Node Parsers - AdaptationSet", () => { attributes: {}, children: { Accessibility: [], - BaseURL: [{ value: "foo bar" }], + BaseURL: [{ value: "foo bar", attributes: { serviceLocation: "4" } }], Representation: [], ContentComponent: [], ContentProtection: [], @@ -1154,7 +1154,10 @@ describe("DASH Node Parsers - AdaptationSet", () => { attributes: {}, children: { Accessibility: [], - BaseURL: [{ value: "a" }, { value: "b" }], + BaseURL: [ + { value: "a", attributes: { serviceLocation: "" } }, + { value: "b", attributes: { serviceLocation: "http://test.com" } }, + ], Representation: [], ContentComponent: [], ContentProtection: [], diff --git a/tests/unit/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.test.ts b/tests/unit/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.test.ts new file mode 100644 index 0000000000..ddc6cf64c2 --- /dev/null +++ b/tests/unit/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import parseBaseURL from "../../../../../../../../src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts"; +import type { ITNode } from "../../../../../../../../src/utils/xml-parser.ts"; +import { parseXml } from "../../../../../../../../src/utils/xml-parser.ts"; + +describe("DASH Node Parsers - BaseURL", () => { + it("should preserve an empty BaseURL and its serviceLocation", () => { + const element = parseXml('')[0] as ITNode; + + expect(parseBaseURL(element)).toEqual([ + { value: "", attributes: { serviceLocation: "cdn-a" } }, + [], + ]); + }); + + it("should correctly parse a BaseURL and its serviceLocation", () => { + const element = parseXml( + 'https://cdn.example/', + )[0] as ITNode; + + expect(parseBaseURL(element)).toEqual([ + { + value: "https://cdn.example/", + attributes: { serviceLocation: "cdn-a" }, + }, + [], + ]); + }); +}); diff --git a/tests/unit/src/parsers/manifest/dash/js-parser/parse_from_xml_string.test.ts b/tests/unit/src/parsers/manifest/dash/js-parser/parse_from_xml_string.test.ts index ad51acddbd..5789697bbf 100644 --- a/tests/unit/src/parsers/manifest/dash/js-parser/parse_from_xml_string.test.ts +++ b/tests/unit/src/parsers/manifest/dash/js-parser/parse_from_xml_string.test.ts @@ -22,4 +22,104 @@ describe("parseFromString", () => { }); }).toThrow("document root should be MPD"); }); + + it("uses the manifest URL as refresh URL when no Location is present", () => { + const response = parseFromString("", { + url: "https://example.com/path/manifest.mpd", + unsafelyBaseOnPreviousManifest: null, + }); + + expect(response.type).toBe("done"); + if (response.type === "done") { + expect(response.value.parsed.refreshUrls).toEqual([ + { baseUrl: "https://example.com/path/manifest.mpd", id: undefined }, + ]); + } + }); + + it("uses Locations instead of the manifest URL for refreshes", () => { + const response = parseFromString( + 'refresh.mpd' + + 'https://other.example/manifest.mpd' + + "", + { + url: "https://example.com/path/manifest.mpd", + unsafelyBaseOnPreviousManifest: null, + }, + ); + + expect(response.type).toBe("done"); + if (response.type === "done") { + expect(response.value.parsed.refreshUrls).toEqual([ + { baseUrl: "https://example.com/path/refresh.mpd", id: "first" }, + { baseUrl: "https://other.example/manifest.mpd", id: "second" }, + ]); + } + }); + + it("warns and ignores a relative Location when the manifest URL is unknown", () => { + const response = parseFromString( + "refresh.mpd", + { + unsafelyBaseOnPreviousManifest: null, + }, + ); + + expect(response.type).toBe("done"); + if (response.type === "done") { + expect(response.value.parsed.refreshUrls).toEqual([]); + expect(response.value.warnings).toHaveLength(1); + expect(response.value.warnings[0].message).toBe( + 'DASH Parser: Cannot resolve relative URL without a manifest URL: "refresh.mpd"', + ); + } + }); + + it("keeps an absolute Location when the manifest URL is unknown", () => { + const response = parseFromString( + "https://example.com/refresh.mpd", + { unsafelyBaseOnPreviousManifest: null }, + ); + + expect(response.type).toBe("done"); + if (response.type === "done") { + expect(response.value.parsed.refreshUrls).toEqual([ + { baseUrl: "https://example.com/refresh.mpd", id: undefined }, + ]); + expect(response.value.warnings).toEqual([]); + } + }); + + it("resolves an empty Location to the manifest URL and keeps its serviceLocation", () => { + const response = parseFromString( + '', + { + url: "https://example.com/path/manifest.mpd", + unsafelyBaseOnPreviousManifest: null, + }, + ); + + expect(response.type).toBe("done"); + if (response.type === "done") { + expect(response.value.parsed.refreshUrls).toEqual([ + { baseUrl: "https://example.com/path/manifest.mpd", id: "origin" }, + ]); + } + }); + + it("ignores an empty Location when the manifest URL is unknown", () => { + const response = parseFromString( + '', + { unsafelyBaseOnPreviousManifest: null }, + ); + + expect(response.type).toBe("done"); + if (response.type === "done") { + expect(response.value.parsed.refreshUrls).toEqual([]); + expect(response.value.warnings).toHaveLength(1); + expect(response.value.warnings[0].message).toBe( + 'DASH Parser: Cannot resolve relative URL without a manifest URL: ""', + ); + } + }); }); diff --git a/tests/unit/src/utils/url-utils.test.ts b/tests/unit/src/utils/url-utils.test.ts index eb28b228a5..0870ab35b7 100644 --- a/tests/unit/src/utils/url-utils.test.ts +++ b/tests/unit/src/utils/url-utils.test.ts @@ -2,9 +2,25 @@ import { describe, it, expect } from "vitest"; import { getFilenameIndexInUrl, getRelativeUrl, + isAbsoluteURL, resolveURL, } from "../../../../src/utils/url-utils.ts"; +describe(`utils - isAbsoluteURL ${isAbsoluteURL.name}`, () => { + it("should identify URLs containing an RFC 3986 scheme", () => { + expect(isAbsoluteURL("https://example.com/manifest.mpd")).toBe(true); + expect(isAbsoluteURL("urn:mpeg:dash:schema:mpd:2011")).toBe(true); + expect(isAbsoluteURL("custom+scheme:value")).toBe(true); + }); + + it("should reject relative and scheme-relative URLs", () => { + expect(isAbsoluteURL("refresh.mpd")).toBe(false); + expect(isAbsoluteURL("/refresh.mpd")).toBe(false); + expect(isAbsoluteURL("//example.com/refresh.mpd")).toBe(false); + expect(isAbsoluteURL("")).toBe(false); + }); +}); + describe(`utils - resolveURL ${resolveURL.name}`, () => { it("should return an empty string if no argument is given", () => { expect(resolveURL()).toBe("");