mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-28 19:33:51 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user