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
+364
View File
@@ -2643,3 +2643,367 @@ export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags
return commentHeader;
};
// ============================================================================
// AC-3 / E-AC-3 Parsing
// Reference: ETSI TS 102 366 V1.4.1
// ============================================================================
/** Sample rates indexed by fscod (Table 4.1) */
export const AC3_SAMPLE_RATES = [48000, 44100, 32000] as const;
/**
* Channel counts indexed by acmod (Table 4.3).
* Does NOT include LFE - add lfeon to get total channel count.
*/
export const AC3_ACMOD_CHANNEL_COUNTS = [2, 1, 2, 3, 3, 4, 4, 5] as const;
export interface Ac3FrameInfo {
/** Sample rate code */
fscod: number;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Bit rate code (0-18, maps to bitrate via Table F.4.1) */
bitRateCode: number;
}
/**
* Parse an AC-3 syncframe to extract BSI (Bit Stream Information) fields.
* Section 4.3
*/
export const parseAc3SyncFrame = (data: Uint8Array): Ac3FrameInfo | null => {
if (data.length < 7) {
return null;
}
// Check sync word (0x0B77)
if (data[0] !== 0x0B || data[1] !== 0x77) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(16); // sync word
bitstream.skipBits(16); // crc1
const fscod = bitstream.readBits(2);
if (fscod === 3) {
return null; // Reserved, invalid
}
const frmsizecod = bitstream.readBits(6);
const bsid = bitstream.readBits(5);
// Verify this is AC-3
if (bsid > 8) {
return null;
}
const bsmod = bitstream.readBits(3);
const acmod = bitstream.readBits(3);
// Skip cmixlev (center downmix level) if three front channels are in use (L, C, R).
if ((acmod & 0x1) !== 0 && acmod !== 0x1) {
bitstream.skipBits(2);
}
// Skip surmixlev (surround downmix level) if surround channels are in use.
if ((acmod & 0x4) !== 0) {
bitstream.skipBits(2);
}
// Skip dsurmod if stereo (acmod === 2)
if (acmod === 0x2) {
bitstream.skipBits(2);
}
const lfeon = bitstream.readBits(1);
const bitRateCode = Math.floor(frmsizecod / 2);
return { fscod, bsid, bsmod, acmod, lfeon, bitRateCode };
};
/**
* AC-3 frame sizes in bytes, indexed by [3 * frmsizecod + fscod].
* fscod: 0=48kHz, 1=44.1kHz, 2=32kHz
* Values are 16-bit words * 2 (to convert to bytes).
* Table 4.13
*/
export const AC3_FRAME_SIZES = [
// frmsizecod, [48kHz, 44.1kHz, 32kHz] in bytes
64 * 2, 69 * 2, 96 * 2,
64 * 2, 70 * 2, 96 * 2,
80 * 2, 87 * 2, 120 * 2,
80 * 2, 88 * 2, 120 * 2,
96 * 2, 104 * 2, 144 * 2,
96 * 2, 105 * 2, 144 * 2,
112 * 2, 121 * 2, 168 * 2,
112 * 2, 122 * 2, 168 * 2,
128 * 2, 139 * 2, 192 * 2,
128 * 2, 140 * 2, 192 * 2,
160 * 2, 174 * 2, 240 * 2,
160 * 2, 175 * 2, 240 * 2,
192 * 2, 208 * 2, 288 * 2,
192 * 2, 209 * 2, 288 * 2,
224 * 2, 243 * 2, 336 * 2,
224 * 2, 244 * 2, 336 * 2,
256 * 2, 278 * 2, 384 * 2,
256 * 2, 279 * 2, 384 * 2,
320 * 2, 348 * 2, 480 * 2,
320 * 2, 349 * 2, 480 * 2,
384 * 2, 417 * 2, 576 * 2,
384 * 2, 418 * 2, 576 * 2,
448 * 2, 487 * 2, 672 * 2,
448 * 2, 488 * 2, 672 * 2,
512 * 2, 557 * 2, 768 * 2,
512 * 2, 558 * 2, 768 * 2,
640 * 2, 696 * 2, 960 * 2,
640 * 2, 697 * 2, 960 * 2,
768 * 2, 835 * 2, 1152 * 2,
768 * 2, 836 * 2, 1152 * 2,
896 * 2, 975 * 2, 1344 * 2,
896 * 2, 976 * 2, 1344 * 2,
1024 * 2, 1114 * 2, 1536 * 2,
1024 * 2, 1115 * 2, 1536 * 2,
1152 * 2, 1253 * 2, 1728 * 2,
1152 * 2, 1254 * 2, 1728 * 2,
1280 * 2, 1393 * 2, 1920 * 2,
1280 * 2, 1394 * 2, 1920 * 2,
];
/** Number of samples per AC-3 syncframe (always 1536) */
export const AC3_SAMPLES_PER_FRAME = 1536;
/**
* AC-3 registration_descriptor for MPEG-TS.
* Section A.2.3
*/
export const AC3_REGISTRATION_DESCRIPTOR = new Uint8Array([0x05, 0x04, 0x41, 0x43, 0x2d, 0x33]);
/** E-AC-3 registration_descriptor for MPEG-TS/ */
export const EAC3_REGISTRATION_DESCRIPTOR = new Uint8Array([0x05, 0x04, 0x45, 0x41, 0x43, 0x33]);
/** E-AC-3 reduced sample rates for fscod2 per ATSC A/52:2018 */
const EAC3_REDUCED_SAMPLE_RATES = [24000, 22050, 16000] as const;
/** Number of audio blocks per syncframe, indexed by numblkscod */
export const EAC3_NUMBLKS_TABLE = [1, 2, 3, 6] as const;
/**
* E-AC-3 independent substream info.
* Each independent substream represents a separate audio program.
*/
export interface Eac3SubstreamInfo {
/** Sample rate code */
fscod: number;
/** Sample rate code 2 (ATSC A/52:2018) */
fscod2: number | null;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Number of dependent substreams */
numDepSub: number;
/** Channel locations for dependent substreams */
chanLoc: number;
}
/**
* E-AC-3 decoder configuration (dec3 box contents).
*/
export interface Eac3FrameInfo {
/** Data rate in kbps */
dataRate: number;
/** Independent substreams */
substreams: Eac3SubstreamInfo[];
}
/**
* Parse an E-AC-3 syncframe to extract BSI fields.
* Section E.1.2
*/
export const parseEac3SyncFrame = (data: Uint8Array): Eac3FrameInfo | null => {
if (data.length < 6) {
return null;
}
// Check sync word (0x0B77)
if (data[0] !== 0x0B || data[1] !== 0x77) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(16); // sync word
const strmtyp = bitstream.readBits(2);
bitstream.skipBits(3); // substreamid
// Only parse independent substreams (strmtyp 0 or 2)
if (strmtyp !== 0 && strmtyp !== 2) {
return null;
}
const frmsiz = bitstream.readBits(11);
const fscod = bitstream.readBits(2);
let fscod2 = 0;
let numblkscod: number;
if (fscod === 3) {
// fscod2 enables reduced sample rates (24/22.05/16 kHz) per ATSC A/52:2018
fscod2 = bitstream.readBits(2);
numblkscod = 3; // Implicitly 6 blocks when fscod=3
} else {
numblkscod = bitstream.readBits(2);
}
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
const bsid = bitstream.readBits(5);
// Verify this is E-AC-3
if (bsid < 11 || bsid > 16) {
return null;
}
// Calculate data rate: ((frmsiz + 1) * fs) / (numblks * 16)
const numblks = EAC3_NUMBLKS_TABLE[numblkscod]!;
let fs: number;
if (fscod < 3) {
fs = AC3_SAMPLE_RATES[fscod]! / 1000;
} else {
fs = EAC3_REDUCED_SAMPLE_RATES[fscod2]! / 1000;
}
const dataRate = Math.round(((frmsiz + 1) * fs) / (numblks * 16));
// These fields require parsing beyond the first frame.
// Defaults are correct for almost all content.
const bsmod = 0;
const numDepSub = 0;
const chanLoc = 0;
const substream: Eac3SubstreamInfo = {
fscod,
fscod2,
bsid,
bsmod,
acmod,
lfeon,
numDepSub,
chanLoc,
};
return {
dataRate,
substreams: [substream],
};
};
/**
* Parse a dec3 box to extract E-AC-3 parameters.
* Section F.6
*/
export const parseEac3Config = (data: Uint8Array): Eac3FrameInfo | null => {
if (data.length < 2) {
return null;
}
const bitstream = new Bitstream(data);
const dataRate = bitstream.readBits(13);
const numIndSub = bitstream.readBits(3);
const substreams: Eac3SubstreamInfo[] = [];
for (let i = 0; i <= numIndSub; i++) {
// Check we have enough data for this substream
// Each substream needs at least 24 bits (3 bytes) without dependent subs
if (Math.ceil(bitstream.pos / 8) + 3 > data.length) {
break;
}
const fscod = bitstream.readBits(2);
const bsid = bitstream.readBits(5);
bitstream.skipBits(1); // reserved
bitstream.skipBits(1); // asvc
const bsmod = bitstream.readBits(3);
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
bitstream.skipBits(3); // reserved
const numDepSub = bitstream.readBits(4);
let chanLoc = 0;
if (numDepSub > 0) {
chanLoc = bitstream.readBits(9);
} else {
bitstream.skipBits(1); // reserved
}
substreams.push({
fscod,
fscod2: null,
bsid,
bsmod,
acmod,
lfeon,
numDepSub,
chanLoc,
});
}
if (substreams.length === 0) {
return null;
}
return { dataRate, substreams };
};
/**
* Get sample rate from E-AC-3 config.
* See ATSC A/52:2018 for handling fscod2.
*/
export const getEac3SampleRate = (config: Eac3FrameInfo): number | null => {
const sub = config.substreams[0];
assert(sub);
if (sub.fscod < 3) {
return AC3_SAMPLE_RATES[sub.fscod]!;
} else if (sub.fscod2 !== null && sub.fscod2 < 3) {
return EAC3_REDUCED_SAMPLE_RATES[sub.fscod2]!;
}
return null;
};
/**
* Get channel count from E-AC-3 config (first independent substream only).
*/
export const getEac3ChannelCount = (config: Eac3FrameInfo): number => {
const sub = config.substreams[0];
assert(sub);
let channels = AC3_ACMOD_CHANNEL_COUNTS[sub.acmod]! + sub.lfeon;
// Add channels from dependent substreams
if (sub.numDepSub > 0) {
const CHAN_LOC_COUNTS = [2, 2, 1, 1, 2, 2, 2, 1, 1];
for (let bit = 0; bit < 9; bit++) {
if (sub.chanLoc & (1 << (8 - bit))) {
channels += CHAN_LOC_COUNTS[bit]!;
}
}
}
return channels;
};
+29 -1
View File
@@ -70,6 +70,8 @@ export const NON_PCM_AUDIO_CODECS = [
'mp3',
'vorbis',
'flac',
'ac3',
'eac3',
] as const;
/**
* List of known audio codecs, ordered by encoding preference.
@@ -528,6 +530,10 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
return 'vorbis';
} else if (codec === 'flac') {
return 'flac';
} else if (codec === 'ac3') {
return 'ac-3';
} else if (codec === 'eac3') {
return 'ec-3';
} else if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return codec;
}
@@ -573,6 +579,10 @@ export const extractAudioCodecString = (trackInfo: {
return 'vorbis';
} else if (codec === 'flac') {
return 'flac';
} else if (codec === 'ac3') {
return 'ac-3';
} else if (codec === 'eac3') {
return 'ec-3';
} else if (codec && (PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return codec;
}
@@ -742,6 +752,10 @@ export const inferCodecFromCodecString = (codecString: string): MediaCodec | nul
return 'vorbis';
} else if (codecString === 'flac') {
return 'flac';
} else if (codecString === 'ac-3' || codecString === 'ac3') {
return 'ac3';
} else if (codecString === 'ec-3' || codecString === 'eac3') {
return 'eac3';
} else if (codecString === 'ulaw') {
return 'ulaw';
} else if (codecString === 'alaw') {
@@ -933,7 +947,9 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
}
};
const VALID_AUDIO_CODEC_STRING_PREFIXES = ['mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm'];
const VALID_AUDIO_CODEC_STRING_PREFIXES = [
'mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm', 'ac-3', 'ec-3',
];
export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
if (!metadata) {
@@ -1051,6 +1067,18 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
+ ' adhere to the format described in https://www.w3.org/TR/webcodecs-flac-codec-registration/.',
);
}
} else if (metadata.decoderConfig.codec.startsWith('ac-3') || metadata.decoderConfig.codec.startsWith('ac3')) {
// AC3-specific validation
if (metadata.decoderConfig.codec !== 'ac-3') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for AC-3 must be "ac-3".');
}
} else if (metadata.decoderConfig.codec.startsWith('ec-3') || metadata.decoderConfig.codec.startsWith('eac3')) {
// EAC3-specific validation
if (metadata.decoderConfig.codec !== 'ec-3') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for EC-3 must be "ec-3".');
}
} else if (
metadata.decoderConfig.codec.startsWith('pcm')
|| metadata.decoderConfig.codec.startsWith('ulaw')
+2
View File
@@ -375,6 +375,8 @@ export class Quality {
opus: 64000, // 64kbps base for Opus
mp3: 160000, // 160kbps base for MP3
vorbis: 64000, // 64kbps base for Vorbis
ac3: 384000, // 384kbps base for AC-3
eac3: 192000, // 192kbps base for E-AC-3
};
const baseBitrate = baseRates[codec as keyof typeof baseRates];
+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: [],
+2
View File
@@ -744,6 +744,8 @@ export const CODEC_STRING_MAP: Partial<Record<MediaCodec, string>> = {
'opus': 'A_OPUS',
'vorbis': 'A_VORBIS',
'flac': 'A_FLAC',
'ac3': 'A_AC3',
'eac3': 'A_EAC3',
'pcm-u8': 'A_PCM/INT/LIT',
'pcm-s16': 'A_PCM/INT/LIT',
'pcm-s16be': 'A_PCM/INT/BIG',
+6
View File
@@ -1084,6 +1084,12 @@ export class MatroskaDemuxer extends Demuxer {
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.flac) {
this.currentTrack.info.codec = 'flac';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.ac3) {
this.currentTrack.info.codec = 'ac3';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.eac3) {
this.currentTrack.info.codec = 'eac3';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
} else if (this.currentTrack.codecId === 'A_PCM/INT/LIT') {
if (this.currentTrack.info.bitDepth === 8) {
this.currentTrack.info.codec = 'pcm-u8';
+186 -1
View File
@@ -19,16 +19,25 @@ import {
VideoCodec,
} from '../codec';
import {
AC3_ACMOD_CHANNEL_COUNTS,
AC3_SAMPLE_RATES,
AC3_SAMPLES_PER_FRAME,
AvcDecoderConfigurationRecord,
AvcNalUnitType,
extractAvcDecoderConfigurationRecord,
extractHevcDecoderConfigurationRecord,
extractNalUnitTypeForAvc,
extractNalUnitTypeForHevc,
EAC3_NUMBLKS_TABLE,
getEac3ChannelCount,
getEac3SampleRate,
HevcDecoderConfigurationRecord,
HevcNalUnitType,
parseAc3SyncFrame,
parseAvcSps,
parseEac3SyncFrame,
parseHevcSps,
AC3_FRAME_SIZES,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
@@ -266,7 +275,20 @@ export class MpegTsDemuxer extends Demuxer {
bitstream.skipBits(6);
const esInfoLength = bitstream.readBits(10);
bitstream.skipBits(8 * esInfoLength);
// Check ES descriptors to detect AC-3/E-AC-3 in System B
const esInfoEndPos = bitstream.pos + 8 * esInfoLength;
let hasAc3Descriptor = false;
let hasEac3Descriptor = false;
while (bitstream.pos < esInfoEndPos) {
const descriptorTag = bitstream.readBits(8);
const descriptorLength = bitstream.readBits(8);
if (descriptorTag === 0x6a) {
hasAc3Descriptor = true;
} else if (descriptorTag === 0x7a || descriptorTag === 0xcc) {
hasEac3Descriptor = true;
}
bitstream.skipBits(8 * descriptorLength);
}
let info: ElementaryStream['info'] | null = null;
@@ -306,6 +328,46 @@ export class MpegTsDemuxer extends Demuxer {
};
}; break;
case MpegTsStreamType.AC3_SYSTEM_A: {
info = {
type: 'audio',
codec: 'ac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
}; break;
case MpegTsStreamType.EAC3_SYSTEM_A: {
info = {
type: 'audio',
codec: 'eac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
}; break;
case MpegTsStreamType.PRIVATE_DATA: {
if (hasEac3Descriptor) {
info = {
type: 'audio',
codec: 'eac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
} else if (hasAc3Descriptor) {
info = {
type: 'audio',
codec: 'ac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
}
}; break;
default: {
// If we don't recognize the codec, we don't surface the track at all. This is because
// we can't determine its metadata and also have no idea how to packetize its data.
@@ -436,6 +498,44 @@ export class MpegTsDemuxer extends Demuxer {
elementaryStream.info.numberOfChannels = result.header.channel === 3 ? 1 : 2;
elementaryStream.info.sampleRate = result.header.sampleRate;
elementaryStream.initialized = true;
} else if (elementaryStream.info.codec === 'ac3') {
const frameInfo = parseAc3SyncFrame(pesPacket.data);
if (!frameInfo) {
throw new Error(
'Invalid AC-3 audio stream; could not read sync frame from first packet.',
);
}
if (frameInfo.fscod === 3) {
throw new Error(
'Invalid AC-3 audio stream; reserved sample rate code found in first packet.',
);
}
elementaryStream.info.numberOfChannels
= AC3_ACMOD_CHANNEL_COUNTS[frameInfo.acmod]! + frameInfo.lfeon;
elementaryStream.info.sampleRate = AC3_SAMPLE_RATES[frameInfo.fscod]!;
elementaryStream.initialized = true;
} else if (elementaryStream.info.codec === 'eac3') {
const frameInfo = parseEac3SyncFrame(pesPacket.data);
if (!frameInfo) {
throw new Error(
'Invalid E-AC-3 audio stream; could not read sync frame from first packet.',
);
}
const sampleRate = getEac3SampleRate(frameInfo);
if (sampleRate === null) {
throw new Error(
'Invalid E-AC-3 audio stream; reserved sample rate code found in first packet.',
);
}
elementaryStream.info.numberOfChannels = getEac3ChannelCount(frameInfo);
elementaryStream.info.sampleRate = sampleRate;
elementaryStream.initialized = true;
} else {
throw new Error('Unhandled.');
@@ -1686,6 +1786,91 @@ const markNextPacket = async (context: PacketReadingContext) => {
} else {
context.seekTo(possibleHeaderStartPos + 1);
}
} else if (codec === 'ac3') {
if (byte !== 0x0b) {
continue;
}
context.skip(-1);
const possibleSyncPos = context.currentPos;
// Need at least 5 bytes for sync word + CRC + fscod/frmsizecod
let remaining = context.ensureBuffered(5);
if (remaining instanceof Promise) remaining = await remaining;
if (remaining < 5) {
return;
}
const headerBytes = context.readBytes(5);
// Verify sync word (0x0B77)
if (headerBytes[0] !== 0x0b || headerBytes[1] !== 0x77) {
context.seekTo(possibleSyncPos + 1);
continue;
}
const fscod = headerBytes[4]! >> 6;
const frmsizecod = headerBytes[4]! & 0x3f;
if (fscod === 3 || frmsizecod > 37) {
// Invalid
context.seekTo(possibleSyncPos + 1);
continue;
}
const frameSize = AC3_FRAME_SIZES[3 * frmsizecod + fscod];
assert(frameSize !== undefined);
context.seekTo(possibleSyncPos);
remaining = context.ensureBuffered(frameSize);
if (remaining instanceof Promise) remaining = await remaining;
const duration = Math.round(
AC3_SAMPLES_PER_FRAME * TIMESCALE / elementaryStream.info.sampleRate,
);
return context.supplyPacket(remaining, duration);
} else if (codec === 'eac3') {
if (byte !== 0x0b) {
continue;
}
context.skip(-1);
const possibleSyncPos = context.currentPos;
// Need at least 5 bytes for E-AC-3 header parsing (sync word + frmsiz + fscod/numblkscod)
let remaining = context.ensureBuffered(5);
if (remaining instanceof Promise) remaining = await remaining;
if (remaining < 5) {
return;
}
const headerBytes = context.readBytes(5);
if (headerBytes[0] !== 0x0b || headerBytes[1] !== 0x77) {
context.seekTo(possibleSyncPos + 1);
continue;
}
const frmsiz = ((headerBytes[2]! & 0x07) << 8) | headerBytes[3]!;
const frameSize = (frmsiz + 1) * 2;
const fscod = headerBytes[4]! >> 6;
const numblkscod = fscod === 3 ? 3 : (headerBytes[4]! >> 4) & 0x03;
const numblks = EAC3_NUMBLKS_TABLE[numblkscod]!;
context.seekTo(possibleSyncPos);
remaining = context.ensureBuffered(frameSize);
if (remaining instanceof Promise) remaining = await remaining;
// Duration = numblks * 256 samples per block
const samplesPerFrame = numblks * 256;
const duration = Math.round(
samplesPerFrame * TIMESCALE / elementaryStream.info.sampleRate,
);
return context.supplyPacket(remaining, duration);
} else {
throw new Error('Unhandled.');
}
+3
View File
@@ -13,6 +13,9 @@ export const enum MpegTsStreamType {
MP3_MPEG1 = 0x03,
MP3_MPEG2 = 0x04,
AAC = 0x0f,
AC3_SYSTEM_A = 0x81,
EAC3_SYSTEM_A = 0x87,
PRIVATE_DATA = 0x06,
AVC = 0x1b,
HEVC = 0x24,
}
+56 -7
View File
@@ -9,11 +9,13 @@
import { parseAacAudioSpecificConfig, validateAudioChunkMetadata, validateVideoChunkMetadata } from '../codec';
import { buildAdtsHeaderTemplate, writeAdtsFrameLength } from '../adts/adts-misc';
import {
AC3_REGISTRATION_DESCRIPTOR,
AvcDecoderConfigurationRecord,
AvcNalUnitType,
concatNalUnitsInAnnexB,
deserializeAvcDecoderConfigurationRecord,
deserializeHevcDecoderConfigurationRecord,
EAC3_REGISTRATION_DESCRIPTOR,
extractNalUnitTypeForAvc,
extractNalUnitTypeForHevc,
HevcDecoderConfigurationRecord,
@@ -151,11 +153,34 @@ export class MpegTsMuxer extends Muxer {
assert(meta?.decoderConfig);
const codec = track.source._codec;
assert(codec === 'aac' || codec === 'mp3');
assert(codec === 'aac' || codec === 'mp3' || codec === 'ac3' || codec === 'eac3');
let streamType: MpegTsStreamType;
let streamId: number;
switch (codec) {
case 'aac': {
streamType = MpegTsStreamType.AAC;
streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
}; break;
case 'mp3': {
streamType = MpegTsStreamType.MP3_MPEG1;
streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
}; break;
case 'ac3': {
streamType = MpegTsStreamType.AC3_SYSTEM_A;
streamId = 0xbd;
}; break;
case 'eac3': {
streamType = MpegTsStreamType.EAC3_SYSTEM_A;
streamId = 0xbd;
}; break;
}
const streamType = codec === 'aac' ? MpegTsStreamType.AAC : MpegTsStreamType.MP3_MPEG1;
const pid = FIRST_TRACK_PID + this.trackDatas.length;
const streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
const newTrackData: MpegTsTrackData = {
track,
@@ -373,7 +398,7 @@ export class MpegTsMuxer extends Muxer {
): Uint8Array {
const codec = (trackData.track as OutputAudioTrack).source._codec;
if (codec === 'mp3') {
if (codec === 'mp3' || codec === 'ac3' || codec === 'eac3') {
// We're good
return packet.data;
}
@@ -695,7 +720,18 @@ const PAT_SECTION = new Uint8Array(16);
}
const buildPmt = (trackDatas: MpegTsTrackData[]) => {
const sectionLength = 9 + trackDatas.length * 5 + 4;
let totalEsBytes = 0;
for (const trackData of trackDatas) {
totalEsBytes += 5;
if (trackData.streamType === MpegTsStreamType.AC3_SYSTEM_A) {
totalEsBytes += AC3_REGISTRATION_DESCRIPTOR.length;
} else if (trackData.streamType === MpegTsStreamType.EAC3_SYSTEM_A) {
totalEsBytes += EAC3_REGISTRATION_DESCRIPTOR.length;
}
}
const sectionLength = 9 + totalEsBytes + 4;
const section = new Uint8Array(3 + sectionLength - 4);
const view = toDataView(section);
@@ -714,8 +750,21 @@ const buildPmt = (trackDatas: MpegTsTrackData[]) => {
section[offset++] = trackData.streamType; // stream_type
view.setUint16(offset, 0xE000 | (trackData.pid & 0x1FFF), false); // reserved=111, elementary_PID
offset += 2;
view.setUint16(offset, 0xF000, false); // reserved=1111, ES_info_length=0
offset += 2;
if (trackData.streamType === MpegTsStreamType.AC3_SYSTEM_A) {
view.setUint16(offset, 0xF000 | AC3_REGISTRATION_DESCRIPTOR.length, false);
offset += 2;
section.set(AC3_REGISTRATION_DESCRIPTOR, offset);
offset += AC3_REGISTRATION_DESCRIPTOR.length;
} else if (trackData.streamType === MpegTsStreamType.EAC3_SYSTEM_A) {
view.setUint16(offset, 0xF000 | EAC3_REGISTRATION_DESCRIPTOR.length, false);
offset += 2;
section.set(EAC3_REGISTRATION_DESCRIPTOR, offset);
offset += EAC3_REGISTRATION_DESCRIPTOR.length;
} else {
view.setUint16(offset, 0xF000, false); // reserved=1111, ES_info_length=0
offset += 2;
}
}
const crc = computeMpegTsCrc32(section);
+4 -2
View File
@@ -292,7 +292,8 @@ export class Mp4OutputFormat extends IsobmffOutputFormat {
return [
...VIDEO_CODECS,
...NON_PCM_AUDIO_CODECS,
// These are supported via ISO/IEC 23003-5
// These are supported via ISO/IEC 23003-5:
'pcm-s16',
'pcm-s16be',
'pcm-s24',
@@ -303,6 +304,7 @@ export class Mp4OutputFormat extends IsobmffOutputFormat {
'pcm-f32be',
'pcm-f64',
'pcm-f64be',
...SUBTITLE_CODECS,
];
}
@@ -1032,7 +1034,7 @@ export class MpegTsOutputFormat extends OutputFormat {
getSupportedCodecs(): MediaCodec[] {
return [
...VIDEO_CODECS.filter(codec => ['avc', 'hevc'].includes(codec)),
...AUDIO_CODECS.filter(codec => ['aac', 'mp3'].includes(codec)),
...AUDIO_CODECS.filter(codec => ['aac', 'mp3', 'ac3', 'eac3'].includes(codec)),
];
}