mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(spf): multi cdn failover (#1671)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
33873bc4ae
commit
b89f1e944c
@@ -1,13 +1,19 @@
|
||||
import type { MaybeResolvedPresentation, TrackType } from '../types';
|
||||
|
||||
/**
|
||||
* Identify the CDN a URL is served from, used to group redundant-stream
|
||||
* variants that point at the same content on different hosts. Defaults to the
|
||||
* URL's origin (scheme + host + port); falls back to the raw string when the
|
||||
* URL can't be parsed, so the return value is always a stable grouping key.
|
||||
*
|
||||
* Sub-feature 1 (sticky CDN pick) uses origin-based identity; a more advanced
|
||||
* or consumer-configurable derivation can replace this default later.
|
||||
* Derive a stable grouping key for the CDN a URL is served from. Synchronous and
|
||||
* pure (deliberately not a `resolve*` — no fetch). Consumers override the
|
||||
* default via the engine's `getCdnId` config (e.g. to key on Mux's `cdn=` query
|
||||
* param instead of the host); every CDN-identity site reads that same function
|
||||
* so keys stay comparable across `cdnPriority`, `failedCdns`, and the
|
||||
* track-switching constraint + scope.
|
||||
*/
|
||||
export type GetCdnId = (url: string) => string;
|
||||
|
||||
/**
|
||||
* Default {@link GetCdnId}: the URL's origin (scheme + host + port); falls back
|
||||
* to the raw string when the URL can't be parsed, so the return value is always
|
||||
* a stable grouping key.
|
||||
*/
|
||||
export function getCdnId(url: string): string {
|
||||
try {
|
||||
@@ -34,9 +40,11 @@ const CDN_TYPE_PRIORITY: Record<TrackType, number> = { video: 0, audio: 1, text:
|
||||
*
|
||||
* Redundant-stream sources list the same content on multiple hosts (e.g. Mux's
|
||||
* `?redundant_streams=true`), so each host contributes its own candidate tracks;
|
||||
* this collapses them to the set of CDNs across every track type.
|
||||
* this collapses them to the set of CDNs across every track type. The CDN-id
|
||||
* derivation defaults to {@link getCdnId}; pass a consumer-configured `getId` to
|
||||
* key on something other than origin.
|
||||
*/
|
||||
export function getOrderedCdnIds(presentation: MaybeResolvedPresentation): string[] {
|
||||
export function getOrderedCdnIds(presentation: MaybeResolvedPresentation, getId: GetCdnId = getCdnId): string[] {
|
||||
const seen = new Set<string>();
|
||||
const ids: string[] = [];
|
||||
// Stable sort keeps manifest order among same-type selection sets.
|
||||
@@ -46,7 +54,7 @@ export function getOrderedCdnIds(presentation: MaybeResolvedPresentation): strin
|
||||
for (const selectionSet of selectionSets) {
|
||||
for (const switchingSet of selectionSet.switchingSets) {
|
||||
for (const track of switchingSet.tracks) {
|
||||
const id = getCdnId(track.url);
|
||||
const id = getId(track.url);
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
ids.push(id);
|
||||
@@ -55,3 +63,13 @@ export function getOrderedCdnIds(presentation: MaybeResolvedPresentation): strin
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a CDN id to a failed-CDN list, preserving order and ignoring duplicates.
|
||||
* Idempotent: re-adding an already-present id returns the same array reference
|
||||
* (so a no-op trip doesn't churn the `failedCdns` signal). The failover trip in
|
||||
* `resolve-track` and the segment loaders feed this into `failedCdns` via `update`.
|
||||
*/
|
||||
export function addFailedCdn(failed: string[] | undefined, cdn: string): string[] {
|
||||
return failed?.includes(cdn) ? failed : [...(failed ?? []), cdn];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { MaybeResolvedPresentation } from '../../types';
|
||||
import { getCdnId, getOrderedCdnIds } from '../cdn';
|
||||
import { addFailedCdn, getCdnId, getOrderedCdnIds } from '../cdn';
|
||||
|
||||
const presentationWith = (urlsByType: {
|
||||
video?: string[];
|
||||
@@ -119,3 +119,22 @@ describe('getOrderedCdnIds', () => {
|
||||
expect(getOrderedCdnIds(presentation)).toEqual(['https://cdn-a.example.com', 'https://cdn-b.example.com']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFailedCdn', () => {
|
||||
it('appends to an undefined or empty list', () => {
|
||||
expect(addFailedCdn(undefined, 'https://cdn-a.example.com')).toEqual(['https://cdn-a.example.com']);
|
||||
expect(addFailedCdn([], 'https://cdn-a.example.com')).toEqual(['https://cdn-a.example.com']);
|
||||
});
|
||||
|
||||
it('appends in order', () => {
|
||||
expect(addFailedCdn(['https://cdn-a.example.com'], 'https://cdn-b.example.com')).toEqual([
|
||||
'https://cdn-a.example.com',
|
||||
'https://cdn-b.example.com',
|
||||
]);
|
||||
});
|
||||
|
||||
it('is idempotent — re-adding a present CDN returns the same array reference', () => {
|
||||
const failed = ['https://cdn-a.example.com'];
|
||||
expect(addFailedCdn(failed, 'https://cdn-a.example.com')).toBe(failed);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user