import { expect, test } from 'vitest'; import path from 'node:path'; import { Input } from '../../src/input.js'; import { BufferSource, FilePathSource } from '../../src/source.js'; import { ADTS, ALL_FORMATS } from '../../src/input-format.js'; import { EncodedPacketSink } from '../../src/media-sink.js'; import { Output } from '../../src/output.js'; import { BufferTarget } from '../../src/target.js'; import { Mp4OutputFormat } from '../../src/output-format.js'; import { Conversion } from '../../src/conversion.js'; import { assert } from '../../src/misc.js'; import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js'; import { EncodedPacket } from '../../src/packet.js'; const __dirname = new URL('.', import.meta.url).pathname; test('ISOBMFF muxer internally converts ADTS to AAC', async () => { using input = new Input({ source: new FilePathSource(path.join(__dirname, '../public/sample3.aac')), formats: ALL_FORMATS, }); expect(await input.getFormat()).toBe(ADTS); const inputTrack = await input.getPrimaryAudioTrack(); assert(inputTrack); const inputDecoderConfig = await inputTrack.getDecoderConfig(); expect(inputDecoderConfig!.description).toBeUndefined(); // ADTS input has no description const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); const conversion = await Conversion.init({ input, output, showWarnings: false }); await conversion.execute(); using outputAsInput = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const outputTrack = await outputAsInput.getPrimaryAudioTrack(); assert(outputTrack); expect(await outputTrack.getCodec()).toBe('aac'); expect(await outputTrack.getSampleRate()).toBe(await inputTrack.getSampleRate()); expect(await outputTrack.getNumberOfChannels()).toBe(await inputTrack.getNumberOfChannels()); const outputDecoderConfig = await outputTrack.getDecoderConfig(); expect(outputDecoderConfig!.description).toBeDefined(); const outputSink = new EncodedPacketSink(outputTrack); let count = 0; for await (const packet of outputSink.packets()) { // Packets should NOT be ADTS frames (should not start with 0xFFF sync word) const isAdts = packet.data[0] === 0xff && (packet.data[1]! & 0xf0) === 0xf0; expect(isAdts).toBe(false); count++; } expect(count).toBe(4557); }); test('Fragmented fMP4 with video+audio preserves B-frame CTS', async () => { using input = new Input({ source: new FilePathSource(path.join(__dirname, '../public/video.mp4')), formats: ALL_FORMATS, }); const videoTrack = await input.getPrimaryVideoTrack(); const audioTrack = await input.getPrimaryAudioTrack(); assert(videoTrack); assert(audioTrack); const originalVideoSink = new EncodedPacketSink(videoTrack); const originalTimestamps: number[] = []; for await (const packet of originalVideoSink.packets()) { originalTimestamps.push(packet.timestamp); } const output = new Output({ format: new Mp4OutputFormat({ fastStart: 'fragmented' }), target: new BufferTarget(), }); const conversion = await Conversion.init({ input, output, showWarnings: false }); await conversion.execute(); using outputAsInput = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const outputVideoTrack = await outputAsInput.getPrimaryVideoTrack(); assert(outputVideoTrack); const videoSink = new EncodedPacketSink(outputVideoTrack); const timestamps: number[] = []; for await (const packet of videoSink.packets()) { timestamps.push(packet.timestamp); } expect(timestamps).toEqual(originalTimestamps); }); test('Zero start timestamp, regular MP4', async () => { const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); const source = new EncodedVideoPacketSource('vp8'); output.addVideoTrack(source); await output.start(); const meta = { decoderConfig: { codec: 'vp8', codedWidth: 1280, codedHeight: 720 } }; await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 0.1), meta); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 0.1, 0.1)); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 0.2, 0.1)); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 0.3, 0.1)); await output.finalize(); // Hacky but works const str = String.fromCharCode(...new Uint8Array(output.target.buffer!)); expect(str.includes('edts') || str.includes('elst')).toBe(false); }); test('Non-zero start timestamp, regular MP4', async () => { const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); const source = new EncodedVideoPacketSource('vp8'); output.addVideoTrack(source); await output.start(); const meta = { decoderConfig: { codec: 'vp8', codedWidth: 1280, codedHeight: 720 } }; await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 1, 0.1), meta); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 1.1, 0.1)); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 1.2, 0.1)); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 1.3, 0.1)); await output.finalize(); // Hacky but works const str = String.fromCharCode(...new Uint8Array(output.target.buffer!)); expect(str.includes('edts') && str.includes('elst')).toBe(true); using input = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const track = await input.getPrimaryVideoTrack(); assert(track); const sink = new EncodedPacketSink(track); const timestamps: number[] = []; const durations: number[] = []; for await (const packet of sink.packets()) { timestamps.push(packet.timestamp); durations.push(packet.duration); } expect(timestamps).toEqual([1, 1.1, 1.2, 1.3]); expect(durations).toEqual([0.1, 0.1, 0.1, 0.1]); }); test('Non-zero start timestamp, fragmented MP4', async () => { const output = new Output({ format: new Mp4OutputFormat({ fastStart: 'fragmented' }), target: new BufferTarget(), }); const source = new EncodedVideoPacketSource('vp8'); output.addVideoTrack(source); await output.start(); const meta = { decoderConfig: { codec: 'vp8', codedWidth: 1280, codedHeight: 720 } }; await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 1, 0.1), meta); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 1.1, 0.1)); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 1.2, 0.1)); await source.add(new EncodedPacket(new Uint8Array(1024), 'delta', 1.3, 0.1)); await output.finalize(); // Hacky but works const str = String.fromCharCode(...new Uint8Array(output.target.buffer!)); expect(str.includes('edts') || str.includes('elst')).toBe(false); using input = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const track = await input.getPrimaryVideoTrack(); assert(track); const sink = new EncodedPacketSink(track); const timestamps: number[] = []; const durations: number[] = []; for await (const packet of sink.packets()) { timestamps.push(packet.timestamp); durations.push(packet.duration); } expect(timestamps).toEqual([1, 1.1, 1.2, 1.3]); expect(durations).toEqual([0.1, 0.1, 0.1, 0.1]); }); test('PCM audio', async () => { const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); const source = new EncodedAudioPacketSource('pcm-s16'); output.addAudioTrack(source); await output.start(); const meta = { decoderConfig: { codec: 'pcm-s16', numberOfChannels: 2, sampleRate: 48000 } }; await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 1024 / 2 / 2 / 48000), meta); await output.finalize(); const input = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); expect(await audioTrack.getFirstTimestamp()).toBe(0); }); test('PCM audio with non-zero timestamp', async () => { const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); const source = new EncodedAudioPacketSource('pcm-s16'); output.addAudioTrack(source); await output.start(); const meta = { decoderConfig: { codec: 'pcm-s16', numberOfChannels: 2, sampleRate: 48000 } }; await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 1, 1024 / 2 / 2 / 48000), meta); await output.finalize(); const input = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); expect(await audioTrack.getFirstTimestamp()).toBe(1); }); test('PCM audio, silence padding', async () => { const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); const source = new EncodedAudioPacketSource('pcm-s16'); output.addAudioTrack(source); await output.start(); const meta = { decoderConfig: { codec: 'pcm-s16', numberOfChannels: 2, sampleRate: 48000 } }; await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 1024 / 2 / 2 / 48000), meta); await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 1, 1024 / 2 / 2 / 48000), meta); await output.finalize(); const input = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); expect(await audioTrack.getCodec()).toBe('pcm-s16'); const numChannels = await audioTrack.getNumberOfChannels(); const expectedFrameCount = 48000 + 256; const sink = new EncodedPacketSink(audioTrack); let frameCount = 0; for await (const packet of sink.packets()) { frameCount += packet.byteLength / 2 / numChannels; } expect(frameCount).toBe(expectedFrameCount); }); test('PCM audio, no silence padding with approximate timestamps', async () => { const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget(), }); const source = new EncodedAudioPacketSource('pcm-s16'); output.addAudioTrack(source); await output.start(); const meta = { decoderConfig: { codec: 'pcm-s16', numberOfChannels: 2, sampleRate: 48000 } }; await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0, 1024 / 2 / 2 / 48000), meta); // 0.006 is 256/48000 "rounded up", but it's close enough for silence padding not to kick in await source.add(new EncodedPacket(new Uint8Array(1024), 'key', 0.006, 1024 / 2 / 2 / 48000), meta); await output.finalize(); const input = new Input({ source: new BufferSource(output.target.buffer!), formats: ALL_FORMATS, }); const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); expect(await audioTrack.getCodec()).toBe('pcm-s16'); const numChannels = await audioTrack.getNumberOfChannels(); const expectedFrameCount = 256 + 256; const sink = new EncodedPacketSink(audioTrack); let frameCount = 0; for await (const packet of sink.packets()) { frameCount += packet.byteLength / 2 / numChannels; } expect(frameCount).toBe(expectedFrameCount); });