mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
203 lines
8.5 KiB
JavaScript
203 lines
8.5 KiB
JavaScript
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
|