mirror of
https://github.com/zoriya/v10.git
synced 2026-08-06 14:18:10 +00:00
fix(spf): call sourceBuffer.abort() on AbortError to reset MSE parser state (#1081)
This commit is contained in:
@@ -23,12 +23,36 @@ export async function appendSegment(sourceBuffer: SourceBuffer, data: AppendData
|
||||
if (data instanceof ArrayBuffer) {
|
||||
await appendChunk(sourceBuffer, data);
|
||||
} else {
|
||||
for await (const chunk of data) {
|
||||
// Check between chunks so an abort can stop streaming before the next
|
||||
// appendBuffer call. The current chunk (if any) has already landed in the
|
||||
// SourceBuffer; the partial: true flag in the actor model reflects this.
|
||||
if (signal?.aborted) throw signal.reason ?? new DOMException('Aborted', 'AbortError');
|
||||
await appendChunk(sourceBuffer, chunk);
|
||||
try {
|
||||
for await (const chunk of data) {
|
||||
// Check between chunks so an abort can stop streaming before the next
|
||||
// appendBuffer call. The current chunk (if any) has already landed in the
|
||||
// SourceBuffer; the partial: true flag in the actor model reflects this.
|
||||
if (signal?.aborted) throw signal.reason ?? new DOMException('Aborted', 'AbortError');
|
||||
await appendChunk(sourceBuffer, chunk);
|
||||
}
|
||||
} catch (e) {
|
||||
// Reset the MSE segment parser on any abort to discard partial fMP4 box
|
||||
// data from the SourceBuffer's internal byte buffer. Two paths reach here:
|
||||
//
|
||||
// 1. Explicit abort check above (signal.aborted between chunks) — the
|
||||
// last appended chunk may have left the parser mid-fragment.
|
||||
//
|
||||
// 2. The fetch was cancelled (signal fired), causing the underlying
|
||||
// ReadableStream to error and the for-await loop to throw directly
|
||||
// without ever passing through the signal check above. Same cleanup
|
||||
// is needed: partial box data must be cleared before the next append.
|
||||
//
|
||||
// Without this, the next appendBuffer call sees stale partial data and
|
||||
// Chrome throws CHUNK_DEMUXER_ERROR_APPEND_FAILED.
|
||||
if (e instanceof DOMException && e.name === 'AbortError' && !sourceBuffer.updating) {
|
||||
try {
|
||||
sourceBuffer.abort();
|
||||
} catch {
|
||||
// Thrown if the MediaSource is not "open" (e.g. during teardown).
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ function makeSourceBuffer(): SourceBuffer {
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
abort: vi.fn(),
|
||||
appendBuffer: vi.fn(() => {
|
||||
setTimeout(() => {
|
||||
for (const listener of listeners.updateend ?? []) listener(new Event('updateend'));
|
||||
@@ -104,6 +105,50 @@ describe('appendSegment', () => {
|
||||
await expect(appendSegment(sb, errorStream())).rejects.toThrow('stream failed');
|
||||
});
|
||||
|
||||
it('calls sourceBuffer.abort() and throws when signal is aborted between chunks', async () => {
|
||||
const sb = makeSourceBuffer();
|
||||
const controller = new AbortController();
|
||||
|
||||
async function* twoChunks(): AsyncGenerator<Uint8Array> {
|
||||
yield new Uint8Array(4);
|
||||
controller.abort();
|
||||
yield new Uint8Array(4);
|
||||
}
|
||||
|
||||
await expect(appendSegment(sb, twoChunks(), controller.signal)).rejects.toMatchObject({
|
||||
name: 'AbortError',
|
||||
});
|
||||
expect(sb.abort).toHaveBeenCalledOnce();
|
||||
// Only the first chunk should have been appended
|
||||
expect(sb.appendBuffer).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls sourceBuffer.abort() when the stream itself throws an AbortError', async () => {
|
||||
const sb = makeSourceBuffer();
|
||||
|
||||
async function* abortingStream(): AsyncGenerator<Uint8Array> {
|
||||
yield new Uint8Array(4);
|
||||
throw new DOMException('Aborted', 'AbortError');
|
||||
}
|
||||
|
||||
await expect(appendSegment(sb, abortingStream())).rejects.toMatchObject({
|
||||
name: 'AbortError',
|
||||
});
|
||||
expect(sb.abort).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not call sourceBuffer.abort() for non-abort stream errors', async () => {
|
||||
const sb = makeSourceBuffer();
|
||||
|
||||
async function* errorStream(): AsyncGenerator<Uint8Array> {
|
||||
yield new Uint8Array(4);
|
||||
throw new Error('network error');
|
||||
}
|
||||
|
||||
await expect(appendSegment(sb, errorStream())).rejects.toThrow('network error');
|
||||
expect(sb.abort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes chunk bytes through to appendBuffer unchanged', async () => {
|
||||
const sb = makeSourceBuffer();
|
||||
const data = new Uint8Array([1, 2, 3, 4]);
|
||||
|
||||
Reference in New Issue
Block a user