16 KiB
status, date, definition
| status | date | definition |
|---|---|---|
| draft | 2026-06-05 | sketched |
Multi-CDN failover
CDN selection and failover for HLS sources that publish the same content
on more than one host (e.g. Mux Video's ?redundant_streams=true). The
redundant variants parse as ordinary candidate tracks — one per
(rendition × CDN) — so the work is selecting which CDN to use and
keeping the whole presentation on it, modeled inside the
track-switching rule model rather than as
URL rewriting at fetch time.
Two sub-features, mapping onto the two rule kinds in that model:
- Sticky CDN pick (implemented) — a session-level behavior picks a
CDN (the manifest-head host) and holds it; a shared scope rule
(
preferActiveCdn) narrows every track type's candidates to that CDN, so video / audio / text resolve from one host. This is the active-pathway scope the track-switching model lists for multi-cdn-failover. - Failover (deferred) — when requests to the active CDN fail too often within a window, a constraint removes that CDN's tracks from the candidate set during cooldown and the session behavior rotates the active CDN. This is the failed-CDN constraint the model lists, and it consumes network-resilience's per-host circuit-breaker — its hard prerequisite.
A Media-src feature for sources that genuinely require failover, with a Player feature surface at the failover tier (customer-customizable CDN policy). Notion epic #9 classifies as "Media-src? / Player?" — the ambiguity reflects that split.
Status
- Composition: sub-feature 1 (sticky CDN pick) is implemented in
createSimpleHlsEngineandcreateHlsAudioOnlyEngine. TheresolveCdnPrioritybehavior owns thecdnPrioritysignal (the manifest-ordered CDN list, most-preferred first); thepreferActiveCdnscope rule (shared by the video + audio chains intrack-switching) narrows candidates to the highest-priority CDN with surviving tracks. Failover (sub-feature 2) is not implemented — no constraints pass, no per-CDN failure tracking, no rotation. - Definition depth: sketched — sub-feature 1 has a populated implementation surface + verification; sub-feature 2 stays at the scope-and-constraints level pending its prerequisite.
- Hard prerequisite (failover only): network-resilience. The failed-CDN constraint consumes the foundation's per-host circuit-breaker / retry-exhaustion state. Sub-feature 1 has no such dependency — it's pure selection over the already-parsed candidate set.
- Governing model: track-switching-model.md — multi-CDN is the canonical constraint + scope feature there. The active-CDN scope is a soft filter in the rule chain; the failed-CDN constraint is a hard filter in the (not-yet-built) constraints pre-pass.
How redundant streams are modeled
There is no alternateUris field and no fetch-time URL rotation. A
redundant-streams manifest lists each rendition once per CDN (duplicate
#EXT-X-STREAM-INF / #EXT-X-MEDIA entries on different hosts), and the
existing parser already emits one Track per entry with a unique id and
its own absolute url. So the candidate set for each type already
contains one variant per CDN. CDN identity is derived from each track
URL's origin (getCdnId); the set of CDNs is published as a single
per-presentation ordered signal (cdnPriority, most-preferred first —
mirroring HLS content steering's PATHWAY-PRIORITY). The active CDN is
not stored: the scope derives it as the highest-priority cdnPriority
entry that still has tracks after the constraints pass. Selecting a
CDN-tagged track id means resolveTrack / segment loading fetch from that
CDN with no further plumbing.
This list shape is what makes failover fall out cleanly: the failed-CDN
constraint (sub-feature 2) prunes a cooled-down CDN's tracks, so
"first-with-survivors" moves to the next CDN automatically and returns to
the primary when it recovers — no reactive rewrite of an "active" value.
Content steering, likewise, just reorders cdnPriority (pathway priority
as a sort key).
This is why sub-feature 1 needed no parser change and no new data shape — only a selection behavior and a scope rule over existing tracks.
Phases of complexity
| Phase | Sub-feature | Kind | What | State |
|---|---|---|---|---|
| Sticky CDN pick | 1 | scope | resolveCdnPriority publishes the manifest-ordered CDN list (cdnPriority); preferActiveCdn narrows every type's candidates to the highest-priority CDN with surviving tracks, falling through when nothing matches. Shared list → all types on one CDN |
Implemented |
| Failed-CDN constraint | 2 | constraint | A constraint in the track-switching constraints pre-pass removes a cooled-down CDN's tracks from the candidate set. The scope then picks the next cdnPriority entry automatically. Requires the generic constraints phase (track-switching-model "Phase 2") to be built first |
Deferred |
| Per-CDN failure tracking | 2 | — | Count per-CDN fetch failures within a window; mark a CDN unhealthy / in cooldown. Consumes network-resilience's circuit-breaker |
Deferred (prereq) |
| CDN priority override / steering | 2 | scope | Reorder cdnPriority to bias the pick (region-preferred, weighted, or content-steering's pathway priority). No reactive "active" rewrite needed — the order is the policy |
Deferred |
| Customer CDN-id derivation | 2 | config | Pluggable getCdnId for non-origin identity. The origin-based default is built in; a config seam is anticipated |
Deferred |
What's in scope vs out of scope
In scope:
- Sticky per-presentation CDN selection (sub-feature 1, done)
- Failover via a track-switching constraint + active-CDN rotation (sub-feature 2)
- Per-CDN health derived from
network-resilience's circuit-breaker - Customer-configurable CDN-id derivation / rotation policy
Out of scope (separate cluster G sister features):
- network-resilience (foundation, prerequisite for failover) — retry + backoff + circuit-breaker. Multi-CDN consumes; doesn't reimplement.
- content-steering — HLS content-steering
protocol (server-advertised, dynamically-updated host pool). Different
mechanism than static redundant streams, but the same active-pathway
scope shape: content-steering picks the active pathway dynamically; the
scope reflecting it is the one implemented here. Designed-with-in-mind:
cdnPriorityis a reorderable list a steering behavior writes (pathway priority as a sort key), and the scope honors the order unchanged.
Out of scope (different architectural layer):
- Adapter-layer customer-facing UI ("Switch CDN" buttons). Consumer policy is expressed via the failover tier's config seam.
- CDN-side load balancer / origin-shield infrastructure. Service-side.
- DRM key-server failover — lives under drm-support.
Likely cross-cutting impact
- Per-presentation, not per-rendition (resolved). A single
cdnPrioritylist governs all track types — the per-presentation coherence requirement. The track-switching model's "cross-type consistency is a composition convention" applies: both the video and audio chains reference the samepreferActiveCdndefinition reading the same list, so they agree on the CDN even if their per-type track arrays differ. (The doc's earlier per-rendition lean is superseded.) cdnPrioritywriter composition.resolveCdnPriorityis the sole writer today (publishes the manifest order). Failover needs no second writer — the failed-CDN constraint prunes tracks and the scope re-derives the active CDN. Content-steering would reordercdnPriority(still a single owning behavior; the list reflects one upstream priority).- Active CDN is derived, not stored. The scope computes "highest
priority with surviving tracks," so failover and recovery need no extra
state: pruning moves the pick to the next CDN, un-pruning returns it to
the primary. This is why the array beats a single reactive
activeCdnvalue — the failed-set information is applied once (in the constraint), not duplicated into an active-value rewrite. - Constraints phase is a shared prerequisite. The failed-CDN
constraint can't land until the generic constraints pre-pass
(
applyConstraints+ theconstraintsconfig field + empty-playable-set terminal state) is built intosetupTrackSwitching. The seam exists (candidateSetcomputed); the machinery does not. capability-probing shares this prerequisite. - Live + multi-CDN. During live playback the reload loop re-resolves
the presentation;
resolveCdnPriorityre-publishes only when the CDN set changes (idempotent for a stable manifest). Cross-feature with live-stream-support (not yet implemented). - Priority state across source changes.
cdnPrioritytears down with the source via the resolved/unresolved cascade (per source-replacement); per-host circuit-breaker state innetwork-resiliencemay outlive a source.
Open questions
- Constraints-phase shape. How
applyConstraintsand the empty-playable-set terminal state are modeled — owned by the track-switching constraints work, consumed here. (See track-switching-model.md → Fitting the model to the track-switching behavior.) - Failure-window policy. Threshold count / window length / cooldown
duration for marking a CDN unhealthy. Empirical; lives with
network-resilience's circuit-breaker. - CDN-id derivation configurability. Origin-based
getCdnIdis the default; whether/where to expose a consumer override (config field vs. rule config view) is deferred until a non-origin case appears. - Composition with content-steering. Static redundant CDNs +
dynamic steered host pool: does steering replace, intersect, or
reprioritize the candidate CDNs? Open until content-steering lands; the
reorderable
cdnPrioritylist keeps it tractable (steering reorders it).
Resolved during sub-feature 1 implementation
- Signal shape → a per-presentation ordered
cdnPrioritylist (active = first-with-survivors, derived), not a single storedactiveCdnvalue and not per-rendition. The list makes failover a pure constraint and composes with content-steering as a reorder. Matches the cross-type coherence requirement (one shared list). - CDN identity → URL origin (
getCdnId); configurable derivation deferred. - Manifest syntax / parser → no change. Redundant variants already
parse as separate per-CDN tracks; no
alternateUrisfield needed. - Architecture → constraint + scope in the track-switching model, not
active-URI rotation in
resolveTrack.
Implementation surface
packages/spf/src/media/utils/cdn.ts—getCdnId(url)(origin-based CDN identity) andgetOrderedCdnIds(presentation)(distinct CDNs in manifest order; head = primary).packages/spf/src/playback/behaviors/resolve-cdn-priority.ts—resolveCdnPrioritybehavior +ResolveCdnPriorityState. Machine reactor onpresentation-unresolved↔presentation-resolved; owns thecdnPrioritysignal; publishesgetOrderedCdnIds(presentation)(skipping the write when the CDN set is unchanged), clears on exit.packages/spf/src/playback/behaviors/track-switching.ts—preferActiveCdnscope rule (soft filter oncdnPriority: narrow to the highest-priority CDN with surviving tracks), added to both variants' chains:[filterByUserSelection, preferActiveCdn, rankByBandwidth].SwitchableTrackgainsurl(the rule's input).packages/spf/src/playback/engines/hls/engine.ts+engine-audio-only.ts—resolveCdnPrioritycomposed afterresolvePresentation;cdnPriority?: string[]added to both engine state interfaces.
State signal: cdnPriority?: string[] (CDN origins, most-preferred first),
owned by resolveCdnPriority, read optionally by preferActiveCdn.
Verification
Sub-feature 1:
media/utils/tests/cdn.test.ts—getCdnId(origin extraction; same/different host; scheme+port; unparseable fallback);getOrderedCdnIds(distinct CDNs in order; dedupe; single-CDN; unresolved →[]).playback/behaviors/tests/resolve-cdn-priority.test.ts— publishes the manifest-ordered CDN list; single-CDN source; skips the write when a resolved swap keeps the same CDNs; updates when the order changes; clears on src unload + on destroy; re-publishes after a src reset.playback/behaviors/tests/track-switching.test.ts(preferActiveCdnblock) — narrows to the highest-priority CDN overriding manifest track order; keeps the pick on the primary; falls through to the next CDN when the first has no survivors; falls through to all when none match; no-op whencdnPriorityabsent; re-picks reactively when the order changes (steering/failover seam); same scope applied to the audio chain (cross-type coherence).playback/engines/hls/tests/engine.test.ts— integration: a redundant-stream presentation yieldscdnPriority= manifest order and a video selection on the primary; reorderingcdnPriorityre-narrows and the selection follows.
Out of scope / deferred:
- End-to-end + sandbox verification against a real Mux
?redundant_streams=truesource (needs a fixture). - All of sub-feature 2 (failover): blocked on the constraints phase +
network-resilience.
Related features
- track-switching-model.md (governing model) — multi-CDN is its canonical constraint + scope feature; the active-CDN scope is implemented against the rule chain it specifies.
- network-resilience (hard prerequisite for failover) — per-host circuit-breaker the failed-CDN constraint consumes.
- content-steering — dynamic host-pool
sibling; shares the active-pathway scope shape (
cdnPriorityas a reorderable reflected list — pathway priority as a sort key). - capability-probing — shares the not-yet-built constraints pre-pass with the failed-CDN constraint.
- live-stream-support (not implemented)
— reload-loop re-resolution;
cdnPriorityis republished only when the CDN set changes. - source-replacement —
cdnPrioritytears down via the resolved/unresolved cascade on source change. - video-abr / audio-abr — ABR ranks within the CDN-narrowed set; the scope runs before the ranker.
See also
- track-switching-model.md — the rule model (constraints → soft filters → ranker) this feature composes into
- clusters.md § Selection resilience
— cluster G; this feature is the selection-side resilience sister to
network-resilience's response-error handling - clusters.md § Selection / filtering across clusters — cluster G's role: alternate-CDN selection within the chosen track
- network-resilience.md — circuit-breaker foundation the failover tier consumes
- SPF Epics Working Doc — source material; epic #9 (Multi-CDN Failover)
- Mux Video —
?redundant_streams=true— Mux Video convention for redundant-CDN sources