Files
v10/dist/dev/playback/actors/text-track-segment-loader.js
T

149 lines
5.4 KiB
JavaScript

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