mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
fix(spf): propagate byteRange when building segment load tasks (#904)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ff8fb3fd36
commit
801be291c3
@@ -1,6 +1,4 @@
|
||||
export type SourceId = 'hls-1' | 'hls-2' | 'hls-3' | 'hls-4' | 'hls-5' | 'mp4-1';
|
||||
|
||||
export const SOURCES: Record<SourceId, { label: string; url: string; type: 'hls' | 'mp4'; subType?: 'mp4' | 'ts' }> = {
|
||||
export const SOURCES = {
|
||||
'hls-1': {
|
||||
label: 'HLS - Big Buck Bunny',
|
||||
url: 'https://stream.mux.com/VcmKA6aqzIzlg3MayLJDnbF55kX00mds028Z65QxvBYaA.m3u8',
|
||||
@@ -36,7 +34,9 @@ export const SOURCES: Record<SourceId, { label: string; url: string; type: 'hls'
|
||||
url: 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4',
|
||||
type: 'mp4',
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
export type SourceId = keyof typeof SOURCES;
|
||||
|
||||
export const SOURCE_IDS = Object.keys(SOURCES) as SourceId[];
|
||||
export const MP4_SOURCE_IDS = SOURCE_IDS.filter((id) => SOURCES[id].type === 'mp4');
|
||||
|
||||
@@ -162,7 +162,12 @@ export function createSegmentLoaderActor(
|
||||
|
||||
// Case 2: Init
|
||||
if (actorCtx.initTrackId !== track.id) {
|
||||
tasks.push({ type: 'append-init', meta: { trackId: track.id }, url: track.initialization.url });
|
||||
tasks.push({
|
||||
type: 'append-init',
|
||||
meta: { trackId: track.id },
|
||||
url: track.initialization.url,
|
||||
...(track.initialization.byteRange !== undefined && { byteRange: track.initialization.byteRange }),
|
||||
});
|
||||
}
|
||||
|
||||
// Case 3: Segments
|
||||
@@ -190,6 +195,7 @@ export function createSegmentLoaderActor(
|
||||
trackBandwidth: track.bandwidth,
|
||||
},
|
||||
url: segment.url,
|
||||
...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,6 +902,134 @@ describe('loadSegments forward buffer flushing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Byte-range segment fetching (fMP4 / CMAF range-request streams)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('loadSegments byte-range segment fetching', () => {
|
||||
it('sends Range headers for byte-range init and media segments', async () => {
|
||||
const segments: Segment[] = [
|
||||
{
|
||||
id: 's0',
|
||||
url: 'http://example.com/video.mp4',
|
||||
startTime: 0,
|
||||
duration: 6,
|
||||
byteRange: { start: 1000, end: 2999 },
|
||||
},
|
||||
{
|
||||
id: 's1',
|
||||
url: 'http://example.com/video.mp4',
|
||||
startTime: 6,
|
||||
duration: 6,
|
||||
byteRange: { start: 3000, end: 4999 },
|
||||
},
|
||||
];
|
||||
|
||||
const rangeHeaders: string[] = [];
|
||||
globalThis.fetch = vi.fn().mockImplementation((input: RequestInfo | URL) => {
|
||||
const range = (input as Request).headers?.get('Range');
|
||||
if (range) rangeHeaders.push(range);
|
||||
return Promise.resolve(new Response(new ArrayBuffer(100)));
|
||||
});
|
||||
|
||||
const { loadSegments } = await import('../load-segments');
|
||||
const { createState: cs } = await import('../../../core/state/create-state');
|
||||
|
||||
const track = {
|
||||
type: 'video' as const,
|
||||
id: 'track-1',
|
||||
url: 'http://example.com/video.m3u8',
|
||||
mimeType: 'video/mp4',
|
||||
codecs: ['avc1.42E01E'],
|
||||
bandwidth: 1_000_000,
|
||||
initialization: { url: 'http://example.com/video.mp4', byteRange: { start: 0, end: 999 } },
|
||||
segments,
|
||||
startTime: 0,
|
||||
duration: 12,
|
||||
};
|
||||
|
||||
const state = cs<SegmentLoadingState>({
|
||||
preload: 'auto',
|
||||
selectedVideoTrackId: 'track-1',
|
||||
currentTime: 0,
|
||||
presentation: {
|
||||
id: 'p1',
|
||||
url: 'http://example.com/playlist.m3u8',
|
||||
startTime: 0,
|
||||
duration: 12,
|
||||
selectionSets: [{ id: 'ss1', type: 'video', switchingSets: [{ id: 'sw1', type: 'video', tracks: [track] }] }],
|
||||
},
|
||||
});
|
||||
|
||||
const { sourceBuffer, actor } = makeSourceBufferWithActor();
|
||||
const owners = cs<SegmentLoadingOwners>({ videoBuffer: sourceBuffer, videoBufferActor: actor });
|
||||
|
||||
const cleanup = loadSegments({ state, owners }, { type: 'video' });
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(owners.current.videoBufferActor?.snapshot.context.segments).toHaveLength(2);
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
expect(rangeHeaders).toContain('bytes=0-999'); // init segment
|
||||
expect(rangeHeaders).toContain('bytes=1000-2999'); // s0
|
||||
expect(rangeHeaders).toContain('bytes=3000-4999'); // s1
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('does not send Range header for non-byte-range segments', async () => {
|
||||
const segments = [makeSegment('s0', 0, 10)];
|
||||
|
||||
const rangeHeaders: string[] = [];
|
||||
globalThis.fetch = vi.fn().mockImplementation((input: RequestInfo | URL) => {
|
||||
const range = (input as Request).headers?.get('Range');
|
||||
if (range) rangeHeaders.push(range);
|
||||
return Promise.resolve(new Response(new ArrayBuffer(100)));
|
||||
});
|
||||
|
||||
const { loadSegments } = await import('../load-segments');
|
||||
const { createState: cs } = await import('../../../core/state/create-state');
|
||||
|
||||
const state = cs<SegmentLoadingState>({
|
||||
preload: 'auto',
|
||||
selectedVideoTrackId: 'track-1',
|
||||
currentTime: 0,
|
||||
presentation: {
|
||||
id: 'p1',
|
||||
url: 'http://example.com/playlist.m3u8',
|
||||
startTime: 0,
|
||||
duration: 10,
|
||||
selectionSets: [
|
||||
{
|
||||
id: 'ss1',
|
||||
type: 'video',
|
||||
switchingSets: [{ id: 'sw1', type: 'video', tracks: [makeResolvedVideoTrack(segments)] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { sourceBuffer, actor } = makeSourceBufferWithActor();
|
||||
const owners = cs<SegmentLoadingOwners>({ videoBuffer: sourceBuffer, videoBufferActor: actor });
|
||||
|
||||
const cleanup = loadSegments({ state, owners }, { type: 'video' });
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(owners.current.videoBufferActor?.snapshot.context.segments).toHaveLength(1);
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
expect(rangeHeaders).toHaveLength(0);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Streaming bandwidth tracking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user