mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 09:59:44 +00:00
298 lines
17 KiB
Markdown
298 lines
17 KiB
Markdown
---
|
||
status: implemented
|
||
date: 2026-05-20
|
||
definition: sketched
|
||
---
|
||
|
||
# Buffer management
|
||
|
||
The engine's segment-level buffer policy: deciding which segments to
|
||
load, which to evict, how to handle the buffered ranges during seeks
|
||
and quality switches. Covers the per-type segment-loader actors that
|
||
plan and execute fetches, the forward-buffer / back-buffer policy
|
||
primitives, and the dispatcher behaviors that turn gate state +
|
||
currentTime + selected tracks into `load` messages.
|
||
|
||
Sits between `preload-modes` (produces the gate state this feature
|
||
consumes) and `mse-mms-pipeline` (owns the `SourceBufferActor` this
|
||
feature's operations land on).
|
||
|
||
This doc captures the **capability surface**: what works, what doesn't,
|
||
which behaviors / actors / primitives implement it, and how
|
||
seek + playback-rate behavior emerges from the planner's design.
|
||
|
||
## Status
|
||
|
||
- **Composition:** `createSimpleHlsEngine` (HLS VoD)
|
||
- **Definition depth:** sketched — capability surface and implementation
|
||
footprint documented; richer forward-buffer / back-buffer policies and
|
||
the codec-change extension (`changeType()`) are tracked as candidates
|
||
|
||
## Phases of complexity
|
||
|
||
What's implemented today, organized as capability slices around the
|
||
engine's buffer policy. Phases are non-orthogonal in places — seek
|
||
handling depends on forward-flush; quality-aware planning depends on
|
||
the in-flight tracking — but each row is a slice that earns its place.
|
||
|
||
| Phase | What | Notes |
|
||
|---|---|---|
|
||
| Forward-buffer planning | Engine maintains ~30 seconds ahead of the playhead via `getSegmentsToLoad`. Re-plans on segment-boundary crossings (debounced via `segmentStartForTime` — within-segment `currentTime` ticks don't trigger re-planning). Configurable via `forwardBuffer.bufferDuration` | Default 30 s; threaded through both dispatcher (range calc) and actor (planner) |
|
||
| Back-buffer eviction | Engine evicts older buffered ranges, keeping `keepSegments` behind the playhead. Bounded memory regardless of session length | Default `keepSegments: 2`; v/a actor only — text-track loader doesn't evict |
|
||
| Quality-aware buffer planning | Skip re-loading a segment if equal-or-higher-quality content is already buffered at the same time slot. Compares `trackBandwidth` on actor-tracked segments | Preserves buffered high-quality content during ABR downgrade; replans on upgrade |
|
||
| Seek handling | Non-contiguous buffer planning (gap-filling for the `[currentTime, currentTime + bufferDuration)` window) + forward-flush after seek-back. The forward-flush prevents unbounded scattered buffer that can cause `QuotaExceededError` on long-form content | Works because the planner is currentTime-driven and operates over actor-tracked buffered segments rather than `SourceBuffer.buffered` ranges directly |
|
||
| In-flight continue / preempt | New `load` message during `'loading'` — if the in-flight segment / init is still needed by the new plan (same track + same id), continue (`abortPending` + schedule remainder); otherwise preempt (`abortAll` + cancel SourceBuffer if needed + replan) | Driven by `inFlightInitTrackId` + `inFlightSegmentId` in actor context; the SourceBuffer cancel is conditional to avoid clobbering an init append that's still needed |
|
||
| Streaming append for media segments | Media segment body stream passed to `SourceBufferActor` as it arrives — chunks appended in flight. Init segments accumulate fully before append (`minChunkSize: Infinity`) | Affects time-to-first-frame for the first segment; init must land complete before media-segment chunks can be parsed |
|
||
| Per-type segment loading | Video / audio / text dispatcher variants share `setupSegmentLoading` helper + (`preload`, `loadActivated`) → FSM mapping. V/A share `SegmentLoaderActor`; text uses `TextTrackSegmentLoaderActor` (no MSE buffer, no codec, no append distinction) | Cross-type constraint: all three variants honor the same gate FSM, so preload behavior is uniform across rendition types |
|
||
| Playback rate | `playbackRate ≠ 1` works (the engine doesn't break) but the forward buffer is not rate-aware — 30 s of media at 2× rate is 15 s of wall-time runway | Today: basic. Consumers can manually override `forwardBuffer.bufferDuration` for higher rates; engine-side adaptation is open work — see *Smarter forward-buffer sizing* below |
|
||
|
||
## What's not implemented
|
||
|
||
Today's policy choices are simple-by-design; richer policies are tracked
|
||
here as candidate phases. The list will grow as motivating use cases
|
||
surface.
|
||
|
||
- **Smarter forward-buffer sizing** — fixed `bufferDuration` regardless
|
||
of context. Three known motivations for a richer policy:
|
||
- **Playback-rate-aware** — at 2× rate, 30 s of media is 15 s of
|
||
wall-time runway; at 0.5× rate, 60 s (wasteful).
|
||
- **`canplaythrough`-equivalent** — bigger buffer when network
|
||
throughput is high enough to "playthrough to end without rebuffer."
|
||
Mirrors HTMLMediaElement's `canplaythrough` event semantics.
|
||
- **Network-conditions / seek-density-aware** — longer runway on slow
|
||
networks; shorter runway during heavy seeking.
|
||
Each variant is a candidate phase row when implemented.
|
||
|
||
- **Smarter back-buffer eviction** — fixed `keepSegments` proactive
|
||
policy today. Alternative shapes to track:
|
||
- **Quota-learning eviction** — record observed `QuotaExceededError`
|
||
byte-thresholds and use them as a per-device ceiling for both
|
||
eviction *and* forward-buffer fetching. Reference point:
|
||
[mux-background-video's `evictBuffer`](https://github.com/muxinc/mux-background-video/blob/main/src/engines/hls-mini/mediasource.ts#L173-L183)
|
||
catches the error and evicts past `currentTime - 5 s`, but doesn't
|
||
remember the threshold for next time. A smarter variant would track
|
||
total buffered bytes; if the error fires at 300 MB but didn't at
|
||
299 MB, future planning keeps the buffer below ~300 MB proactively
|
||
— both for back-buffer eviction and to constrain forward-buffer
|
||
fetching at long `bufferDuration` / high bitrate.
|
||
- **Time-based / size-based** — "keep N seconds" or "keep ≤ M MB"
|
||
rather than segment count.
|
||
|
||
- **Loop-around buffer fetching for `loop=true`** — when the media
|
||
element has `loop=true`, playback wraps from near-end back to 0. The
|
||
planner today sees the wrap as a seek-back (gap-fill works) but
|
||
doesn't *anticipate* it. A loop-aware policy would pre-fetch the
|
||
beginning while still playing the end.
|
||
|
||
- **`changeType()` codec-change buffer transitions** — the planner's
|
||
Case 1 (removes) deliberately does *not* fire on track switch
|
||
("appending new content overwrites existing buffer ranges, and the
|
||
actor's time-aligned deduplication keeps the segment model accurate").
|
||
Same-codec switches work; cross-codec switches don't.
|
||
|
||
- **Buffer health observability** — no surfaced state for "buffered
|
||
ahead" / "back buffer used." Consumers compute from
|
||
`SourceBuffer.buffered` + `state.currentTime` themselves.
|
||
|
||
## Implementation surface
|
||
|
||
**Composition:** `packages/spf/src/playback/engines/hls/engine.ts` —
|
||
dispatchers composed after MSE setup, after `trackCurrentTime` and
|
||
`switchVideoQuality`:
|
||
|
||
```ts
|
||
trackCurrentTime,
|
||
switchVideoQuality,
|
||
loadVideoSegments,
|
||
loadAudioSegments,
|
||
// ...
|
||
syncTextTracks,
|
||
setupTextTrackActors,
|
||
loadTextTrackSegments,
|
||
```
|
||
|
||
The per-type segment-loader **actors** are owned by the MSE setup
|
||
behaviors (`setupVideoBufferActors` / `setupAudioBufferActors` /
|
||
`setupTextTrackActors`) — they're constructed there and published on
|
||
context for these dispatchers to read.
|
||
|
||
**Behaviors:**
|
||
|
||
| Behavior | File | Responsibility |
|
||
|---|---|---|
|
||
| `loadVideoSegments` / `loadAudioSegments` / `loadTextTrackSegments` | `packages/spf/src/playback/behaviors/dom/load-segments.ts` | Per-type dispatcher; shares `setupSegmentLoading` helper. 4-state FSM (`preconditions-unmet` / `dormant` / `metadata-only` / `full-range`) consumes `(preload, loadActivated)` from `preload-modes`. Sends `load` messages to the variant's loader actor |
|
||
|
||
**Actors:**
|
||
|
||
| Actor | File | Role |
|
||
|---|---|---|
|
||
| `SegmentLoaderActor` (v/a) | `packages/spf/src/playback/actors/dom/segment-loader.ts` | 3-state (`idle` / `loading` / `destroyed`). Planner produces ordered `LoadTask` list (Case 1 removes / Case 2 init / Case 3 segments). In-flight continue/preempt via `inFlightInitTrackId` + `inFlightSegmentId`. Owns the actual fetch + append-to-SourceBufferActor sequence |
|
||
| `TextTrackSegmentLoaderActor` | `packages/spf/src/playback/actors/text-track-segment-loader.ts` | Text-track variant satisfying the same `SegmentLoaderLike<Track>` contract for the dispatcher. Different actor shape (no SourceBuffer; appends to `TextTracksActor` cue cache instead) |
|
||
|
||
**Policy primitives (DOM-free):** `packages/spf/src/media/buffer/`
|
||
|
||
| Module | Exports | Role |
|
||
|---|---|---|
|
||
| `forward-buffer.ts` | `getSegmentsToLoad`, `calculateForwardFlushPoint`, `segmentStartForTime`, `ForwardBufferConfig` | Forward buffer planning + segment-boundary debounce primitive |
|
||
| `back-buffer.ts` | `calculateBackBufferFlushPoint`, `BackBufferConfig` | Back buffer eviction policy |
|
||
|
||
**State slots — reads only.** This feature consumes engine state and
|
||
emits actor messages; it doesn't write state.
|
||
|
||
- Reads: `presentation`, `preload`, `currentTime`, `loadActivated`,
|
||
`selectedVideoTrackId` (or audio / text per variant)
|
||
|
||
**Context slots — reads only.**
|
||
|
||
- Reads: `videoSegmentLoaderActor` / `audioSegmentLoaderActor` /
|
||
`textTrackSegmentLoaderActor` (each variant reads only its own type's
|
||
loader-actor slot). The actors themselves are owned and written by
|
||
`setupVideoBufferActors` / `setupAudioBufferActors` /
|
||
`setupTextTrackActors` (under `mse-mms-pipeline` and `subtitles`).
|
||
|
||
**Downstream:** `SegmentLoaderActor` sends `append-init` /
|
||
`append-segment` / `remove` / `cancel` messages to the
|
||
`SourceBufferActor` (`mse-mms-pipeline`). Awaits the actor's `'idle'`
|
||
snapshot between operations rather than awaiting `send()` directly.
|
||
|
||
## Config surface
|
||
|
||
```ts
|
||
{
|
||
forwardBuffer?: Partial<ForwardBufferConfig>;
|
||
// ForwardBufferConfig: { bufferDuration: number } // default 30 (seconds)
|
||
backBuffer?: Partial<BackBufferConfig>;
|
||
// BackBufferConfig: { keepSegments: number } // default 2
|
||
}
|
||
```
|
||
|
||
- `forwardBuffer.bufferDuration` is threaded into the dispatcher
|
||
behaviors (for `range` calculation) and into the v/a + text segment-
|
||
loader actors (for planner forward-flush + load planning).
|
||
- `backBuffer.keepSegments` is threaded into the v/a `SegmentLoaderActor`
|
||
only. The text-track loader doesn't perform back-buffer eviction;
|
||
cue caches are bounded per-track-lifetime instead.
|
||
|
||
## Verification
|
||
|
||
- **Unit tests (behaviors):**
|
||
- `packages/spf/src/playback/behaviors/dom/tests/load-segments.test.ts` —
|
||
dispatcher FSM transitions, range calculation, segment-boundary
|
||
debounce, per-type wiring
|
||
- `packages/spf/src/playback/behaviors/dom/tests/load-segments-track-switch.test.ts` —
|
||
in-flight continue/preempt during track switches (ABR + audio
|
||
rendition); covers the v/a `SegmentLoaderActor` through the
|
||
dispatcher
|
||
- `packages/spf/src/playback/behaviors/dom/tests/track-current-time.test.ts` —
|
||
`currentTime` mirroring (precondition input)
|
||
- **Unit tests (policy primitives):**
|
||
- `packages/spf/src/media/buffer/tests/forward-buffer.test.ts` —
|
||
`getSegmentsToLoad`, `calculateForwardFlushPoint`,
|
||
`segmentStartForTime` algorithm coverage
|
||
- `packages/spf/src/media/buffer/tests/back-buffer.test.ts` —
|
||
`calculateBackBufferFlushPoint` algorithm coverage
|
||
- **Coverage gap:** no direct `packages/spf/src/playback/actors/dom/tests/segment-loader.test.ts`
|
||
for the v/a `SegmentLoaderActor`. The actor's planner, continue/
|
||
preempt logic, and SourceBuffer-cancel decisions are exercised through
|
||
the behavior-level tests. Direct actor-level tests would isolate
|
||
regressions from dispatcher changes.
|
||
- **Sandbox:**
|
||
- `apps/sandbox/src/spf-segment-loading/` — main SPF demo; exercises
|
||
forward-buffer, back-buffer, seek, and ABR-driven quality switches
|
||
end-to-end
|
||
|
||
## Open questions
|
||
|
||
- **Quality-aware filter granularity.** The filter is "≥ bandwidth" —
|
||
it preserves any same-bandwidth or higher-bandwidth content. Should
|
||
it also consider codec / rendition identity (e.g., HEVC and H.264 at
|
||
the same bandwidth might both be "≥" by bps but represent different
|
||
rendition choices)? Today's assumption: bandwidth is the canonical
|
||
quality comparator.
|
||
- **`LoadTask` / `Task` naming collision.** The actor file's own
|
||
`@todo` flags: "LoadTask risks confusion with the Task class used for
|
||
SourceBufferActor scheduling. These are closer to operation
|
||
descriptors or messages than tasks in that sense." Rename pending;
|
||
also affects `planTasks` / `scheduleAll`.
|
||
- **Direct actor-level tests.** Coverage gap noted above. When does the
|
||
cost of behavior-level-only testing exceed the cost of writing actor
|
||
tests?
|
||
|
||
## Related features
|
||
|
||
- **preload-modes** — produces the gate state this feature consumes.
|
||
The 4-state load FSM maps directly to `(preload, loadActivated)`.
|
||
- **source-replacement** — segment-loader actors and in-flight fetches
|
||
tear down via the same resolved/unresolved cascade that drives source
|
||
replacement. New buffer-management behaviors that gate on resolved
|
||
presentation must honor the cleanup contract.
|
||
- **mse-mms-pipeline** — owns the `SourceBufferActor` this feature's
|
||
operations land on, plus the loader-actor lifecycle
|
||
(`setupVideoBufferActors` etc.). The fetch path
|
||
(`createTrackedFetch` for video) is wired in there.
|
||
- **video-abr** — quality-aware buffer planning preserves buffered
|
||
high-quality content during ABR downgrade; bandwidth sampling lands
|
||
in this feature's fetch path (`fetchBytes = createTrackedFetch`).
|
||
- **subtitles** — `loadTextTrackSegments` is the text-track variant of
|
||
this feature's dispatcher; `TextTrackSegmentLoaderActor` is the
|
||
text-side counterpart to `SegmentLoaderActor`.
|
||
- **multi-language-audio** *(coarse)* — Tier 2 audio-flush-on-switch
|
||
will use the `remove` message primitive surfaced through this
|
||
feature's actor.
|
||
- **Audio SourceBuffer flush orchestration** — not a separately-
|
||
scoped feature. The primitives (`SourceBufferActor.remove` message
|
||
+ `flushBuffer` helper) live in
|
||
[mse-mms-pipeline](./mse-mms-pipeline.md) and surface through this
|
||
feature's actor; orchestration on `selectedAudioTrackId` change is
|
||
part of [multi-language-audio](./multi-language-audio.md)'s Tier 2
|
||
mid-stream-switching phase.
|
||
- **[ll-hls-support](./ll-hls-support.md)** *(candidate)* — extends the
|
||
forward-buffer planner with partial-segment-head tracking past the
|
||
last complete segment. Planner extension shape is an open question
|
||
in that feature's doc.
|
||
- **[dvr-event-stream-support](./dvr-event-stream-support.md)**
|
||
*(candidate)* — puts pressure on the back-buffer eviction policy
|
||
(default `keepSegments: 2` evicts history before user can back-
|
||
seek to it). Variant-specific policy vs configurable threshold is
|
||
an open question shared with this doc's "Smarter back-buffer
|
||
eviction" section.
|
||
- **5.1-surround-selection** / **hevc-variant-selection**
|
||
*(candidates)* — cross-codec switches need a `changeType()`
|
||
extension beyond today's same-codec planning.
|
||
|
||
## Use cases that compose this feature
|
||
|
||
- **[`audio-only-mode-override`](../use-cases/audio-only-mode-override.md)**
|
||
*(partial — Phase 1 landed)* — Phase 1 baseline constituent (used
|
||
as-is via `createHlsAudioOnlyEngine`); Phase 3 surfaces
|
||
alternative-default-config candidates (shorter
|
||
`forwardBuffer.bufferDuration` for audio-only) and a Path-B
|
||
candidate (audio-only-tuned buffer-management behavior).
|
||
- **[`video-only-mode-override`](../use-cases/video-only-mode-override.md)**
|
||
*(coarse)* — Phase 1 baseline constituent (used as-is). Loop-
|
||
friendly buffer fetching is the peer `[background-looping-
|
||
video]` use case's concern, not this one's; video-only-mode-
|
||
override's Phase 3 may surface other defaults but not
|
||
loop-around.
|
||
|
||
## See also
|
||
|
||
- [presentation-modeling.md](../presentation-modeling.md) — architectural
|
||
deep-dive on the format-neutral data shape and per-track resolution;
|
||
this feature consumes resolved per-track segments surfaced by that
|
||
layer
|
||
- [text-track-architecture.md](../text-track-architecture.md) — peer
|
||
architectural deep-dive (text-track-loader internals; same SPF
|
||
shape as v/a)
|
||
- [packages/spf/docs/hls-engine.md](../../../../packages/spf/docs/hls-engine.md)
|
||
— engine composition walkthrough (Stage 7: segment loading covers
|
||
this feature's dispatcher + actor surface)
|
||
- [conventions/behaviors.md](../conventions/behaviors.md) — per-type
|
||
specialization details (`setupSegmentLoading` is a canonical
|
||
instance)
|
||
- [conventions/actors.md](../conventions/actors.md) — actor + tasks
|
||
pattern (`SegmentLoaderActor` shape)
|
||
- [conventions/signals.md](../conventions/signals.md) — read-only
|
||
state-slot conventions (this feature consumes the engine's state
|
||
surface without writing to it)
|