Files
v10/dist/dev/playback/primitives/relocation-pipelines.js
T

203 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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