From f151ce85e844217795eb1821101388684f99cf0c Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Wed, 13 May 2026 19:55:22 +0200 Subject: [PATCH] Add eccentric timestamp tests for server extension --- packages/server/src/audio-encoder.ts | 5 ++- test/node/server-extension.test.ts | 55 ++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/server/src/audio-encoder.ts b/packages/server/src/audio-encoder.ts index 4dd4494..7b9dd16 100644 --- a/packages/server/src/audio-encoder.ts +++ b/packages/server/src/audio-encoder.ts @@ -260,8 +260,9 @@ export class NodeAvAudioEncoder extends CustomAudioEncoder { metadata = {}; } else { // To compensate for any negative timestamp things that FFmpeg might do. It does these for a reason, to - // indicate encoder delay, but the notion of this is not yet supported in Mediabunny. - this.outputTimestampOffset = this.firstExpectedTimestamp - timestamp; + // indicate encoder delay, but the notion of this is not yet supported in Mediabunny. Yes, this technically + // introduces audio sync drift. + this.outputTimestampOffset = Math.max(this.firstExpectedTimestamp - timestamp, 0); const codecString = this.config.codec; let description = this.codecContext.extraData diff --git a/test/node/server-extension.test.ts b/test/node/server-extension.test.ts index 6267dff..916d94b 100644 --- a/test/node/server-extension.test.ts +++ b/test/node/server-extension.test.ts @@ -10,7 +10,13 @@ import { NodeAvVideoEncoder } from '../../packages/server/src/video-encoder.js'; import { NodeAvAudioDecoder } from '../../packages/server/src/audio-decoder.js'; import { NodeAvAudioEncoder } from '../../packages/server/src/audio-encoder.js'; import { AudioSample, VideoSample } from '../../src/sample.js'; -import { AudioCodec, buildAudioCodecString, buildVideoCodecString, VideoCodec } from '../../src/codec.js'; +import { + AudioCodec, + buildAudioCodecString, + buildVideoCodecString, + NON_PCM_AUDIO_CODECS, + VideoCodec, +} from '../../src/codec.js'; import { EncodedPacket } from '../../src/packet.js'; import { AvcNalUnitType, @@ -1368,11 +1374,52 @@ describe('Audio', async () => { }); }); + for (const codec of NON_PCM_AUDIO_CODECS) { + test(`${codec} encode & decode, negative timestamps`, async () => { + await timestampTest(codec, -1); + }); + + test(`${codec} encode & decode, positive timestamps`, async () => { + await timestampTest(codec, 1); + }); + + test(`${codec} encode & decode, huge timestamps`, async () => { + await timestampTest(codec, 1e9); + }); + } + + const timestampTest = async (codec: AudioCodec, startTimestamp: number) => { + let previousPacket: EncodedPacket | null = null; + let previousSample: AudioSample | null = null; + + const testDuration = codec !== 'opus'; + const testSampleStart = codec !== 'vorbis'; + + await encodeDecodeTest(codec, {}, async (packet, _meta, i) => { + if (i === 0) { + expect(packet.timestamp).toBe(startTimestamp); + } else if (testDuration) { + expect(packet.timestamp).toBeCloseTo(previousPacket!.timestamp + previousPacket!.duration); + } + previousPacket = packet; + }, async (sample, i) => { + if (i === 0) { + if (testSampleStart) { + expect(sample.timestamp).toBe(startTimestamp); + } + } else if (testDuration) { + expect(sample.timestamp).toBeCloseTo(previousSample!.timestamp + previousSample!.duration); + } + previousSample = sample; + }, startTimestamp); + }; + const encodeDecodeTest = async ( codec: AudioCodec, extraConfig: Partial, onPacket: (packet: EncodedPacket, meta: EncodedAudioChunkMetadata, i: number, n: number) => Promise, onSample: (sample: AudioSample, i: number, n: number) => Promise, + startTimestamp = 0, ) => { const sampleRate = 48000; const numberOfChannels = 2; @@ -1404,7 +1451,7 @@ describe('Audio', async () => { using inputSample = new AudioSample({ data, format: 'f32', - timestamp: 0, + timestamp: startTimestamp, numberOfChannels, sampleRate, }); @@ -1438,7 +1485,9 @@ describe('Audio', async () => { await decoder.flush(); expect(decodedSamples.length).toBeGreaterThan(0); - expect(last(decodedSamples)!.timestamp + last(decodedSamples)!.duration).toBeGreaterThanOrEqual(2); + expect(last(decodedSamples)!.timestamp + last(decodedSamples)!.duration).toBeGreaterThanOrEqual( + startTimestamp + 2, + ); for (let i = 0; i < decodedSamples.length; i++) { await onSample(decodedSamples[i]!, i, decodedSamples.length);