Files
v10/internal/design/spf/features/preload-modes.md

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.
  • loadActivated re-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 / seekingloadedmetadata, canplay, time-threshold crossings, viewport intersection, and similar hooks aren't wired in. Adding one is local to trackLoadTriggers.
  • External-state-write race during attachsyncPreload's read effect runs before the write on mediaElement swap, so a freshly attached element with a W3C preload attribute wins on attach. The read uses peek on state.preload to 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.preloadmediaElement.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.
    • syncPreload read effect (DOM-driven, W3C-only) — copies mediaElement.preload into state.preload on mediaElement swap or presentation.url change, gated on isStandardPreload and on state.preload not already holding an extended value.
    • syncPreload default-backfill — when neither DOM nor external code has supplied a value, backfills from config.defaultPreload.
  • state.loadActivated — multi-writer:
    • trackLoadTriggers writes true from DOM play / seeking listeners, plus immediate-true on entry if !el.paused || el.seeking. Writes false on source-identity reset.
    • Adapter / external code writes true directly 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 (syncPreload for attach-time DOM-read re-firing; trackLoadTriggers for 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 behavior
    • packages/spf/src/playback/behaviors/dom/tests/track-load-triggers.test.ts — FSM transitions, play / seeking triggering, 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-end
    • apps/sandbox/src/simple-hls-html/ / simple-hls-react/ — adapter integration paths showing how SimpleHlsMediaMixin.play() writes loadActivated programmatically alongside the DOM path

Open questions

  • Triggers beyond play / seeking. Should loadedmetadata, canplay, or viewport-intersection hooks contribute to loadActivated? trackLoadTriggers is 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 in resolvePresentation / 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 (selectedTextTrackId is the other current concordant slot; selectedAudioTrackId is the proposed third for multi-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 = false from a yet-to-be-named lifecycle behavior.
  • mse-mms-pipeline — gated indirectly through resolvePresentation. setupMediaSource only fires once the presentation is fully resolved, which only happens once the preload gate opens.
  • video-abr — same: gated indirectly via presentation resolution.
  • subtitlesloadTextTrackSegments uses 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 in loadVideoSegments / loadAudioSegments / loadTextTrackSegments and 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 loadActivated reset 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