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
@@ -0,0 +1,16 @@
import { MediaContainerData } from "../../media/types/index.js";
//#region src/playback/primitives/derive-start-media-time.d.ts
/** The selected v/a track ids a {@link DeriveStartMediaTime} may coordinate across. */
interface DeriveStartMediaTimeContext {
selectedVideoTrackId?: string;
selectedAudioTrackId?: string;
}
/**
* Reduce the discovered {@link MediaContainerData} (keyed by track type) into each type's
* `startMediaTime` origin; `undefined` means "not ready yet". Pure and injected — the single
* coordination seam the reactor and the loader stamp share.
*/
type DeriveStartMediaTime = (containerData: Record<string, MediaContainerData>, ctx: DeriveStartMediaTimeContext) => Record<string, number | undefined>;
//#endregion
export { DeriveStartMediaTime, DeriveStartMediaTimeContext };
//# sourceMappingURL=derive-start-media-time.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"derive-start-media-time.d.ts","names":[],"sources":["../../../../src/playback/primitives/derive-start-media-time.ts"],"mappings":";;;UAWiB;EACf;EACA;;;;;;;KAQU,wBACV,eAAe,eAAe,qBAC9B,KAAK,gCACF"}
+42
View File
@@ -0,0 +1,42 @@
import { update } from "../../core/signals/primitives.js";
import { findTrackById } from "../../media/utils/tracks.js";
import { addFailedCdn, getCdnId } from "../../media/utils/cdn.js";
//#region src/playback/primitives/failover-fetch.ts
/**
* Decorate a fetch so a failed request trips the **selected track's** CDN into
* `failedCdns`. The decorated fetch's type is preserved, so this wraps both
* `resolve-track`'s playlist `FetchText` and the segment loaders' `FetchBytes`.
*
* The CDN id comes from the selected track's media-playlist URL, never the
* failed addressable: a segment URL resolves relative to its playlist and, per
* RFC 3986, drops the playlist's query string (`…/r.m3u8?cdn=fastly` → `…/0.ts`),
* so a query-keyed `getCdnId` (e.g. Mux's `cdn=`) keyed on it would derive an id
* that never matches the ones `deriveCdnPriority` / track-switching build from
* `track.url`. The in-flight fetch belongs to the selected track — a source or
* track switch aborts it, and aborts don't trip — so the selected track is the
* right CDN to fail over. For `resolve-track` the resolving track *is* the
* selected track, so this is identical to keying on its addressable.
*
* No-op when no failover monitor is composed (it owns the signal) or the
* selected track can't be located.
*/
function failoverFetch(baseFetch, state, config) {
const getCdnId$1 = config.getCdnId ?? getCdnId;
return (async (addressable, options) => {
try {
return await baseFetch(addressable, options);
} catch (error) {
if (!options?.signal?.aborted && state.failedCdns) {
const presentation = state.presentation.get();
const trackId = state[config.selectedKey].get();
const track = presentation && trackId ? findTrackById(presentation, trackId) : void 0;
if (track) update(state.failedCdns, (cdns) => addFailedCdn(cdns, getCdnId$1(track.url)));
}
throw error;
}
});
}
//#endregion
export { failoverFetch };
//# sourceMappingURL=failover-fetch.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"failover-fetch.js","names":["getCdnId","defaultGetCdnId"],"sources":["../../../../src/playback/primitives/failover-fetch.ts"],"sourcesContent":["import { type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../media/types';\nimport { addFailedCdn, getCdnId as defaultGetCdnId, type GetCdnId } from '../../media/utils/cdn';\nimport { findTrackById } from '../../media/utils/tracks';\nimport type { FetchOptions, Resource } from '../../network/fetch';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\n\n/**\n * State a failover-decorated fetch reads: the presentation, the per-type\n * selected-track slot, and the failover monitor's `failedCdns`.\n *\n * `failedCdns` is *optional* — the failover monitor owns that slot, so a\n * behavior's narrow state is assignable here without the behavior declaring it\n * (and the intersection shares keys, so it isn't a weak type). When no monitor\n * is composed the slot is absent and tracking no-ops.\n */\ntype FailoverState<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n failedCdns?: Signal<string[] | undefined>;\n} & { [P in K]: ReadonlySignal<string | undefined> };\n\n/** Any `Resource`-addressable fetch — both `FetchText` and `FetchBytes` qualify. */\ntype FailoverableFetch = (addressable: Resource, options?: FetchOptions) => Promise<unknown>;\n\n/**\n * Decorate a fetch so a failed request trips the **selected track's** CDN into\n * `failedCdns`. The decorated fetch's type is preserved, so this wraps both\n * `resolve-track`'s playlist `FetchText` and the segment loaders' `FetchBytes`.\n *\n * The CDN id comes from the selected track's media-playlist URL, never the\n * failed addressable: a segment URL resolves relative to its playlist and, per\n * RFC 3986, drops the playlist's query string (`…/r.m3u8?cdn=fastly` → `…/0.ts`),\n * so a query-keyed `getCdnId` (e.g. Mux's `cdn=`) keyed on it would derive an id\n * that never matches the ones `deriveCdnPriority` / track-switching build from\n * `track.url`. The in-flight fetch belongs to the selected track — a source or\n * track switch aborts it, and aborts don't trip — so the selected track is the\n * right CDN to fail over. For `resolve-track` the resolving track *is* the\n * selected track, so this is identical to keying on its addressable.\n *\n * No-op when no failover monitor is composed (it owns the signal) or the\n * selected track can't be located.\n */\nexport function failoverFetch<K extends SelectedTrackKey, Fetch extends FailoverableFetch>(\n baseFetch: Fetch,\n state: FailoverState<K>,\n config: { selectedKey: K; getCdnId?: GetCdnId }\n): Fetch {\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n return (async (addressable: Resource, options?: FetchOptions) => {\n try {\n return await baseFetch(addressable, options);\n } catch (error) {\n if (!options?.signal?.aborted && state.failedCdns) {\n const presentation = state.presentation.get();\n const trackId = state[config.selectedKey].get();\n const track = presentation && trackId ? findTrackById(presentation, trackId) : undefined;\n if (track) update(state.failedCdns, (cdns) => addFailedCdn(cdns, getCdnId(track.url)));\n }\n throw error;\n }\n }) as Fetch;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,cACd,WACA,OACA,QACO;CACP,MAAMA,aAAW,OAAO,YAAYC;CACpC,QAAQ,OAAO,aAAuB,YAA2B;EAC/D,IAAI;GACF,OAAO,MAAM,UAAU,aAAa,OAAO;EAC7C,SAAS,OAAO;GACd,IAAI,CAAC,SAAS,QAAQ,WAAW,MAAM,YAAY;IACjD,MAAM,eAAe,MAAM,aAAa,IAAI;IAC5C,MAAM,UAAU,MAAM,OAAO,YAAY,CAAC,IAAI;IAC9C,MAAM,QAAQ,gBAAgB,UAAU,cAAc,cAAc,OAAO,IAAI,KAAA;IAC/E,IAAI,OAAO,OAAO,MAAM,aAAa,SAAS,aAAa,MAAMD,WAAS,MAAM,GAAG,CAAC,CAAC;GACvF;GACA,MAAM;EACR;CACF;AACF"}
+44
View File
@@ -0,0 +1,44 @@
//#region src/playback/primitives/head-peek.ts
/**
* Eager head-of-stream peek. Pulls chunks from an async byte stream only until
* `tryParse` signals it has read what it needs (or the stream ends), then returns
* a stream that re-emits the pulled head followed by the untouched tail — so
* reading a head-of-stream mp4 box (`mdhd` / `tfdt`) doesn't break streaming of
* the append. Once the caller has what it wants, the rest streams as normal.
*/
function concat(chunks) {
if (chunks.length === 1) return chunks[0];
const total = chunks.reduce((n, c) => n + c.length, 0);
const out = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
out.set(chunk, offset);
offset += chunk.length;
}
return out;
}
/** Re-emit the eagerly-pulled head chunks, then stream the untouched tail. */
async function* reassemble(head, tail) {
yield* head;
for (let next = await tail.next(); !next.done; next = await tail.next()) yield next.value;
}
/**
* Pull chunks until `tryParse(accumulatedHead)` returns `true` — it found and read
* its box — or the stream ends, then return a stream re-emitting the pulled head
* followed by the untouched tail. `tryParse` is called with the growing head after
* each chunk; it performs the side effect (publishing the parsed value) and returns
* whether it's done.
*/
async function peekHead(data, tryParse) {
const iterator = data[Symbol.asyncIterator]();
const head = [];
for (let next = await iterator.next(); !next.done; next = await iterator.next()) {
head.push(next.value);
if (tryParse(concat(head))) break;
}
return reassemble(head, iterator);
}
//#endregion
export { peekHead };
//# sourceMappingURL=head-peek.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"head-peek.js","names":[],"sources":["../../../../src/playback/primitives/head-peek.ts"],"sourcesContent":["/**\n * Eager head-of-stream peek. Pulls chunks from an async byte stream only until\n * `tryParse` signals it has read what it needs (or the stream ends), then returns\n * a stream that re-emits the pulled head followed by the untouched tail — so\n * reading a head-of-stream mp4 box (`mdhd` / `tfdt`) doesn't break streaming of\n * the append. Once the caller has what it wants, the rest streams as normal.\n */\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 1) return chunks[0]!;\n const total = chunks.reduce((n, c) => n + c.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\n/** Re-emit the eagerly-pulled head chunks, then stream the untouched tail. */\nasync function* reassemble(head: Uint8Array[], tail: AsyncIterator<Uint8Array>): AsyncIterable<Uint8Array> {\n yield* head;\n for (let next = await tail.next(); !next.done; next = await tail.next()) yield next.value;\n}\n\n/**\n * Pull chunks until `tryParse(accumulatedHead)` returns `true` — it found and read\n * its box — or the stream ends, then return a stream re-emitting the pulled head\n * followed by the untouched tail. `tryParse` is called with the growing head after\n * each chunk; it performs the side effect (publishing the parsed value) and returns\n * whether it's done.\n */\nexport async function peekHead(\n data: AsyncIterable<Uint8Array>,\n tryParse: (bytes: Uint8Array) => boolean\n): Promise<AsyncIterable<Uint8Array>> {\n const iterator = data[Symbol.asyncIterator]();\n const head: Uint8Array[] = [];\n for (let next = await iterator.next(); !next.done; next = await iterator.next()) {\n head.push(next.value);\n if (tryParse(concat(head))) break;\n }\n return reassemble(head, iterator);\n}\n"],"mappings":";;;;;;;;AAQA,SAAS,OAAO,QAAkC;CAChD,IAAI,OAAO,WAAW,GAAG,OAAO,OAAO;CACvC,MAAM,QAAQ,OAAO,QAAQ,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC;CACrD,MAAM,MAAM,IAAI,WAAW,KAAK;CAChC,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,IAAI,OAAO,MAAM;EACrB,UAAU,MAAM;CAClB;CACA,OAAO;AACT;;AAGA,gBAAgB,WAAW,MAAoB,MAA4D;CACzG,OAAO;CACP,KAAK,IAAI,OAAO,MAAM,KAAK,KAAK,GAAG,CAAC,KAAK,MAAM,OAAO,MAAM,KAAK,KAAK,GAAG,MAAM,KAAK;AACtF;;;;;;;;AASA,eAAsB,SACpB,MACA,UACoC;CACpC,MAAM,WAAW,KAAK,OAAO,cAAc,CAAC;CAC5C,MAAM,OAAqB,CAAC;CAC5B,KAAK,IAAI,OAAO,MAAM,SAAS,KAAK,GAAG,CAAC,KAAK,MAAM,OAAO,MAAM,SAAS,KAAK,GAAG;EAC/E,KAAK,KAAK,KAAK,KAAK;EACpB,IAAI,SAAS,OAAO,IAAI,CAAC,GAAG;CAC9B;CACA,OAAO,WAAW,MAAM,QAAQ;AAClC"}
+203
View File
@@ -0,0 +1,203 @@
import { peek, update } from "../../core/signals/primitives.js";
import { effect } from "../../core/signals/effect.js";
import { resolveVttSegmentMetadata } from "../../media/text/resolve-vtt-metadata.js";
import { findTrackById, getTracksByType } from "../../media/utils/tracks.js";
import { dispatchCuesStep, textStepWiring } from "./text-segment-load-pipeline.js";
import { dispatchStep, fetchStep } from "./segment-load-pipeline.js";
import { findMediaTrack, readBaseMediaDecodeTime } from "../../media/mp4/timestamp-origin.js";
import { peekHead } from "./head-peek.js";
//#region src/playback/primitives/relocation-pipelines.ts
/** Assert the relocation state view from the opaque step deps — this module knows the slots the composition provides. */
function relocationState(deps) {
return deps.state;
}
function containerSlot(deps) {
return relocationState(deps).mediaContainerData;
}
/** Synchronous RMW of the per-type entry — disjoint keys across producers, so no lost update. */
function writeContainer(slot, trackType, patch) {
update(slot, (current) => ({
...current,
[trackType]: {
...current?.[trackType],
...patch
}
}));
}
/**
* Resolve once `read()` returns a number. Shared by the A/V stamp (waits for the
* `derive`d origin — immediate for per-type, the shared-`min` barrier for coordinated)
* and the text step (waits for the primary A/V origin). No bound: for fMP4 the origin
* always establishes.
*/
function awaitDefined(read) {
return new Promise((resolve) => {
let stop;
stop = effect(() => {
const value = read();
if (value !== void 0) {
stop?.();
resolve(value);
}
});
});
}
/**
* Relocation pipelines for one track type — a plain config `messagePipelines`.
* Keyed by **track type** (`'video'` / `'audio'`), so ABR rungs of a type share the
* origin (discover skips once the type's value is present). The steps read/write
* `state.mediaContainerData[trackType]` via their call-time `deps`; the stamp applies
* the same `derive` seam the reactor uses (pass the composition's resolved
* `deriveStartMediaTime` so the buffer offset and the model's `startMediaTime` agree).
*/
function relocationPipelinesFor(trackType, derive) {
const handlerType = trackType === "video" ? "vide" : "soun";
/**
* Init step: head-peek the buffered media track's `track_id` + `mdhd` timescale into
* `mediaContainerData[trackType]`. Matching by handler (`vide`/`soun`) skips a muxed
* `clcp` caption track, and the `track_id` lets `readSegmentOrigin` read *this*
* track's `tfdt` rather than the first `traf` in the segment.
*/
const readInitTrackInfo = async (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== "append-init" || !frame.data) return;
const slot = containerSlot(deps);
if (peek(slot)?.[trackType]?.timescale !== void 0) return;
frame.data = await peekHead(frame.data, (bytes) => {
const track = findMediaTrack(bytes, handlerType);
if (track === void 0) return false;
writeContainer(slot, trackType, {
trackId: track.trackId,
timescale: track.timescale
});
return true;
});
};
/**
* Media-segment step: head-peek the `tfdt` baseMediaDecodeTime of the media track's
* `traf` (matched by the `track_id` discovered from the init), recording the
* segment's 0-based `startTime` with it — the origin is `bmdt/ts segmentStartTime`,
* so the first *loaded* segment need not be the 0th. Without a discovered `track_id`
* (non-fMP4 / mock init) there's no media track to relocate, so the step no-ops and
* the append stays native.
*/
const readSegmentOrigin = async (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== "append-segment" || !frame.data) return;
const slot = containerSlot(deps);
const container = peek(slot)?.[trackType];
if (container?.baseMediaDecodeTime !== void 0) return;
const { trackId } = container ?? {};
if (trackId === void 0) return;
const segmentStartTime = op.meta.startTime;
frame.data = await peekHead(frame.data, (bytes) => {
const baseMediaDecodeTime = readBaseMediaDecodeTime(bytes, trackId);
if (baseMediaDecodeTime === void 0) return false;
writeContainer(slot, trackType, {
baseMediaDecodeTime,
segmentStartTime
});
return true;
});
};
/**
* Stamp step — tier-agnostic apply. Relocate by the `derive`d `startMediaTime` for
* this type (`offset = startMediaTime`). Applies the **same** `derive` the reactor
* uses, over the shared `mediaContainerData` slot — so the buffer offset matches the
* model's stamped `startMediaTime`, and it's robust to `established` + late tracks
* (the slot persists; the model value may not be re-stamped after the reactor goes
* sticky). Awaited: per-type resolves at once (own origin discovered earlier in this
* pipeline); shared-`min` waits until every selected A/V origin is in — the barrier,
* filled by the other type's discover step. A derived `0` (0-PTS / below threshold)
* leaves the append native — setting `timestampOffset` at all can ripple.
*/
const stampStartMediaTime = async (frame, signal, deps) => {
if (frame.op.type !== "append-segment") return;
const state = relocationState(deps);
const own = peek(state.mediaContainerData)?.[trackType];
if (own?.timescale === void 0 || own.baseMediaDecodeTime === void 0 || own.segmentStartTime === void 0) return;
const startMediaTime = await awaitDefined(() => {
const containerData = state.mediaContainerData.get();
if (!containerData) return void 0;
return derive(containerData, {
selectedVideoTrackId: state.selectedVideoTrackId?.get(),
selectedAudioTrackId: state.selectedAudioTrackId?.get()
})[trackType];
});
if (signal.aborted || startMediaTime === 0) return;
frame.meta = {
...frame.meta ?? frame.op.meta,
timestampOffset: -startMediaTime
};
};
return () => ({
remove: [dispatchStep],
"append-init": [
fetchStep,
readInitTrackInfo,
dispatchStep
],
"append-segment": [
fetchStep,
readSegmentOrigin,
stampStartMediaTime,
dispatchStep
]
});
}
/**
* Resolve step for the relocation text pipeline. Reuses the injected host resolver
* (the loader's folded `resolveSegment`, via `textStepWiring`) for cues and fetches
* the `X-TIMESTAMP-MAP` header in parallel, stashing it on `frame.metadata` for
* `relocateCuesStep`. Replaces the
* base `resolveCuesStep` (which fetches cues only) — text's native `<track>` parser
* discards the header, so the map needs its own raw-bytes fetch.
*/
const resolveWithMetadataStep = async (frame, signal, deps) => {
const [cues, metadata] = await Promise.all([textStepWiring(deps).resolveSegment(frame.op.segment.url), resolveVttSegmentMetadata(frame.op.segment.url)]);
if (signal.aborted) return;
frame.cues = cues;
frame.metadata = metadata;
};
/**
* Relocate step — shifts each cue onto the 0-based presentation timeline:
* `cueFinal = cueNative startMediaTime`, where `startMediaTime` is the primary
* A/V track's origin (selected **video**, else **audio** — the single-anchor rule,
* and defensive like the reactor's optional selection) and `cueNative` folds in the
* `X-TIMESTAMP-MAP` correction (`mpegts/90000 local`) for map-bearing VTT (Apple)
* or is the absolute cue time (no map, e.g. Mux). Text can resolve before A/V
* establishes, so the origin is awaited; fMP4 always establishes it (0-PTS → 0),
* and a text-only source (no A/V selected) simply gets offset 0.
*/
const relocateCuesStep = async (frame, signal, deps) => {
if (!frame.cues?.length) return;
const state = deps.state;
const startMediaTime = await awaitDefined(() => {
const presentation = state.presentation.get();
if (!presentation) return void 0;
const primaryId = state.selectedVideoTrackId.get() ?? state.selectedAudioTrackId.get();
if (primaryId !== void 0) return findTrackById(presentation, primaryId)?.startMediaTime;
return getTracksByType(presentation, "video").length > 0 || getTracksByType(presentation, "audio").length > 0 ? void 0 : 0;
});
if (signal.aborted) return;
const { timestampMap } = frame.metadata ?? {};
const delta = (timestampMap ? timestampMap.mpegts / 9e4 - timestampMap.local : 0) - startMediaTime;
if (delta !== 0) for (const cue of frame.cues) {
cue.startTime += delta;
cue.endTime += delta;
}
};
/**
* Relocation text pipeline — the text analog of `relocationPipelinesFor(type)`.
* `resolveWithMetadata` (cues + `X-TIMESTAMP-MAP`) → `relocateCues` (shift by the
* primary A/V origin) → `dispatchCues`.
*/
const relocatingTextPipelines = () => [
resolveWithMetadataStep,
relocateCuesStep,
dispatchCuesStep
];
//#endregion
export { relocatingTextPipelines, relocationPipelinesFor };
//# sourceMappingURL=relocation-pipelines.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
import "../../core/signals/primitives.js";
import "../../core/composition/create-composition.js";
+96
View File
@@ -0,0 +1,96 @@
import { effect } from "../../core/signals/effect.js";
//#region src/playback/primitives/segment-load-pipeline.ts
/**
* Base-step view of the loader's own wiring. `createSegmentLoaderActor` folds its
* `sourceBufferActor` + `fetch` into the threaded `config` so base steps read them
* from the uniform passthrough — present whether the loader runs inside a composition
* or standalone. `config` is loose (`object`), so assert the shape here (one cast, like
* relocation's `containerSlot`).
*/
function stepWiring(deps) {
return deps.config;
}
/**
* Resolves when the SourceBufferActor snapshot reaches 'idle'.
* Rejects if the signal is aborted or the actor is destroyed.
*
* Used to sequence SourceBufferActor operations without awaiting send()
* directly — send() is fire-and-forget; callers observe completion via
* state transition.
*/
function waitForIdle(snapshot, signal) {
return new Promise((resolve, reject) => {
if (snapshot.get().value === "idle") {
resolve();
return;
}
if (snapshot.get().value === "destroyed") {
reject(new DOMException("Aborted", "AbortError"));
return;
}
if (signal.aborted) {
reject(signal.reason);
return;
}
let stop;
const cleanup = (fn) => {
stop?.();
signal.removeEventListener("abort", onAbort);
fn();
};
const onAbort = () => cleanup(() => reject(signal.reason));
stop = effect(() => {
const value = snapshot.get().value;
if (value === "idle") cleanup(resolve);
else if (value === "destroyed") cleanup(() => reject(new DOMException("Aborted", "AbortError")));
});
signal.addEventListener("abort", onAbort, { once: true });
});
}
/** Build the SourceBuffer message a completed frame dispatches. `fetchStep` always precedes `dispatchStep` in append pipelines, so `data` is set by now. */
function toMessage({ op, data, meta }) {
switch (op.type) {
case "remove": return op;
case "append-init": return {
type: "append-init",
data,
meta: op.meta
};
case "append-segment": return {
type: "append-segment",
data,
meta: meta ?? op.meta
};
}
}
/**
* Fetch this op's bytes into the frame. Init segments need the full body
* (`minChunkSize: Infinity`) before appending; media segments stream so chunks
* append as they arrive. Awaiting headers eagerly also starts the HTTP
* connection (and records the fetch in observers like tests).
*/
const fetchStep = async (frame, signal, deps) => {
const { op } = frame;
if (op.type === "remove") return;
const { fetch } = stepWiring(deps);
frame.data = await fetch(op, op.type === "append-init" ? {
signal,
minChunkSize: Infinity
} : { signal });
};
/** Dispatch the frame's message to the SourceBufferActor and await its return to idle. */
const dispatchStep = async (frame, signal, deps) => {
const { sourceBufferActor } = stepWiring(deps);
sourceBufferActor.send(toMessage(frame));
await waitForIdle(sourceBufferActor.snapshot, signal);
};
/** Tier 0 default: fetch (for ops that carry bytes) then dispatch. No relocation vocabulary. */
const DEFAULT_MESSAGE_PIPELINES = () => ({
remove: [dispatchStep],
"append-init": [fetchStep, dispatchStep],
"append-segment": [fetchStep, dispatchStep]
});
//#endregion
export { DEFAULT_MESSAGE_PIPELINES, dispatchStep, fetchStep };
//# sourceMappingURL=segment-load-pipeline.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,43 @@
import { Segment, SegmentData, Track } from "../../media/types/index.js";
//#region src/playback/primitives/source-buffer-messages.d.ts
type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {
trackId: Track['id'];
/** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */
trackBandwidth?: number;
/**
* Non-zero-PTS relocation: when present, applied as `SourceBuffer.timestampOffset`
* before this append so native PTS is relocated onto a 0-based presentation
* timeline. A relocating composition stamps it (constant per source) onto each
* media segment's meta; the apply is idempotent-guarded. Absent = no relocation.
*/
timestampOffset?: number;
};
type AppendInitMessage = {
type: 'append-init';
data: SegmentData;
/**
* `language` is captured alongside `trackId` so downstream loaders can
* compare the buffered track's language to the newly-selected track's
* language and decide whether ahead-buffer flush is warranted on track
* switch (see `segment-loader`'s `planTasks`). Undefined for video and
* for audio without explicit `LANGUAGE` attribute.
*/
meta: {
trackId: Track['id'];
language?: string;
};
};
type AppendSegmentMessage = {
type: 'append-segment';
data: SegmentData;
meta: AppendSegmentMeta;
};
type RemoveMessage = {
type: 'remove';
start: number;
end: number;
};
type IndividualSourceBufferMessage = AppendInitMessage | AppendSegmentMessage | RemoveMessage;
//#endregion
export { AppendInitMessage, AppendSegmentMessage, AppendSegmentMeta, IndividualSourceBufferMessage, RemoveMessage };
//# sourceMappingURL=source-buffer-messages.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"source-buffer-messages.d.ts","names":[],"sources":["../../../../src/playback/primitives/source-buffer-messages.ts"],"mappings":";;KAUY,oBAAoB,KAAK;EACnC,SAAS;;EAET;;;;;;;EAOA;;KAGU;EACV;EACA,MAAM;;;;;;;;EAQN;IAAQ,SAAS;IAAa;;;KAEpB;EAAyB;EAAwB,MAAM;EAAa,MAAM;;KAC1E;EAAkB;EAAgB;EAAe;;KACjD,gCAAgC,oBAAoB,uBAAuB"}
@@ -0,0 +1,65 @@
import { Cue, Segment } from "../../media/types/index.js";
import { AnySlotMap } from "../../core/composition/create-composition.js";
//#region src/playback/primitives/text-segment-load-pipeline.d.ts
/**
* Resolves a text-track segment URL into the array of cues it contains.
*
* "Resolve" because the fn covers both network fetch and parse into the
* domain model. Host-agnostic the concrete resolver (e.g. the
* browser's native VTT resolver) is supplied at engine-assembly time,
* so this stays DOM-free. A pure `url → cues` primitive (the text
* analog of the v/a loader's `fetchBytes`); composition-awareness lives in
* the injected {@link TextLoadStep}s, not here.
*/
type TextTrackSegmentResolver<C extends Cue = Cue> = (url: string) => Promise<C[]>;
/** Internal load-task descriptor — one segment fetch + dispatch unit. */
interface TextLoadTask {
segment: Segment;
trackId: string;
}
/**
* A text load in mid-pipeline the text analog of the v/a loader's `Frame`.
* `resolveCuesStep` fills `cues`; `dispatchCuesStep` sends them. `metadata` is
* opaque header metadata a resolve step may attach for a later step to read
* (e.g. relocation stashes the `X-TIMESTAMP-MAP` correlation here for its rebase
* step). Typed `unknown` so the generic loader stays host-agnostic the step
* that reads it knows its concrete shape (mirrors `StepDeps.state`).
*/
interface TextFrame<C extends Cue = Cue> {
readonly op: TextLoadTask;
cues?: C[];
metadata?: unknown;
}
/**
* One stage of a text message pipeline the text analog of the v/a loader's
* `LoadStep`. Mutates the {@link TextFrame} in place and may be async; the runner
* checks `signal.aborted` before each step and passes the actor's
* {@link TextStepDeps} on every call, so a stateless step (`resolveCuesStep`) is a
* plain value and a step that needs composition signals (relocation's cue rebase)
* reads them from `deps` at call time.
*/
type TextLoadStep<C extends Cue = Cue> = (frame: TextFrame<C>, signal: AbortSignal, deps: TextStepDeps) => void | Promise<void>;
/**
* The uniform passthrough handed to each {@link TextLoadStep} the composition triple,
* the text analog of `StepDeps`. `state`/`context` are the composition signal maps;
* `config` is the threaded config with the loader's wiring folded in (see
* {@link textStepWiring} + `createTextTrackSegmentLoaderActor`). Typed loose: composition
* steps read `state`; base steps read the folded wiring off `config`.
*/
interface TextStepDeps {
state: AnySlotMap;
context: AnySlotMap;
config: object;
}
/**
* Builds the ordered step list, called **once per actor** (mirrors the v/a loader's
* `MessagePipelines`, but text has a single op type so it's a flat array, not a
* `Record`). The default ({@link DEFAULT_TEXT_MESSAGE_PIPELINES}) is
* `resolveCues → dispatchCues`; a non-zero-PTS composition returns a list that
* inserts a cue-rebase step (see `relocatingTextPipelines`), so the loader stays
* oblivious to relocation.
*/
type TextMessagePipelines<C extends Cue = Cue> = () => TextLoadStep<C>[];
//#endregion
export { TextFrame, TextLoadStep, TextLoadTask, TextMessagePipelines, TextStepDeps, TextTrackSegmentResolver };
//# sourceMappingURL=text-segment-load-pipeline.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"text-segment-load-pipeline.d.ts","names":[],"sources":["../../../../src/playback/primitives/text-segment-load-pipeline.ts"],"mappings":";;;;;;;;;;;;;KA6CY,yBAAyB,UAAU,MAAM,QAAQ,gBAAgB,QAAQ;;UAGpE;EACf,SAAS;EACT;;;;;;;;;;UAWe,UAAU,UAAU,MAAM;WAChC,IAAI;EACb,OAAO;EACP;;;;;;;;;;KAWU,aAAa,UAAU,MAAM,QACvC,OAAO,UAAU,IACjB,QAAQ,aACR,MAAM,wBACI;;;;;;;;UASK;EACf,OAAO;EACP,SAAS;EACT;;;;;;;;;;KAwBU,qBAAqB,UAAU,MAAM,aAAa,aAAa"}
@@ -0,0 +1,37 @@
//#region src/playback/primitives/text-segment-load-pipeline.ts
/**
* Base-step view of the loader's wiring, folded into `config` by
* `createTextTrackSegmentLoaderActor` so base steps read it from the uniform passthrough
* present in both composition and standalone use. `config` is loose (`object`), so
* assert the shape here (mirrors the v/a loader's `stepWiring`). The sink is the
* structural {@link CueSink}, not the concrete actor.
*/
function textStepWiring(deps) {
return deps.config;
}
/** Resolve the op's cues (via the injected host primitive) into the frame. The text analog of `fetchStep`. */
const resolveCuesStep = async (frame, signal, deps) => {
const cues = await textStepWiring(deps).resolveSegment(frame.op.segment.url);
if (signal.aborted) return;
frame.cues = cues;
};
/** Dispatch the frame's cues to the TextTracksActor as `add-cues`. The text analog of `dispatchStep`. */
const dispatchCuesStep = (frame, _signal, deps) => {
const { op } = frame;
textStepWiring(deps).textTracksActor.send({
type: "add-cues",
meta: {
trackId: op.trackId,
id: op.segment.id,
startTime: op.segment.startTime,
duration: op.segment.duration
},
cues: frame.cues ?? []
});
};
/** Tier 0 default: resolve then dispatch. No relocation vocabulary. */
const DEFAULT_TEXT_MESSAGE_PIPELINES = () => [resolveCuesStep, dispatchCuesStep];
//#endregion
export { DEFAULT_TEXT_MESSAGE_PIPELINES, dispatchCuesStep, resolveCuesStep, textStepWiring };
//# sourceMappingURL=text-segment-load-pipeline.js.map
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
import { Cue, Segment } from "../../media/types/index.js";
//#region src/playback/primitives/text-track-messages.d.ts
/** Segment identity and timing — mirrors AppendSegmentMeta without trackId (keyed separately). */
type CueSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {
trackId: string;
};
interface AddCuesMessage<C extends Cue = Cue> {
type: 'add-cues';
meta: CueSegmentMeta;
cues: C[];
}
//#endregion
export { AddCuesMessage, CueSegmentMeta };
//# sourceMappingURL=text-track-messages.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"text-track-messages.d.ts","names":[],"sources":["../../../../src/playback/primitives/text-track-messages.ts"],"mappings":";;;KASY,iBAAiB,KAAK;EAA8C;;UAE/D,eAAe,UAAU,MAAM;EAC9C;EACA,MAAM;EACN,MAAM"}
+61
View File
@@ -0,0 +1,61 @@
//#region src/playback/primitives/track-types.ts
/**
* **Per-type config bundles for per-type behavior variants.**
*
* Each `*_TYPE_CONFIG` constant names the per-type slot keys that
* per-type-variant setup-shape helpers consume. Variants reference the
* shared constant at their `defineBehavior` setup body rather than
* constructing the config inline, so the per-type identity (which state
* slot, which context slot, which discriminant) lives in one place per
* type.
*
* Variants spread their composition-time `config` over the defaults so
* engines can layer composition-supplied additions on top (e.g. a
* custom fetch closure, a non-default segment resolver). Defaults
* first, engine config second.
*
* Helpers continue to consume their slice via the existing typed-key
* generics (`<K extends SelectedTrackKey>` etc.); the extra fields on
* the config object (carrying facets other helpers care about) are fine
* under structural typing.
*/
/**
* Type-identity bundle for video-track-typed behaviors.
*
* @see setupVideoBufferActors, loadVideoSegments
*/
const VIDEO_TYPE_CONFIG = {
type: "video",
selectedKey: "selectedVideoTrackId",
actorKey: "videoBufferActor",
loaderKey: "videoSegmentLoaderActor"
};
/**
* Type-identity bundle for audio-track-typed behaviors.
*
* @see setupAudioBufferActors, loadAudioSegments
*/
const AUDIO_TYPE_CONFIG = {
type: "audio",
selectedKey: "selectedAudioTrackId",
actorKey: "audioBufferActor",
loaderKey: "audioSegmentLoaderActor"
};
/**
* Type-identity bundle for text-track-typed behaviors. Text tracks have
* no `SourceBufferActor` (MSE doesn't apply), so this bundle omits
* `actorKey`. The `loaderKey` points at the text-track-segment-loader
* actor (a `MessageActor` with continue-vs-preempt semantics, parallel
* to v/a's `SegmentLoaderActor`).
*
* @see loadTextTrackSegments, setupTextTrackActors
*/
const TEXT_TYPE_CONFIG = {
type: "text",
selectedKey: "selectedTextTrackId",
loaderKey: "textTrackSegmentLoaderActor"
};
//#endregion
export { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG };
//# sourceMappingURL=track-types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"track-types.js","names":[],"sources":["../../../../src/playback/primitives/track-types.ts"],"sourcesContent":["/**\n * **Per-type config bundles for per-type behavior variants.**\n *\n * Each `*_TYPE_CONFIG` constant names the per-type slot keys that\n * per-type-variant setup-shape helpers consume. Variants reference the\n * shared constant at their `defineBehavior` setup body rather than\n * constructing the config inline, so the per-type identity (which state\n * slot, which context slot, which discriminant) lives in one place per\n * type.\n *\n * Variants spread their composition-time `config` over the defaults so\n * engines can layer composition-supplied additions on top (e.g. a\n * custom fetch closure, a non-default segment resolver). Defaults\n * first, engine config second.\n *\n * Helpers continue to consume their slice via the existing typed-key\n * generics (`<K extends SelectedTrackKey>` etc.); the extra fields on\n * the config object (carrying facets other helpers care about) are fine\n * under structural typing.\n */\n\n/**\n * Type-identity bundle for video-track-typed behaviors.\n *\n * @see setupVideoBufferActors, loadVideoSegments\n */\nexport const VIDEO_TYPE_CONFIG = {\n type: 'video',\n selectedKey: 'selectedVideoTrackId',\n actorKey: 'videoBufferActor',\n loaderKey: 'videoSegmentLoaderActor',\n} as const;\n\n/**\n * Type-identity bundle for audio-track-typed behaviors.\n *\n * @see setupAudioBufferActors, loadAudioSegments\n */\nexport const AUDIO_TYPE_CONFIG = {\n type: 'audio',\n selectedKey: 'selectedAudioTrackId',\n actorKey: 'audioBufferActor',\n loaderKey: 'audioSegmentLoaderActor',\n} as const;\n\n/**\n * Type-identity bundle for text-track-typed behaviors. Text tracks have\n * no `SourceBufferActor` (MSE doesn't apply), so this bundle omits\n * `actorKey`. The `loaderKey` points at the text-track-segment-loader\n * actor (a `MessageActor` with continue-vs-preempt semantics, parallel\n * to v/a's `SegmentLoaderActor`).\n *\n * @see loadTextTrackSegments, setupTextTrackActors\n */\nexport const TEXT_TYPE_CONFIG = {\n type: 'text',\n selectedKey: 'selectedTextTrackId',\n loaderKey: 'textTrackSegmentLoaderActor',\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,oBAAoB;CAC/B,MAAM;CACN,aAAa;CACb,UAAU;CACV,WAAW;AACb;;;;;;AAOA,MAAa,oBAAoB;CAC/B,MAAM;CACN,aAAa;CACb,UAAU;CACV,WAAW;AACb;;;;;;;;;;AAWA,MAAa,mBAAmB;CAC9B,MAAM;CACN,aAAa;CACb,WAAW;AACb"}