diff --git a/internal/design/spf/architecture.md b/internal/design/spf/architecture.md new file mode 100644 index 00000000..028db5c6 --- /dev/null +++ b/internal/design/spf/architecture.md @@ -0,0 +1,253 @@ +--- +status: draft +date: 2026-03-11 +--- + +# Architecture + +> **This document describes the current SPF codebase as a snapshot in time — not the target design.** The architecture, implementation details, and component boundaries documented here are highly tentative and subject to significant change. The initial implementation captured useful lessons (source buffer coordination, end-of-stream timing, streaming response bodies, etc.), but the underlying architecture, primitives, and structure are expected to be substantially reworked in the near term. See [primitives.md](primitives.md) for the forward-looking design. + +Internal structure of SPF. + +## Overview + +``` +┌─────────────────────────────────────────────────────────┐ +│ core/ (DOM-free) │ +│ state ─ actor ─ task ─ HLS parser ─ ABR ─ buffer math │ +└─────────────────────────────────────────────────────────┘ + │ +┌──────────────────────────▼──────────────────────────────┐ +│ dom/ (browser) │ +│ │ +│ PlaybackEngine │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Reactors (features/) │ │ +│ │ loadSegments · endOfStream · setupMediaSource │ │ +│ │ setupSourceBuffers · qualitySwitching · ... │ │ +│ └─────────────────┬────────────────────────────────┘ │ +│ │ send messages │ +│ ┌─────────────────▼──────────────┐ │ +│ │ Actors │ │ +│ │ SegmentLoaderActor │ │ +│ │ SourceBufferActor (×2) │ │ +│ └─────────────────┬──────────────┘ │ +│ │ execute │ +│ ┌─────────────────▼──────────────┐ │ +│ │ MSE │ │ +│ │ MediaSource · SourceBuffer │ │ +│ └────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +The `core/` layer is runtime-agnostic — no DOM APIs, no fetch. The `dom/` layer wires browser platform APIs (MSE, fetch, HTMLMediaElement) into the core abstractions. + +--- + +## Core Layer + +### Reactive State (`core/state/create-state.ts`) + +A batched, subscription-based state container. All feature coordination flows through state. + +```ts +interface State { + get(): S; + patch(partial: Partial): void; + flush(): void; + subscribe(listener: Listener): () => void; +} +``` + +**Key behavior:** +- `patch()` defers via `queueMicrotask` — multiple synchronous patches are coalesced into one notification. +- `flush()` drains the pending patch immediately. Call when downstream subscribers need to react before the next tick (e.g., ABR sampling). +- Selector subscriptions fire only when the selected slice changes, using a custom equality function. + +### Actor (`core/actor.ts` + `core/task.ts`) + +An actor owns a `snapshot` (status + context) and serializes its own work via a runner. + +```ts +interface Actor { + snapshot: ActorSnapshot; + subscribe(listener: () => void): () => void; +} +``` + +**Task** — wraps an async function with an `AbortController`. Abortable at any point. + +**SerialRunner** — executes tasks one at a time. Used by SourceBufferActor because the SourceBuffer API is inherently serial (one `appendBuffer` at a time). + +**ConcurrentRunner** — deduplicates by ID. Used where parallel work is safe but duplicate tasks are wasteful. + +### HLS Parsing (`core/hls/`) + +Parses multivariant and media playlists into typed structures (`Presentation`, `Track`, `Segment`). URL resolution is handled separately in `resolve-url.ts`, making the parsers pure functions of text input. + +### ABR (`core/abr/`) + +Two components: + +**EWMA** — fast/slow exponentially weighted moving average pair. The fast weight tracks recent conditions; the slow weight anchors against outliers. Exported estimate is the minimum of both (conservative). + +**Quality selection** (`quality-selection.ts`) — given a bandwidth estimate and a list of tracks sorted by bitrate, picks the highest track whose bitrate fits within the estimate. Upgrades are subject to a `minUpgradeInterval` gate (default 8 s) to prevent oscillation; downgrades are immediate. + +> **Zero-factor correction:** Raw EWMA starts near zero. The displayed/used estimate must apply `estimate / (1 - α^totalWeight)` to correct for the initialization bias. + +### Buffer Math (`core/buffer/`) + +**Forward buffer** (`forward-buffer.ts`) — computes the target load window: `[currentTime, currentTime + forwardBufferDuration]`. Also computes the flush point (segments behind `currentTime - backBufferDuration`). + +**Back buffer** (`back-buffer.ts`) — computes the portion of the buffer to evict when the engine is under memory pressure. + +--- + +## DOM Layer + +### PlaybackEngine (`dom/playback-engine/engine.ts`) + +The orchestration hub. Initializes all features in a fixed order, wiring shared state, owners, and a single event stream. + +**Feature init order:** + +| Step | Feature | Purpose | +|------|---------|---------| +| 0a | `syncPreloadAttribute` | Read `preload` attr from `