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 `` 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