mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
build(spf): build26 from 45504a2b
This commit is contained in:
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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";
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
Reference in New Issue
Block a user