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