Remove sample correlation logic in decoder pipeline

This commit is contained in:
Vanilagy
2025-02-08 15:55:54 +01:00
parent b4d4c71916
commit 1d823805cf
8 changed files with 75 additions and 53 deletions
+5
View File
@@ -11,6 +11,7 @@ export interface InputTrackBacking {
getFirstTimestamp(): Promise<number>; getFirstTimestamp(): Promise<number>;
computeDuration(): Promise<number>; computeDuration(): Promise<number>;
getLanguageCode(): Promise<string>; getLanguageCode(): Promise<string>;
getTimeResolution(): Promise<number>;
} }
/** @public */ /** @public */
@@ -52,6 +53,10 @@ export abstract class InputTrack {
getLanguageCode() { getLanguageCode() {
return this._backing.getLanguageCode(); return this._backing.getLanguageCode();
} }
getTimeResolution() {
return this._backing.getTimeResolution();
}
} }
export interface InputVideoTrackBacking extends InputTrackBacking { export interface InputVideoTrackBacking extends InputTrackBacking {
+4
View File
@@ -1743,6 +1743,10 @@ abstract class IsobmffTrackBacking<
return firstSample?.timestamp ?? 0; return firstSample?.timestamp ?? 0;
} }
async getTimeResolution() {
return this.internalTrack.timescale;
}
abstract createSample( abstract createSample(
data: Uint8Array, data: Uint8Array,
byteLength: number, byteLength: number,
+4
View File
@@ -998,6 +998,10 @@ abstract class MatroskaTrackBacking<
return firstSample?.timestamp ?? 0; return firstSample?.timestamp ?? 0;
} }
async getTimeResolution() {
return this.internalTrack.segment.timestampFactor;
}
abstract createSample( abstract createSample(
data: Uint8Array, data: Uint8Array,
byteLength: number, byteLength: number,
+29 -47
View File
@@ -4,7 +4,6 @@ import { InputAudioTrack, InputVideoTrack } from './input-track';
import { import {
AnyIterable, AnyIterable,
assert, assert,
binarySearchLessOrEqual,
getInt24, getInt24,
getUint24, getUint24,
mapAsyncGenerator, mapAsyncGenerator,
@@ -140,9 +139,8 @@ export abstract class BaseSampleSink<Sample extends EncodedVideoSample | Encoded
} }
} }
export type WrappedMediaFrame<T extends VideoFrame | AudioData, S extends EncodedVideoSample | EncodedAudioSample> = { export type WrappedMediaFrame<T extends VideoFrame | AudioData> = {
frame: T; frame: T;
sample: S;
timestamp: number; timestamp: number;
duration: number; duration: number;
}; };
@@ -150,7 +148,7 @@ export type WrappedMediaFrame<T extends VideoFrame | AudioData, S extends Encode
abstract class DecoderWrapper< abstract class DecoderWrapper<
Sample extends EncodedVideoSample | EncodedAudioSample, Sample extends EncodedVideoSample | EncodedAudioSample,
MediaFrame extends VideoFrame | AudioData, MediaFrame extends VideoFrame | AudioData,
WrappedFrame extends WrappedMediaFrame<MediaFrame, Sample> = WrappedMediaFrame<MediaFrame, Sample>, WrappedFrame extends WrappedMediaFrame<MediaFrame> = WrappedMediaFrame<MediaFrame>,
> { > {
constructor( constructor(
public onFrame: (frame: WrappedFrame) => unknown, public onFrame: (frame: WrappedFrame) => unknown,
@@ -168,7 +166,7 @@ export abstract class BaseMediaFrameSink<
Sample extends EncodedVideoSample | EncodedAudioSample, Sample extends EncodedVideoSample | EncodedAudioSample,
MediaFrame extends VideoFrame | AudioData, MediaFrame extends VideoFrame | AudioData,
/** @internal */ /** @internal */
WrappedFrame extends WrappedMediaFrame<MediaFrame, Sample> = WrappedMediaFrame<MediaFrame, Sample>, WrappedFrame extends WrappedMediaFrame<MediaFrame> = WrappedMediaFrame<MediaFrame>,
> { > {
/** @internal */ /** @internal */
abstract _createDecoder( abstract _createDecoder(
@@ -361,7 +359,7 @@ export abstract class BaseMediaFrameSink<
): AsyncGenerator<WrappedFrame | null, void, unknown> { ): AsyncGenerator<WrappedFrame | null, void, unknown> {
validateAnyIterable(timestamps); validateAnyIterable(timestamps);
const timestampIterator = toAsyncIterator(timestamps); const timestampIterator = toAsyncIterator(timestamps);
const samplesOfInterest: Sample[] = []; const timestampsOfInterest: number[] = [];
const MAX_QUEUE_SIZE = 8; const MAX_QUEUE_SIZE = 8;
const frameQueue: (WrappedFrame | null)[] = []; const frameQueue: (WrappedFrame | null)[] = [];
@@ -395,11 +393,11 @@ export abstract class BaseMediaFrameSink<
let frameUsed = false; let frameUsed = false;
while ( while (
samplesOfInterest.length > 0 timestampsOfInterest.length > 0
&& samplesOfInterest[0]!.is(wrappedFrame.sample as EncodedVideoSample & EncodedAudioSample) && wrappedFrame.timestamp - timestampsOfInterest[0]! > -1e-10 // Give it a little epsilon
) { ) {
pushToQueue(this._duplicateFrame(wrappedFrame)); pushToQueue(this._duplicateFrame(wrappedFrame));
samplesOfInterest.shift(); timestampsOfInterest.shift();
frameUsed = true; frameUsed = true;
} }
@@ -445,7 +443,7 @@ export abstract class BaseMediaFrameSink<
continue; continue;
} }
samplesOfInterest.push(targetSample); timestampsOfInterest.push(targetSample.timestamp);
if ( if (
lastKeySample lastKeySample
@@ -454,13 +452,13 @@ export abstract class BaseMediaFrameSink<
) { ) {
assert(lastSample); assert(lastSample);
if (targetSample.timestamp === lastSample.timestamp && samplesOfInterest.length === 1) { if (targetSample.timestamp === lastSample.timestamp && timestampsOfInterest.length === 1) {
// Special case: We have a repeat sample, but the frame for that sample has already been // Special case: We have a repeat sample, but the frame for that sample has already been
// decoded. Therefore, we need to push the frame here instead of in the decoder callback. // decoded. Therefore, we need to push the frame here instead of in the decoder callback.
if (lastUsedFrame) { if (lastUsedFrame) {
pushToQueue(this._duplicateFrame(lastUsedFrame)); pushToQueue(this._duplicateFrame(lastUsedFrame));
} }
samplesOfInterest.shift(); timestampsOfInterest.shift();
} }
} else { } else {
lastKeySample = keySample; lastKeySample = keySample;
@@ -581,32 +579,27 @@ export class EncodedVideoSampleSink extends BaseSampleSink<EncodedVideoSample> {
class VideoDecoderWrapper extends DecoderWrapper<EncodedVideoSample, VideoFrame> { class VideoDecoderWrapper extends DecoderWrapper<EncodedVideoSample, VideoFrame> {
decoder: VideoDecoder | null = null; decoder: VideoDecoder | null = null;
pendingSamples: EncodedVideoSample[] = [];
customDecoder: CustomVideoDecoder | null = null; customDecoder: CustomVideoDecoder | null = null;
lastCustomDecoderPromise = Promise.resolve(); lastCustomDecoderPromise = Promise.resolve();
customDecoderQueueSize = 0; customDecoderQueueSize = 0;
constructor( constructor(
onFrame: (frame: WrappedMediaFrame<VideoFrame, EncodedVideoSample>) => unknown, onFrame: (frame: WrappedMediaFrame<VideoFrame>) => unknown,
onError: (error: DOMException) => unknown, onError: (error: DOMException) => unknown,
codec: VideoCodec, codec: VideoCodec,
decoderConfig: VideoDecoderConfig, decoderConfig: VideoDecoderConfig,
timeResolution: number,
) { ) {
super(onFrame, onError); super(onFrame, onError);
const frameHandler = (frame: VideoFrame) => { const frameHandler = (frame: VideoFrame) => {
const sample = this.pendingSamples.shift(); // Round the microsecond timestamps to the time resolution
assert(sample); const timestamp = Math.round(frame.timestamp / 1e6 * timeResolution) / timeResolution;
const duration = Math.round((frame.duration ?? 0) / 1e6 * timeResolution) / timeResolution;
// Let's get these from the sample instead of the frame, as the frame has no innate timing info
// (unlike AudioData), so the sample will always be more accurate.
const timestamp = sample.timestamp;
const duration = sample.duration;
onFrame({ onFrame({
frame, frame,
sample,
timestamp, timestamp,
duration, duration,
}); });
@@ -634,10 +627,6 @@ class VideoDecoderWrapper extends DecoderWrapper<EncodedVideoSample, VideoFrame>
} }
decode(sample: EncodedVideoSample) { decode(sample: EncodedVideoSample) {
// We know the decoder spits out frames in sorted order, so we need to insert the sample in the right place
const insertionIndex = binarySearchLessOrEqual(this.pendingSamples, sample.timestamp, x => x.timestamp);
this.pendingSamples.splice(insertionIndex + 1, 0, sample);
if (this.customDecoder) { if (this.customDecoder) {
this.customDecoderQueueSize++; this.customDecoderQueueSize++;
this.lastCustomDecoderPromise = this.lastCustomDecoderPromise.then(() => { this.lastCustomDecoderPromise = this.lastCustomDecoderPromise.then(() => {
@@ -694,7 +683,7 @@ export class VideoFrameSink extends BaseMediaFrameSink<EncodedVideoSample, Video
/** @internal */ /** @internal */
async _createDecoder( async _createDecoder(
onFrame: (frame: WrappedMediaFrame<VideoFrame, EncodedVideoSample>) => unknown, onFrame: (frame: WrappedMediaFrame<VideoFrame>) => unknown,
onError: (error: DOMException) => unknown, onError: (error: DOMException) => unknown,
) { ) {
if (!(await this._videoTrack.canDecode())) { if (!(await this._videoTrack.canDecode())) {
@@ -706,9 +695,10 @@ export class VideoFrameSink extends BaseMediaFrameSink<EncodedVideoSample, Video
const codec = await this._videoTrack.getCodec(); const codec = await this._videoTrack.getCodec();
const decoderConfig = await this._videoTrack.getDecoderConfig(); const decoderConfig = await this._videoTrack.getDecoderConfig();
const timeResolution = await this._videoTrack.getTimeResolution();
assert(codec && decoderConfig); assert(codec && decoderConfig);
return new VideoDecoderWrapper(onFrame, onError, codec, decoderConfig); return new VideoDecoderWrapper(onFrame, onError, codec, decoderConfig, timeResolution);
} }
/** @internal */ /** @internal */
@@ -717,7 +707,7 @@ export class VideoFrameSink extends BaseMediaFrameSink<EncodedVideoSample, Video
} }
/** @internal */ /** @internal */
_wrappedFrameToWrappedVideoFrame(frame: WrappedMediaFrame<VideoFrame, EncodedVideoSample>): WrappedVideoFrame { _wrappedFrameToWrappedVideoFrame(frame: WrappedMediaFrame<VideoFrame>): WrappedVideoFrame {
return { return {
frame: frame.frame, frame: frame.frame,
timestamp: frame.timestamp, timestamp: frame.timestamp,
@@ -888,31 +878,27 @@ export class EncodedAudioSampleSink extends BaseSampleSink<EncodedAudioSample> {
class AudioDecoderWrapper extends DecoderWrapper<EncodedAudioSample, AudioData> { class AudioDecoderWrapper extends DecoderWrapper<EncodedAudioSample, AudioData> {
decoder: AudioDecoder | null = null; decoder: AudioDecoder | null = null;
pendingSamples: EncodedAudioSample[] = [];
customDecoder: CustomAudioDecoder | null = null; customDecoder: CustomAudioDecoder | null = null;
lastCustomDecoderPromise = Promise.resolve(); lastCustomDecoderPromise = Promise.resolve();
customDecoderQueueSize = 0; customDecoderQueueSize = 0;
constructor( constructor(
onData: (data: WrappedMediaFrame<AudioData, EncodedAudioSample>) => unknown, onData: (data: WrappedMediaFrame<AudioData>) => unknown,
onError: (error: DOMException) => unknown, onError: (error: DOMException) => unknown,
codec: AudioCodec, codec: AudioCodec,
decoderConfig: AudioDecoderConfig, decoderConfig: AudioDecoderConfig,
timeResolution: number,
) { ) {
super(onData, onError); super(onData, onError);
const dataHandler = (data: AudioData) => { const dataHandler = (data: AudioData) => {
const sample = this.pendingSamples.shift(); // Round the microsecond timestamps to the time resolution
assert(sample); const timestamp = Math.round(data.timestamp / 1e6 * timeResolution) / timeResolution;
const duration = Math.round(data.duration / 1e6 * timeResolution) / timeResolution;
// We use the timing information from the data instead of sample as it will be more accurate
const timestamp = Math.round(data.timestamp / 1e6 * decoderConfig.sampleRate) / decoderConfig.sampleRate;
const duration = Math.round(data.duration / 1e6 * decoderConfig.sampleRate) / decoderConfig.sampleRate;
onData({ onData({
frame: data, frame: data,
sample,
timestamp, timestamp,
duration, duration,
}); });
@@ -940,10 +926,6 @@ class AudioDecoderWrapper extends DecoderWrapper<EncodedAudioSample, AudioData>
} }
decode(sample: EncodedAudioSample) { decode(sample: EncodedAudioSample) {
// We know the decoder spits out data in sorted order, so we need to insert the sample in the right place
const insertionIndex = binarySearchLessOrEqual(this.pendingSamples, sample.timestamp, x => x.timestamp);
this.pendingSamples.splice(insertionIndex + 1, 0, sample);
if (this.customDecoder) { if (this.customDecoder) {
this.customDecoderQueueSize++; this.customDecoderQueueSize++;
this.lastCustomDecoderPromise = this.lastCustomDecoderPromise.then(() => { this.lastCustomDecoderPromise = this.lastCustomDecoderPromise.then(() => {
@@ -993,7 +975,7 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<EncodedAudioSample, AudioDat
currentTimestamp: number | null = null; currentTimestamp: number | null = null;
constructor( constructor(
onData: (data: WrappedMediaFrame<AudioData, EncodedAudioSample>) => unknown, onData: (data: WrappedMediaFrame<AudioData>) => unknown,
onError: (error: DOMException) => unknown, onError: (error: DOMException) => unknown,
public decoderConfig: AudioDecoderConfig, public decoderConfig: AudioDecoderConfig,
) { ) {
@@ -1131,7 +1113,6 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<EncodedAudioSample, AudioDat
// Since all other decoders are async, we'll make this one behave async as well // Since all other decoders are async, we'll make this one behave async as well
queueMicrotask(() => this.onFrame({ queueMicrotask(() => this.onFrame({
frame: audioData, frame: audioData,
sample,
timestamp: preciseTimestamp, timestamp: preciseTimestamp,
duration: preciseDuration, duration: preciseDuration,
})); }));
@@ -1170,7 +1151,7 @@ export class AudioDataSink extends BaseMediaFrameSink<EncodedAudioSample, AudioD
/** @internal */ /** @internal */
async _createDecoder( async _createDecoder(
onData: (data: WrappedMediaFrame<AudioData, EncodedAudioSample>) => unknown, onData: (data: WrappedMediaFrame<AudioData>) => unknown,
onError: (error: DOMException) => unknown, onError: (error: DOMException) => unknown,
) { ) {
if (!(await this._audioTrack.canDecode())) { if (!(await this._audioTrack.canDecode())) {
@@ -1187,12 +1168,13 @@ export class AudioDataSink extends BaseMediaFrameSink<EncodedAudioSample, AudioD
if ((PCM_AUDIO_CODECS as readonly string[]).includes(decoderConfig.codec)) { if ((PCM_AUDIO_CODECS as readonly string[]).includes(decoderConfig.codec)) {
return new PcmAudioDecoderWrapper(onData, onError, decoderConfig); return new PcmAudioDecoderWrapper(onData, onError, decoderConfig);
} else { } else {
return new AudioDecoderWrapper(onData, onError, codec, decoderConfig); const timeResolution = await this._audioTrack.getTimeResolution();
return new AudioDecoderWrapper(onData, onError, codec, decoderConfig, timeResolution);
} }
} }
/** @internal */ /** @internal */
_wrappedFrameToWrappedAudioData(frame: WrappedMediaFrame<AudioData, EncodedAudioSample>): WrappedAudioData { _wrappedFrameToWrappedAudioData(frame: WrappedMediaFrame<AudioData>): WrappedAudioData {
return { return {
data: frame.frame, data: frame.frame,
timestamp: frame.timestamp, timestamp: frame.timestamp,
+9 -6
View File
@@ -8,8 +8,6 @@ import { EncodedAudioSample, PLACEHOLDER_DATA } from '../sample';
import { FrameHeader, getXingOffset, INFO, XING } from './mp3-misc'; import { FrameHeader, getXingOffset, INFO, XING } from './mp3-misc';
import { Mp3Reader } from './mp3-reader'; import { Mp3Reader } from './mp3-reader';
const AUDIO_SAMPLES_PER_FRAME = 1152;
type Sample = { type Sample = {
timestamp: number; timestamp: number;
duration: number; duration: number;
@@ -46,7 +44,7 @@ export class Mp3Demuxer extends Demuxer {
this.reader.pos += id3Tag.size; this.reader.pos += id3Tag.size;
} }
let nextTimestamp = 0; let nextTimestampInSamples = 0;
// Let's read all samples // Let's read all samples
while (true) { while (true) {
@@ -71,16 +69,16 @@ export class Mp3Demuxer extends Demuxer {
this.firstFrameHeader = header; this.firstFrameHeader = header;
} }
const sampleDuration = AUDIO_SAMPLES_PER_FRAME / header.sampleRate; const sampleDuration = header.audioSamplesInFrame / header.sampleRate;
const sample: Sample = { const sample: Sample = {
timestamp: nextTimestamp, timestamp: nextTimestampInSamples / header.sampleRate,
duration: sampleDuration, duration: sampleDuration,
dataStart: header.startPos, dataStart: header.startPos,
dataSize: header.totalSize, dataSize: header.totalSize,
}; };
this.allSamples.push(sample); this.allSamples.push(sample);
nextTimestamp += sampleDuration; nextTimestampInSamples += header.audioSamplesInFrame;
} }
if (!this.firstFrameHeader) { if (!this.firstFrameHeader) {
@@ -121,6 +119,11 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
return 0; return 0;
} }
async getTimeResolution() {
assert(this.demuxer.firstFrameHeader);
return this.demuxer.firstFrameHeader.sampleRate / this.demuxer.firstFrameHeader.audioSamplesInFrame;
}
computeDuration() { computeDuration() {
return this.demuxer.computeDuration(); return this.demuxer.computeDuration();
} }
+15
View File
@@ -44,6 +44,7 @@ export type FrameHeader = {
copyright: number; copyright: number;
original: number; original: number;
emphasis: number; emphasis: number;
audioSamplesInFrame: number;
}; };
export const computeMp3FrameSize = (layer: number, bitrate: number, sampleRate: number, padding: number) => { export const computeMp3FrameSize = (layer: number, bitrate: number, sampleRate: number, padding: number) => {
@@ -114,6 +115,19 @@ export const readFrameHeader = (word: number, reader: { pos: number; fileSize: n
return null; return null;
} }
let audioSamplesInFrame: number;
if (mpegVersionId === 3) {
audioSamplesInFrame = layer === 3 ? 384 : 1152;
} else {
if (layer === 3) {
audioSamplesInFrame = 384;
} else if (layer === 2) {
audioSamplesInFrame = 1152;
} else {
audioSamplesInFrame = 576;
}
}
return { return {
startPos: startPos, startPos: startPos,
totalSize: frameLength, totalSize: frameLength,
@@ -127,5 +141,6 @@ export const readFrameHeader = (word: number, reader: { pos: number; fileSize: n
copyright, copyright,
original, original,
emphasis, emphasis,
audioSamplesInFrame,
}; };
}; };
+4
View File
@@ -418,6 +418,10 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
return this.bitstream.sampleRate; return this.bitstream.sampleRate;
} }
async getTimeResolution() {
return this.bitstream.sampleRate;
}
async getCodec() { async getCodec() {
return this.bitstream.codec; return this.bitstream.codec;
} }
+5
View File
@@ -219,6 +219,11 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
return this.demuxer.audioInfo.sampleRate; return this.demuxer.audioInfo.sampleRate;
} }
async getTimeResolution() {
assert(this.demuxer.audioInfo);
return this.demuxer.audioInfo.sampleRate;
}
async getLanguageCode() { async getLanguageCode() {
return UNDETERMINED_LANGUAGE; return UNDETERMINED_LANGUAGE;
} }