feat(codec): add detection support for AC-3, E-AC-3 audio codecs (#275)

* ac3/eac3 detection

* fix tests

* review pass + follow codebase comments style

* Add ac3/eac3 to docs

* WIP mpeg-2

* Set registration_descriptor

* add support for ac3 system b in mpeg-ts

* Update docs with ac3 in .ts support

* fix formatting

* Remove default to fix linter

* Add eac3 to mpeg-ts

* fix formatting

* Fix failing test

* Refactor AC3/EAC3 code a little

---------

Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
jepcd
2026-02-11 15:23:12 +01:00
committed by GitHub
co-authored by Vanilagy
parent 612642c5ad
commit 417c85d8ed
24 changed files with 1061 additions and 16 deletions
+1 -1
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 },
+124
View File
@@ -0,0 +1,124 @@
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';
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);
});
+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);
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.