import { peek } from "../../core/signals/primitives.js"; import { SerialRunner, Task } from "../../core/tasks/task.js"; import { createMachineActor } from "../../core/actors/create-machine-actor.js"; import { DEFAULT_FORWARD_BUFFER_CONFIG, getSegmentsToLoad } from "../../media/buffer/forward-buffer.js"; import { DEFAULT_TEXT_MESSAGE_PIPELINES } from "../primitives/text-segment-load-pipeline.js"; //#region src/playback/actors/text-track-segment-loader.ts /** * Loads text-track segments for a track and delegates cue management * to a TextTracksActor. Mirrors the v/a `SegmentLoaderActor` shape (FSM * with `idle` / `loading` and `inFlight*` context for continue-vs-preempt), * adapted to text: * * - No init segment, no flush ops (text cues don't need eviction — they're * small and the playhead-relative window is enforced by the runtime). * - Single in-flight identity (`inFlightSegmentId`) — text has only the * media-segment path, no init-segment path. * * Planning is done in the load handler on every incoming message: * `getSegmentsToLoad` filters to the forward window, then the segments * not already in `TextTracksActor`'s context are scheduled. When a new * `load` arrives mid-run, the handler replans and either: * * - **Continues**: the in-flight segment is still in the new plan → * `abortPending` only, schedule the rest of the plan (minus the * in-flight item, which covers its slot). * - **Preempts**: in-flight segment is no longer wanted (track switch, * large seek out of window) → `abortAll`, schedule the new plan * from scratch. * * The cue parser is injected so this factory is host-agnostic. A DOM * host supplies a VTT parser backed by ``/`TextTrack` APIs; a * non-DOM host (worker, test fake, alternate runtime) supplies its own. */ function createTextTrackSegmentLoaderActor(textTracksActor, resolveSegment, config = {}, compositionDeps = { state: {}, context: {}, config: {} }) { const forwardBufferConfig = { ...DEFAULT_FORWARD_BUFFER_CONFIG, ...config.forwardBuffer }; const deps = { state: compositionDeps.state, context: compositionDeps.context, config: { ...compositionDeps.config, textTracksActor, resolveSegment } }; const pipeline = (config.messagePipelines ?? DEFAULT_TEXT_MESSAGE_PIPELINES)(); /** * Translate a load message into an ordered TextLoadTask list based on * committed actor state. In-flight awareness is handled separately in * the `loading` state's load handler. * * Metadata mode (no `range`) is a no-op for text — text tracks have * no init-segment concept, so there's nothing to load until a range * arrives via `'full-range'` dispatch. */ const planTasks = (message) => { const { track, range } = message; if (!range) return []; const trackId = track.id; const bufferedSegments = peek(textTracksActor.snapshot).context.segments[trackId] ?? []; return getSegmentsToLoad(track.segments, bufferedSegments, range.start, forwardBufferConfig).map((segment) => ({ segment, trackId })); }; /** * Wraps a TextLoadTask into a Task that runs the op's step pipeline * (resolve/relocate/dispatch, per the composition's `messagePipelines`). * Updates `inFlightSegmentId` around the async region so the load handler can * make accurate continue/preempt decisions, and checks the abort signal before * each step. * * Text degrades gracefully: a step throwing (e.g. a failed segment fetch) is * logged and swallowed so the runner continues to the next segment — unlike the * v/a loader, where a failed init must abort the remaining tasks. */ const makeLoadTask = (op, { getContext, setContext }) => { return new Task(async (signal) => { if (signal.aborted) return; const frame = { op }; setContext({ ...getContext(), inFlightTrackId: op.trackId, inFlightSegmentId: op.segment.id }); try { for (const step of pipeline) { if (signal.aborted) return; await step(frame, signal, deps); } } catch (error) { console.error("Failed to load text-track segment:", error); } finally { setContext({ ...getContext(), inFlightTrackId: null, inFlightSegmentId: null }); } }); }; const scheduleAll = (tasks, ctx) => { for (const op of tasks) ctx.runner.schedule(makeLoadTask(op, ctx)).then(void 0, (e) => { if (e instanceof Error && e.name === "AbortError") return; console.error("Unexpected error in text-track segment loader:", e); ctx.runner.abortPending(); }); }; return createMachineActor({ runner: () => new SerialRunner(), initial: "idle", context: { inFlightTrackId: null, inFlightSegmentId: null }, states: { idle: { on: { load: (msg, ctx) => { const tasks = planTasks(msg); if (tasks.length === 0) return; ctx.transition("loading"); scheduleAll(tasks, ctx); } } }, loading: { onSettled: "idle", on: { load: (msg, ctx) => { const { context, runner } = ctx; const tasks = planTasks(msg); if (context.inFlightTrackId !== null && context.inFlightSegmentId !== null && tasks.some((t) => t.trackId === context.inFlightTrackId && t.segment.id === context.inFlightSegmentId)) { runner.abortPending(); scheduleAll(tasks.filter((t) => !(t.trackId === context.inFlightTrackId && t.segment.id === context.inFlightSegmentId)), ctx); } else { runner.abortAll(); scheduleAll(tasks, ctx); } } } } } }); } //#endregion export { createTextTrackSegmentLoaderActor }; //# sourceMappingURL=text-track-segment-loader.js.map