---
status: implemented
date: 2026-05-20
definition: sketched
---

# Video ABR

Bandwidth-driven adaptive bitrate selection for video. The engine measures throughput from in-progress segment fetches, maintains a dual-EWMA bandwidth estimate, and dynamically picks the highest-quality video track that fits the current bandwidth — with hysteresis to prevent oscillation. Manual track overrides coexist with ABR via a single constraint slot.

This doc captures the **capability surface**: what works, what doesn't, which behavior implements it, and how it relates to other features.

## Status

- **Composition:** `createSimpleHlsEngine` (HLS VoD)
- **Definition depth:** sketched — algorithm, config, state flow documented; deeper algorithm justification (why Shaka's dual-EWMA, why these margins) is not yet written down

## Phases of complexity

| Phase | What | Notes |
|---|---|---|
| Bandwidth-aware initial selection | On presentation resolve, pick the highest track that fits `initialBandwidth` (default 5 Mbps) | Custom `picker` config can override (e.g., "first track regardless of bandwidth, then let ABR adjust") |
| Dynamic bandwidth-driven adjustment | Re-pick whenever `bandwidthState` changes — downgrade immediately, upgrade only past `upgradeMargin` headroom | Hysteresis is structural (margin check), not temporal (no smoothing window beyond EWMA itself) |
| Dual-EWMA bandwidth estimate | Fast (2s half-life) + slow (5s half-life) accumulators; ABR consumes `min(fast, slow)` | Asymmetric by design — adapts down rapidly, upgrades sluggishly. Zero-factor correction applied at read time, not sample time |
| Safety margin on candidate selection | `selectQuality` requires `currentBandwidth >= track.bandwidth / safetyMargin` (default 0.85 = 15% headroom) | Pairs with `upgradeMargin` to prevent thrash; track uses ≤85% of measured bandwidth |
| Manual override via `userVideoTrackSelection` | Consumer sets `userVideoTrackSelection = { id: 'X' }` to narrow candidates; when only one remains, ABR short-circuits | No `abrDisabled` flag — the constraint pattern is the mechanism |
| ABR re-enable | Clear `userVideoTrackSelection` to restore dynamic selection | Symmetric with override |

## What's not implemented

- **Audio ABR** — sampling infrastructure exists (`createTrackedFetch`), but `setupAudioBufferActors` uses plain `fetchStream` instead. Wiring it through is a one-liner; a separate audio-quality-switching behavior would still be needed. See [audio-abr.md](./audio-abr.md) for the design surface — bandwidth-state sharing, multi-writer coordination with multi-language-audio's Tier 2, EWMA accumulator design under mixed-source sampling.
- **Rendition selection caps** — no max-height, max-bitrate, or max-FPS constraints on the candidate set. `selectQuality` operates over all video tracks. See [rendition-selection-caps.md](./rendition-selection-caps.md) for the umbrella feature covering billing-driven, viewport-driven, max-bitrate, and max-FPS caps.
- **Screen-size / viewport adaptation** — initial pick *could* be viewport-aware via custom `picker`, but ABR re-selection ignores device dimensions.
- **Non-bandwidth signals** — CPU/thermal throttling, network-type (WiFi vs. cellular), battery state are not factored. See [multi-signal-abr](./multi-signal-abr.md) for the scope — five per-signal phases (network / thermal / battery / visibility / customer preference) + a signal-fusion algorithm-extension phase. Bias-factor weighting is the leaning fusion strategy.
- **Buffer fullness as ABR input** — forward buffer (default 30s) gates *when segment loading starts*, not which track is selected. Bandwidth is the sole ABR input.
- **Pluggable strategy** — EWMA + hysteresis are fixed. Custom logic is only injectable for *initial* selection via `picker`; ABR re-evaluation always uses `selectQuality`.

## Implementation surface

**Composition:** `packages/spf/src/playback/engines/hls/engine.ts` — composed in place of `selectVideoTrack` (the two are alternatives; only one writes `selectedVideoTrackId` for a given engine).

**Behaviors:**

| Behavior | File | Responsibility |
|---|---|---|
| `switchVideoQuality` | `packages/spf/src/playback/behaviors/quality-switching.ts` | ABR-driven `selectedVideoTrackId` lifecycle: pick default, adjust on bandwidth changes, clear on src unload |

**Algorithm:**

| Module | File | Role |
|---|---|---|
| `selectQuality` | `packages/spf/src/media/abr/quality-selection.ts` | Pure function: given candidate tracks, current bandwidth, and config, return the track to pick (applies safety margin + upgrade margin) |
| `sampleBandwidth` / `getBandwidthEstimate` | `packages/spf/src/network/bandwidth-estimator.ts` | Dual-EWMA accumulator update + read-time estimate computation (with zero-factor correction) |
| `createTrackedFetch` | `packages/spf/src/network/fetch.ts` | Fetch wrapper that emits per-chunk bandwidth samples; consumed by `setupVideoBufferActors` to populate `bandwidthState` |

**State slots:**

- **Reads:** `presentation`, `bandwidthState`, `selectedVideoTrackId` (when constrained), `userVideoTrackSelection`
- **Writes:** `selectedVideoTrackId` (sole writer for ABR-driven cases; `userVideoTrackSelection` is the consumer-facing constraint, not a direct write to selection)
- **Cross-feature dependency:** `bandwidthState` is written by `setupVideoBufferActors` (sampling baked into segment-loader fetch), not by an ABR-specific behavior. Sampling and selection are decoupled — sampling lives with segment loading; selection lives with ABR.

**Alternative composition:** `selectVideoTrack` (in `packages/spf/src/playback/behaviors/select-tracks.ts`) is a simpler "pick first track" default; not composed in the current HLS engine but available for engine variants that don't want ABR (e.g., a fixed-quality engine). The two behaviors are mutually exclusive — both write `selectedVideoTrackId`.

## Config surface

```ts
{
  bandwidth?: {
    fastHalfLife?: number;        // default 2000ms — fast EWMA reactivity
    slowHalfLife?: number;        // default 5000ms — slow EWMA stability
    minTotalBytes?: number;       // sample filtering thresholds
    minBytes?: number;
    minDuration?: number;
  };
  quality?: {
    safetyMargin?: number;        // default 0.85 — track-bandwidth headroom
    upgradeMargin?: number;       // default 1.15 — upgrade hysteresis margin
  };
  initialBandwidth?: number;      // default 5_000_000 — bps, fallback before samples
  picker?: TrackPicker<QualitySwitchingConfig>;  // override initial selection
}
```

`picker` is invoked once when the slot is empty in `'presentation-resolved'` state. If it returns `undefined`, falls back to the bandwidth-aware default pick. ABR re-evaluation (after initial selection) always uses `selectQuality` — `picker` does not participate.

## Verification

- **Unit tests:**
  - `packages/spf/src/playback/behaviors/tests/quality-switching.test.ts` (~450 lines) — lifecycle (clear on unload, re-pick after reset), default-pick with initialBandwidth fallback, downgrade/upgrade hysteresis, user-constraint narrowing + ABR short-circuit, picker override, effect re-firing on bandwidth changes
  - `packages/spf/src/network/tests/bandwidth-estimator.test.ts` — EWMA math, zero-factor correction, sample filtering
  - `packages/spf/src/media/abr/tests/quality-selection.test.ts` — `selectQuality` algorithm in isolation
- **Sandbox:** `apps/sandbox/src/spf-segment-loading/` exposes the ABR surface: current bandwidth estimate (fast/slow), track list with bitrates, selected track + mode label, manual override buttons, ABR re-enable button. Tuning knobs (margins, half-lives) are not surfaced — they use defaults.

## Related features

- **single-video-track-default-selection** *(not yet documented)* — `selectVideoTrack` behavior, alternative to `switchVideoQuality` for non-ABR engine variants.
- **buffer-management** — `bandwidthState` is written by `setupVideoBufferActors` via `createTrackedFetch`; samples land on `buffer-management`'s fetch path (`fetchBytes` inside `SegmentLoaderActor`). Sampling is structurally co-located with segment loading, not ABR.
- **bandwidth-estimation** *(coarse, not yet documented)* — dual-EWMA accumulator is a reusable primitive in `network/`. Could be promoted to its own feature doc when audio ABR or other consumers arrive.
- **[audio-abr](./audio-abr.md)** — parallel-sibling feature on the audio axis. Same sampling-baked-into-loading pattern, same `selectQuality` algorithm reuse, same constraint-slot manual-override. Audio-specific design surface (bandwidth-state sharing, multi-writer coordination with multi-language-audio Tier 2) captured there.
- **[rendition-selection-caps](./rendition-selection-caps.md)** — billing-driven (1080p+), viewport-driven (screen-size), max-bitrate, and max-FPS caps. All filter the candidate set before `selectQuality` runs; `userVideoTrackSelection` is the constraint+filter precedent that feature builds on.
- **[multi-signal-abr](./multi-signal-abr.md)** — algorithm extension on `selectQuality`'s input axis. CPU/thermal throttling, network type, battery state, viewport visibility, customer preference as additional inputs. Distinct from rendition-selection-caps (constraint+filter): multi-signal-abr biases selection within the candidate set rather than narrowing it.
- **[capability-probing](./capability-probing.md)** — narrows the candidate set ABR operates over. `selectQuality` doesn't change shape; just sees a filtered candidate set with browser-unsupported renditions already excluded.
- **[hevc-variant-selection](./hevc-variant-selection.md)** — codec-aware variant filtering (HEVC if supported, AVC fallback). Same constraint+filter pattern as `userVideoTrackSelection`; narrows the candidate set ABR operates over by codec axis. Tier 2 mid-stream `changeType()` phase introduces cross-codec ABR (no algorithm change, just buffer-side switching).

## Use cases that compose this feature

- **[`video-only-mode-override`](../use-cases/video-only-mode-override.md)** *(coarse)* — Phase 1 baseline constituent. The video-only delivery variant composes video-abr as-is for multi-bitrate video selection.

## See also

- [presentation-modeling.md](../presentation-modeling.md) — architectural deep-dive on the format-neutral data shape; ABR consumes resolved video tracks surfaced by the parser interface this layer defines.
- [preload-modes.md](./preload-modes.md) — gates this feature indirectly via `resolvePresentation`; ABR can't fire until the presentation is resolved, and resolution is gated by the preload-modes contract.
- [mse-mms-pipeline.md](./mse-mms-pipeline.md) — where `setupVideoBufferActors` lives and where `createTrackedFetch` is wired into segment-loader construction; bandwidth sampling is structurally co-located with MSE buffer setup, ABR selection is the separate consumer documented here.
- [conventions/behaviors.md](../conventions/behaviors.md) — when to define a behavior; behavior shape
- [conventions/signals.md](../conventions/signals.md) — multi-writer slot conventions (relevant for `selectedVideoTrackId` write coordination if audio ABR follows the same pattern)
- [packages/spf/docs/hls-engine.md](../../../../packages/spf/docs/hls-engine.md) — full HLS engine composition walkthrough
