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

204 lines
12 KiB
Markdown

---
status: implemented
date: 2026-05-20
definition: 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` / `seeking`** — `loadedmetadata`,
`canplay`, time-threshold crossings, viewport intersection, and similar
hooks aren't wired in. Adding one is local to `trackLoadTriggers`.
- **External-state-write race during attach** — `syncPreload`'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.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.
- **`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
```ts
{
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.
## Related features
- **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.
- **subtitles** — `loadTextTrackSegments` 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
- [conventions/behaviors.md](../conventions/behaviors.md) — behavior
shape; slot-driven FSM pattern (`trackLoadTriggers` is a canonical
instance)
- [conventions/signals.md](../conventions/signals.md) — multi-writer
slot conventions (`loadActivated` is the second concordant data point
alongside `selectedTextTrackId`)
- [clusters.md § Gating / prerequisite chains](./clusters.md#gating--prerequisite-chains)
— cross-cluster pattern this feature exemplifies
- [packages/spf/docs/hls-engine.md](../../../../packages/spf/docs/hls-engine.md)
— engine composition walkthrough (Stage 1 covers `syncPreload` and
`trackLoadTriggers`)