mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Use edit lists to model non-zero start timestamps for regular MP4 (fixes #336); fix MP4 PCM logic breaking down with approximate packet timestamps
This commit is contained in:
@@ -9,6 +9,8 @@ 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;
|
||||
|
||||
@@ -104,3 +106,249 @@ test('Fragmented fMP4 with video+audio preserves B-frame CTS', async () => {
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user