feat(spf): multi-cdn support#1668
Conversation
Redundant-streams sources (e.g. Mux ?redundant_streams=true) publish the same renditions on multiple hosts, which already parse as separate candidate tracks. Add session-level CDN selection that keeps the whole presentation on one CDN, modeled in the track-switching rule model: - getCdnId / getOrderedCdnIds — origin-based CDN identity - selectActiveCdn behavior: picks the manifest-head CDN, sticky, clears on src unload; owns the activeCdn signal - preferActiveCdn scope rule (shared by the video + audio chains): narrows candidates to the active CDN, falls through when nothing matches - compose selectActiveCdn into the default + audio-only engines Failover (failed-CDN constraint + rotation) is deferred; it needs the track-switching constraints phase and network-resilience's circuit-breaker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersede the active-URI-rotation framing with the track-switching constraint+scope model: sub-feature 1 (sticky CDN pick) implemented, sub-feature 2 (failover) deferred. Add implementation surface + verification sections; record the per-presentation / origin-identity / no-parser-change decisions. Advance definition coarse -> sketched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the single `activeCdn` string with an ordered `cdnPriority: string[]` signal (most-preferred first; name mirrors HLS content steering's PATHWAY-PRIORITY). The scope rule `preferActiveCdn` now narrows to the highest-priority CDN that still has tracks, so "active" is derived rather than stored. This makes failover a pure consequence of the (future) failed-CDN constraint — pruning a cooled-down CDN's tracks moves the pick to the next entry and returns to the primary on recovery, with no reactive rewrite — and lets content steering compose by reordering the list (pathway priority as a sort key). Behavior is unchanged for sub-feature 1 (no constraint yet): first-with-survivors is the manifest head. - rename selectActiveCdn/activeCdn -> resolveCdnPriority/cdnPriority (behavior file select-active-cdn.ts -> resolve-cdn-priority.ts) - resolveCdnPriority publishes getOrderedCdnIds, skipping the write when the CDN set is unchanged; no sticky-pick state needed - update both engine state interfaces + composition wiring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reflect the ordered-list signal: active CDN derived as first-with-survivors, failover as a pure constraint, content steering as a reorder. Update the implementation surface, verification, cross-cutting notes, and resolved decisions accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…effects Two regressions that distinguish "the cdnPriority scope works" from "it coincidentally matches track order / behavior order": - engine cross-type test: audio renditions list a different CDN first than video, so the shared (video-derived) cdnPriority must pull audio onto the primary CDN — a no-op scope would leave it on its manifest-first track. Exercises the real resolveCdnPriority → preferActiveCdn pipeline with cdn-order != per-type track-order, without a manual reorder. - track-switching late-arrival test: cdnPriority starts undefined (the worst case = resolveCdnPriority composed last) and is written after the first pick; the scope must re-fire and correct the selection, proving the settled state is composition-order independent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document why resolveCdnPriority is composed before the switch* behaviors: it's a mild optimization, not a correctness requirement. Selection is reactive, so a late cdnPriority converges on the same pick; order only affects a transient wasted media-playlist fetch, and only for an asymmetric manifest (a track type listing a non-primary CDN first). Symmetric redundant streams never hit it. In the audio-only engine, with a single track type, the order is not load-bearing at all — it's there for forward-consistency and future failover / steering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for vjs10-site ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📦 Bundle Size Report🎨 @videojs/html — no changesPresets (7)
Media (9)
Players (5)
Skins (30)
UI Components (37)
Sizes are marginal over the root entry point. ⚛️ @videojs/react — no changesPresets (7)
Media (8)
Skins (27)
UI Components (31)
Sizes are marginal over the root entry point. 🧩 @videojs/core — no changesEntries (10)
🏷️ @videojs/element — no changesEntries (2)
📦 @videojs/store — no changesEntries (3)
🔧 @videojs/utils — no changesEntries (10)
📦 @videojs/spf — no changesEntries (4)
ℹ️ How to interpretAll sizes are standalone totals (minified + brotli).
Run |
The spf-segment-loading rendition picker listed every video track, so a
redundant-stream source (same renditions per CDN) showed duplicate buttons,
and clicking pinned by track id — tying the choice to one CDN's track.
Mirror the audio picker: collapse to one button per bitrate + resolution
identity, and pin via a { bandwidth, width, height } partial-track filter
so the preference is CDN-agnostic (preferActiveCdn resolves the active
CDN's copy). The status row now reflects the pinned bitrate + width +
height. Uses the audio picker's build/update split so frequent ABR
switches don't tear down buttons mid-click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,91 @@ | |||
| /** | |||
There was a problem hiding this comment.
note: I'll be renaming this in a followup PR to avoid confusion. Since "resolve" has been used for what I've been calling "resolvables" (things with a URL that are asynchronously "resolved" to a value, like Presentations or Tracks), this should be renamed. Only holding off on renaming in this PR to avoid rebase issues on the stacked branch for implementing CDN failover.
There was a problem hiding this comment.
See: https://github.com/videojs/v10/pull/1671/changes#diff-155a147e408eff6f818a47f55fa9b66e32536290dd1770aafe6f02bf6ce842ca (renamed to derive-cdn-priority)
There was a problem hiding this comment.
thought(non-blocking): do/should we track these naming conventions in any document? I think rule nomenclature is also a nice to have. Short glossary of contraints, scopes, ranks, etc.
As long as it's not repetitive, of course.
There was a problem hiding this comment.
Yes, I think there are lots of things we should start documenting. A "living document" glossary sounds like a very good idea imo. While there are other documents that use this jargon, none of them are structured around a glossary of terms.
| * Sub-feature 1 (sticky CDN pick) uses origin-based identity; a more advanced | ||
| * or consumer-configurable derivation can replace this default later. | ||
| */ | ||
| export function getCdnId(url: string): string { |
There was a problem hiding this comment.
note: Some of these will end up being config-driven in a followup PR to allow for customization of what counts as "the same CDN", since different content providers may handle these things slightly differently.
There was a problem hiding this comment.
|
Testing this out on the sandbox I see that the example stream only show one host, and the different cdns are represented by [
"https://manifest-gcp-us-east4-vop1.fastly.mux.com",
""
] |
spuppo-mux
left a comment
There was a problem hiding this comment.
Left mainly non-blocking comments and a couple small blocking comments. Up to you if that falls within scope for this PR
A few things here:
Below is the response I see for https://stream.mux.com/ihZa7qP1zY8oyLSQW9TS602VgwQvNdyIvlk9LInEGU2s.m3u8?redundant_streams=true. Note that it uses both hostname/origin ( #EXTM3U
#EXT-X-VERSION:5
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="English",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/nuiK0102hHVlorYh8pGhDZA8zPhJxOfakGGrmaxfRrVloUoDj1H00jepXLA6w7a7YIrC7NjXZ15IUI/subtitles.m3u8?cdn=edgemv&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfM2ZmYzFmNDBhNzgwMDQ1MTg2ZDc2MTFiNDdjZjY3MTA4ZjIwNTRmOWU2Y2M0MDljOGM1MDQ4YjBlZTc2MGZkOA=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Swedish",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="sv",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/uj1zN4yF01nYcblfu3ak02wftTiH027hLk4N44TBnto5yijd5dpyLXAvY7pCVqGMnh8R7BmVNo007xg/subtitles.m3u8?cdn=edgemv&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfZjQwNWUzZDg5MDU3NmNjOTFiODRmZDg3NWJmYTRhMTYzZDRmOGQwMGQ1ZWZlMWIwNjY4ZTY5MmY5MTZmYTM2OQ=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Arabic",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="ar",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/okdNYL00ZUd00pdgqlFOKzqP025oBEcF5VMNj00014UIeLRpNSad9XqYr56AOVtWvyyBFbRRYY01smSfY/subtitles.m3u8?cdn=edgemv&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfOWQ3NTBlZDFmNDY2YmQzZjM1NGU4NGZkZTc3ZWMyZjZhZTBkYWE4ZjY4MjY1N2ZhMWM1MTA4MzRmNDZjYjMyMQ=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="English",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/nuiK0102hHVlorYh8pGhDZA8zPhJxOfakGGrmaxfRrVloUoDj1H00jepXLA6w7a7YIrC7NjXZ15IUI/subtitles.m3u8?cdn=fastly&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfOTM4ZDVlM2VlMGY4OTI5ODAzNDU5MGU1NmFkM2Q4YTQxMzNiNDRmMTA5MmFiMzdjNTI5NjZhN2NhODhiODA0Yg=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Swedish",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="sv",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/uj1zN4yF01nYcblfu3ak02wftTiH027hLk4N44TBnto5yijd5dpyLXAvY7pCVqGMnh8R7BmVNo007xg/subtitles.m3u8?cdn=fastly&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfYWRiMmZhNjI1MTFjZDE1OWYwYTRiOWQxZmY5ZThlYzVmMTk2ZmExMzM5NmRlOGZiYzgwMGZkYzQ5YWE0NTlhNg=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Arabic",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="ar",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/okdNYL00ZUd00pdgqlFOKzqP025oBEcF5VMNj00014UIeLRpNSad9XqYr56AOVtWvyyBFbRRYY01smSfY/subtitles.m3u8?cdn=fastly&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfNmZhNjVkZWJjMjkzYmI1YzhmNDdmNzUxMzkxNjE1MjFjYTQ5MzkxMjE2NjYyODU0Y2JiNWY0NzUyNWJjOGU3Yg=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub3",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="English",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/nuiK0102hHVlorYh8pGhDZA8zPhJxOfakGGrmaxfRrVloUoDj1H00jepXLA6w7a7YIrC7NjXZ15IUI/subtitles.m3u8?cdn=cloudflare&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfZWY4N2Y2ODI2N2U3YmY0Njk4NzM3MDE5NzQwMDZkMTU3ZjdiMGY4ZjRkYTNkMzdkYjM1Y2VkNjhhZTIwY2FlYg=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub3",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Swedish",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="sv",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/uj1zN4yF01nYcblfu3ak02wftTiH027hLk4N44TBnto5yijd5dpyLXAvY7pCVqGMnh8R7BmVNo007xg/subtitles.m3u8?cdn=cloudflare&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfMmNkOTg1YzFkZmNmMzgxOGUzY2IxYTNiN2E5MTkxZTk5NjA4ZjhlMmQ4YmRkZDVhNjYwZTJlOGRmODk1YjE4YQ=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub3",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Arabic",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="ar",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/okdNYL00ZUd00pdgqlFOKzqP025oBEcF5VMNj00014UIeLRpNSad9XqYr56AOVtWvyyBFbRRYY01smSfY/subtitles.m3u8?cdn=cloudflare&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfMzYxNWIxNGVmMTQ3ZTFkMDk5NzQzYmY0Mzk2N2U4NDhmMTNiNjQ5MTU1OGY1NDljZjc3MmVlNjdjY2Y3Zjg3ZA=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub4",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="English",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/nuiK0102hHVlorYh8pGhDZA8zPhJxOfakGGrmaxfRrVloUoDj1H00jepXLA6w7a7YIrC7NjXZ15IUI/subtitles.m3u8?cdn=edgemv&expires=1781708400&skid=default&signature=NmEzMmJiNGVfYWI1ODQ5Y2IzODMxNzgzMDhjOTQyYzQ2MjgzZTBiYTYzNDZmOTdmZTE3NzQyODE3YjVhYTNjMDZhYmFjN2FhYQ=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub4",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Swedish",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="sv",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/uj1zN4yF01nYcblfu3ak02wftTiH027hLk4N44TBnto5yijd5dpyLXAvY7pCVqGMnh8R7BmVNo007xg/subtitles.m3u8?cdn=edgemv&expires=1781708400&skid=default&signature=NmEzMmJiNGVfZjhkODJiNjI4ZTZiMzU4M2MxMDgzZjE4MzRkNzFlMzc1NDBmNjgyYTc2ZmNhMmFhYmM1OTA5N2FjNjBhMjBjYg=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub4",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Arabic",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="ar",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/okdNYL00ZUd00pdgqlFOKzqP025oBEcF5VMNj00014UIeLRpNSad9XqYr56AOVtWvyyBFbRRYY01smSfY/subtitles.m3u8?cdn=edgemv&expires=1781708400&skid=default&signature=NmEzMmJiNGVfNzdhNjE5OWZkNWYyMTUwODg0NDZmYmIxNzQ1ZDlhMGViZjBiMjk1MDBjYTU2YWIyZThiZTIxMmEzM2JjODExMg=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub5",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="English",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/nuiK0102hHVlorYh8pGhDZA8zPhJxOfakGGrmaxfRrVloUoDj1H00jepXLA6w7a7YIrC7NjXZ15IUI/subtitles.m3u8?cdn=fastly&expires=1781708400&skid=default&signature=NmEzMmJiNGVfMjk2MTA1OWIwNjI1YTc4YjAyYjA1OTk3MDY3MzExYTgyYjZlMTlhZjBiOGI5OGU0ZDUyYzMwMmUyM2M0ZWZmOQ=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub5",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Swedish",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="sv",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/uj1zN4yF01nYcblfu3ak02wftTiH027hLk4N44TBnto5yijd5dpyLXAvY7pCVqGMnh8R7BmVNo007xg/subtitles.m3u8?cdn=fastly&expires=1781708400&skid=default&signature=NmEzMmJiNGVfM2VkNGFlMjdiN2M3NDc1MDM4NmY2ZjlkZDNkMjQ0MjBmZGEzNjk3MjY1OWU0NGY0NWYxMmIzMmZiZDJkZWY4MA=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub5",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Arabic",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="ar",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/okdNYL00ZUd00pdgqlFOKzqP025oBEcF5VMNj00014UIeLRpNSad9XqYr56AOVtWvyyBFbRRYY01smSfY/subtitles.m3u8?cdn=fastly&expires=1781708400&skid=default&signature=NmEzMmJiNGVfMTNkMDdiMjBkZDAyNTA5N2VlYjJlY2YwZWE3NzVjM2QwZjRjMGMxM2E3Y2E3MThkZmNiNTk3YTE3YjQ4NjliZA=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub6",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="English",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/nuiK0102hHVlorYh8pGhDZA8zPhJxOfakGGrmaxfRrVloUoDj1H00jepXLA6w7a7YIrC7NjXZ15IUI/subtitles.m3u8?cdn=cloudflare&expires=1781708400&skid=default&signature=NmEzMmJiNGVfODEzZWY5NDYxMGMyNjlhNGQwOTRjODlkZjVjMTdmMTg1YjhjMDFhNDQ3ZWUzNTM1ZTBiZmY1YjE4YWNmMTUzYQ=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub6",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Swedish",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="sv",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/uj1zN4yF01nYcblfu3ak02wftTiH027hLk4N44TBnto5yijd5dpyLXAvY7pCVqGMnh8R7BmVNo007xg/subtitles.m3u8?cdn=cloudflare&expires=1781708400&skid=default&signature=NmEzMmJiNGVfNzEwNzU1YTU4ODg1ODZjZjViNGNiOTc3N2NhNTRhOTViYWM2MjRiYTQzZDcwZGYyYzRkODQ3MzMyNGU1MWRiNw=="
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub6",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",NAME="Arabic",AUTOSELECT=YES,DEFAULT=NO,FORCED=NO,LANGUAGE="ar",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/okdNYL00ZUd00pdgqlFOKzqP025oBEcF5VMNj00014UIeLRpNSad9XqYr56AOVtWvyyBFbRRYY01smSfY/subtitles.m3u8?cdn=cloudflare&expires=1781708400&skid=default&signature=NmEzMmJiNGVfYTFkNjQ4ZTQ1MmMzNWE3MjdkYWRiMGFhMzJiZmU0MmRiMzUwN2RhMzkyM2NlYTFjMTZhYjg3MDE4MzAyMTg5Mg=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",NAME="Default",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",NAME="Spanish",AUTOSELECT=YES,LANGUAGE="es",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/dpPxNR00XO3XAQzPZ85ZSUX7JSd02Y7CcXaUuoEEcALf5bRHrsoo92Wj8TRauenK4tKFjuxOJo94rfK0264HRdKFzajgI7Bx466/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfOTQ2ZmVmNDk3MDMyYzJhZmQyM2RiOTQzNWU1MzFjODc2OWU3ZGY3NWM4MjVmZTE1Yjg2Zjk4NDJmMzU4YTg3Nw=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",NAME="Commentary",AUTOSELECT=YES,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/Yg62TT57f7lvN02hoWEf02G4yJbnE1MeSfScNBNDa02sbm01C7Us901eaat02fIG5ymype1n01WUPlYVamdWL02Bq9hYAsxgFKhL1Y8i/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfMWFlZTQ3MTg3M2QzZTBkMzQ4NDJiZmFhNjU2ZTAzZWNhYmRjZmQ2NzdlNDg4OGE1NTBiYjViYWM0ODM0NTRjMg=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",NAME="Default",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",NAME="Spanish",AUTOSELECT=YES,LANGUAGE="es",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/dpPxNR00XO3XAQzPZ85ZSUX7JSd02Y7CcXaUuoEEcALf5bRHrsoo92Wj8TRauenK4tKFjuxOJo94rfK0264HRdKFzajgI7Bx466/rendition.m3u8?cdn=fastly&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfYzdkYTdjNzg2YjhhODhmZWI3ODcxNDgzOWU0ZWJjNTJkNmQ1MjUwMGEwMzVjMWY4NTliZWY3NGE5NWFmOWU2Nw=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",NAME="Commentary",AUTOSELECT=YES,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/Yg62TT57f7lvN02hoWEf02G4yJbnE1MeSfScNBNDa02sbm01C7Us901eaat02fIG5ymype1n01WUPlYVamdWL02Bq9hYAsxgFKhL1Y8i/rendition.m3u8?cdn=fastly&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfNTBhYWM2MGM3NmIzYjcwZWI4YmRiNDRiMzllZjhiYjIwYTViMTk1NmJkYmJmNjJhODcxMTg0M2ZjOTczMTllYw=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",NAME="Default",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",NAME="Spanish",AUTOSELECT=YES,LANGUAGE="es",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/dpPxNR00XO3XAQzPZ85ZSUX7JSd02Y7CcXaUuoEEcALf5bRHrsoo92Wj8TRauenK4tKFjuxOJo94rfK0264HRdKFzajgI7Bx466/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfOWZiZmZiZDQwYTk3Mzk1ODgzMmUwZDk4YjYzY2ZiYjVlNDg0N2QyM2Q4YTVhOGQxYjA5YjUxOWZlNTQ4YWMyZQ=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",NAME="Commentary",AUTOSELECT=YES,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.edgemv.mux.com/Yg62TT57f7lvN02hoWEf02G4yJbnE1MeSfScNBNDa02sbm01C7Us901eaat02fIG5ymype1n01WUPlYVamdWL02Bq9hYAsxgFKhL1Y8i/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfNjkyZGE5NGE5MmVhMTA4M2U1ZTdjNjgwOGY2ZTNkOTRlMjMwZGNjMGI2YTZjNTI2MDQzNTllZDA5ZjQyZWE0Ng=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud4",NAME="Default",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud4",NAME="Spanish",AUTOSELECT=YES,LANGUAGE="es",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/dpPxNR00XO3XAQzPZ85ZSUX7JSd02Y7CcXaUuoEEcALf5bRHrsoo92Wj8TRauenK4tKFjuxOJo94rfK0264HRdKFzajgI7Bx466/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=default&signature=NmEzMmJiNGVfNDU2ZTViZDM1Yjk4MGQyYjYxNTg5ZjU2MTRmZDMxZjk3YWJhZjk0NWFhNjM4NThlMjJmODA0YmQ4OTk4OTRmMg=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud4",NAME="Commentary",AUTOSELECT=YES,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/Yg62TT57f7lvN02hoWEf02G4yJbnE1MeSfScNBNDa02sbm01C7Us901eaat02fIG5ymype1n01WUPlYVamdWL02Bq9hYAsxgFKhL1Y8i/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=default&signature=NmEzMmJiNGVfZjg2MDdkZjZhNGI4NThjYjIzMzA0NmZmOTRlYWFkMGU1ODg2NWE0MWFlYmRmMTBhOTQ2NmRmZGNjMjU1MzNiMw=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud5",NAME="Default",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud5",NAME="Spanish",AUTOSELECT=YES,LANGUAGE="es",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/dpPxNR00XO3XAQzPZ85ZSUX7JSd02Y7CcXaUuoEEcALf5bRHrsoo92Wj8TRauenK4tKFjuxOJo94rfK0264HRdKFzajgI7Bx466/rendition.m3u8?cdn=fastly&expires=1781708400&skid=default&signature=NmEzMmJiNGVfYWY5ZTM0ZjU2MDA5YTQ1ZDRmZWFjZjM1YzBiMDdjZjA3N2Y4M2M2NmYwODBkYmE3ODlhMzAwOGIzNjQyYzdmZg=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud5",NAME="Commentary",AUTOSELECT=YES,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/Yg62TT57f7lvN02hoWEf02G4yJbnE1MeSfScNBNDa02sbm01C7Us901eaat02fIG5ymype1n01WUPlYVamdWL02Bq9hYAsxgFKhL1Y8i/rendition.m3u8?cdn=fastly&expires=1781708400&skid=default&signature=NmEzMmJiNGVfNGY2ZjQ5MmE1NDI5ZTE3MDQ3MmQxZmIzMmVlYTViMzFkYjA3YmQyYWMwN2E4ZDZjMjZlY2EyYTg1ZmE0Nzc0NA=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud6",NAME="Default",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud6",NAME="Spanish",AUTOSELECT=YES,LANGUAGE="es",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/dpPxNR00XO3XAQzPZ85ZSUX7JSd02Y7CcXaUuoEEcALf5bRHrsoo92Wj8TRauenK4tKFjuxOJo94rfK0264HRdKFzajgI7Bx466/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=default&signature=NmEzMmJiNGVfOWQwYjNiNDlkY2Y3N2JjNGFlMDUxOTBhZWMyOGQ3ZGY2Zjk5Y2FhMzllMmJlNmZkZjY1MWM1ZmM5OThmMzk4Mg=="
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud6",NAME="Commentary",AUTOSELECT=YES,LANGUAGE="en",URI="https://manifest-gcp-us-east4-vop1.fastly.mux.com/Yg62TT57f7lvN02hoWEf02G4yJbnE1MeSfScNBNDa02sbm01C7Us901eaat02fIG5ymype1n01WUPlYVamdWL02Bq9hYAsxgFKhL1Y8i/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=default&signature=NmEzMmJiNGVfYzBiYmRkNzYyNWJmNjJkZDJmMGMyYmJiNGVjYWIzNzQzYjE3ZjM2ZGZmZDIyMTBiMTRkNjdjYzM1MzlkYjE5Mw=="
#EXT-X-STREAM-INF:BANDWIDTH=917400,AVERAGE-BANDWIDTH=917400,CODECS="mp4a.40.2,avc1.64001f",AUDIO="aud1",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,SUBTITLES="sub1"
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/sBwl00pNy02PrG9W4GRrOqxohsMYWvmoZ02RYRbA57aZxs00f00Syk8YMD01ukt2oFLDqRokJrFSONKiI8OLML5zjkP1V6oyXnhL9P/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfZWU1ZmJmZmYwYTJlODc1MWQ0ZjEyMmRjODg4YTRlNzY5OGY4ODcxZTkyMWY4OGJlMWQ5YTk3NTIyZGQ4YzUzOA==
#EXT-X-STREAM-INF:BANDWIDTH=917400,AVERAGE-BANDWIDTH=917400,CODECS="mp4a.40.2,avc1.64001f",AUDIO="aud2",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,SUBTITLES="sub2"
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/sBwl00pNy02PrG9W4GRrOqxohsMYWvmoZ02RYRbA57aZxs00f00Syk8YMD01ukt2oFLDqRokJrFSONKiI8OLML5zjkP1V6oyXnhL9P/rendition.m3u8?cdn=fastly&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfMGYzMGEyYzgyNTI1NzMxZjIyMGEzYzExNTIwZTkxZTE2YzFhNzBkOWVkOTNlODhhOTNiYzI1N2EzZGRmNGYwNg==
#EXT-X-STREAM-INF:BANDWIDTH=917400,AVERAGE-BANDWIDTH=917400,CODECS="mp4a.40.2,avc1.64001f",AUDIO="aud3",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,SUBTITLES="sub3"
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/sBwl00pNy02PrG9W4GRrOqxohsMYWvmoZ02RYRbA57aZxs00f00Syk8YMD01ukt2oFLDqRokJrFSONKiI8OLML5zjkP1V6oyXnhL9P/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfMDJiYTQzMDg5MjFlOTQzMTk4MjFiZGYzODI3N2RlNjEyYTMwODA3Nzc0ODM3NmRhOWI1ZDk2MGRlYTIwNzhiNw==
#EXT-X-STREAM-INF:BANDWIDTH=917400,AVERAGE-BANDWIDTH=917400,CODECS="mp4a.40.2,avc1.64001f",AUDIO="aud4",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,SUBTITLES="sub4"
https://manifest-gcp-us-east4-vop1.fastly.mux.com/sBwl00pNy02PrG9W4GRrOqxohsMYWvmoZ02RYRbA57aZxs00f00Syk8YMD01ukt2oFLDqRokJrFSONKiI8OLML5zjkP1V6oyXnhL9P/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=default&signature=NmEzMmJiNGVfZDI3MmMzZDNhNGU5MDEzZGM5NjY5ZWYzZmMyMjE1YTcxMWZkYWIxYTgxMTQyZGMyYzQ5YmMzOWM0N2RiMDZjZg==
#EXT-X-STREAM-INF:BANDWIDTH=917400,AVERAGE-BANDWIDTH=917400,CODECS="mp4a.40.2,avc1.64001f",AUDIO="aud5",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,SUBTITLES="sub5"
https://manifest-gcp-us-east4-vop1.fastly.mux.com/sBwl00pNy02PrG9W4GRrOqxohsMYWvmoZ02RYRbA57aZxs00f00Syk8YMD01ukt2oFLDqRokJrFSONKiI8OLML5zjkP1V6oyXnhL9P/rendition.m3u8?cdn=fastly&expires=1781708400&skid=default&signature=NmEzMmJiNGVfMzc2NWMzNGE2ODg0ZDY3NzhhMmM2ZTI0ZDM2ZDFiMGJlZjVkOTdmNWY3OWNmYzc1YzhmNzU0MGI3ZGRkNjZjMA==
#EXT-X-STREAM-INF:BANDWIDTH=917400,AVERAGE-BANDWIDTH=917400,CODECS="mp4a.40.2,avc1.64001f",AUDIO="aud6",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,SUBTITLES="sub6"
https://manifest-gcp-us-east4-vop1.fastly.mux.com/sBwl00pNy02PrG9W4GRrOqxohsMYWvmoZ02RYRbA57aZxs00f00Syk8YMD01ukt2oFLDqRokJrFSONKiI8OLML5zjkP1V6oyXnhL9P/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=default&signature=NmEzMmJiNGVfY2U0MzYwZTM3MDg2YmJiNzk5YTYzMDQ4ZDY5OWE3NjBiZWVhNjgyNWVhMjM3NTk2MGU2YWZiNTM3MGE0YWE2Yg==
#EXT-X-STREAM-INF:BANDWIDTH=595100,AVERAGE-BANDWIDTH=595100,CODECS="mp4a.40.2,avc1.64001e",AUDIO="aud1",RESOLUTION=480x270,CLOSED-CAPTIONS=NONE,SUBTITLES="sub1"
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/8pH023cAaqGarBbNTH2npj16YYTWzuLLaUvdUPaOLpKUNz1GN01OuO01pcEKgxQH7On5WNyPWHL1ujCRD6mK9viGp95BV2QKpUv/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfM2FmODUyMjI1OWQ4Y2Q4MTU0OGI5NTQ1NGI3ZWQyMTgyN2EwMjY5YjNmYzg1MDAzNDJhOGRiNmY1ODNmNTYyMQ==
#EXT-X-STREAM-INF:BANDWIDTH=595100,AVERAGE-BANDWIDTH=595100,CODECS="mp4a.40.2,avc1.64001e",AUDIO="aud2",RESOLUTION=480x270,CLOSED-CAPTIONS=NONE,SUBTITLES="sub2"
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/8pH023cAaqGarBbNTH2npj16YYTWzuLLaUvdUPaOLpKUNz1GN01OuO01pcEKgxQH7On5WNyPWHL1ujCRD6mK9viGp95BV2QKpUv/rendition.m3u8?cdn=fastly&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfMDA4MmE1MzgzM2VkODA2ZjhhNzljYzBjODFiNDQwNmQ4MWMxZTQ4ODYxN2Q0OWZjZmNjYzc2ZWFiOTYyZmViMA==
#EXT-X-STREAM-INF:BANDWIDTH=595100,AVERAGE-BANDWIDTH=595100,CODECS="mp4a.40.2,avc1.64001e",AUDIO="aud3",RESOLUTION=480x270,CLOSED-CAPTIONS=NONE,SUBTITLES="sub3"
https://manifest-gcp-us-east4-vop1.edgemv.mux.com/8pH023cAaqGarBbNTH2npj16YYTWzuLLaUvdUPaOLpKUNz1GN01OuO01pcEKgxQH7On5WNyPWHL1ujCRD6mK9viGp95BV2QKpUv/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=edgemv-default-1&signature=NmEzMmJiNGVfNTA4YjQyNWRmMTlhY2UwOWU5NTBmMmUyOGQ2NTQxMzhkNTA0NjYyZDFlMDI3MGNmYWM5MWU1M2QyOWQ5ODAzOQ==
#EXT-X-STREAM-INF:BANDWIDTH=595100,AVERAGE-BANDWIDTH=595100,CODECS="mp4a.40.2,avc1.64001e",AUDIO="aud4",RESOLUTION=480x270,CLOSED-CAPTIONS=NONE,SUBTITLES="sub4"
https://manifest-gcp-us-east4-vop1.fastly.mux.com/8pH023cAaqGarBbNTH2npj16YYTWzuLLaUvdUPaOLpKUNz1GN01OuO01pcEKgxQH7On5WNyPWHL1ujCRD6mK9viGp95BV2QKpUv/rendition.m3u8?cdn=edgemv&expires=1781708400&skid=default&signature=NmEzMmJiNGVfZThkNzFkNTMwNmIyZjU4MGNkZDEzYWVkYzMxNDgwYTllNmZhY2MxZDg1YzY1N2JlNWZhM2IyNzZkMzNmNjE1Yw==
#EXT-X-STREAM-INF:BANDWIDTH=595100,AVERAGE-BANDWIDTH=595100,CODECS="mp4a.40.2,avc1.64001e",AUDIO="aud5",RESOLUTION=480x270,CLOSED-CAPTIONS=NONE,SUBTITLES="sub5"
https://manifest-gcp-us-east4-vop1.fastly.mux.com/8pH023cAaqGarBbNTH2npj16YYTWzuLLaUvdUPaOLpKUNz1GN01OuO01pcEKgxQH7On5WNyPWHL1ujCRD6mK9viGp95BV2QKpUv/rendition.m3u8?cdn=fastly&expires=1781708400&skid=default&signature=NmEzMmJiNGVfOTU0YzJiNmFkY2I2NjE0NDc5ODZiMWM3Y2I4YzA3ZThmM2Y3MTg5M2Y1MGMwZjlkODdhZjc2ZDRmZDc3MzRiZA==
#EXT-X-STREAM-INF:BANDWIDTH=595100,AVERAGE-BANDWIDTH=595100,CODECS="mp4a.40.2,avc1.64001e",AUDIO="aud6",RESOLUTION=480x270,CLOSED-CAPTIONS=NONE,SUBTITLES="sub6"
https://manifest-gcp-us-east4-vop1.fastly.mux.com/8pH023cAaqGarBbNTH2npj16YYTWzuLLaUvdUPaOLpKUNz1GN01OuO01pcEKgxQH7On5WNyPWHL1ujCRD6mK9viGp95BV2QKpUv/rendition.m3u8?cdn=cloudflare&expires=1781708400&skid=default&signature=NmEzMmJiNGVfMWRmOTI1OTMxMTBkODc3ZmRjYzhlNWNlNmJlMmQwMGQ1OWI5ZjQ5NGU5NTcxZTA4MmMwZDY3NzAzZDE1YTA1ZQ==
|
|
Ok, I see, the response I get for https://stream.mux.com/ihZa7qP1zY8oyLSQW9TS602VgwQvNdyIvlk9LInEGU2s.m3u8?redundant_streams=true does not contain the edgemv urls. As for the empty cdn name I'm seeing, I suspect it is because both default audio tracks have an implicit URL (audio comes with the video ts files) which may not be being inferred by the parser. However, I also see those tracks in your manifest, but it sounds like you are not seeing it in cdnPriority |
onCdn read like a callback/event-listener; it's the candidate tracks served from a given CDN. Rename for clarity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getOrderedCdnIds iterated selectionSets in parse order, so "video CDN is primary" held only as a side effect of how the HLS multivariant parser happens to order tracks — not a guarantee. preferActiveCdn anchors every track type to the head of cdnPriority, so an audio-first manifest could silently make an audio CDN primary. Sort selection sets by track-type priority (video, then audio, then text) before collecting CDN ids; a stable sort preserves manifest order within a type. The head of cdnPriority is now video-derived by construction. Strengthen the engine test to a doubly-adversarial source (audio set listed first, cdn-b first within it) so it depends on the ordering rather than array order, and add a getOrderedCdnIds unit test pinning it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6af8f5f. Configure here.
| if (tracksUsingCdn.length) return tracksUsingCdn; | ||
| } | ||
| return tracks; | ||
| } |
There was a problem hiding this comment.
Per-type CDN scope splits hosts
High Severity
preferActiveCdn walks cdnPriority and returns the first entry that has matching candidates in the current type’s track list only. Video and audio each run that logic independently, so if the primary CDN has video renditions but no audio candidates (e.g. a single default #EXT-X-MEDIA row or implicit audio not duplicated per CDN), audio can settle on the next CDN while video stays on the primary—segment requests split across hosts despite the sticky single-CDN goal.
Reviewed by Cursor Bugbot for commit 6af8f5f. Configure here.


TL;DR
Implements sticky multi-CDN selection (sub-feature 1) for redundant-stream HLS — e.g. Mux's
?redundant_streams=true, which serves the same renditions on multiple hosts. Those already parse as separate candidate tracks, so this is purely a selection-layer change: a newresolveCdnPrioritybehavior publishes the manifest-ordered CDN list (cdnPriority), and a newpreferActiveCdnsoft-filter rule narrows every track type to the highest-priority CDN that still has tracks — keeping the whole presentation on one host. No-op for single-CDN sources; no parser changes. Failover (failed-CDN constraint + recovery) is deliberately deferred — it needs the track-switching constraints phase andnetwork-resilience's circuit-breaker.For reviewers — how to read this PR
Runtime (focus here)
Read fully:
resolve-cdn-priority.ts(new) — new slot-owner behavior; single-writer oncdnPriority; lifecycle mirrorssetupTrackSwitching(owns the slot only whilepresentation-resolved, clears on exit). Note the order-sensitive no-churn guard (samePriority) — see the Bugbot callout below.cdn.ts(new) — CDN-identity contract:getCdnId(URL origin, raw-string fallback so the key is always stable) +getOrderedCdnIds(first-occurrence-wins dedup across all track types = manifest priority order).Targeted careful read:
track-switching.ts(+67/−13) — newpreferActiveCdnsoft filter inserted mid-chain (filterByUserSelection→preferActiveCdn→rankByBandwidth); soft-filter fall-through (nocdnPrioritysignal/value, or no CDN matches →applyRulesskips it, never empties the set);SwitchableTrackgainsurl. Verify the narrow is a genuine no-op for single-CDN sources and that the scope sits before the ranker so ABR runs only within the chosen CDN.Skim structure only:
engine.ts(+27),engine-audio-only.ts(+19) —resolveCdnPrioritycomposed before theswitch*behaviors;cdnPriorityadded to each engine's state interface.apps/sandbox/templates/spf-segment-loading/main.ts— test-harness rendition picker, no shipped runtime impact. De-dupes video buttons by bitrate+resolution and pins via a{ bandwidth, width, height }filter (CDN-agnostic) instead of{ id }, mirroring the audio picker's group/build/update split.Skim file: tests (4 files) — assertion shape. The two distinguishing cases:
engine.test.tscross-type test (audio lists a different CDN first than video → shared video-derivedcdnPrioritymust pull audio onto the primary, not a coincidental track-order match) andtrack-switching.test.tslate-arrival test (cdnPrioritystarts undefined, written after the first pick → scope must re-fire and correct, proving the settled pick is composition-order independent).cdn.test.tsandresolve-cdn-priority.test.tscover identity/ordering + lifecycle.Design doc (skim)
Skim file:
multi-cdn-failover.md(+241/−174) — reframed from active-URI-rotation to the track-switching constraint+scope model. Sub-feature 1 (this PR) marked implemented; failover deferred. Confirm the doc's "implemented vs deferred" split matches the code.Smoke test
Sandbox:
/spf-segment-loading/?src=https://stream.mux.com/s41JYeqIpBMBzE4OzxDyGR2yrp2hD1CQ6gJN9SlVGDQ.m3u8?redundant_streams=truepnpm dev:sandbox→ paste the path into the running Vite server) or via the PR's deploy preview. This page drivescreateSimpleHlsEnginedirectly (the engine that composesresolveCdnPriority) and reads the source from thesrcquery param.preload=none, so nothing fetches until playback starts).{ bandwidth, width, height }filter, and the badge moves to that button.What changed — by surface
CDN identity + priority list.
getCdnIdderives a stable grouping key from each track URL's origin;getOrderedCdnIdscollapses the per-CDN candidate tracks to the distinct CDNs in manifest order.resolveCdnPrioritypublishes that as thecdnPrioritysignal while a presentation is resolved and clears it on src unload — the shared list is the per-presentation coherence guarantee (every track type reads the same one).Selection-layer scope.
preferActiveCdnis a soft filter in the shared video/audio rule chain: it narrows candidates to the highest-prioritycdnPriorityentry that still has tracks, so the ranker only ever picks within one CDN. "Active" is derived (first-with-survivors), not stored — which is what makes the future failed-CDN constraint a pure consequence (pruning a cooled-down CDN's tracks moves the pick to the next entry and snaps back on recovery) and lets future content steering compose as a reorder ofcdnPriority.Notable design decisions
cdnPriority: string[], not a storedactiveCdnstring. The active CDN is derived as the first entry with surviving tracks. Alternative considered: store the active CDN directly plus sticky-pick state. Rejected because deriving it keeps failover a pure consequence of constraint-pruning (no reactive rewrite of an "active" field) and lets steering compose as a list reorder — the name mirrors HLS content steering'sPATHWAY-PRIORITY.alternateUrischange. Redundant-stream renditions already parse as distinct tracks per host. Alternative considered: model redundant CDNs at the parser / URI-rotation layer. Rejected because the selection layer already enumerates them, so a scope rule reuses the existing rule chain with zero parser surface.resolveCdnPrioritycomposed beforeswitch*is a mild optimization, not a correctness requirement. Selection is reactive, so a latecdnPriorityconverges on the same pick (covered by the late-arrival test). Ordering only affects a transient wasted media-playlist fetch, and only for an asymmetric manifest (a track type listing a non-primary CDN first); symmetric redundant streams never hit it.Note
Medium Risk
Changes core track-selection for all HLS engine loads (new rule in the chain); behavior is intended as a no-op for single-CDN sources but incorrect CDN ordering or scope logic could mis-route fetches on redundant streams.
Overview
Adds sticky multi-CDN selection for redundant-stream HLS (e.g. Mux
?redundant_streams=true) without parser or fetch-time URL rotation: per-CDN renditions stay separate candidate tracks, and selection keeps the whole presentation on one host.Runtime: New
getCdnId/getOrderedCdnIds(video-first CDN ordering) andresolveCdnPriority, which ownscdnPrioritywhile the presentation is resolved.preferActiveCdnis inserted into the shared video/audio rule chain beforerankByBandwidth, narrowing candidates to the firstcdnPriorityentry with surviving tracks; ABR then runs only within that CDN. Both HLS engines composeresolveCdnPrioritybeforeswitch*and exposecdnPriorityon engine state.Sandbox: The segment-loading rendition picker groups redundant CDN duplicates by bitrate+resolution and pins manual selection via a partial-track filter instead of per-track
id.Docs:
multi-cdn-failover.mdis reframed around track-switching scope + constraint (sticky pick implemented; failover deferred).Reviewed by Cursor Bugbot for commit 6af8f5f. Bugbot is set up for automated code reviews on this repo. Configure here.