build(spf): build26 from 45504a2b

This commit is contained in:
publish
2026-08-05 18:57:39 +02:00
commit 8920135306
424 changed files with 17907 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
//#region src/media/utils/cdn.d.ts
/**
* Derive a stable grouping key for the CDN a URL is served from. Synchronous and
* pure (deliberately not a `resolve*` — no fetch). Consumers override the
* default via the engine's `getCdnId` config (e.g. to key on Mux's `cdn=` query
* param instead of the host); every CDN-identity site reads that same function
* so keys stay comparable across `cdnPriority`, `failedCdns`, and the
* track-switching constraint + scope.
*/
type GetCdnId = (url: string) => string;
//#endregion
export { GetCdnId };
//# sourceMappingURL=cdn.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cdn.d.ts","names":[],"sources":["../../../../src/media/utils/cdn.ts"],"mappings":";;;;;;;;;KAUY,YAAY"}
+56
View File
@@ -0,0 +1,56 @@
//#region src/media/utils/cdn.ts
/**
* Default {@link GetCdnId}: the URL's origin (scheme + host + port); falls back
* to the raw string when the URL can't be parsed, so the return value is always
* a stable grouping key.
*/
function getCdnId(url) {
try {
return new URL(url).origin;
} catch {
return url;
}
}
const CDN_TYPE_PRIORITY = {
video: 0,
audio: 1,
text: 2
};
/**
* The distinct CDNs a presentation's tracks are served from, ordered video CDNs
* first, then audio, then text (manifest order within a type). The head is the
* primary CDN — the one a sticky pick defaults to — and is always video-derived
* when the source has video. Returns `[]` for an unresolved presentation with
* no tracks.
*
* Redundant-stream sources list the same content on multiple hosts (e.g. Mux's
* `?redundant_streams=true`), so each host contributes its own candidate tracks;
* this collapses them to the set of CDNs across every track type. The CDN-id
* derivation defaults to {@link getCdnId}; pass a consumer-configured `getId` to
* key on something other than origin.
*/
function getOrderedCdnIds(presentation, getId = getCdnId) {
const seen = /* @__PURE__ */ new Set();
const ids = [];
const selectionSets = [...presentation.selectionSets ?? []].sort((a, b) => CDN_TYPE_PRIORITY[a.type] - CDN_TYPE_PRIORITY[b.type]);
for (const selectionSet of selectionSets) for (const switchingSet of selectionSet.switchingSets) for (const track of switchingSet.tracks) {
const id = getId(track.url);
if (seen.has(id)) continue;
seen.add(id);
ids.push(id);
}
return ids;
}
/**
* Add a CDN id to a failed-CDN list, preserving order and ignoring duplicates.
* Idempotent: re-adding an already-present id returns the same array reference
* (so a no-op trip doesn't churn the `failedCdns` signal). The failover trip in
* `resolve-track` and the segment loaders feed this into `failedCdns` via `update`.
*/
function addFailedCdn(failed, cdn) {
return failed?.includes(cdn) ? failed : [...failed ?? [], cdn];
}
//#endregion
export { addFailedCdn, getCdnId, getOrderedCdnIds };
//# sourceMappingURL=cdn.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cdn.js","names":[],"sources":["../../../../src/media/utils/cdn.ts"],"sourcesContent":["import type { MaybeResolvedPresentation, TrackType } from '../types';\n\n/**\n * Derive a stable grouping key for the CDN a URL is served from. Synchronous and\n * pure (deliberately not a `resolve*` — no fetch). Consumers override the\n * default via the engine's `getCdnId` config (e.g. to key on Mux's `cdn=` query\n * param instead of the host); every CDN-identity site reads that same function\n * so keys stay comparable across `cdnPriority`, `failedCdns`, and the\n * track-switching constraint + scope.\n */\nexport type GetCdnId = (url: string) => string;\n\n/**\n * Default {@link GetCdnId}: the URL's origin (scheme + host + port); falls back\n * to the raw string when the URL can't be parsed, so the return value is always\n * a stable grouping key.\n */\nexport function getCdnId(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n\n// Track-type priority for CDN ordering: video first, then audio, then text.\n// Selection sets are visited in this order so the head of the returned list is\n// always video-derived. `preferActiveCdn` anchors every track type to the\n// first CDN with surviving tracks (the head), so this makes \"the primary CDN\n// is the video CDN\" a guarantee of `getOrderedCdnIds` rather than a side effect\n// of the order tracks happen to be parsed in.\nconst CDN_TYPE_PRIORITY: Record<TrackType, number> = { video: 0, audio: 1, text: 2 };\n\n/**\n * The distinct CDNs a presentation's tracks are served from, ordered video CDNs\n * first, then audio, then text (manifest order within a type). The head is the\n * primary CDN — the one a sticky pick defaults to — and is always video-derived\n * when the source has video. Returns `[]` for an unresolved presentation with\n * no tracks.\n *\n * Redundant-stream sources list the same content on multiple hosts (e.g. Mux's\n * `?redundant_streams=true`), so each host contributes its own candidate tracks;\n * this collapses them to the set of CDNs across every track type. The CDN-id\n * derivation defaults to {@link getCdnId}; pass a consumer-configured `getId` to\n * key on something other than origin.\n */\nexport function getOrderedCdnIds(presentation: MaybeResolvedPresentation, getId: GetCdnId = getCdnId): string[] {\n const seen = new Set<string>();\n const ids: string[] = [];\n // Stable sort keeps manifest order among same-type selection sets.\n const selectionSets = [...(presentation.selectionSets ?? [])].sort(\n (a, b) => CDN_TYPE_PRIORITY[a.type] - CDN_TYPE_PRIORITY[b.type]\n );\n for (const selectionSet of selectionSets) {\n for (const switchingSet of selectionSet.switchingSets) {\n for (const track of switchingSet.tracks) {\n const id = getId(track.url);\n if (seen.has(id)) continue;\n seen.add(id);\n ids.push(id);\n }\n }\n }\n return ids;\n}\n\n/**\n * Add a CDN id to a failed-CDN list, preserving order and ignoring duplicates.\n * Idempotent: re-adding an already-present id returns the same array reference\n * (so a no-op trip doesn't churn the `failedCdns` signal). The failover trip in\n * `resolve-track` and the segment loaders feed this into `failedCdns` via `update`.\n */\nexport function addFailedCdn(failed: string[] | undefined, cdn: string): string[] {\n return failed?.includes(cdn) ? failed : [...(failed ?? []), cdn];\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,SAAS,KAAqB;CAC5C,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACtB,QAAQ;EACN,OAAO;CACT;AACF;AAQA,MAAM,oBAA+C;CAAE,OAAO;CAAG,OAAO;CAAG,MAAM;AAAE;;;;;;;;;;;;;;AAenF,SAAgB,iBAAiB,cAAyC,QAAkB,UAAoB;CAC9G,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,MAAgB,CAAC;CAEvB,MAAM,gBAAgB,CAAC,GAAI,aAAa,iBAAiB,CAAC,CAAE,CAAC,CAAC,MAC3D,GAAG,MAAM,kBAAkB,EAAE,QAAQ,kBAAkB,EAAE,KAC5D;CACA,KAAK,MAAM,gBAAgB,eACzB,KAAK,MAAM,gBAAgB,aAAa,eACtC,KAAK,MAAM,SAAS,aAAa,QAAQ;EACvC,MAAM,KAAK,MAAM,MAAM,GAAG;EAC1B,IAAI,KAAK,IAAI,EAAE,GAAG;EAClB,KAAK,IAAI,EAAE;EACX,IAAI,KAAK,EAAE;CACb;CAGJ,OAAO;AACT;;;;;;;AAQA,SAAgB,aAAa,QAA8B,KAAuB;CAChF,OAAO,QAAQ,SAAS,GAAG,IAAI,SAAS,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG;AACjE"}
+22
View File
@@ -0,0 +1,22 @@
//#region src/media/utils/preload.ts
function isStandardPreload(value) {
return value === "auto" || value === "metadata" || value === "none";
}
/**
* Default `preload` value used as the fallback across behaviors
* (`syncPreload`, `resolvePresentation`, `isBlockingPreload`). Matches the
* `<video>`/`<audio>` element's implicit default.
*/
const DEFAULT_PRELOAD = "metadata";
/**
* True when the preload value blocks initial resolution / loading.
* Falsy values (undefined, empty) fall back to `defaultPreload` (default
* `DEFAULT_PRELOAD`); the resolved value blocks iff it is `'none'`.
*/
function isBlockingPreload(preload, defaultPreload = DEFAULT_PRELOAD) {
return (preload || defaultPreload) === "none";
}
//#endregion
export { DEFAULT_PRELOAD, isBlockingPreload, isStandardPreload };
//# sourceMappingURL=preload.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"preload.js","names":[],"sources":["../../../../src/media/utils/preload.ts"],"sourcesContent":["/**\n * The W3C-standard set of `<video>`/`<audio>` `preload` attribute values.\n * SPF allows extended values (e.g. `'canplay'`) on `state.preload`, which\n * are *not* reflected to the DOM — predicate below is the discriminator.\n */\nexport type StandardPreload = 'auto' | 'metadata' | 'none';\n\nexport function isStandardPreload(value: unknown): value is StandardPreload {\n return value === 'auto' || value === 'metadata' || value === 'none';\n}\n\n/**\n * Default `preload` value used as the fallback across behaviors\n * (`syncPreload`, `resolvePresentation`, `isBlockingPreload`). Matches the\n * `<video>`/`<audio>` element's implicit default.\n */\nexport const DEFAULT_PRELOAD = 'metadata';\n\n/**\n * True when the preload value blocks initial resolution / loading.\n * Falsy values (undefined, empty) fall back to `defaultPreload` (default\n * `DEFAULT_PRELOAD`); the resolved value blocks iff it is `'none'`.\n */\nexport function isBlockingPreload(\n preload: string | undefined,\n defaultPreload: StandardPreload = DEFAULT_PRELOAD\n): boolean {\n return (preload || defaultPreload) === 'none';\n}\n"],"mappings":";AAOA,SAAgB,kBAAkB,OAA0C;CAC1E,OAAO,UAAU,UAAU,UAAU,cAAc,UAAU;AAC/D;;;;;;AAOA,MAAa,kBAAkB;;;;;;AAO/B,SAAgB,kBACd,SACA,iBAAkC,iBACzB;CACT,QAAQ,WAAW,oBAAoB;AACzC"}
+47
View File
@@ -0,0 +1,47 @@
import { isResolvedTrack } from "../types/index.js";
//#region src/media/utils/track-selection.ts
/**
* Map track type to selected track ID property key in state.
*/
const SelectedTrackIdKeyByType = {
video: "selectedVideoTrackId",
audio: "selectedAudioTrackId",
text: "selectedTextTrackId"
};
/**
* Get selected track from state by type.
* Returns properly typed track (partially or fully resolved) or undefined.
* Type parameter T is inferred from the type argument.
*
* @example
* const videoTrack = getSelectedTrack(state, 'video');
* if (videoTrack && isResolvedTrack(videoTrack)) {
* // videoTrack is VideoTrack
* }
*/
function getSelectedTrack(state, type) {
const { presentation } = state;
if (!presentation?.selectionSets) return void 0;
const trackId = state[SelectedTrackIdKeyByType[type]];
return presentation.selectionSets.find(({ type: selectionSetType }) => selectionSetType === type)?.switchingSets[0]?.tracks.find(({ id }) => id === trackId);
}
/**
* Returns the duration of the first resolved selected track, preferring
* video over audio. A track is "resolved" once its media playlist has been
* parsed (per {@link isResolvedTrack}). Returns `undefined` if neither
* selected track is resolved.
*/
function getResolvedSelectedTrackDuration(state) {
if (state.selectedVideoTrackId) {
const video = getSelectedTrack(state, "video");
if (video && isResolvedTrack(video)) return video.duration;
}
if (state.selectedAudioTrackId) {
const audio = getSelectedTrack(state, "audio");
if (audio && isResolvedTrack(audio)) return audio.duration;
}
}
//#endregion
export { SelectedTrackIdKeyByType, getResolvedSelectedTrackDuration, getSelectedTrack };
//# sourceMappingURL=track-selection.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"track-selection.js","names":[],"sources":["../../../../src/media/utils/track-selection.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * State shape for track selection.\n * Minimal shape containing presentation and selected track IDs.\n */\nexport interface TrackSelectionState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string | undefined;\n selectedAudioTrackId?: string | undefined;\n selectedTextTrackId?: string | undefined;\n}\n\n/**\n * Map track type to selected track ID property key in state.\n */\nexport const SelectedTrackIdKeyByType = {\n video: 'selectedVideoTrackId',\n audio: 'selectedAudioTrackId',\n text: 'selectedTextTrackId',\n} as const;\n\n/**\n * Get selected track from state by type.\n * Returns properly typed track (partially or fully resolved) or undefined.\n * Type parameter T is inferred from the type argument.\n *\n * @example\n * const videoTrack = getSelectedTrack(state, 'video');\n * if (videoTrack && isResolvedTrack(videoTrack)) {\n * // videoTrack is VideoTrack\n * }\n */\nexport function getSelectedTrack<T extends TrackType>(\n state: TrackSelectionState,\n type: T\n): T extends 'video'\n ? PartiallyResolvedVideoTrack | VideoTrack | undefined\n : T extends 'audio'\n ? PartiallyResolvedAudioTrack | AudioTrack | undefined\n : T extends 'text'\n ? PartiallyResolvedTextTrack | TextTrack | undefined\n : never {\n const { presentation } = state;\n\n if (!presentation?.selectionSets) return undefined as any;\n\n // Get track ID based on type\n const trackIdKey = SelectedTrackIdKeyByType[type];\n const trackId = state[trackIdKey];\n return presentation.selectionSets\n .find(({ type: selectionSetType }) => selectionSetType === type)\n ?.switchingSets[0]?.tracks.find(({ id }) => id === trackId) as any;\n}\n\n/**\n * Returns the duration of the first resolved selected track, preferring\n * video over audio. A track is \"resolved\" once its media playlist has been\n * parsed (per {@link isResolvedTrack}). Returns `undefined` if neither\n * selected track is resolved.\n */\nexport function getResolvedSelectedTrackDuration(state: TrackSelectionState): number | undefined {\n if (state.selectedVideoTrackId) {\n const video = getSelectedTrack(state, 'video');\n if (video && isResolvedTrack(video)) return video.duration;\n }\n if (state.selectedAudioTrackId) {\n const audio = getSelectedTrack(state, 'audio');\n if (audio && isResolvedTrack(audio)) return audio.duration;\n }\n return undefined;\n}\n"],"mappings":";;;;;AA0BA,MAAa,2BAA2B;CACtC,OAAO;CACP,OAAO;CACP,MAAM;AACR;;;;;;;;;;;;AAaA,SAAgB,iBACd,OACA,MAOY;CACZ,MAAM,EAAE,iBAAiB;CAEzB,IAAI,CAAC,cAAc,eAAe,OAAO,KAAA;CAIzC,MAAM,UAAU,MADG,yBAAyB;CAE5C,OAAO,aAAa,cACjB,MAAM,EAAE,MAAM,uBAAuB,qBAAqB,IAAI,CAAC,EAC9D,cAAc,EAAE,EAAE,OAAO,MAAM,EAAE,SAAS,OAAO,OAAO;AAC9D;;;;;;;AAQA,SAAgB,iCAAiC,OAAgD;CAC/F,IAAI,MAAM,sBAAsB;EAC9B,MAAM,QAAQ,iBAAiB,OAAO,OAAO;EAC7C,IAAI,SAAS,gBAAgB,KAAK,GAAG,OAAO,MAAM;CACpD;CACA,IAAI,MAAM,sBAAsB;EAC9B,MAAM,QAAQ,iBAAiB,OAAO,OAAO;EAC7C,IAAI,SAAS,gBAAgB,KAAK,GAAG,OAAO,MAAM;CACpD;AAEF"}
+130
View File
@@ -0,0 +1,130 @@
import { isResolvedTrack } from "../types/index.js";
//#region src/media/utils/tracks.ts
/**
* Get the tracks of the given type from a presentation's first switching set.
*
* Returns `[]` when the presentation is unresolved, when no selection set of
* `type` exists, or when its first switching set is empty. Returned tracks may
* be partially resolved (URL only) or fully resolved (with segments) — callers
* narrow as needed.
*
* The "first switching set" assumption matches the rest of the codebase
* (HLS typically has one switching set per type); multi-group / multi-period
* support would generalize this.
*/
function getTracksByType(presentation, type) {
return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];
}
/**
* Find a track of the given type and id within a presentation.
*
* Returns the matching track from the first switching set of the matching
* selection set, or `undefined` if either is missing. The returned track may
* be partially resolved (URL only) or fully resolved (with segments) — callers
* narrow as needed.
*/
function findTrack(presentation, type, trackId) {
return getTracksByType(presentation, type).find(({ id }) => id === trackId);
}
/**
* Find a track by id across all selection sets in a presentation, without
* knowing its type up front. Used when the caller has a track id obtained
* from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and
* needs to locate the corresponding track in the presentation.
*
* Track ids are unique within a presentation per the HLS spec; the first
* match wins.
*/
function findTrackById(presentation, trackId) {
for (const selectionSet of presentation.selectionSets ?? []) {
const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);
if (track) return track;
}
}
/**
* Find a text track of the given id within a presentation and narrow it to
* the fully-resolved `TextTrack` shape (segments populated). Returns
* `undefined` if no track matches the id, the matching track isn't a text
* track, or it hasn't been resolved yet.
*
* The segments-non-empty check stays at the call site — a resolved track
* with zero segments is a valid state, distinct from "ready to load."
*/
function findResolvedTextTrack(presentation, trackId) {
if (!presentation || !trackId) return void 0;
const track = findTrack(presentation, "text", trackId);
if (track?.type !== "text" || !isResolvedTrack(track)) return void 0;
return track;
}
function findResolvedVideoTrack(presentation, trackId) {
if (!presentation || !trackId) return void 0;
const track = findTrack(presentation, "video", trackId);
if (track?.type !== "video" || !isResolvedTrack(track)) return void 0;
return track;
}
function findResolvedAudioTrack(presentation, trackId) {
if (!presentation || !trackId) return void 0;
const track = findTrack(presentation, "audio", trackId);
if (track?.type !== "audio" || !isResolvedTrack(track)) return void 0;
return track;
}
/**
* Whether a track carries a non-empty `codecs` array. Both partially-
* resolved and fully-resolved tracks may carry codecs — they come from
* the multivariant playlist's `EXT-X-STREAM-INF` line, not from the
* per-type media playlist — so this works at either resolution stage.
*
* `TextTrack` doesn't declare a `codecs` field; the `'codecs' in track`
* check narrows it out for the false branch.
*/
function hasCodecs(track) {
return !!track && "codecs" in track && !!track.codecs?.length;
}
/**
* Set `mimeType` on every track of one `type` (immutably). Used to propagate a
* detected container across a type's renditions: an ABR ladder is the same
* content at different bitrates, so one rendition's container holds for all of
* them — capability probing + SourceBuffer setup then get the right MIME for the
* whole type from a single resolved media playlist, without fetching the rest.
*
* Scoped to one type on purpose: propagating *across* audio/video would be wrong
* for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races
* concurrent per-type resolution. Same-type writes are disjoint and safe.
* Idempotent — tracks already at `mimeType` are left as-is.
*/
function applyContainerMimeType(presentation, type, mimeType) {
return {
...presentation,
selectionSets: presentation.selectionSets.map((selectionSet) => selectionSet.type === type ? {
...selectionSet,
switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
...switchingSet,
tracks: switchingSet.tracks.map((track) => track.mimeType === mimeType ? track : {
...track,
mimeType
})
}))
} : selectionSet)
};
}
/**
* Updates a track within a presentation (immutably). Generic — works for
* video, audio, or text tracks.
*/
function updateTrackInPresentation(presentation, resolvedTrack) {
const trackId = resolvedTrack.id;
return {
...presentation,
selectionSets: presentation.selectionSets.map((selectionSet) => ({
...selectionSet,
switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
...switchingSet,
tracks: switchingSet.tracks.map((track) => track.id === trackId ? resolvedTrack : track)
}))
}))
};
}
//#endregion
export { applyContainerMimeType, findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack, findTrack, findTrackById, getTracksByType, hasCodecs, updateTrackInPresentation };
//# sourceMappingURL=tracks.js.map
File diff suppressed because one or more lines are too long