mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-28 03:13: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:
@@ -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
@@ -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')
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user