12 KiB
status, date
| status | date |
|---|---|
| draft | 2026-05-07 |
SPF evaluation axes
Reference frame for evaluating SPF code — both cleanups of existing code and new feature work added to behaviors, composition, and the surrounding media/network layers. The axes name the dimensions a change is scored on, the decision criteria for each, and the tensions that show up when an axis is pushed in isolation.
Purpose
Establish a stable, reusable lens so that:
- Cleanup pushes are evaluated against the same criteria across iterations and contributors (human or agent).
- Feature additions are scored on the same axes a cleanup would be — preventing regressions that "ship a feature" but degrade the codebase against a stated axis.
- Tradeoffs are made explicitly. Each axis has natural tensions with the others; pushing one without acknowledging tension on another is the normal failure mode.
This doc is the axes. Companion docs (conventions/) define when to reach for which SPF primitive and the patterns those choices entail. Implementation-level "how it works" lives in architecture.md, primitives.md, signals.md, and packages/spf/docs/hls-engine.md — so a Reactor implementation refactor doesn't force editing the "when to use a Reactor" guidance.
Scope
Applies to packages/spf/src/**. The axes are framed broadly enough to apply to VJSv10 outside SPF (especially E — Size, which is cross-cutting), but examples and tensions in this doc are SPF-specific.
The first concrete user is the cleanup pass tracked in .claude/plans/spf/behaviors-and-media-cleanup.md. Future feature work (DRM, audio-only, instant clips, multi-language audio, LL-HLS, codec capability checks, multi-CDN, …) should evaluate the same way.
The five axes
A — Reusability under feature pressure
Decision criterion: can this code accommodate any feature on the upcoming-features pressure list (see Pressure list) without architectural surgery?
"Reusable" is not "supports any imagined consumer" — that produces gold-plating. Reusable means doesn't bake in things we already know we'll need to vary. The pressure list is the concrete forcing function.
Healthy patterns:
- Configuration-driven branching for things that vary per asset/session (DRM, codec, audio-only).
- Behaviors that operate on signal contracts, not specific concrete sources.
- Pre-append wait conditions exposed as composable hooks (DRM key handshake) rather than baked-in
ifchecks.
Anti-patterns:
- Hardcoded video+audio source buffer creation when audio-only is on the pressure list.
- Implicit "HLS parser is always present" assumption baked into behavior bodies.
- Pre-flight checks that can't be extended (e.g., a startup gate that doesn't compose with future DRM gates).
B — Robustness
Decision criterion: does this code make implicit ordering, lifecycle, or state assumptions that break under realistic concurrency, error, or teardown patterns?
The MS/SB lifecycle (MediaSource readiness, SourceBuffer updating state, append/remove sequencing) is the canonical place this axis surfaces in SPF. Brittleness here is rarely about "missing error handling" — it's about implicit protocol assumptions between actors.
Healthy patterns:
- Sequencing made explicit through actor message ordering or signal-derived gates, not timing.
- Teardown that runs under
AbortControllerso partial state never leaks. - Failure modes that surface as state, not silent dead-ends.
Anti-patterns:
- "It works because the previous step always finishes first."
- Cleanup paths that assume happy-path ordering.
- Locks/flags that protect a sequence the type system could have enforced.
C — Patternability
Decision criterion: does this code follow an established SPF pattern? If not, should one exist — and if so, the cleanup produces both the convention and the code that follows it.
Patternability is what makes code subjectively easier to reason about: a reader who has paid the learning cost for the pattern once can scan any conforming code at low cost. This is the axis where the conventions docs are produced — by mining for what is and isn't pattern-shaped.
Healthy patterns:
- Behavior helpers shaped like behaviors (so wiring looks the same).
- Read/write intent annotated uniformly via
Signal<T>/ReadonlySignal<T>typing. - Repeated problems solved the same way at every layer (initial selection, error mapping, teardown).
Anti-patterns:
- Three different ways to extract a helper from a behavior (factory, free function, closed-over signal access).
- Bespoke flag-based state machines (
abrDisabled) when the codebase has primitives for the same job. - One-off naming/shape conventions that don't match any nearby code.
D — Simplicity
Decision criterion: is this code more elaborate than the problem requires? Can existing primitives (Reactor, Task + Runner, Signal, Actor, shareSignals) replace ad-hoc machinery?
Simplicity is about objective complexity reduction — fewer moving parts for the same behavior. This is distinct from C (which is about conformance) and from E (which is about size in compiled output): code can be small but elaborate, or large but trivial.
Healthy patterns:
- Replacing ad-hoc subscribe/cleanup with Reactor when the shape fits.
- Collapsing "two flags and a callback" into a single signal-derived state.
- Using
defineBehaviorconsistently rather than hand-rolled behavior objects.
Anti-patterns:
- Four-step state machines for what is really a derived signal.
- Hand-rolled task scheduling alongside Tasks + Runners.
- Coordination protocols between behaviors that should be one behavior.
E — Size
Decision criterion: does this code minimize compiled output size? Cross-cutting (applies broadly to VJSv10) and uniquely measurable among the axes — pnpm -F @videojs/spf size (public API) and size:all (all exports) provide direct numbers.
Size is its own axis because smaller and simpler are correlated but not identical. Terse code via higher-order abstractions, FP composition, or metaprogramming can shrink output while increasing line-by-line density. That tradeoff is acceptable when (a) the abstraction is a documented convention and (b) comments cover the non-obvious why. Size is also load-bearing for SPF specifically — it is shipped to browsers and the public API surface has measured size budgets.
Healthy patterns:
- Shared helpers with narrow surfaces, exported once, reused broadly.
- Conditional features behind tree-shakable subpath imports rather than runtime branches.
- Small declarative configs over imperative wiring repeated per call site.
Anti-patterns:
- Per-behavior copies of the same wiring scaffold.
- Defensive runtime guards for invariants the type system already enforces.
- Wide enums/discriminants that prevent dead-code elimination.
Tensions
Pushing one axis tends to apply pressure to another. These are the recurring ones:
| Tension | What it looks like |
|---|---|
| A vs D | Pushing assumptions to config can inflate code with branches/options. The right call is per-case: if the assumption is on the pressure list, A wins; if speculative, D wins. |
| A vs E | More configurability = more code paths in the bundle. Mitigate via subpath splitting (/dom, /playback-engine) so each consumer pays only for what they import. |
| A vs C | A new feature may need a new convention. Stretching an existing pattern beyond fit is worse than introducing a new one — but introducing one without writing it down is worse still. |
| B vs D | Defensive code reads as bigger and more elaborate. The real fix is usually to repair the protocol so the guard becomes unnecessary, not to add the guard. |
| B vs E | Same as above, in compiled output terms. |
| C vs D | Conventions can mandate elaborate-feeling shapes (always wrap in X). Sometimes inline is simpler in isolation but harder to scan in aggregate — the convention earns its keep across many call sites, not at any one. |
| C vs E | A convention can either be the size win (shared helpers) or cost size (mandatory wrappers). Conventions doc should note which when it matters. |
| D vs E | Terse-via-HOF/metaprogramming can shrink size while reading denser. Acceptable when the abstraction is a convention with a documented when-to-use. |
Pressure list (axis A target)
The concrete forcing function for A — Reusability. From the Mux Video Content Permutations doc, filtered to items in or imminent for SPF (MPEG-TS explicitly out of near-term scope):
- DRM — Widevine, PlayReady, FairPlay (incl. legacy
com.apple.fps.1_0fallback and AirPlay path); pre-append key handshake gating. - DRM security level configurations — per-asset/session policy enforcement.
- Audio-only streams — no video source buffer; UI/feature suppression.
- Multi-language audio —
AudioTrackListwiring, BCP-47-aware default selection. - Multi-bitrate audio (audio ABR) — quality selection across audio renditions, not just video.
- Instant clips / non-zero PTS — timeline normalization equivalent to hls.js
initPTS/timestampOffset. - LL-HLS — blocking reload, partial segments, delta playlists, preload hints.
- Live / DVR sliding window — manifest re-polling, sliding/growing playlist handling.
- Multi-CDN failover — alternate-URI rotation on persistent rendition failure.
- Codec capability detection — HEVC, 5.1 surround, etc.;
isTypeSupported-based filtering. - Mux platform behaviors — pseudo-ended detection, edit-list compensation, VRLT pacing audit, 1080p+ cap, stream termination (
#EXT-X-ENDLIST), playback token expiry, buffer stall recovery.
When evaluating axis A on a piece of code, the question is "which of these would force me to rewrite this?" — not "could I imagine any future change?"
Mapping back to the original framing
The five axes consolidate the original six goals while preserving their distinctions:
| Original goal | Lands on axis |
|---|---|
| 1. Cleanliness / complexity reduction (with subjective ease of reasoning) | C (subjective half) + D (objective half) |
2. Implementation detail changes (abrDisabled, smarter initial track selection) |
Backlog items, scored on multiple axes — not an axis itself |
| 3. Brittleness | B |
| 4. Reduce assumptions | A |
| 5. Code consistency (helpers conform to behavior shape, establish patterns) | C |
| 6. Code size | E |
Goal 2 is the meaningful exception: it's a category of work, not a cleanup principle. abrDisabled rework scores on C + D + A; smarter initial track selection is feature-leaning and tagged as such on the backlog so it isn't conflated with axes-driven cleanup.
Related docs
internal/design/spf/conventions/(to be drafted) — when to reach for which SPF primitive, paired patterns. Cross-references this doc for axes; cross-referencesarchitecture.md/primitives.mdfor implementation.internal/design/spf/architecture.md— current implementation snapshot.internal/design/spf/primitives.md— Tasks, Actors, Reactors, State.internal/design/spf/signals.md— signals decision and tradeoffs.packages/spf/docs/hls-engine.md— current HLS engine composition walkthrough..claude/plans/spf/behaviors-and-media-cleanup.md— the cleanup pass driving the first concrete use of these axes..claude/plans/spf/behaviors-and-media-assessment.md(to be drafted) — current-state mapping of in-scope code against these axes.