mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 18:04:49 +00:00
279 lines
21 KiB
Markdown
279 lines
21 KiB
Markdown
---
|
||
status: partial
|
||
date: 2026-05-20
|
||
definition: sketched
|
||
---
|
||
|
||
# Capability probing
|
||
|
||
The engine's foundation for determining what the browser can actually
|
||
play: codec / container support via `MediaSource.isTypeSupported` and
|
||
`canPlayType`, key-system support via `requestMediaKeySystemAccess`,
|
||
and `SourceBuffer.changeType()` availability for mid-stream codec
|
||
transitions. Filters the candidate set *before* selection, so
|
||
unsupported renditions don't survive into the pipeline and fail late.
|
||
|
||
A **Media-src feature** in the framing from
|
||
[clusters.md § Feature classification axes](./clusters.md#feature-classification-axes):
|
||
without it, sources with browser-incompatible variants fail late at
|
||
`createSourceBuffer` instead of failing gracefully (or falling back to
|
||
a compatible variant). Cluster D foundation; the
|
||
[clusters.md § Capability probing](./clusters.md#capability-probing)
|
||
section flags this as "deserves a dedicated home" with high reuse —
|
||
consumers include [HEVC variant selection](./hevc-variant-selection.md), [5.1 surround selection](./5.1-surround-selection.md),
|
||
DRM, and the unsupported-case error mapping.
|
||
|
||
## Status
|
||
|
||
- **Composition:** synchronous codec filtering is live in
|
||
`createSimpleHlsEngine`. The `canPlayTrack` probe is injected into
|
||
`track-switching`'s hard-constraints pre-pass (`excludeUnplayableTracks`),
|
||
so undecodable renditions are pruned *before* selection. The pre-existing
|
||
late-failure check (`isCodecSupported` inside `createSourceBuffer`) stays as
|
||
a defensive backstop and should now rarely fire.
|
||
- **Definition depth:** sketched — Phase 1 (probe primitive) and Phase 2
|
||
(multivariant CODECS filtering) are implemented; key-system probing,
|
||
`changeType()` probing, segment-level checking, no-playable surfacing
|
||
(Phase 6), and Tier 2 overrides remain unimplemented. Source
|
||
material: [SPF Epics Working Doc — candidate epics #17 (codec / container),
|
||
#18 (multivariant CODECS),
|
||
#19 (key-system)](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4).
|
||
- **Foundational** for cluster D — `[hevc-variant-selection]`,
|
||
`[5.1-surround-selection]`, `[unsupported-case-error-mapping]`, and
|
||
[drm-support](./drm-support.md) (GitHub issue #1411) all consume
|
||
probing output.
|
||
|
||
## Phases of complexity
|
||
|
||
Scope slices around the capability-probing contract. Tier 1
|
||
(spec-compliant filtering) and Tier 2 (customer-driven overrides)
|
||
layer onto specific phases per the
|
||
[Tier 1 / Tier 2 framing](./clusters.md#tier-1-spec-compliant-baseline-vs-tier-2-custom-behavior).
|
||
|
||
| Phase | What | Status |
|
||
|---|---|---|
|
||
| Codec / container probing primitive | Uniform API wrapping `MediaSource.isTypeSupported` + `canPlayType`. Helpers for "given a `Track`, can we play it?" Builds on today's `isCodecSupported` | **Implemented** (codec half) — `canPlayTrack` in `media/dom/capabilities.ts`, memoized by MIME. `canPlayType` not wrapped yet |
|
||
| Multivariant CODECS-attribute filtering | Post-parse, drop renditions whose `CODECS` doesn't decode on this browser, before selection. Filtered set is what selection behaviors operate over | **Implemented** — `excludeUnplayableTracks` constraint in `track-switching`'s pre-pass. Tier 1 (spec-compliant). The late-failure path is now a defensive fallback |
|
||
| Media-playlist / segment-level capability checking | Per-segment CODECS verification + container detection at the media-playlist level. Catches mismatches the multivariant didn't declare | Not implemented. Tier 1; largely defensive, rare for well-formed manifests |
|
||
| Key-system capability probing | `requestMediaKeySystemAccess` for each candidate key system (Widevine, PlayReady, FairPlay, FairPlay-AirPlay). Returns supported configurations. **DRM-adjacent boundary:** this feature owns Tier 1 probing only; EME setup, license fetch, key delivery live under [drm-support](./drm-support.md) (GitHub issue #1411) | Not implemented. Async — a slot-writer behavior, *not* the synchronous config-predicate route codec filtering took (see resolved open question) |
|
||
| Cross-codec transition (`changeType()`) probing | Probe whether `SourceBuffer.changeType()` is available, plus pair-wise support for specific codec transitions (AVC ↔ HEVC, AAC stereo ↔ AC-3 5.1, etc.). Browser support is fragile and pair-specific | Not implemented. Consumers decide whether to attempt mid-stream switches based on this probe; the `changeType()` call itself lives in those consumer features |
|
||
| Unsupported-case error surfacing | When no candidate survives filtering, surface a clear state rather than failing late in `createSourceBuffer` | Not implemented. A speculative per-type `noPlayable{Video,Audio}Tracks` flag was prototyped and **removed** (no consumer — write-only state + derivation stored as a slot). Today, when constraints prune a type that *has* tracks to empty, the behavior clears the selection (no pick) and `console.error`s as a placeholder; the late `createSourceBuffer` check is the backstop. Deferred in full to `[unsupported-case-error-mapping]`, which will replace the `console.error` with a surfaced shape (likely a derived `computed`, not a stored slot) once a consumer exists |
|
||
| Tier 2: customer probing overrides | Config-driven biases: "force AVC even when HEVC supported," "prefer hardware-backed DRM," "exclude codec X." Layered on top of Tier 1's spec-compliant filtering | Not implemented. The `canPlayTrack` config injection point is the natural seam (override the default probe) |
|
||
|
||
## What's in scope vs out of scope
|
||
|
||
**In scope:**
|
||
- All phases above
|
||
- Browser-API wrappers (`isTypeSupported`, `canPlayType`,
|
||
`requestMediaKeySystemAccess`, `changeType()` availability)
|
||
- Filter-writer behavior + the filtered-candidate-set slot pattern
|
||
- Error-surfacing primitive (used by both this feature and
|
||
`[unsupported-case-error-mapping]`)
|
||
|
||
**Out of scope (separate Media-src candidate features):**
|
||
- **[hevc-variant-selection](./hevc-variant-selection.md)** —
|
||
*consumer*. Uses Tier 1 (select HEVC if supported, fallback to AVC)
|
||
+ cross-codec transition probing (mid-stream switching).
|
||
- **[5.1-surround-selection](./5.1-surround-selection.md)** —
|
||
*consumer*. Same shape as HEVC, on the audio channel-count axis.
|
||
Adds a 5.1-specific runtime-detection phase (downstream-environment-
|
||
aware channel preference) with no HEVC analog.
|
||
- **[drm-support](./drm-support.md)** (GitHub issue #1411) — EME
|
||
setup, license handling. This feature owns the "what key systems
|
||
are available?" probe; drm-support uses that answer to set up
|
||
keys.
|
||
- **Multi-language-audio Tier 2 mid-stream codec switch** —
|
||
*consumer* of `changeType()` probing.
|
||
- **`[unsupported-case-error-mapping]`** — sister feature; maps the
|
||
error-surfacing primitive to consumer-facing codes / messages.
|
||
|
||
**Out of scope (different architectural layer):**
|
||
- Adapter / consumer-side error display
|
||
- Consumer-specific error-code mappings (above-engine)
|
||
|
||
## Implementation surface
|
||
|
||
**Probe primitive:**
|
||
|
||
| Export | File | Role |
|
||
|---|---|---|
|
||
| `canPlayTrack` | `media/dom/capabilities.ts` | `(track) => boolean` — builds the MIME codec string and checks `MediaSource.isTypeSupported`, memoized by MIME. Unprobeable tracks (no `mimeType` / no `codecs`) pass through. **Asserts detected non-fMP4 containers (`video/mp2t`, `audio/aac`) unsupported without probing** — TS because the probe false-positives + no transmux; raw AAC because the pipeline assumes init segments (a temporary limitation — browser supports it) |
|
||
| `CanPlayTrack` (type) | `media/types/index.ts` | DOM-free predicate type the constraint consumes; `canPlayTrack` is its DOM implementation |
|
||
| Container detection (`CONTAINER_MIME_BY_EXTENSION`, `NON_FMP4_CONTAINER_MIMES`) | `media/hls/parse-media-playlist.ts` | Detects non-fMP4 containers per media playlist (no `#EXT-X-MAP` **and** a recognized segment extension: `.ts` → `video/mp2t`, `.aac` → `audio/aac`) and relabels the resolved track's `mimeType` from the fMP4 default. `canPlayTrack` then prunes them |
|
||
| `applyContainerMimeType` | `media/utils/tracks.ts` | Propagates the detected container to every rendition of the **same type** (called from `resolve-track`): one resolved non-fMP4 playlist relabels all of that type's renditions, so the type is pruned from a single fetch. Scoped to one type — never crosses audio↔video (mixed-container sources exist), which also keeps per-type resolutions' writes disjoint (race-free) |
|
||
|
||
**Constraint (in `playback/behaviors/track-switching.ts`):**
|
||
|
||
| Piece | Role |
|
||
|---|---|
|
||
| `excludeUnplayableTracks` | Hard-constraint in the `applyConstraints` pre-pass; reads `config.canPlayTrack`, drops undecodable renditions before the rule chain. Shared by `switchVideoTrack` / `switchAudioTrack`, pooled with `excludeFailedCdns`. When it prunes a type that *has* tracks to empty, the selection clears (no pick) so a pick made before a relabel can't linger as unplayable |
|
||
|
||
**Engine wiring (`playback/engines/hls/engine.ts` + `engine-audio-only.ts`):**
|
||
- `canPlayTrack` config — both engine factories default it to the DOM `canPlayTrack` in `finalConfig` (the audio-only variant too, so filtering isn't inert there); override to force-exclude a codec (the Tier 2 seam). Adapters forward it via `...config`.
|
||
|
||
**State slots:**
|
||
- **Reads (constraint):** `presentation` candidates' `mimeType` + `codecs`, via `config.canPlayTrack`.
|
||
- **Writes:** none — the constraint only narrows the candidate set the existing `selected*TrackId` writers pick from.
|
||
|
||
## Verification
|
||
|
||
- **Unit — `media/dom/tests/capabilities.test.ts`:** `canPlayTrack` returns the `isTypeSupported` verdict for a track's built MIME; memoizes per unique MIME (probes once); passes through (`true`) for unprobeable tracks (no `mimeType`, or empty/absent `codecs`); asserts non-fMP4 containers (`video/mp2t`, `audio/aac`) unsupported without consulting `isTypeSupported` (even with codecs).
|
||
- **Unit — `media/hls/tests/parse-media-playlist.test.ts`:** relabels to `video/mp2t` / `audio/aac` when there's no `#EXT-X-MAP` and segments are `.ts` / `.aac` (query string ignored; `video/mp2t` for audio TS too); keeps the fMP4 default when an `#EXT-X-MAP` is present or the extension is unrecognized.
|
||
- **Unit — `media/utils/tests/tracks.test.ts`:** `applyContainerMimeType` sets the MIME on every track of the given type, leaves other types untouched (never crosses audio↔video), idempotent.
|
||
- **Unit — `playback/behaviors/tests/track-switching.test.ts`:** `excludeUnplayableTracks` prunes undecodable renditions before ranking (picks best playable codec); passes through with no probe wired; a user-selected unplayable track is still excluded (hard constraint beats the soft user filter); makes no pick when the constraint prunes every rendition from a cold start; **clears a prior pick when a later relabel prunes every rendition to empty** (the warm path — a pick made under the initial mp4 label, then the type relabeled to `video/mp2t`).
|
||
- **Integration — `playback/engines/hls/tests/engine.test.ts`:** a mixed HEVC+AVC source with a `canPlayTrack` rejecting HEVC selects the AVC rendition; an all-undecodable source makes no video pick.
|
||
|
||
**Live smoke test:** verified in the SPF sandbox against the Apple `bipbop_4x3` stream (muxed-TS video + raw-`.aac` audio) — video relabels `video/mp2t`, audio relabels `audio/aac`, and both types are pruned to empty (per-type propagation: one fetch per type, not per rendition), so the selection clears (no pick) instead of leaving the initial mp4-labeled pick selected to stall deep in the pipeline. An audio-only `.aac` source likewise makes no pick.
|
||
|
||
**Out of scope / deferred:** `canPlayType` wrapper, key-system probing, `changeType()` probing, per-segment CODECS checking, no-playable surfacing / the full error-code interface, and Tier 2 override config. Container *detection* covers MPEG-TS + raw ADTS AAC (not `.mp3` etc. yet); both are asserted **unplayable** for now. *Playing* them is separate follow-up work — TS needs a transmux pipeline; **raw AAC is genuinely browser-supported (Chrome/Safari) and could be played by removing the pipeline's init-segment assumption** (the segment loader queues an `append-init` with an empty URL, and append handling is fMP4-shaped).
|
||
|
||
## Likely cross-cutting impact
|
||
|
||
- **Selection behaviors** — `selectVideoTrack` / `switchVideoQuality`
|
||
/ `selectAudioTrack` / `selectTextTrack` pickers read filtered set,
|
||
not raw `presentation.selectionSets`. Same shape as the
|
||
`userVideoTrackSelection` constraint pattern in `video-abr`.
|
||
- **`mse-mms-pipeline` late-failure path** — `createSourceBuffer`'s
|
||
throw on unsupported codec becomes a defensive fallback. With
|
||
upstream filtering it should rarely fire; the throw stays as a
|
||
structural guarantee.
|
||
- **`presentation-modeling`** — parser stays format-neutral.
|
||
Capability filtering is post-parse, before selection. The
|
||
architectural doc's parser-interface contract isn't affected.
|
||
- **`video-abr`** — `selectQuality` operates over the filtered set.
|
||
No code change in ABR itself; just narrower input.
|
||
- **DRM gate** — key-system probing becomes the *first* DRM gate.
|
||
Subsequent EME setup, license fetch, key delivery happen under
|
||
[drm-support](./drm-support.md), gated on probing's verdict.
|
||
Crisp boundary:
|
||
probing answers "can we?"; DRM-support answers "set it up."
|
||
- **Cross-codec transition consumers** — HEVC, 5.1, multi-language-
|
||
audio Tier 2 all need `changeType()` probing. The actual
|
||
`changeType()` call lives in their own feature work; this feature
|
||
provides the "can we?" answer.
|
||
|
||
## Resolved during Phase 1–2 implementation
|
||
|
||
- **Filter-writer pattern → split (not unified).** Synchronous codec
|
||
probing is a pure function of (track, environment), so it landed as a
|
||
**config-injected predicate** (`canPlayTrack`) read by the
|
||
`excludeUnplayableTracks` hard constraint — no behavior, no state slot.
|
||
This mirrors `getCdnId` injection, not the `failedCdns ←
|
||
setupFailoverMonitor` slot-writer (which exists because CDN cooldown is
|
||
*dynamic*; codec support is static). Async key-system probing (Phase 4)
|
||
will take the slot-writer route, coexisting in the same pre-pass — the
|
||
two are genuinely split by sync-vs-async.
|
||
- **Cache-eager vs lazy → lazy + memoized.** The constraint probes each
|
||
candidate at apply time; `canPlayTrack` memoizes by built MIME string, so
|
||
each unique MIME is asked once without an upfront sweep.
|
||
- **No-playable surfacing was removed (not deferred-as-built).** A per-type
|
||
`noPlayable*` flag was prototyped — set when a type's candidates pruned to
|
||
empty (codec filtering *or* CDN-failover cooldown). It was deleted: nothing
|
||
consumed it (write-only state), and it stored a derivation in a slot an
|
||
effect wrote rather than a `computed`. Today an emptied type that *had* tracks
|
||
clears the selection (no pick) and `console.error`s as a placeholder; the late
|
||
`createSourceBuffer` check is the backstop, and a track with no declared
|
||
`CODECS` (optional per spec) is unprobeable and passes through. The
|
||
*had-candidates-vs-never-had-candidates* distinction a bare "candidate set is
|
||
empty" check can't make (a video-only source's absent audio is empty but not
|
||
an error) is **already** made — the clear/error is gated on a non-zero
|
||
pre-constraint track count for the type. When a consumer materializes,
|
||
`[unsupported-case-error-mapping]` replaces the `console.error` with the
|
||
surfaced shape (likely a `computed`).
|
||
- **Container-detection scope → non-fMP4 detection (TS + raw AAC), per-track-type,
|
||
marked unplayable.** The media-playlist parser relabels a resolved non-fMP4
|
||
rendition (no `#EXT-X-MAP` + a recognized extension: `.ts` → `video/mp2t`,
|
||
`.aac` → `audio/aac`), and `resolve-track` propagates that MIME to every
|
||
rendition of the **same type** (`applyContainerMimeType`), so a type is pruned
|
||
from a single resolved playlist. Propagation is **same-type, not cross-type**:
|
||
Apple `bipbop_4x3` is mixed-container (muxed-TS video + raw-`.aac` audio), so a
|
||
cross-type "whole source is one container" assumption was both *wrong* for that
|
||
stream and racy under concurrent per-type resolution; same-type is safe (an ABR
|
||
ladder shares its container) and race-free (disjoint writes). Both are asserted
|
||
unplayable, for different reasons: TS because `isTypeSupported('video/mp2t…')`
|
||
false-positives on Chromium *and* there's no transmux pipeline; raw AAC as a
|
||
**temporary** limitation — the browser genuinely supports it (Chrome/Safari
|
||
decode bare `audio/aac`; Firefox doesn't), but our segment loader / append
|
||
pipeline assumes an `EXT-X-MAP` init segment, so it would fetch-but-never-buffer
|
||
(a silent stall) today. Making raw AAC playable (remove the init-segment
|
||
assumption; bare-MIME probe + projection) is deliberately out of scope here —
|
||
see [container-support](./container-support.md).
|
||
|
||
## Open questions
|
||
|
||
- **Surfacing "nothing playable" as a (fatal) error.** The pre-pass can prune a
|
||
type to empty — every rendition undecodable (fatal: the source can't play at
|
||
all) or, transiently, every CDN cooled down (recoverable). Today both clear
|
||
the selection (no pick) and `console.error` — *uniformly*, without yet
|
||
distinguishing fatal from recoverable; the late `createSourceBuffer` check
|
||
remains the backstop. `[unsupported-case-error-mapping]` will want this
|
||
surfaced. Open: a derived
|
||
`computed` read by that consumer (preferred — see the removed `noPlayable*`
|
||
note under "Resolved"), or a dedicated **behavior** if surfacing needs to own
|
||
emit/clear, the fatal-vs-recoverable classification, and the
|
||
had-vs-never-had-candidates gating? Lean `computed` unless that
|
||
lifecycle/coordination forces a behavior. Whatever lands must distinguish
|
||
fatal (no decodable rendition) from recoverable (failover cooldown) so the
|
||
consumer doesn't fire a terminal error on a transient condition.
|
||
- **Sharing the constraint pre-pass with user-selection logic.** Constraints
|
||
(`excludeUnplayableTracks`, `excludeFailedCdns`) live *inside*
|
||
`track-switching`'s pre-pass, invisible outside it. A hard constraint already
|
||
correctly beats a user pick (tested — the user's unplayable selection is
|
||
pruned before `filterByUserSelection` runs), but the override is *silent* and
|
||
a user-facing track list can still present a rendition that will never play.
|
||
Open: expose the post-constraint candidate set (per type) as shared state for
|
||
selection UIs / the error surface to consume, or factor the constraint list
|
||
into a reusable unit those surfaces apply themselves? Same "expose the
|
||
candidate set" thread as the `noPlayable*` removal.
|
||
- **`changeType()` pair-wise probing API.** Probe all pairs upfront,
|
||
probe lazily on switch attempt, or expose a `canChangeType(from,
|
||
to)` predicate that callers invoke?
|
||
- **Tier 2 customer override surface.** Config-driven (engine-wide)
|
||
vs per-source vs both? Per-source is more flexible but harder to
|
||
wire.
|
||
|
||
## Related features
|
||
|
||
- **mse-mms-pipeline** — owns `isCodecSupported` + the late-failure
|
||
path. This feature builds on those and moves the check upstream.
|
||
- **presentation-modeling** *(architectural)* — data shape this
|
||
feature filters; parser stays format-neutral.
|
||
- **video-abr** — quality selection narrowed to filtered candidates.
|
||
- **audio-playback** / **subtitles** — per-type selection over
|
||
filtered candidates.
|
||
- **multi-language-audio** *(coarse)* — Tier 2 mid-stream codec
|
||
switch consumes `changeType()` probing.
|
||
- **[hevc-variant-selection](./hevc-variant-selection.md)** —
|
||
consumer; selection + cross-codec switching.
|
||
- **[5.1-surround-selection](./5.1-surround-selection.md)** —
|
||
consumer.
|
||
- **[drm-support](./drm-support.md)** (GitHub issue #1411) — owns EME +
|
||
license; consumes key-system probing.
|
||
- **`[unsupported-case-error-mapping]`** *(candidate)* — sister;
|
||
consumer-facing error mapping on top of this feature's error
|
||
primitive.
|
||
- **[container-support](./container-support.md)** — standalone
|
||
feature (cluster-less) for *playing* non-fMP4 containers. The
|
||
detection half landed here: MPEG-TS (`.ts` → `video/mp2t`) and raw
|
||
ADTS AAC (`.aac` → `audio/aac`) are detected and asserted unplayable.
|
||
Playing them lives there — TS needs a transmux pipeline; raw AAC just
|
||
needs the init-segment assumption removed from the segment loader /
|
||
append pipeline (the browser already supports it).
|
||
|
||
## See also
|
||
|
||
- [clusters.md § Capability probing](./clusters.md#capability-probing)
|
||
- [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes)
|
||
— Constraint + filter pattern, Composition vs Policy vs middle
|
||
pattern (middle pattern), Tier 1 / Tier 2 framing
|
||
- [mse-mms-pipeline.md](./mse-mms-pipeline.md) — `isCodecSupported`
|
||
+ the late-failure `createSourceBuffer` throw
|
||
- [presentation-modeling.md](../presentation-modeling.md) — data
|
||
shape this feature filters
|
||
- [SPF Epics Working Doc](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
|
||
— epics #17 (codec / container), #18 (multivariant CODECS),
|
||
#19 (key-system), #20 (unsupported-case error mapping)
|
||
- [Permutations Matrix — Upcoming Features + Unsupported Case Handling](https://www.notion.so/32c97a7f89d08191b84dd30f06685490)
|
||
— source material for the consumer features (HEVC, 5.1, DRM
|
||
security levels) and error-handling scope
|