diff --git a/packages/spf/src/playback/actors/dom/segment-loader.ts b/packages/spf/src/playback/actors/dom/segment-loader.ts index 6453e152..e123e0c6 100644 --- a/packages/spf/src/playback/actors/dom/segment-loader.ts +++ b/packages/spf/src/playback/actors/dom/segment-loader.ts @@ -137,22 +137,32 @@ export interface Frame { */ export type LoadStep = (frame: Frame, signal: AbortSignal, deps: StepDeps) => void | Promise; -/** Per-actor + composition dependencies, passed to each {@link LoadStep} on every call. */ +/** + * The uniform passthrough handed to every {@link LoadStep} on every call — the + * composition triple, nothing more. `state`/`context` are the full composition signal + * maps; `config` is the threaded config with the loader's own wiring folded in (see + * {@link stepWiring} + `createSegmentLoaderActor`). All three are typed loose: the + * loader is a conduit and never reads them; a step asserts the slots it knows are + * present (composition steps read `state`; base steps read the folded wiring off + * `config`). + */ export interface StepDeps { - sourceBufferActor: SourceBufferActor; - fetchBytes: FetchBytes; - /** - * The composition deps — full `state`/`context` signal maps + engine `config` — - * passed opaquely so a step that needs composition signals reads them at call - * time (e.g. relocation writing/reading `state.mediaContainerData`). Typed loose: - * the loader is a conduit and never reads them; a step asserts the slots it knows - * the composition provides. Base steps (`fetch`/`dispatch`) ignore them. - */ state: AnySlotMap; context: AnySlotMap; config: object; } +/** + * Base-step view of the loader's own wiring. `createSegmentLoaderActor` folds its + * `sourceBufferActor` + `fetch` into the threaded `config` so base steps read them + * from the uniform passthrough — present whether the loader runs inside a composition + * or standalone. `config` is loose (`object`), so assert the shape here (one cast, like + * relocation's `containerSlot`). + */ +function stepWiring(deps: StepDeps): { sourceBufferActor: SourceBufferActor; fetch: FetchBytes } { + return deps.config as { sourceBufferActor: SourceBufferActor; fetch: FetchBytes }; +} + /** * Builds the ordered step list for each message type. Called **once per actor** * — its role is per-actor instantiation, so stateful steps (relocation's origin @@ -253,13 +263,15 @@ function toMessage({ op, data, meta }: Frame): IndividualSourceBufferMessage { export const fetchStep: LoadStep = async (frame, signal, deps) => { const { op } = frame; if (op.type === 'remove') return; // fetchStep only appears in append pipelines - frame.data = await deps.fetchBytes(op, op.type === 'append-init' ? { signal, minChunkSize: Infinity } : { signal }); + const { fetch } = stepWiring(deps); + frame.data = await fetch(op, op.type === 'append-init' ? { signal, minChunkSize: Infinity } : { signal }); }; /** Dispatch the frame's message to the SourceBufferActor and await its return to idle. */ export const dispatchStep: LoadStep = async (frame, signal, deps) => { - deps.sourceBufferActor.send(toMessage(frame)); - await waitForIdle(deps.sourceBufferActor.snapshot, signal); + const { sourceBufferActor } = stepWiring(deps); + sourceBufferActor.send(toMessage(frame)); + await waitForIdle(sourceBufferActor.snapshot, signal); }; /** Tier 0 default: fetch (for ops that carry bytes) then dispatch. No relocation vocabulary. */ @@ -341,14 +353,21 @@ export function createSegmentLoaderActor( sourceBufferActor: SourceBufferActor, fetchBytes: FetchBytes, config: SegmentLoaderActorConfig = {}, - compositionDeps: Pick = { state: {}, context: {}, config: {} } + compositionDeps: StepDeps = { state: {}, context: {}, config: {} } ): SegmentLoaderActor { type UserState = Exclude; type Ctx = HandlerContext SerialRunner>; const forwardBufferConfig: ForwardBufferConfig = { ...DEFAULT_FORWARD_BUFFER_CONFIG, ...config.forwardBuffer }; const backBufferConfig: BackBufferConfig = { ...DEFAULT_BACK_BUFFER_CONFIG, ...config.backBuffer }; - const deps: StepDeps = { sourceBufferActor, fetchBytes, ...compositionDeps }; + // Fold the loader's own wiring into the passthrough `config` (see `stepWiring`) so + // base steps read it from the uniform `{state,context,config}` — present in both + // composition and standalone use. + const deps: StepDeps = { + state: compositionDeps.state, + context: compositionDeps.context, + config: { ...compositionDeps.config, sourceBufferActor, fetch: fetchBytes }, + }; // Built once per actor (fresh stateful steps per source); default is `fetch → dispatch`. const pipelines = (config.messagePipelines ?? DEFAULT_MESSAGE_PIPELINES)(); diff --git a/packages/spf/src/playback/actors/text-track-segment-loader.ts b/packages/spf/src/playback/actors/text-track-segment-loader.ts index 8a4ba15d..a3b82d98 100644 --- a/packages/spf/src/playback/actors/text-track-segment-loader.ts +++ b/packages/spf/src/playback/actors/text-track-segment-loader.ts @@ -89,24 +89,34 @@ export interface TextFrame { export type TextLoadStep = ( frame: TextFrame, signal: AbortSignal, - deps: TextStepDeps + deps: TextStepDeps ) => void | Promise; -/** Per-actor + composition dependencies, passed to each {@link TextLoadStep} on every call — the text analog of `StepDeps`. */ -export interface TextStepDeps { - textTracksActor: TextTracksActor; - resolveSegment: TextTrackSegmentResolver; - /** - * The composition deps — full `state`/`context` signal maps + engine `config` — - * passed opaquely so a step that needs composition signals reads them at call - * time (e.g. relocation reading the primary A/V track's `startMediaTime`). Typed - * loose: the loader is a conduit and never reads them; base steps ignore them. - */ +/** + * The uniform passthrough handed to each {@link TextLoadStep} — the composition triple, + * the text analog of `StepDeps`. `state`/`context` are the composition signal maps; + * `config` is the threaded config with the loader's wiring folded in (see + * {@link textStepWiring} + `createTextTrackSegmentLoaderActor`). Typed loose: composition + * steps read `state`; base steps read the folded wiring off `config`. + */ +export interface TextStepDeps { state: AnySlotMap; context: AnySlotMap; config: object; } +/** + * Base-step view of the loader's wiring, folded into `config` by + * `createTextTrackSegmentLoaderActor` so base steps read it from the uniform passthrough + * — present in both composition and standalone use. `config` is loose (`object`), so + * assert the shape here (mirrors the v/a loader's `stepWiring`). + */ +export function textStepWiring( + deps: TextStepDeps +): { textTracksActor: TextTracksActor; resolveSegment: TextTrackSegmentResolver } { + return deps.config as { textTracksActor: TextTracksActor; resolveSegment: TextTrackSegmentResolver }; +} + /** * Builds the ordered step list, called **once per actor** (mirrors the v/a loader's * `MessagePipelines`, but text has a single op type so it's a flat array, not a @@ -145,17 +155,17 @@ interface TextLoadTask { // ============================================================================= // Base steps are generic over the cue type `C` (generic arrow consts, not -// `TextLoadStep` values): `TextStepDeps` is contravariant in `C` via -// `textTracksActor.send`, so a `Cue`-typed step wouldn't slot into a `VTTCue` -// pipeline. A generic function assigns to any `TextLoadStep` instantiation. +// `TextLoadStep` values): each touches `C` — `frame.cues: C[]` and the +// `C`-typed `textStepWiring` — so a `Cue`-typed const wouldn't slot into a +// `VTTCue` pipeline. A generic function assigns to any `TextLoadStep`. /** Resolve the op's cues (via the injected host primitive) into the frame. The text analog of `fetchStep`. */ export const resolveCuesStep = async ( frame: TextFrame, signal: AbortSignal, - deps: TextStepDeps + deps: TextStepDeps ): Promise => { - const cues = await deps.resolveSegment(frame.op.segment.url); + const cues = await textStepWiring(deps).resolveSegment(frame.op.segment.url); if (signal.aborted) return; frame.cues = cues; }; @@ -164,10 +174,10 @@ export const resolveCuesStep = async ( export const dispatchCuesStep = ( frame: TextFrame, _signal: AbortSignal, - deps: TextStepDeps + deps: TextStepDeps ): void => { const { op } = frame; - deps.textTracksActor.send({ + textStepWiring(deps).textTracksActor.send({ type: 'add-cues', meta: { trackId: op.trackId, @@ -216,13 +226,20 @@ export function createTextTrackSegmentLoaderActor( // Composition deps threaded opaquely into each step's `TextStepDeps` (relocation // reads composition state). The loader never reads them. Defaults empty for // standalone / base-pipeline use. - compositionDeps: Pick, 'state' | 'context' | 'config'> = { state: {}, context: {}, config: {} } + compositionDeps: TextStepDeps = { state: {}, context: {}, config: {} } ): TextTrackSegmentLoaderActor { type UserState = Exclude; type Ctx = HandlerContext SerialRunner>; const forwardBufferConfig: ForwardBufferConfig = { ...DEFAULT_FORWARD_BUFFER_CONFIG, ...config.forwardBuffer }; - const deps: TextStepDeps = { textTracksActor, resolveSegment, ...compositionDeps }; + // Fold the loader's wiring into the passthrough `config` (see `textStepWiring`) so + // base steps read it from the uniform `{state,context,config}` — present in both + // composition and standalone use. + const deps: TextStepDeps = { + state: compositionDeps.state, + context: compositionDeps.context, + config: { ...compositionDeps.config, textTracksActor, resolveSegment }, + }; // Built once per actor; default is `resolveCues → dispatchCues`. const pipeline = (config.messagePipelines ?? DEFAULT_TEXT_MESSAGE_PIPELINES)(); diff --git a/packages/spf/src/playback/behaviors/dom/relocation-steps.ts b/packages/spf/src/playback/behaviors/dom/relocation-steps.ts index f58359b4..cdb0057d 100644 --- a/packages/spf/src/playback/behaviors/dom/relocation-steps.ts +++ b/packages/spf/src/playback/behaviors/dom/relocation-steps.ts @@ -32,7 +32,12 @@ import { type MessagePipelines, type StepDeps, } from '../../actors/dom/segment-loader'; -import { dispatchCuesStep, type TextLoadStep, type TextMessagePipelines } from '../../actors/text-track-segment-loader'; +import { + dispatchCuesStep, + type TextLoadStep, + type TextMessagePipelines, + textStepWiring, +} from '../../actors/text-track-segment-loader'; import { peekHead } from '../../primitives/head-peek'; import type { EstablishStartMediaTimeState } from '../establish-start-media-time'; @@ -139,14 +144,15 @@ function awaitDefined(read: () => number | undefined): Promise { /** * Resolve step for the relocation text pipeline. Reuses the injected host resolver - * (`deps.resolveSegment`) for cues and fetches the `X-TIMESTAMP-MAP` header in - * parallel, stashing it on `frame.metadata` for `relocateCuesStep`. Replaces the + * (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 `` parser * discards the header, so the map needs its own raw-bytes fetch. */ const resolveWithMetadataStep: TextLoadStep = async (frame, signal, deps) => { const [cues, metadata] = await Promise.all([ - deps.resolveSegment(frame.op.segment.url), + textStepWiring(deps).resolveSegment(frame.op.segment.url), resolveVttSegmentMetadata(frame.op.segment.url), ]); if (signal.aborted) return;