Merge branch 'main' into hls

This commit is contained in:
Vanilagy
2026-02-16 15:55:59 +01:00
96 changed files with 4106 additions and 212 deletions
+12 -10
View File
@@ -16,7 +16,7 @@ test('MPEG-TS output format', async () => {
expect(format.mimeType).toBe('video/MP2T');
expect(format.fileExtension).toBe('.ts');
expect(format.supportsVideoRotationMetadata).toBe(false);
expect(format.getSupportedCodecs()).toEqual(['avc', 'hevc', 'aac', 'mp3']);
expect(format.getSupportedCodecs()).toEqual(['avc', 'hevc', 'aac', 'mp3', 'ac3', 'eac3']);
expect(format.getSupportedTrackCounts()).toEqual({
video: { min: 0, max: 16 },
audio: { min: 0, max: 32 },
@@ -70,7 +70,7 @@ test('MPEG-TS muxing with AVC and AAC', async () => {
started = true;
const fps = 30;
const duration = 5;
const duration = 2;
const frameCount = fps * duration;
const frameDuration = 1 / fps;
@@ -164,9 +164,9 @@ test('MPEG-TS muxing with AVC and AAC', async () => {
}
// Check that seeking works
const middlePacket = await videoSink.getPacket(2.5);
const middlePacket = await videoSink.getPacket(1);
assert(middlePacket);
expect(middlePacket.timestamp).toBeCloseTo(2.5);
expect(middlePacket.timestamp).toBeCloseTo(1);
expect(videoPacketCount).toBe(frameCount);
@@ -189,9 +189,9 @@ test('MPEG-TS muxing with AVC and AAC', async () => {
}
// Check that seeking works
const audioMiddlePacket = await audioSink.getPacket(2.5);
const audioMiddlePacket = await audioSink.getPacket(1);
assert(audioMiddlePacket);
expect(audioMiddlePacket.timestamp).toBeCloseTo(2.5);
expect(audioMiddlePacket.timestamp).toBeCloseTo(1, 1);
expect(audioPacketCount).toBeGreaterThan(0);
@@ -199,8 +199,8 @@ test('MPEG-TS muxing with AVC and AAC', async () => {
const videoDuration = await videoTrack.computeDuration();
const audioDuration = await audioTrack.computeDuration();
expect(videoDuration).toBeCloseTo(5, 1);
expect(audioDuration).toBeCloseTo(5, 1);
expect(videoDuration).toBeCloseTo(duration, 1);
expect(audioDuration).toBeCloseTo(duration, 1);
});
test('MPEG-TS muxing with HEVC and MP3', async () => {
@@ -703,12 +703,14 @@ test('MPEG-TS muxing with StreamTarget', async () => {
const frameDuration = 1 / fps;
for (let i = 0; i < frameCount; i++) {
await videoSource.add(i * frameDuration, frameDuration);
await videoSource.add(i * frameDuration, frameDuration, {
keyFrame: true, // Otherwise all packets get written at once due to the DTS logic
});
}
await output.finalize();
expect(chunks.length).toBe(frameCount);
expect(chunks).toHaveLength(frameCount);
const buffer = new Uint8Array(nextPos);
nextPos = 0;
+327
View File
@@ -0,0 +1,327 @@
import { expect, test } from 'vitest';
import { Input } from '../../src/input.js';
import { BufferSource, FilePathSource } from '../../src/source.js';
import path from 'node:path';
import { ALL_FORMATS } from '../../src/input-format.js';
import { Output } from '../../src/output.js';
import { MpegTsOutputFormat } from '../../src/output-format.js';
import { BufferTarget } from '../../src/target.js';
import { Conversion } from '../../src/conversion.js';
import { AC3_REGISTRATION_DESCRIPTOR, EAC3_REGISTRATION_DESCRIPTOR } from '../../src/codec-data.js';
import { canEncode } from '../../src/encode.js';
import { AudioSampleSink, EncodedPacketSink } from '../../src/media-sink.js';
import { AudioSampleSource } from '../../src/media-source.js';
import { Mp4OutputFormat } from '../../src/output-format.js';
import { AudioSample } from '../../src/sample.js';
import { registerAc3Decoder, registerAc3Encoder } from '@mediabunny/ac3';
const __dirname = new URL('.', import.meta.url).pathname;
test('reads AC-3 from MP4', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/ac3.mp4')),
formats: ALL_FORMATS,
});
const audioTrack = (await input.getPrimaryAudioTrack())!;
const decoderConfig = (await audioTrack.getDecoderConfig())!;
expect(audioTrack.codec).toBe('ac3');
expect(decoderConfig.description).toBeUndefined();
});
test('reads E-AC-3 from MP4', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/eac3.mp4')),
formats: ALL_FORMATS,
});
const audioTrack = (await input.getPrimaryAudioTrack())!;
const decoderConfig = (await audioTrack.getDecoderConfig())!;
expect(audioTrack.codec).toBe('eac3');
expect(decoderConfig.description).toBeUndefined();
});
test('reads and writes AC-3 in MPEG-TS', async () => {
using originalInput = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/ac3.ts')),
formats: ALL_FORMATS,
});
const originalAudioTrack = (await originalInput.getPrimaryAudioTrack())!;
expect(originalAudioTrack.codec).toBe('ac3');
expect(originalAudioTrack.internalCodecId).toBe(0x81);
const output = new Output({
format: new MpegTsOutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input: originalInput, output });
await conversion.execute();
using newInput = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const newAudioTrack = (await newInput.getPrimaryAudioTrack())!;
expect(newAudioTrack.codec).toBe('ac3');
expect(newAudioTrack.internalCodecId).toBe(0x81);
expect(newAudioTrack.numberOfChannels).toBe(originalAudioTrack.numberOfChannels);
expect(newAudioTrack.sampleRate).toBe(originalAudioTrack.sampleRate);
// Verify registration_descriptor is present
const buffer = new Uint8Array(output.target.buffer!);
let found = false;
for (let i = 0; i < buffer.length - AC3_REGISTRATION_DESCRIPTOR.length; i++) {
if (AC3_REGISTRATION_DESCRIPTOR.every((byte, j) => buffer[i + j] === byte)) {
found = true;
break;
}
}
expect(found).toBe(true);
});
test('reads and writes E-AC-3 in MPEG-TS', async () => {
using originalInput = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/eac3.ts')),
formats: ALL_FORMATS,
});
const originalAudioTrack = (await originalInput.getPrimaryAudioTrack())!;
expect(originalAudioTrack.codec).toBe('eac3');
expect(originalAudioTrack.internalCodecId).toBe(0x87);
const output = new Output({
format: new MpegTsOutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input: originalInput, output });
await conversion.execute();
using newInput = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const newAudioTrack = (await newInput.getPrimaryAudioTrack())!;
expect(newAudioTrack.codec).toBe('eac3');
expect(newAudioTrack.internalCodecId).toBe(0x87);
expect(newAudioTrack.numberOfChannels).toBe(originalAudioTrack.numberOfChannels);
expect(newAudioTrack.sampleRate).toBe(originalAudioTrack.sampleRate);
// Verify registration_descriptor is present
const buffer = new Uint8Array(output.target.buffer!);
let found = false;
for (let i = 0; i < buffer.length - EAC3_REGISTRATION_DESCRIPTOR.length; i++) {
if (EAC3_REGISTRATION_DESCRIPTOR.every((byte, j) => buffer[i + j] === byte)) {
found = true;
break;
}
}
expect(found).toBe(true);
});
test('Custom coder registration', async () => {
using ac3Input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/ac3.mp4')),
formats: ALL_FORMATS,
});
using eac3Input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/eac3.mp4')),
formats: ALL_FORMATS,
});
const ac3Track = (await ac3Input.getPrimaryAudioTrack())!;
const eac3Track = (await eac3Input.getPrimaryAudioTrack())!;
// Before registration, nothing can decode or encode AC3/EAC3
expect(await ac3Track.canDecode()).toBe(false);
expect(await eac3Track.canDecode()).toBe(false);
expect(await canEncode('ac3')).toBe(false);
expect(await canEncode('eac3')).toBe(false);
registerAc3Decoder();
registerAc3Encoder();
// After registration, both should work
expect(await ac3Track.canDecode()).toBe(true);
expect(await eac3Track.canDecode()).toBe(true);
expect(await canEncode('ac3')).toBe(true);
expect(await canEncode('eac3')).toBe(true);
});
test('AC-3 decoding', async () => {
registerAc3Decoder();
using input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/ac3.mp4')),
formats: ALL_FORMATS,
});
const track = (await input.getPrimaryAudioTrack())!;
const { packetCount } = await track.computePacketStats();
const sink = new AudioSampleSink(track);
let sampleCount = 0;
let nextTimestamp = 0;
for await (using sample of sink.samples()) {
expect(sample.timestamp).toBeCloseTo(nextTimestamp);
expect(sample.duration).toBe(0.032);
expect(sample.format).toBe('f32-planar');
expect(sample.numberOfChannels).toBe(track.numberOfChannels);
expect(sample.sampleRate).toBe(track.sampleRate);
nextTimestamp += sample.duration;
sampleCount++;
}
expect(sampleCount).toBe(packetCount);
});
test('E-AC-3 decoding', async () => {
registerAc3Decoder();
using input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/eac3.mp4')),
formats: ALL_FORMATS,
});
const track = (await input.getPrimaryAudioTrack())!;
const { packetCount } = await track.computePacketStats();
const sink = new AudioSampleSink(track);
let sampleCount = 0;
let nextTimestamp = 0;
for await (using sample of sink.samples()) {
expect(sample.timestamp).toBeCloseTo(nextTimestamp);
expect(sample.duration).toBe(0.032);
expect(sample.format).toBe('f32-planar');
expect(sample.numberOfChannels).toBe(track.numberOfChannels);
expect(sample.sampleRate).toBe(track.sampleRate);
nextTimestamp += sample.duration;
sampleCount++;
}
expect(sampleCount).toBe(packetCount);
});
const createSineWave = (sampleRate: number, channels: number, durationSeconds: number) => {
const totalFrames = sampleRate * durationSeconds;
const data = new Float32Array(totalFrames * channels);
for (let i = 0; i < totalFrames; i++) {
const value = Math.sin(2 * Math.PI * 440 * i / sampleRate);
for (let ch = 0; ch < channels; ch++) {
data[i * channels + ch] = value;
}
}
return data;
};
test('AC-3 encoding', async () => {
registerAc3Encoder();
const sampleRate = 48000;
const channels = 2;
const durationSeconds = 2;
const data = createSineWave(sampleRate, channels, durationSeconds);
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
const audioSource = new AudioSampleSource({ codec: 'ac3', bitrate: 192000 });
output.addAudioTrack(audioSource);
await output.start();
await audioSource.add(new AudioSample({
data,
format: 'f32',
numberOfChannels: channels,
sampleRate,
timestamp: 0,
}));
audioSource.close();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const track = (await input.getPrimaryAudioTrack())!;
expect(track.codec).toBe('ac3');
expect(track.sampleRate).toBe(sampleRate);
expect(track.numberOfChannels).toBe(channels);
const sink = new EncodedPacketSink(track);
let packetCount = 0;
for await (const packet of sink.packets()) {
expect(packet.type).toBe('key');
packetCount++;
}
expect(packetCount).toBeGreaterThan(0);
expect(await track.computeDuration()).toBeCloseTo(2, 1);
});
test('E-AC-3 encoding', async () => {
registerAc3Encoder();
const sampleRate = 48000;
const channels = 2;
const durationSeconds = 2;
const data = createSineWave(sampleRate, channels, durationSeconds);
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
const audioSource = new AudioSampleSource({ codec: 'eac3', bitrate: 192000 });
output.addAudioTrack(audioSource);
await output.start();
await audioSource.add(new AudioSample({
data,
format: 'f32',
numberOfChannels: channels,
sampleRate,
timestamp: 0,
}));
audioSource.close();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const track = (await input.getPrimaryAudioTrack())!;
expect(track.codec).toBe('eac3');
expect(track.sampleRate).toBe(sampleRate);
expect(track.numberOfChannels).toBe(channels);
const sink = new EncodedPacketSink(track);
let packetCount = 0;
for await (const packet of sink.packets()) {
expect(packet.type).toBe('key');
packetCount++;
}
expect(packetCount).toBeGreaterThan(0);
expect(await track.computeDuration()).toBeCloseTo(2, 1);
});
+141
View File
@@ -9,6 +9,7 @@ import { assert } from '../../src/misc.js';
import { EncodedPacketSink } from '../../src/media-sink.js';
import { EncodedPacket } from '../../src/packet.js';
import { MpegTsDemuxer } from '../../src/mpeg-ts/mpeg-ts-demuxer.js';
import { MpegTsStreamType } from '../../src/mpeg-ts/mpeg-ts-misc.js';
const __dirname = new URL('.', import.meta.url).pathname;
@@ -612,6 +613,146 @@ test('MPEG-TS with MP3 audio', async () => {
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with AC-3 audio (System A)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/ac3.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('ac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.AC3_SYSTEM_A);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ac-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with AC-3 audio (System B)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/ac3-system-b.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('ac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.PRIVATE_DATA);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ac-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with E-AC-3 audio (System A)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/eac3.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('eac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.EAC3_SYSTEM_A);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ec-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with E-AC-3 audio (System B)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/eac3-system-b.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('eac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.PRIVATE_DATA);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ec-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS partial reading', async () => {
const fullPath = path.join(__dirname, '../public/193039199_mp4_h264_aac_fhd_7.ts');
const buffer = await fs.promises.readFile(fullPath);
+24
View File
@@ -0,0 +1,24 @@
import { test } from 'vitest';
import path from 'node:path';
import { ALL_FORMATS, EncodedPacketSink, Input, FilePathSource } from '../../src/index.js';
const __dirname = new URL('.', import.meta.url).pathname;
test('Should handle WAV file with oversized ID3 chunk', async () => {
const filePath = path.join(__dirname, '..', 'public/oversized-id3.wav');
using input = new Input({
source: new FilePathSource(filePath),
formats: ALL_FORMATS,
});
const track = await input.getPrimaryAudioTrack();
if (!track) {
throw new Error('No audio track found');
}
const sink = new EncodedPacketSink(track);
for await (const packet of sink.packets()) {
void packet;
}
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.