Add AudioSampleCursor, make sample cursor error behavior more explicit, add sample mapping, and a bunch of extra shit I forgot

This commit is contained in:
Vanilagy
2025-12-14 23:24:50 +01:00
parent 48abcb240f
commit 0f015d8e98
10 changed files with 934 additions and 499 deletions
+559 -469
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { SampleCursor } from './cursors';
import { Demuxer } from './demuxer';
import { InputFormat } from './input-format';
import { assert, polyfillSymbolDispose } from './misc';
@@ -44,6 +45,9 @@ export class Input<S extends Source = Source> implements Disposable {
_reader: Reader;
/** @internal */
_disposed = false;
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_openSampleCursors = new Set<SampleCursor<any>>();
/** True if the input has been disposed. */
get disposed() {
@@ -181,7 +185,9 @@ export class Input<S extends Source = Source> implements Disposable {
this._source._disposed = true;
this._source._dispose();
// TODO this should dispose cursors probably
for (const cursor of [...this._openSampleCursors]) {
void cursor.close();
}
}
/**
+48 -7
View File
@@ -387,7 +387,7 @@ export class EncodedPacketSink {
}
}
abstract class DecoderWrapper<
export abstract class DecoderWrapper<
MediaSample extends VideoSample | AudioSample,
> {
constructor(
@@ -399,6 +399,8 @@ abstract class DecoderWrapper<
abstract decode(packet: EncodedPacket): void;
abstract flush(): Promise<void>;
abstract close(): void;
abstract get closed(): boolean;
}
/**
@@ -1749,17 +1751,20 @@ export class CanvasSink {
}
}
class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
export class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
decoder: AudioDecoder | null = null;
customDecoder: CustomAudioDecoder | null = null;
customDecoderCallSerializer = new CallSerializer();
customDecoderQueueSize = 0;
customDecoderClosed = false;
// Internal state to accumulate a precise current timestamp based on audio durations, not the (potentially
// inaccurate) packet timestamps.
currentTimestamp: number | null = null;
onDequeue: (() => unknown) | null = null;
constructor(
onSample: (sample: AudioSample) => unknown,
onError: (error: Error) => unknown,
@@ -1824,6 +1829,10 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
error: onError,
});
this.decoder.configure(decoderConfig);
this.decoder.addEventListener('dequeue', () => {
this.onDequeue?.();
});
}
}
@@ -1841,7 +1850,10 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
this.customDecoderQueueSize++;
void this.customDecoderCallSerializer
.call(() => this.customDecoder!.decode(packet))
.then(() => this.customDecoderQueueSize--);
.then(() => {
this.customDecoderQueueSize--;
this.onDequeue?.();
});
} else {
assert(this.decoder);
this.decoder.decode(packet.toEncodedAudioChunk());
@@ -1859,17 +1871,36 @@ class AudioDecoderWrapper extends DecoderWrapper<AudioSample> {
close() {
if (this.customDecoder) {
void this.customDecoderCallSerializer.call(() => this.customDecoder!.close());
if (!this.customDecoderClosed) {
this.customDecoderClosed = true;
void this.customDecoderCallSerializer.call(() => this.customDecoder!.close());
}
} else {
assert(this.decoder);
this.decoder.close();
if (this.decoder.state !== 'closed') {
this.decoder.close();
}
}
}
get closed() {
if (this.customDecoder) {
if (this.customDecoderClosed) {
return true;
}
return !this.customDecoderCallSerializer.errored;
} else {
assert(this.decoder);
return this.decoder.state === 'closed';
}
}
}
// There are a lot of PCM variants not natively supported by the browser and by AudioData. Therefore we need a simple
// decoder that maps any input PCM format into a PCM format supported by the browser.
class PcmAudioDecoderWrapper extends DecoderWrapper<AudioSample> {
export class PcmAudioDecoderWrapper extends DecoderWrapper<AudioSample> {
codec: PcmAudioCodec;
inputSampleSize: 1 | 2 | 3 | 4 | 8;
@@ -1883,6 +1914,10 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<AudioSample> {
// inaccurate) packet timestamps.
currentTimestamp: number | null = null;
isClosed = false;
onDequeue: (() => unknown) | null = null;
constructor(
onSample: (sample: AudioSample) => unknown,
onError: (error: Error) => unknown,
@@ -2006,6 +2041,8 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<AudioSample> {
}
decode(packet: EncodedPacket) {
this.onDequeue?.();
const inputView = toDataView(packet.data);
const numberOfFrames = packet.byteLength / this.decoderConfig.numberOfChannels / this.inputSampleSize;
@@ -2048,7 +2085,11 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<AudioSample> {
}
close() {
// Do nothing
this.isClosed = true;
}
get closed() {
return this.isClosed;
}
}
+2
View File
@@ -835,6 +835,8 @@ export const polyfillSymbolDispose = () => {
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html
// @ts-expect-error Readonly
Symbol.dispose ??= Symbol('Symbol.dispose');
// @ts-expect-error Readonly
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');
};
export const isNumber = (x: unknown) => {
+5
View File
@@ -921,6 +921,9 @@ export type AudioSampleCopyToOptions = {
* @public
*/
export class AudioSample implements Disposable {
/** @internal */
static _openSampleCount = 0;
/** @internal */
_data: AudioData | Uint8Array;
/** @internal */
@@ -1033,6 +1036,7 @@ export class AudioSample implements Disposable {
this._data = dataBuffer;
}
AudioSample._openSampleCount++;
finalizationRegistry?.register(this, { type: 'audio', data: this._data }, this);
}
@@ -1275,6 +1279,7 @@ export class AudioSample implements Disposable {
}
this._closed = true;
AudioSample._openSampleCount--;
}
/**