Files
v10/internal/design/spf/features/rendition-selection-caps.md
T

229 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: draft
date: 2026-05-20
definition: technical
---
# Rendition selection caps
Policy-driven constraints that narrow the candidate set of video
renditions before quality selection runs. Each cap is a separate
constraint slot read by `selectQuality` (or the ABR equivalent) and
applied as a filter on `presentation.videoTracks` before selection.
The selection slot itself (`selectedVideoTrackId`) remains
single-writer; caps participate as constraints, not as competing
selectors.
A **Player feature** in the framing from
[clusters.md § Feature classification axes](./clusters.md#feature-classification-axes):
additive functionality not tied to making any source play, used by
the player to bias delivery for billing, device, viewport, or
other consumer-side reasons.
## Status
- **Composition:** not implemented in `createSimpleHlsEngine`. Today
`selectQuality` operates over all `presentation.videoTracks` with
no candidate-set narrowing beyond `userVideoTrackSelection`
(single-track manual override) and bandwidth-driven safety
margins.
- **Definition depth:** technical — scope and constraints
articulated; no implementation. Source material: [SPF Epics
Working Doc](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
Cluster E entries #13 (1080p+ Resolution Cap, eng S, validation S)
and NEW-C (Screen-Size / Player-Size Resolution Cap, eng SM,
validation S), unified per that doc's "Resolution-cap
unification" open question.
## Phases of complexity
Phases. The first four are motivation slices — different signal
sources (config / `ResizeObserver`) feeding the same constraint+filter
mechanism (a per-cap state slot read by `selectQuality` as a filter on
the candidate set before selection). The fifth is meta-policy on top:
it modifies how the combination of caps resolves, not a cap of its
own.
| Phase | What | Mechanism |
|---|---|---|
| Billing-driven max-height cap | Config-driven `maxHeight` (e.g., 1080) excludes higher-resolution variants from the candidate set. Drives the Mux billing use case (cap delivery at 1080p+ for tier-pricing alignment) | **Policy** — config field consumed by `selectQuality`'s filter step; no new behavior. Matching axis is configurable: height (default, simpler) or total pixels (e.g., for exact alignment with resolution-based pricing tiers — `1080p = 2,073,600 pixels` ensures anamorphic / non-standard-aspect variants are matched correctly) |
| Viewport-driven height cap | Cap the candidate set to renditions that fit the player element's rendered dimensions. Avoids serving higher-resolution variants the viewer can't perceive | **Middle pattern**`ResizeObserver` monitor writes cap state on player-element size changes; `selectQuality` reads. New behavior. Pairs with the cap-floor phase below — the floor counters this phase's tendency to over-aggressively downgrade on small players |
| Max-bitrate cap | Config-driven `maxBitrate` excludes variants above the threshold. Useful for bandwidth-constrained delivery contexts (mobile, billing) | **Policy** — same shape as max-height cap |
| Max-FPS cap | Config-driven `maxFps` excludes high-frame-rate variants. Less common motivation but mentioned in [video-abr.md](./video-abr.md)'s "What's not implemented" | **Policy** — same shape |
| Cap floor (minimum effective cap) | Counter-pressure to other caps: when the combination of upper-bound caps (viewport, max-bitrate, etc.) would narrow the effective candidate set below a configured floor (e.g., 720p), the floor wins — the effective cap is `max(otherCaps, floor)`. Motivation: viewport-driven caps over-aggressively downgrade quality on small players; the floor prevents perceptual quality from dropping too far when the player is small | **Meta-policy** — not a cap of its own. New constraint slot (e.g., `videoMinMaxHeight`) read by `selectQuality`'s filter step alongside the other cap slots; combination takes the `max` over the other caps' effective bounds. Mux Player's [`MinCapLevelController.minMaxResolution = 720`](https://github.com/muxinc/elements/blob/main/packages/playback-core/src/min-cap-level-controller.ts) is the reference implementation |
The first four phases share a single mechanism: a constraint slot +
filter step before `selectQuality` consumes the candidates. They
differ in signal source (one-shot config vs reactive `ResizeObserver`)
and constraint axis (height / bitrate / FPS). The cap-floor phase is
meta-policy on top — it modifies how the combination of caps resolves,
not a cap of its own. Per-phase implementation is small (S / S-M per
the Epics doc).
## What's in scope vs out of scope
**In scope:**
- All five phases above for video renditions
- Constraint+filter pattern: per-cap state slots + filter step in
`selectQuality` before bandwidth-based selection runs
- Config surface under existing `quality?: {…}` engine config
- Edge case handling: empty candidate set after cap filtering (open
question — fallback policy)
**Out of scope (separate candidate features):**
- **[multi-signal-abr](./multi-signal-abr.md)** — incorporates non-
bandwidth signals (CPU / thermal / network type / battery / viewport
/ customer preference) into the ABR algorithm itself. Different
concern: caps narrow the candidate set; multi-signal-abr biases
selection within it. Caps could feed multi-signal-abr as one input,
but the algorithm-modification work is its own feature.
- **[audio-abr](./audio-abr.md)** — audio quality switching now
documented; its "Audio caps inclusion" open question explicitly
references this doc as the resolution candidate. When audio-ABR
ships, the audio-caps integration question becomes load-bearing.
- **[`audio-only-mode-override`](../use-cases/audio-only-mode-override.md) / [`video-only-mode-override`](../use-cases/video-only-mode-override.md)** —
use-case compositions that subtract behaviors for mode-only delivery.
Different mechanism (composition, not constraint).
**Out of scope (different architectural layer):**
- Above-engine consumers that write cap state from React / HTML
observers (e.g., a React hook that observes the player container
and writes a cap value). This feature owns the SPF-side state
slots and filter logic; *where* the writer lives (engine-side
`ResizeObserver` behavior vs adapter-level observation writing to
the slot) is partly an open question (see below).
- The Mux Video element's customer-facing attributes
(`max-resolution`, `cap-rendition-to-player-size`) — those are
adapter-layer API surfaces that *consume* this feature's config /
state.
## Likely cross-cutting impact
Things this feature probably forces decisions on, not just additions:
- **`selectQuality` filter-then-select shape.** Today's `selectQuality`
takes `(candidates, bandwidth, config)` and returns a track.
Adding caps means either (a) pre-filtering candidates outside
`selectQuality` and passing the filtered set in, or (b) extending
`selectQuality`'s signature to take the constraint slots and
filter internally. Option (a) keeps `selectQuality` pure and
matches the `userVideoTrackSelection` precedent (where the
filtering happens in `switchVideoQuality` before invoking
`selectQuality`). Option (b) bakes caps into the ABR module
directly.
- **State-slot granularity.** Per-cap slots (`videoMaxHeight`,
`videoMaxBitrate`, `videoMaxFps`, `videoViewportHeight`) vs a
unified slot (`videoRenditionConstraints: { maxHeight?,
maxBitrate?, maxFps?, viewportHeight? }`). Per-cap slots align
with the constraint+filter pattern's "one writer per slot"
shape; a unified slot complicates multi-writer semantics if
different caps have different writers (config-driven for some,
`ResizeObserver`-driven for others).
- **Viewport-driven cap signal-source location.** The cap state is
SPF-side, but the player-element dimensions are DOM-side. A new
engine-side behavior could read `mediaSource.media` (the
`<video>` element) and `ResizeObserver` it, or adapter-level code
could observe the player container and write the cap state slot.
Affects which layer owns the DOM dependency.
- **Empty-candidate-set fallback.** If caps narrow candidates to
zero (e.g., `maxHeight: 480` on a source with only 720p+
variants), `selectQuality` has nothing to return. Fallback
options: ignore the cap entirely, pick lowest available
rendition above the cap, or surface an engine error.
Configurable behavior or engine-level decision.
- **Cap-combination semantics.** When multiple caps are active
(viewport + max-bitrate, or viewport + cap-floor), they combine
via *intersection* on the candidate set — each upper-bound cap
narrows independently. The cap-floor inverts: it computes the
most restrictive other-cap result and enforces a lower bound on
it (`max()`). Where this combination lives is part of the
filter-then-select shape question above: option (a) puts it in
the dispatcher (`switchVideoQuality`) before `selectQuality` is
invoked; option (b) bakes it into `selectQuality` itself.
## Open questions
- **Per-cap slots vs unified constraint slot.** See cross-cutting
bullet above. Open until the second cap lands and the multi-writer
shape becomes concrete.
- **Viewport-driven cap signal-source location.** Engine-side
`ResizeObserver` on `mediaSource.media`, or adapter-side observation
writing to the cap slot? Affects feature shape (one behavior vs
one slot) and DOM dependency placement.
- **Empty-candidate-set fallback policy.** Ignore / pick-lowest /
error? Config-driven or engine-fixed?
- **max-FPS cap inclusion.** Listed in video-abr.md's "What's not
implemented" but lower priority than max-height / max-bitrate.
Document as Phase 4 or defer?
- **Audio caps.** Audio renditions don't have height analogs but
could have max-bitrate or max-channels caps. Extend this feature
to cover audio or carry audio caps separately?
- **Interaction with [capability-probing](./capability-probing.md).**
Capability filtering narrows candidates to "what the browser can
play"; caps narrow to "what the player chooses to deliver." Apply
capability filter first, then policy caps (per current cluster
framing). Confirm ordering when both land.
- **Cap-floor scope: policy caps only, or all candidate-narrowing
signals?** The floor counters the viewport-driven cap (and
potentially max-bitrate / max-FPS caps), but should not override
capability-filter physics. If [capability-probing](./capability-probing.md)
removes unsupported renditions, the floor can't add them back.
Confirm the boundary: floor operates over the policy-cap-narrowed
set, not over the capability-filtered set.
- **Cap-floor default value.** Mux Player hardcodes 720p as
`MinCapLevelController.minMaxResolution`. For SPF, should the
floor default to a value, or require explicit opt-in? Defaulting
risks unexpected high-bitrate delivery on tiny embeds; not
defaulting means most consumers won't get the perceptual benefit
unless they configure it.
## Related features
- **[video-abr](./video-abr.md)** — primary consumer. `selectQuality`
is the read-side; `userVideoTrackSelection` is the existing
constraint+filter precedent. Cap slots layer on top of the same
pattern.
- **[multi-signal-abr](./multi-signal-abr.md)** — different concern
(ABR algorithm extension) but caps could feed it as inputs.
- **[audio-abr](./audio-abr.md)** — audio quality switching that
may extend this feature with audio-side caps; see that doc's
"Audio caps inclusion in rendition-selection-caps.md" open
question.
- **[capability-probing](./capability-probing.md)** — adjacent. Both
narrow the candidate set, but along different axes (capability =
physics, caps = policy). The cluster framing places caps in
cluster E (selection policy) and capability filtering in cluster
D (capability probing primitive); the line matters for
composition order.
- **[hevc-variant-selection](./hevc-variant-selection.md)** — sibling
constraint+filter feature on the codec axis. Same filter-then-select
shape; the open question about per-cap slots vs unified slot is
shared.
- **[`audio-only-mode-override`](../use-cases/audio-only-mode-override.md) / [`video-only-mode-override`](../use-cases/video-only-mode-override.md)** —
use-case compositions for mode-only delivery. Different mechanism
than caps (subtract behaviors vs filter candidates).
## See also
- [clusters.md § Selection policy](./clusters.md#selection-policy)
— cluster E description; this feature is the first concrete cluster
E member, and the cluster's documented foundation
- [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes)
— the Player-feature framing this doc instantiates; mixes
Policy mechanism (config-driven caps) and Middle-pattern
mechanism (viewport-driven cap)
- [clusters.md § Constraint + filter](./clusters.md#constraint--filter)
— cross-cluster pattern this feature instantiates; `video-abr`'s
`userVideoTrackSelection` is the precedent
- [video-abr.md](./video-abr.md) — primary consumer; `selectQuality`
is the read-side, `userVideoTrackSelection` the constraint
precedent
- [SPF Epics Working Doc](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
— source material; Epic #13 (1080p+ Resolution Cap) and NEW-C
(Screen-Size / Player-Size Resolution Cap), unified per that
doc's "Resolution-cap unification" open question
- [Mux Video Permutations Matrix](https://www.notion.so/32c97a7f89d08191b84dd30f06685490)
— Stream Type / Selection Policy section; consumer-facing API
surfaces (`max-resolution`, `cap-rendition-to-player-size`)
documented in the Mux Video element README