Make ADTS demuxer spit out ADTS packets instead of AAC, make ADTS muxer accept both AAC and ADTS input

This commit is contained in:
Vanilagy
2026-01-14 13:52:46 +01:00
parent 06ab9c8433
commit 045df3b9a5
6 changed files with 242 additions and 61 deletions
+3 -22
View File
@@ -16,13 +16,12 @@ import {
AsyncMutex,
binarySearchExact,
binarySearchLessOrEqual,
Bitstream,
UNDETERMINED_LANGUAGE,
} from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { readBytes, Reader } from '../reader';
import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
import { AdtsFrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readAdtsFrameHeader } from './adts-reader';
import { AdtsFrameHeader, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE, readAdtsFrameHeader } from './adts-reader';
export const SAMPLES_PER_AAC_FRAME = 1024;
@@ -95,13 +94,12 @@ export class AdtsDemuxer extends Demuxer {
const sampleRate = aacFrequencyTable[header.samplingFrequencyIndex];
assert(sampleRate !== undefined);
const sampleDuration = SAMPLES_PER_AAC_FRAME / sampleRate;
const headerSize = header.crcCheck ? MAX_FRAME_HEADER_SIZE : MIN_FRAME_HEADER_SIZE;
const sample: Sample = {
timestamp: this.nextTimestampInSamples / sampleRate,
duration: sampleDuration,
dataStart: header.startPos + headerSize,
dataSize: header.frameLength - headerSize,
dataStart: header.startPos,
dataSize: header.frameLength,
};
this.loadedSamples.push(sample);
@@ -198,27 +196,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
async getDecoderConfig(): Promise<AudioDecoderConfig> {
assert(this.demuxer.firstFrameHeader);
const bytes = new Uint8Array(3); // 19 bits max
const bitstream = new Bitstream(bytes);
const { objectType, samplingFrequencyIndex, channelConfiguration } = this.demuxer.firstFrameHeader;
if (objectType > 31) {
bitstream.writeBits(5, 31);
bitstream.writeBits(6, objectType - 32);
} else {
bitstream.writeBits(5, objectType);
}
bitstream.writeBits(4, samplingFrequencyIndex); // samplingFrequencyIndex === 15 is forbidden
bitstream.writeBits(4, channelConfiguration);
return {
codec: `mp4a.40.${this.demuxer.firstFrameHeader.objectType}`,
numberOfChannels: this.getNumberOfChannels(),
sampleRate: this.getSampleRate(),
description: bytes.subarray(0, Math.ceil((bitstream.pos - 1) / 8)),
};
}
+52 -33
View File
@@ -7,7 +7,7 @@
*/
import { AacAudioSpecificConfig, parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec';
import { assert, Bitstream, toUint8Array } from '../misc';
import { Bitstream, toUint8Array } from '../misc';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack } from '../output';
import { AdtsOutputFormat } from '../output-format';
@@ -20,6 +20,7 @@ export class AdtsMuxer extends Muxer {
private header = new Uint8Array(7);
private headerBitstream = new Bitstream(this.header);
private audioSpecificConfig: AacAudioSpecificConfig | null = null;
private inputIsAdts: boolean | null = null;
constructor(output: Output, format: AdtsOutputFormat) {
super(output);
@@ -52,49 +53,67 @@ export class AdtsMuxer extends Muxer {
try {
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
if (!this.audioSpecificConfig) {
if (this.inputIsAdts === null) {
validateAudioChunkMetadata(meta);
const description = meta?.decoderConfig?.description;
assert(description);
this.audioSpecificConfig = parseAacAudioSpecificConfig(toUint8Array(description));
// From the WebCodecs Codec Registry:
// "If description is present, it is assumed to a AudioSpecificConfig as defined in [iso14496-3] section
// 1.6.2.1, Table 1.15, and the bitstream is assumed to be in aac.
// If the description is not present, the bitstream is assumed to be in adts format."
this.inputIsAdts = !description;
const { objectType, frequencyIndex, channelConfiguration } = this.audioSpecificConfig;
const profile = objectType - 1;
if (!this.inputIsAdts) {
this.audioSpecificConfig = parseAacAudioSpecificConfig(toUint8Array(description!));
this.headerBitstream.writeBits(12, 0b1111_11111111); // Syncword
this.headerBitstream.writeBits(1, 0); // MPEG Version
this.headerBitstream.writeBits(2, 0); // Layer
this.headerBitstream.writeBits(1, 1); // Protection absence
this.headerBitstream.writeBits(2, profile); // Profile
this.headerBitstream.writeBits(4, frequencyIndex); // MPEG-4 Sampling Frequency Index
this.headerBitstream.writeBits(1, 0); // Private bit
this.headerBitstream.writeBits(3, channelConfiguration); // MPEG-4 Channel Configuration
this.headerBitstream.writeBits(1, 0); // Originality
this.headerBitstream.writeBits(1, 0); // Home
this.headerBitstream.writeBits(1, 0); // Copyright ID bit
this.headerBitstream.writeBits(1, 0); // Copyright ID start
this.headerBitstream.skipBits(13); // Frame length
this.headerBitstream.writeBits(11, 0x7ff); // Buffer fullness
this.headerBitstream.writeBits(2, 0); // Number of AAC frames minus 1
// Omit CRC check
const { objectType, frequencyIndex, channelConfiguration } = this.audioSpecificConfig;
const profile = objectType - 1;
this.headerBitstream.writeBits(12, 0b1111_11111111); // Syncword
this.headerBitstream.writeBits(1, 0); // MPEG Version
this.headerBitstream.writeBits(2, 0); // Layer
this.headerBitstream.writeBits(1, 1); // Protection absence
this.headerBitstream.writeBits(2, profile); // Profile
this.headerBitstream.writeBits(4, frequencyIndex); // MPEG-4 Sampling Frequency Index
this.headerBitstream.writeBits(1, 0); // Private bit
this.headerBitstream.writeBits(3, channelConfiguration); // MPEG-4 Channel Configuration
this.headerBitstream.writeBits(1, 0); // Originality
this.headerBitstream.writeBits(1, 0); // Home
this.headerBitstream.writeBits(1, 0); // Copyright ID bit
this.headerBitstream.writeBits(1, 0); // Copyright ID start
this.headerBitstream.skipBits(13); // Frame length
this.headerBitstream.writeBits(11, 0x7ff); // Buffer fullness
this.headerBitstream.writeBits(2, 0); // Number of AAC frames minus 1
// Omit CRC check
}
}
const frameLength = packet.data.byteLength + this.header.byteLength;
this.headerBitstream.pos = 30;
this.headerBitstream.writeBits(13, frameLength);
if (this.inputIsAdts) {
// Packets are already ADTS frames, write them directly
const startPos = this.writer.getPos();
this.writer.write(packet.data);
const startPos = this.writer.getPos();
this.writer.write(this.header);
this.writer.write(packet.data);
if (this.format._options.onFrame) {
this.format._options.onFrame(packet.data, startPos);
}
} else {
// Packets are raw AAC, prepend ADTS header
const frameLength = packet.data.byteLength + this.header.byteLength;
this.headerBitstream.pos = 30;
this.headerBitstream.writeBits(13, frameLength);
if (this.format._options.onFrame) {
const frameBytes = new Uint8Array(frameLength);
frameBytes.set(this.header, 0);
frameBytes.set(packet.data, this.header.byteLength);
const startPos = this.writer.getPos();
this.writer.write(this.header);
this.writer.write(packet.data);
this.format._options.onFrame(frameBytes, startPos);
if (this.format._options.onFrame) {
const frameBytes = new Uint8Array(frameLength);
frameBytes.set(this.header, 0);
frameBytes.set(packet.data, this.header.byteLength);
this.format._options.onFrame(frameBytes, startPos);
}
}
await this.writer.flush();