Handle implicit HE-AAC for AAC Main with low sample rate

Fixes #7667
This commit is contained in:
Rob Walch
2026-05-04 16:04:34 -07:00
committed by Rob Walch
parent a9c3e3795a
commit 0e0e5de1c1
3 changed files with 59 additions and 21 deletions
+25 -7
View File
@@ -13,7 +13,7 @@ import type {
} from '../../types/demuxer';
type AudioConfig = {
config: [number, number];
config: [number, number] | [number, number, number, number];
samplerate: number;
channelCount: number;
codec: string;
@@ -52,7 +52,6 @@ export function getAudioConfig(
// MPEG-4 Audio Object Type (profile_ObjectType+1)
const adtsObjectType = ((byte2 >> 6) & 0x3) + 1;
const channelCount = ((data[offset + 3] >> 6) & 0x3) | ((byte2 & 1) << 2);
const codec = 'mp4a.40.' + adtsObjectType;
/* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
ISO/IEC 14496-3 - Table 1.13 — Syntax of AudioSpecificConfig()
Audio Profile / Audio Object Type
@@ -95,12 +94,31 @@ export function getAudioConfig(
// multiply frequency by 2 (see table above, equivalent to substract 3)
aacSampleIndex -= 3;
}
const config: [number, number] = [
(adtsObjectType << 3) | ((aacSampleIndex & 0x0e) >> 1),
((aacSampleIndex & 0x01) << 7) | (channelCount << 3),
];
let config: [number, number] | [number, number, number, number];
const samplingIndexHigh = (aacSampleIndex & 0x0e) >> 1;
const samplingIndexLowAndChannels =
((aacSampleIndex & 0x01) << 7) | (channelCount << 3);
let adtsOverride = 0;
if (adtsObjectType === 1 && adtsSamplingIndex >= 6) {
// Handle implicit HE-AAC by adding explicit SBR signaling
adtsOverride = 5;
const adtsExtensionSamplingIndex = adtsSamplingIndex - 3;
config = [
(adtsOverride << 3) | samplingIndexHigh,
samplingIndexLowAndChannels | ((adtsExtensionSamplingIndex & 0x0e) >> 1),
((adtsExtensionSamplingIndex & 0x01) << 7) | (2 << 2),
0,
];
} else {
config = [
(adtsObjectType << 3) | samplingIndexHigh,
samplingIndexLowAndChannels,
];
}
const codec = 'mp4a.40.' + (adtsOverride || adtsObjectType);
logger.log(
`manifest codec:${manifestCodec}, parsed codec:${codec}, channels:${channelCount}, rate:${samplerate} (ADTS object type:${adtsObjectType} sampling index:${adtsSamplingIndex})`,
`manifest codec:${manifestCodec}, parsed codec:${codec}, channels:${channelCount}, rate:${samplerate} (ADTS object type:${adtsObjectType} sampling index:${adtsSamplingIndex} esds config:[${config.join(',')}])`,
);
return {
config,
+5 -4
View File
@@ -695,7 +695,8 @@ class MP4 {
}
static esds(track: DemuxedAudioTrack) {
const config = track.config as [number, number];
const config = track.config!;
const len = config.length;
return new Uint8Array([
0x00, // version 0
0x00,
@@ -703,7 +704,7 @@ class MP4 {
0x00, // flags
0x03, // descriptor_type
0x19, // length
0x17 + len, // length
0x00,
0x01, // es_id
@@ -711,7 +712,7 @@ class MP4 {
0x00, // stream_priority
0x04, // descriptor_type
0x11, // length
0x0f + len, // length
0x40, // codec : mpeg4_audio
0x15, // stream_type
0x00,
@@ -727,7 +728,7 @@ class MP4 {
0x00, // avgBitrate
0x05, // descriptor_type
0x02, // length
len, // length
...config,
0x06,
0x01,
+29 -10
View File
@@ -50,24 +50,43 @@ describe('getAudioConfig', function () {
});
});
it('should return audio config for 11025Hz', function () {
it('should treat AAC Main (object type 1) with low sample rate as implicit HE-AAC (SBR)', function () {
const observer = new EventEmitter();
const data = new Uint8Array(new ArrayBuffer(4));
data[0] = 0xff;
data[1] = 0xf0; // ID = 0 (MPEG-4), layer = 00, protection_absent = 0
data[2] = 0x28; // sampling_frequency_index = 10
data[2] = 0x28; // profile = 0 (AAC Main), sampling_frequency_index = 10 (11025Hz)
const result = getAudioConfig(observer, data, 0, 'mp4a.40.29');
expect(result, JSON.stringify(result)).to.deep.equal({
config: [13, 0],
config: [45, 3, 136, 0],
samplerate: 11025,
channelCount: 0,
codec: 'mp4a.40.1',
parsedCodec: 'mp4a.40.1',
codec: 'mp4a.40.5',
parsedCodec: 'mp4a.40.5',
manifestCodec: 'mp4a.40.29',
});
});
it('should treat AAC Main at sampling index 6 (24kHz) as implicit HE-AAC', function () {
const observer = new EventEmitter();
const data = new Uint8Array(new ArrayBuffer(4));
data[0] = 0xff;
data[1] = 0xf0; // ID = 0 (MPEG-4), layer = 00, protection_absent = 0
data[2] = 0x18; // profile = 0 (AAC Main), sampling_frequency_index = 6 (24000Hz)
data[3] = 0x80; // channel_configuration = 2 (stereo)
const result = getAudioConfig(observer, data, 0, undefined);
expect(result, JSON.stringify(result)).to.deep.equal({
config: [43, 17, 136, 0],
samplerate: 24000,
channelCount: 2,
codec: 'mp4a.40.5',
parsedCodec: 'mp4a.40.5',
manifestCodec: undefined,
});
});
it('should return audio config if there is no audio codec', function () {
const observer = new EventEmitter();
const data = new Uint8Array(new ArrayBuffer(4));
@@ -106,7 +125,7 @@ describe('getAudioConfig', function () {
});
});
it('should return audio config if there is no audio codec and freq is high enough', function () {
it('should keep AAC Main (object type 1) with high sample rate as mp4a.40.1', function () {
const observer = new EventEmitter();
const data = new Uint8Array(new ArrayBuffer(4));
data[0] = 0xff;
@@ -272,7 +291,7 @@ describe('initTrackConfig', function () {
});
});
it('should call `getAudioConfig` and change track if track.samplerate is undefined', function () {
it('should set track config with implicit HE-AAC for AAC Main with low sample rate', function () {
const track = {};
const observer = {
trigger: sinon.spy(),
@@ -285,11 +304,11 @@ describe('initTrackConfig', function () {
initTrackConfig(track, observer, data, 0, 'mp4a.40.29');
expect(track).to.deep.equal({
config: [13, 0],
config: [45, 3, 136, 0],
samplerate: 11025,
channelCount: 0,
codec: 'mp4a.40.1',
parsedCodec: 'mp4a.40.1',
codec: 'mp4a.40.5',
parsedCodec: 'mp4a.40.5',
manifestCodec: 'mp4a.40.29',
});
});