12 KiB
status, date, definition
| status | date | definition |
|---|---|---|
| implemented | 2026-05-20 | sketched |
Preload modes
The engine's loading-semantics implementation: how preload="none|metadata|auto"
plus user / programmatic activation determine when the engine fetches the
manifest, sets up MSE, and starts segment loading. Together, the
syncPreload and trackLoadTriggers behaviors model loading behavior
similar to native HTMLMediaElement playback — state.preload is the
mode, state.loadActivated is the override that fires on
play / seeking (or programmatic intent), and downstream gates read
both.
This doc captures the capability surface: what works, what doesn't, which behaviors / slots implement it, and how downstream features gate on it.
Status
- Composition:
createSimpleHlsEngine(HLS VoD) - Definition depth: sketched — capability surface and implementation footprint documented; the extended-preload-value mechanism is a forward-compatibility hook with no shipped consumer yet
Phases of complexity
What's implemented today, organized as capability slices around the engine's loading-semantics contract.
| Phase | What | Notes |
|---|---|---|
| W3C preload mode honoring | Engine respects preload="none" | "metadata" | "auto" via the isBlockingPreload predicate that downstream gates consume (resolvePresentation, the per-type segment-loading FSM). 'none' is the strictest gate — blocks manifest fetch and all segment loading until activation. 'metadata' resolves manifests + sets up MSE + fetches init segments only. 'auto' runs the full pipeline |
isBlockingPreload in media/utils/preload.ts is the core predicate |
| Bidirectional DOM ↔ state sync | state.preload and mediaElement.preload stay synchronized for W3C values. Effects registered read-before-write so a freshly mounted <video> element's preload attribute wins on attach. All writes are deduped to break echo loops between the two sides |
syncPreload; load-bearing ordering documented in the behavior |
| Extended preload values | External writes of non-W3C values (e.g. 'canplay') are sticky on state.preload: the DOM-side read won't overwrite them, the state-side write won't push them to the DOM |
Consumer-extension point; no shipped consumer reads extended values today |
| Default backfill | state.preload is never undefined in steady state. When neither the DOM attribute nor external code supplies a value, backfilled from config.defaultPreload (default 'metadata', matching the HTMLMediaElement element default) |
Resolves the empty-state ambiguity at most one place: syncPreload's read effect |
| DOM-triggered load activation | DOM play and seeking events on the attached media element flip state.loadActivated to true. Immediate-true on entry if !el.paused || el.seeking — covers autoplay, native-controls play, and direct-DOM-play() paths the engine wasn't the source of |
trackLoadTriggers; mirrors native HTMLMediaElement preload-override semantics |
| Programmatic load activation | External code writes state.loadActivated = true directly — the canonical path in @videojs/spf/hls is SimpleHlsMediaMixin.play(), which signals playback intent before invoking the element's .play(). Co-writer with trackLoadTriggers; same downstream effect |
Multi-writer with intentionally orthogonal decision domains: DOM-event-driven vs programmatic-intent-driven |
| Per-source reset | state.loadActivated resets to false when source identity changes (URL or mediaElement swap, including direct in-place replacement with no undefined intermediate). New source starts at its appropriate preload mode regardless of the previous source's activation history |
Sticky-per-source-identity; not sticky-per-engine |
What's not implemented
- Granular preload between
'metadata'and'auto'— the extended- preload-value mechanism can carry slot values like'firstSegment'or'firstFrame', but no downstream consumer reads them. Extended-value support is a forward-compatibility hook today, not a shipped capability. loadActivatedre-deactivation within source — sticky-true within a source identity. There's no way to "go back to preload-only" mid- source (e.g., to pause-and-evict on backgrounded tabs). A new source reset is the only way to clear the slot.- Activation triggers beyond
play/seeking—loadedmetadata,canplay, time-threshold crossings, viewport intersection, and similar hooks aren't wired in. Adding one is local totrackLoadTriggers. - External-state-write race during attach —
syncPreload's read effect runs before the write onmediaElementswap, so a freshly attached element with a W3Cpreloadattribute wins on attach. The read usespeekonstate.preloadto avoid re-triggering on external writes; external writes still go through but are queued behind the attach-time DOM read.
Implementation surface
Composition: packages/spf/src/playback/engines/hls/engine.ts — both
behaviors are composed at the top of the pipeline, before
resolvePresentation. syncPreload is in the runtime-agnostic
playback/behaviors/ (operates against MediaElementLike);
trackLoadTriggers is in playback/behaviors/dom/ (consumes
HTMLMediaElement events directly).
Behaviors:
| Behavior | File | Responsibility |
|---|---|---|
syncPreload |
packages/spf/src/playback/behaviors/sync-preload.ts |
Bidirectional state.preload ↔ mediaElement.preload sync; sticky extended values; W3C-only DOM writes; deduped to break echo loops |
trackLoadTriggers |
packages/spf/src/playback/behaviors/dom/track-load-triggers.ts |
3-state slot-driven FSM ('preconditions-unmet' ⟷ 'monitoring' ⟷ 'load-active') writing state.loadActivated = true on play / seeking |
Helpers: packages/spf/src/media/utils/preload.ts
| Export | Role |
|---|---|
StandardPreload |
'auto' | 'metadata' | 'none' — the W3C <video>/<audio> preload value type |
isStandardPreload(value) |
Discriminator separating W3C values from extended values |
DEFAULT_PRELOAD |
'metadata' — matches the HTMLMediaElement element default |
isBlockingPreload(preload, defaultPreload?) |
Returns true iff (preload || defaultPreload) === 'none'. Consumed by resolvePresentation and segment-loading gates |
State slots:
state.preload— multi-writer:- External / extended-value writer (sticky) — external code may write
extended values (e.g.
'canplay');syncPreload's read effect leaves them alone, and its write effect doesn't push them to the DOM. syncPreloadread effect (DOM-driven, W3C-only) — copiesmediaElement.preloadintostate.preloadonmediaElementswap orpresentation.urlchange, gated onisStandardPreloadand onstate.preloadnot already holding an extended value.syncPreloaddefault-backfill — when neither DOM nor external code has supplied a value, backfills fromconfig.defaultPreload.
- External / extended-value writer (sticky) — external code may write
extended values (e.g.
state.loadActivated— multi-writer:trackLoadTriggerswritestruefrom DOMplay/seekinglisteners, plus immediate-true on entry if!el.paused || el.seeking. Writesfalseon source-identity reset.- Adapter / external code writes
truedirectly to signal programmatic intent (canonical path:SimpleHlsMediaMixin.play()).
state.presentation— read-only by these behaviors; the URL field is used by both to detect source identity changes (syncPreloadfor attach-time DOM-read re-firing;trackLoadTriggersfor the per-source reset).
Context slots: context.mediaElement (read-only). Both behaviors
re-fire on element swap.
Config surface
{
defaultPreload?: StandardPreload; // default 'metadata' — backfill for
// state.preload AND fallback for
// isBlockingPreload's gate predicate
}
defaultPreload is consumed by both syncPreload (state backfill) and
resolvePresentation (gate fallback) — keeping them in agreement is the
point of having a single config knob.
Verification
- Unit tests:
packages/spf/src/playback/behaviors/tests/sync-preload.test.ts— bidirectional sync, sticky extended values, DOM-attribute-wins-on- attach ordering, dedupe behaviorpackages/spf/src/playback/behaviors/dom/tests/track-load-triggers.test.ts— FSM transitions,play/seekingtriggering, immediate-true on entry, per-source reset, multi-writer coexistence with external writes
- Sandbox:
apps/sandbox/src/spf-segment-loading/— main SPF demo; exercises preload-aware loading end-to-endapps/sandbox/src/simple-hls-html//simple-hls-react/— adapter integration paths showing howSimpleHlsMediaMixin.play()writesloadActivatedprogrammatically alongside the DOM path
Open questions
- Triggers beyond
play/seeking. Shouldloadedmetadata,canplay, or viewport-intersection hooks contribute toloadActivated?trackLoadTriggersis the natural home, but each trigger has different consumer semantics worth working through. - Extended preload value adoption. No shipped consumer reads
extended values today. The sticky-write mechanism is a
forward-compatibility hook; the first consumer (e.g., a
'firstSegment'mode that fetches one segment then stops) would also need a corresponding gate inresolvePresentation/ the segment-load FSM. - Multi-writer factoring on
loadActivated. Today: DOM listener (trackLoadTriggers) + programmatic adapter (SimpleHlsMediaMixin.play()). Two writers, one slot, intentionally orthogonal. As the multi-writer pattern accumulates data points (selectedTextTrackIdis the other current concordant slot;selectedAudioTrackIdis the proposed third formulti-language-audio), a shared coordination primitive may emerge. - Re-deactivation policy. Sticky-true today; no
in-source-identity reset. A future "pause + evict" policy on
backgrounded tabs would push on this — likely a separate writer to
loadActivated = falsefrom a yet-to-be-named lifecycle behavior.
Related features
- mse-mms-pipeline — gated indirectly through
resolvePresentation.setupMediaSourceonly fires once the presentation is fully resolved, which only happens once the preload gate opens. - video-abr — same: gated indirectly via presentation resolution.
- subtitles —
loadTextTrackSegmentsuses the same preload-aware load-mode FSM as audio/video ('dormant' / 'metadata-only' / 'full-range'). - multi-language-audio (coarse) — would inherit the same gating shape for any new audio behaviors.
- buffer-management — the canonical consumer of this feature's
gate state. The 4-state load-mode FSM (
'preconditions-unmet'/'dormant'/'metadata-only'/'full-range') lives inloadVideoSegments/loadAudioSegments/loadTextTrackSegmentsand maps directly to(preload, loadActivated). Forward-buffer + back-buffer plans also build on top of the gates this feature produces. - source-replacement — the per-source
loadActivatedreset is one piece of the broader source-replacement cleanup cascade. New behaviors that join the engine and gate on resolved presentation must honor that cascade or in-place source replacement breaks silently.
See also
- conventions/behaviors.md — behavior
shape; slot-driven FSM pattern (
trackLoadTriggersis a canonical instance) - conventions/signals.md — multi-writer
slot conventions (
loadActivatedis the second concordant data point alongsideselectedTextTrackId) - clusters.md § Gating / prerequisite chains — cross-cluster pattern this feature exemplifies
- packages/spf/docs/hls-engine.md
— engine composition walkthrough (Stage 1 covers
syncPreloadandtrackLoadTriggers)