12 KiB
status, date, definition
| status | date | definition |
|---|---|---|
| implemented | 2026-05-20 | sketched |
Engine-adapter integration
The engine's external-driving contract: shareSignals exposes the
composition's writable + readonly signal refs to a consumer callback at
setup time, and SimpleHlsMediaMixin is the canonical adapter that
maps a WHATWG HTMLMediaElement-shaped API onto those refs. The
audience for this feature is adapter authors and contributors who
need to drive the engine from outside — not end users, who see the
adapter's API only through whatever wraps it (e.g.,
packages/core's SimpleHlsMedia class).
The feature ships as a pair: the framework-level shareSignals
mechanism + the canonical mixin. New adapter shapes (React hooks, RN
bridges, etc.) would compose on top of shareSignals independently of
the mixin.
Status
- Composition:
createSimpleHlsEngine(HLS VoD);shareSignalscomposed last so other behaviors' setups have run by the time the callback fires - Definition depth: sketched — capability surface and the adapter-rationale open question both documented
Phases of complexity
| Phase | What | Notes |
|---|---|---|
Writable signal refs via onSignalsReady |
shareSignals captures Signal<T> / ReadonlySignal<T> refs into a consumer-supplied callback at setup time. Generic over composition shape (makeShareSignals<S, C>()) |
Per-slot read/write intent is expressed at the use site (callers type captured refs as Signal<T> or ReadonlySignal<T>). Composed last in the engine so initial state writes are visible to the consumer |
| Mixin adapter pattern | SimpleHlsMediaMixin is the canonical consumer: function-of-base-class structure (mix into any base), captures refs once in onSignalsReady, exposes a WHATWG HTMLMediaElement-shaped API mapping each setter/method to engine writes |
Downstream use: class SimpleHlsMedia extends SimpleHlsMediaMixin(HTMLVideoElementHost) {} in packages/core/src/dom/media/simple-hls/ |
| Media element binding | attach(el) writes context.mediaElement; detach() clears it. Engine persists across attach/detach cycles — only src reassignment or explicit destroy() tears it down |
Re-attach to a different element is supported. The engine is the durable state holder; mediaElement is a context slot |
| Source assignment via destroy + recreate | Adapter's set src destroys the current engine and creates a fresh one, re-applies any explicit preload, re-attaches mediaElement to the new engine, and writes the new { url } |
Bypasses the in-place source-replacement path. Rationale not documented in code — see Open questions and source-replacement.md |
| Preload reflection | set preload(value) writes W3C values to state.preload; clearing (preload = '') doesn't patch the current engine but is re-applied on the next src change. Pre-attach src + preload combinations are supported |
Extended preload values flow through state but don't reach the DOM (per preload-modes's sticky-extended-values semantics) |
Programmatic play() with retry |
play() writes state.loadActivated = true (co-writer with trackLoadTriggers's DOM listener path) before invoking native play. Defensive retry: if native play rejects with "no supported sources" while src is pending, wait for loadstart (MSE attaches blob URL) and retry once |
The retry handles MSE pipeline timing — adapter doesn't know exactly when MSE setup attaches the blob URL. Listener canceled on src change |
What's not implemented
- Reactive change-notification surface —
onSignalsReadyfires once at setup. Consumers wanting to react to state changes from outside the engine must keep refs and subscribe via SPF primitives (effect(), signalsubscribe()). The adapter doesn't expose curatedonPlay/onSrcChange/onErrorcallbacks. - Multiple engine instances per adapter — one engine per adapter instance. No built-in pattern for multi-engine scenarios (picture-in-picture with two streams, A/B testing).
- Non-HTMLMediaElement adapter shapes — React-friendly hooks,
React Native bridges, etc. would compose on top of
shareSignalsindependently. Today the canonical adapter is HTMLMediaElement- shaped via the mixin. No bracketed candidate features tracked yet; add when concrete need surfaces. - Curated state / error introspection — consumers can read
signals.state.*.get()directly, but there's no adapter-level "current playback state" / "current error" shape that doesn't require knowing the engine's signal map.
Implementation surface
Composition: packages/spf/src/playback/engines/hls/engine.ts —
shareSignals is the last behavior in the composition. Instantiated
once at module load:
const shareSignals = makeShareSignals<SimpleHlsEngineState, SimpleHlsEngineContext>();
// ...
return createComposition(
[
// ... all other behaviors ...
shareSignals,
],
{ config, initialState }
);
Behavior factory:
| Export | File | Role |
|---|---|---|
makeShareSignals<S, C>() |
packages/spf/src/core/composition/share-signals.ts |
Generic behavior factory. Returns a Behavior<StateSignals<S>, ContextSignals<C>, ShareSignalsConfig<S, C>> whose setup invokes config.onSignalsReady?.({ state, context }) |
ShareSignalsConfig<S, C> |
same | Config interface carrying the onSignalsReady callback |
Canonical adapter:
| Export | File | Role |
|---|---|---|
SimpleHlsMediaMixin<Base> |
packages/spf/src/playback/engines/hls/adapter.ts |
Function-of-base-class mixin. Captures refs in onSignalsReady, exposes WHATWG HTMLMediaElement-shaped API |
SimpleHlsMediaElement |
same | Standalone subclass: SimpleHlsMediaMixin(class {}). Bare-bones reference instance |
SimpleHlsMediaProps / SimpleHlsMediaAPI |
same | The adapter's public-facing shape |
Adapter ↔ engine state/context map:
| Adapter call | Engine write |
|---|---|
attach(el) |
context.mediaElement.set(el) |
detach() |
context.mediaElement.set(undefined) |
destroy() |
engine.destroy() |
set src(value) |
engine.destroy() → new engine → state.presentation.set({ url: value }) |
set preload(value) |
state.preload.set(value) (W3C values only; pre-empties stay engine-local) |
play() |
state.loadActivated.set(true) → native play() with loadstart retry on "no supported sources" |
Downstream consumer: packages/core/src/dom/media/simple-hls/index.ts:
export class SimpleHlsMedia extends SimpleHlsMediaMixin(HTMLVideoElementHost) {}
This is the canonical end consumer — used wherever the HTML player expects an HTMLMediaElement-shaped object backed by SPF.
Config surface
// ShareSignalsConfig<S, C>
{
onSignalsReady?: (signals: {
state: StateSignals<S>;
context: ContextSignals<C>;
}) => void;
}
The HLS engine config (SimpleHlsEngineConfig) extends
ShareSignalsConfig<SimpleHlsEngineState, SimpleHlsEngineContext>, so
onSignalsReady is part of the engine's config surface.
SimpleHlsMediaMixin's constructor takes optional config and threads
it through to every engine instance (including the ones created on
each set src).
Verification
- Unit tests:
packages/spf/src/playback/engines/hls/tests/adapter.test.ts— extensive coverage: src assignment / re-assignment / clear, engine recreation on src change, mediaElement preservation across src changes, play retry onloadstart, preload propagation, attach/detach lifecyclepackages/spf/src/playback/engines/hls/tests/engine.test.ts→ "allows patching state and owners from outside" — direct engine-level write surface (bypasses the mixin)packages/spf/src/core/composition/tests/share-signals.test.ts— the behavior itself
- Downstream usage:
packages/core/src/dom/media/simple-hls/index.ts—SimpleHlsMediaconsumer
- Walkthrough:
packages/spf/docs/hls-engine.mdStage 10 — high-level coverage of the pattern
Open questions
- Destroy-recreate vs in-place source replacement. The canonical
adapter destroys + recreates the engine on every
srcchange, even though the engine's behaviors support in-placestate.presentationoverwrite (validated bysource-replacement's test). The rationale isn't documented in the code or commit history. Possible motivations: stricter isolation between sources; simpler reasoning per-engine; guarding against latent cleanup-cascade bugs. Worth resolving when the cost of either choice surfaces. - Callback timing semantics.
shareSignals's JSDoc explicitly notes the callback fires while other behaviors are still in setup; reads inside the callback may yield only initial-seed values. The documented use is "capture refs, use later." Is read-at-setup-time ever a supported case, or always discouraged? - Mixin base-class genericity.
SimpleHlsMediaMixin<Base extends Constructor<any>>accepts any base; today's only documented consumer isHTMLVideoElementHost. Other bases are structurally allowed but not exercised — if usage broadens, the contract may need tightening.
Related features
- preload-modes — adapter
set preload(value)andplay()are external writers onstate.preloadandstate.loadActivatedrespectively. The adapter's preload-clearing semantics (clear#preloadbut don't patch the current engine) interact withpreload-modes's sticky-extended-values rule. - source-replacement — adapter
set srcis the canonical user-facing entry into source replacement. The adapter's destroy-recreate path bypasses the in-place reactor cascade — seesource-replacement.mdfor the in-place contract and the same open question. - mse-mms-pipeline — adapter
attach(el)binds the element MS attaches to. The engine handles the rest of the MS lifecycle via the resolved/unresolved cascade. - audio-playback / subtitles / video-abr / buffer-management — all driven by engine state the adapter writes through. The adapter doesn't expose these features' surfaces directly; consumers read engine state via the captured signal refs.
Use cases that compose this feature
audio-only-mode-override(partial — Phase 1 landed) — Phase 1 baseline constituent with an alternative adapter shape. The variant ships an independentSimpleHlsAudioOnlyMediaElementadapter (viaSimpleHlsAudioOnlyMediaMixin) parallel toSimpleHlsMediaElement; theshareSignalsmechanism + mixin pattern compose unchanged. The consumer-facing API matches the WHATWGHTMLMediaElementsurface.video-only-mode-override(coarse) — Phase 1 baseline constituent on the inverse axis. Ships an independentSimpleVideoOnlyHlsMediaElement-style adapter parallel toSimpleHlsMediaElement. SameshareSignalspattern; consumer-facing API differs from both default and audio-only-mode-override.
See also
- clusters.md § Engine lifecycle
- packages/spf/docs/hls-engine.md § Stage 10
—
shareSignalsand the adapter pattern walkthrough - conventions/signals.md — per-slot
Signal<T>/ReadonlySignal<T>intent (relevant for how consumers type captured refs at the use site) packages/core/src/dom/media/simple-hls/index.ts— canonical downstream consumer