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
+79 -1
View File
@@ -20,6 +20,7 @@ import {
UNDETERMINED_LANGUAGE,
assertNever,
keyValueIterator,
Bitstream,
} from '../misc';
import {
AudioCodec,
@@ -43,7 +44,7 @@ import {
IsobmffVideoTrackData,
Sample,
} from './isobmff-muxer';
import { parseOpusIdentificationHeader } from '../codec-data';
import { parseAc3SyncFrame, parseEac3SyncFrame, parseOpusIdentificationHeader } from '../codec-data';
import { MetadataTags, RichImageData } from '../metadata';
export class IsobmffBoxWriter {
@@ -907,6 +908,79 @@ const pcmC = (trackData: IsobmffAudioTrackData) => {
]);
};
/** AC3SpecificBox */
const dac3 = (trackData: IsobmffAudioTrackData) => {
const frameInfo = parseAc3SyncFrame(trackData.info.firstPacket.data);
if (!frameInfo) {
throw new Error(
'Couldn\'t extract AC-3 frame info from the audio packet. '
+ 'Ensure the packets contain valid AC-3 sync frames (as specified in ETSI TS 102 366).',
);
}
const bytes = new Uint8Array(3);
const bitstream = new Bitstream(bytes);
bitstream.writeBits(2, frameInfo.fscod);
bitstream.writeBits(5, frameInfo.bsid);
bitstream.writeBits(3, frameInfo.bsmod);
bitstream.writeBits(3, frameInfo.acmod);
bitstream.writeBits(1, frameInfo.lfeon);
bitstream.writeBits(5, frameInfo.bitRateCode);
bitstream.writeBits(5, 0); // reserved
return box('dac3', [...bytes]);
};
/** EC3SpecificBox */
const dec3 = (trackData: IsobmffAudioTrackData) => {
const frameInfo = parseEac3SyncFrame(trackData.info.firstPacket.data);
if (!frameInfo) {
throw new Error(
'Couldn\'t extract E-AC-3 frame info from the audio packet. '
+ 'Ensure the packets contain valid E-AC-3 sync frames (as specified in ETSI TS 102 366).',
);
}
// Calculate size
let totalBits = 16; // header: data_rate (13) + num_ind_sub (3)
for (const sub of frameInfo.substreams) {
totalBits += 23; // fixed fields per substream
if (sub.numDepSub > 0) {
totalBits += 9; // chan_loc
} else {
totalBits += 1; // reserved
}
}
const size = Math.ceil(totalBits / 8);
const bytes = new Uint8Array(size);
const bitstream = new Bitstream(bytes);
bitstream.writeBits(13, frameInfo.dataRate);
bitstream.writeBits(3, frameInfo.substreams.length - 1); // num_ind_sub
for (const sub of frameInfo.substreams) {
bitstream.writeBits(2, sub.fscod);
bitstream.writeBits(5, sub.bsid);
bitstream.writeBits(1, 0); // reserved
bitstream.writeBits(1, 0); // asvc = 0
bitstream.writeBits(3, sub.bsmod);
bitstream.writeBits(3, sub.acmod);
bitstream.writeBits(1, sub.lfeon);
bitstream.writeBits(3, 0); // reserved
bitstream.writeBits(4, sub.numDepSub);
if (sub.numDepSub > 0) {
bitstream.writeBits(9, sub.chanLoc);
} else {
bitstream.writeBits(1, 0); // reserved
}
}
return box('dec3', [...bytes]);
};
export const subtitleSampleDescription = (
compressionType: string,
trackData: IsobmffSubtitleTrackData,
@@ -1606,6 +1680,8 @@ const audioCodecToBoxName = (codec: AudioCodec, isQuickTime: boolean): string =>
case 'alaw': return 'alaw';
case 'pcm-u8': return 'raw ';
case 'pcm-s8': return 'sowt';
case 'ac3': return 'ac-3';
case 'eac3': return 'ec-3';
}
// Logic diverges here
@@ -1645,6 +1721,8 @@ const audioCodecToConfigurationBox = (codec: AudioCodec, isQuickTime: boolean) =
case 'opus': return dOps;
case 'vorbis': return esds;
case 'flac': return dfLa;
case 'ac3': return dac3;
case 'eac3': return dec3;
}
// Logic diverges here
+54
View File
@@ -27,6 +27,11 @@ import {
FlacBlockType,
HevcDecoderConfigurationRecord,
Vp9CodecInfo,
parseEac3Config,
getEac3SampleRate,
getEac3ChannelCount,
AC3_SAMPLE_RATES,
AC3_ACMOD_CHANNEL_COUNTS,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
@@ -952,6 +957,10 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codec = 'ulaw';
} else if (lowercaseBoxName === 'alaw') {
track.info.codec = 'alaw';
} else if (lowercaseBoxName === 'ac-3') {
track.info.codec = 'ac3';
} else if (lowercaseBoxName === 'ec-3') {
track.info.codec = 'eac3';
} else {
console.warn(`Unsupported audio codec (sample entry type '${sampleBoxInfo.name}').`);
}
@@ -1463,6 +1472,51 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codecDescription = description;
}; break;
case 'dac3': { // AC3SpecificBox
const track = this.currentTrack;
if (!track) {
break;
}
assert(track.info?.type === 'audio');
const bytes = readBytes(slice, 3);
const bitstream = new Bitstream(bytes);
const fscod = bitstream.readBits(2);
bitstream.skipBits(5 + 3); // Skip bsid and bsmod
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
if (fscod < 3) {
track.info.sampleRate = AC3_SAMPLE_RATES[fscod]!;
}
track.info.numberOfChannels = AC3_ACMOD_CHANNEL_COUNTS[acmod]! + lfeon;
}; break;
case 'dec3': { // EC3SpecificBox
const track = this.currentTrack;
if (!track) {
break;
}
assert(track.info?.type === 'audio');
const bytes = readBytes(slice, boxInfo.contentSize);
const config = parseEac3Config(bytes);
if (!config) {
console.warn('Invalid dec3 box contents, ignoring.');
break;
}
const sampleRate = getEac3SampleRate(config);
if (sampleRate !== null) {
track.info.sampleRate = sampleRate;
}
track.info.numberOfChannels = getEac3ChannelCount(config);
}; break;
case 'stts': {
const track = this.currentTrack;
if (!track) {
+3 -1
View File
@@ -111,6 +111,7 @@ export type IsobmffTrackData = {
* ADTS-wrapped data.
*/
requiresAdtsStripping: boolean;
firstPacket: EncodedPacket;
};
} | {
track: OutputSubtitleTrack;
@@ -429,8 +430,9 @@ export class IsobmffMuxer extends Muxer {
!this.isFragmented
&& (PCM_AUDIO_CODECS as readonly string[]).includes(track.source._codec),
requiresAdtsStripping,
firstPacket: packet,
},
timescale: meta.decoderConfig.sampleRate,
timescale: decoderConfig.sampleRate,
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],