Implement @mediabunny/prores extension, add onError callbacks to custom coders, fix visibleRect bug in VideoSample

This commit is contained in:
Vanilagy
2026-06-29 17:15:10 +02:00
parent af618f014a
commit 8ae0f9d09e
42 changed files with 1074 additions and 451 deletions
+52 -30
View File
@@ -333,7 +333,8 @@ export class EncodedPacketSink {
// This stores errors that are "out of band" in the sense that they didn't occur in the normal flow of this
// method but instead in a different context. This error should not go unnoticed and must be bubbled up to
// the consumer.
let outOfBandError = null as Error | null;
let outOfBandError = null as unknown;
let hasOutOfBandError = false;
const timestamps: number[] = [];
// The queue should always be big enough to hold 1 second worth of packets
@@ -364,9 +365,10 @@ export class EncodedPacketSink {
ended = true;
onQueueNotEmpty();
})().catch((error: Error) => {
if (!outOfBandError) {
})().catch((error) => {
if (!hasOutOfBandError) {
outOfBandError = error;
hasOutOfBandError = true;
onQueueNotEmpty();
}
});
@@ -380,7 +382,7 @@ export class EncodedPacketSink {
throw new InputDisposedError();
} else if (terminated) {
return { value: undefined, done: true };
} else if (outOfBandError) {
} else if (hasOutOfBandError) {
throw outOfBandError;
} else if (packetQueue.length > 0) {
const value = packetQueue.shift()!;
@@ -423,7 +425,7 @@ abstract class DecoderWrapper<
> {
constructor(
public onSample: (sample: MediaSample) => unknown,
public onError: (error: Error) => unknown,
public onError: (error: unknown) => unknown,
) {}
abstract getDecodeQueueSize(): number;
@@ -446,7 +448,7 @@ export abstract class BaseMediaSampleSink<
/** @internal */
abstract _createDecoder(
onSample: (sample: MediaSample) => unknown,
onError: (error: Error) => unknown
onError: (error: unknown) => unknown
): Promise<DecoderWrapper<MediaSample>>;
/** @internal */
abstract _createPacketSink(): EncodedPacketSink;
@@ -472,7 +474,8 @@ export abstract class BaseMediaSampleSink<
// This stores errors that are "out of band" in the sense that they didn't occur in the normal flow of this
// method but instead in a different context. This error should not go unnoticed and must be bubbled up to
// the consumer.
let outOfBandError = null as Error | null;
let outOfBandError = null as unknown;
let hasOutOfBandError = false;
const packetRetrievalOptions: PacketRetrievalOptions = {
...options,
@@ -518,8 +521,9 @@ export abstract class BaseMediaSampleSink<
({ promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers());
}
}, (error) => {
if (!outOfBandError) {
if (!hasOutOfBandError) {
outOfBandError = error;
hasOutOfBandError = true;
onQueueNotEmpty();
}
});
@@ -572,9 +576,10 @@ export abstract class BaseMediaSampleSink<
decoderIsFlushed = true;
onQueueNotEmpty(); // To unstuck the generator
})().catch((error: Error) => {
if (!outOfBandError) {
})().catch((error) => {
if (!hasOutOfBandError) {
outOfBandError = error;
hasOutOfBandError = true;
onQueueNotEmpty();
}
});
@@ -595,7 +600,7 @@ export abstract class BaseMediaSampleSink<
throw new InputDisposedError();
} else if (terminated) {
return { value: undefined, done: true };
} else if (outOfBandError) {
} else if (hasOutOfBandError) {
closeSamples();
throw outOfBandError;
} else if (sampleQueue.length > 0) {
@@ -645,7 +650,8 @@ export abstract class BaseMediaSampleSink<
// This stores errors that are "out of band" in the sense that they didn't occur in the normal flow of this
// method but instead in a different context. This error should not go unnoticed and must be bubbled up to
// the consumer.
let outOfBandError = null as Error | null;
let outOfBandError = null as unknown;
let hasOutOfBandError = false;
const pushToQueue = (sample: MediaSample | null) => {
sampleQueue.push(sample);
@@ -687,8 +693,9 @@ export abstract class BaseMediaSampleSink<
sample.close();
}
}, (error) => {
if (!outOfBandError) {
if (!hasOutOfBandError) {
outOfBandError = error;
hasOutOfBandError = true;
onQueueNotEmpty();
}
});
@@ -792,9 +799,10 @@ export abstract class BaseMediaSampleSink<
decoderIsFlushed = true;
onQueueNotEmpty(); // To unstuck the generator
})().catch((error: Error) => {
if (!outOfBandError) {
})().catch((error) => {
if (!hasOutOfBandError) {
outOfBandError = error;
hasOutOfBandError = true;
onQueueNotEmpty();
}
});
@@ -814,7 +822,7 @@ export abstract class BaseMediaSampleSink<
throw new InputDisposedError();
} else if (terminated) {
return { value: undefined, done: true };
} else if (outOfBandError) {
} else if (hasOutOfBandError) {
closeSamples();
throw outOfBandError;
} else if (sampleQueue.length > 0) {
@@ -882,7 +890,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
constructor(
onSample: (sample: VideoSample) => unknown,
onError: (error: Error) => unknown,
onError: (error: unknown) => unknown,
public codec: VideoCodec,
public decoderConfig: VideoDecoderConfig,
public rotation: Rotation,
@@ -906,8 +914,14 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
this.finalizeAndEmitSample(sample);
};
// @ts-expect-error It's technically readonly
this.customDecoder.onError = (error) => {
onError(error);
};
void this.customDecoderCallSerializer.call(() => this.customDecoder!.init());
void this.customDecoderCallSerializer
.call(() => this.customDecoder!.init())
.catch(error => onError(error));
} else {
const colorHandler = (frame: VideoFrame) => {
this.frameHandlerSerializer.call(async () => {
@@ -920,7 +934,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
} else {
this.colorQueue.push(frame);
}
}).catch((error: Error) => this.onError(error));
}).catch(error => this.onError(error));
};
if (codec === 'avc' && this.decoderConfig.description && isChromium()) {
@@ -946,7 +960,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
try {
colorHandler(frame);
} catch (error) {
this.onError(error as Error);
this.onError(error);
}
},
error: (error) => {
@@ -984,7 +998,8 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
this.customDecoderQueueSize++;
void this.customDecoderCallSerializer
.call(() => this.customDecoder!.decode(packet))
.then(() => this.customDecoderQueueSize--);
.catch(error => this.onError(error))
.finally(() => this.customDecoderQueueSize--);
} else {
assert(this.decoder);
@@ -1074,7 +1089,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
}
this.alphaDecoderQueueSize--;
}).catch((error: Error) => this.onError(error));
}).catch(error => this.onError(error));
};
const stack = new Error('Decoding error').stack;
@@ -1084,7 +1099,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
try {
alphaHandler(frame);
} catch (error) {
this.onError(error as Error);
this.onError(error);
}
},
error: (error) => {
@@ -1813,7 +1828,7 @@ export class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
/** @internal */
async _createDecoder(
onSample: (sample: VideoSample) => unknown,
onError: (error: Error) => unknown,
onError: (error: unknown) => unknown,
) {
if (!(await this._track.canDecode())) {
throw new Error(
@@ -2215,7 +2230,7 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
constructor(
onSample: (sample: AudioSample) => unknown,
onError: (error: Error) => unknown,
onError: (error: unknown) => unknown,
codec: AudioCodec,
decoderConfig: AudioDecoderConfig,
) {
@@ -2271,8 +2286,14 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
sampleHandler(sample);
};
// @ts-expect-error It's technically readonly
this.customDecoder.onError = (error) => {
onError(error);
};
void this.customDecoderCallSerializer.call(() => this.customDecoder!.init());
void this.customDecoderCallSerializer
.call(() => this.customDecoder!.init())
.catch(error => onError(error));
} else {
const stack = new Error('Decoding error').stack;
@@ -2281,7 +2302,7 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
try {
sampleHandler(new AudioSample(data));
} catch (error) {
this.onError(error as Error);
this.onError(error);
}
},
error: (error) => {
@@ -2307,7 +2328,8 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
this.customDecoderQueueSize++;
void this.customDecoderCallSerializer
.call(() => this.customDecoder!.decode(packet))
.then(() => this.customDecoderQueueSize--);
.catch(error => this.onError(error))
.finally(() => this.customDecoderQueueSize--);
} else {
assert(this.decoder);
@@ -2357,7 +2379,7 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<AudioSample> {
constructor(
onSample: (sample: AudioSample) => unknown,
onError: (error: Error) => unknown,
onError: (error: unknown) => unknown,
public decoderConfig: AudioDecoderConfig,
) {
super(onSample, onError);
@@ -2547,7 +2569,7 @@ export class AudioSampleSink extends BaseMediaSampleSink<AudioSample> {
/** @internal */
async _createDecoder(
onSample: (sample: AudioSample) => unknown,
onError: (error: Error) => unknown,
onError: (error: unknown) => unknown,
) {
if (!(await this._track.canDecode())) {
throw new Error(