From 438ae32771dedec315147cf3c335b3a5f1f6a06e Mon Sep 17 00:00:00 2001 From: Paul Adenot Date: Tue, 8 Jul 2025 19:27:02 +0200 Subject: [PATCH] Add support for demuxing xhe-aac files --- mp4parse/src/lib.rs | 22 +++-- mp4parse/tests/public.rs | 79 ++++++++++++++++++ mp4parse/tests/sine-3s-xhe-aac-44khz-mono.mp4 | Bin 0 -> 6906 bytes 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 mp4parse/tests/sine-3s-xhe-aac-44khz-mono.mp4 diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 04a81886..3c126d2f 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -2050,6 +2050,7 @@ pub enum CodecType { Unknown, MP3, AAC, + XHEAAC, // xHE-AAC (Extended High Efficiency AAC) FLAC, Opus, H264, // 14496-10 @@ -5118,7 +5119,7 @@ fn read_ds_descriptor( }; match audio_object_type { - 1..=4 | 6 | 7 | 17 | 19..=23 => { + 1..=4 | 6 | 7 | 17 | 19..=23 | 42 => { if sample_frequency.is_none() { return Err(Error::Unsupported("unknown frequency")); } @@ -5205,6 +5206,12 @@ fn read_ds_descriptor( esds.extended_audio_object_type = extended_audio_object_type; esds.audio_sample_rate = Some(sample_frequency_value); esds.audio_channel_count = Some(channel_counts); + + // Update codec type for xHE-AAC if audio object type 42 is detected + if audio_object_type == 42 { + esds.audio_codec = CodecType::XHEAAC; + } + if !esds.decoder_specific_data.is_empty() { fail_with_status_if( strictness == ParseStrictness::Strict, @@ -5257,11 +5264,14 @@ fn read_dc_descriptor( )?; } - esds.audio_codec = match object_profile { - 0x40 | 0x66 | 0x67 => CodecType::AAC, - 0x69 | 0x6B => CodecType::MP3, - _ => CodecType::Unknown, - }; + // Only set codec type if it hasn't been set to a more specific type (e.g., XHEAAC) + if esds.audio_codec == CodecType::Unknown { + esds.audio_codec = match object_profile { + 0x40 | 0x66 | 0x67 => CodecType::AAC, + 0x69 | 0x6B => CodecType::MP3, + _ => CodecType::Unknown, + }; + } debug!( "read_dc_descriptor: esds.audio_codec = {:?}", diff --git a/mp4parse/tests/public.rs b/mp4parse/tests/public.rs index 3e9628c2..98b4678d 100644 --- a/mp4parse/tests/public.rs +++ b/mp4parse/tests/public.rs @@ -218,6 +218,8 @@ static VIDEO_H263_3GP: &str = "tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3g // The 1 frame hevc mp4 file generated by ffmpeg with command // "ffmpeg -f lavfi -i color=c=white:s=640x480 -c:v libx265 -frames:v 1 -pix_fmt yuv420p hevc_white_frame.mp4" static VIDEO_HEVC_MP4: &str = "tests/hevc_white_frame.mp4"; +// xHE-AAC test file generated by exhale encoder - 3 seconds, 44.1kHz mono, ~14.6kbps +static AUDIO_XHE_AAC_MP4: &str = "tests/sine-3s-xhe-aac-44khz-mono.mp4"; // The 1 frame AMR-NB 3gp file can be generated by ffmpeg with command // "ffmpeg -i [input file] -f 3gp -acodec amr_nb -ar 8000 -ac 1 -frames:a 1 -vn output.3gp" #[cfg(feature = "3gpp")] @@ -1580,3 +1582,80 @@ fn public_video_mp4v() { }; } } + +#[test] +fn public_audio_xhe_aac() { + let mut fd = File::open(AUDIO_XHE_AAC_MP4).expect("Unknown file"); + let mut buf = Vec::new(); + fd.read_to_end(&mut buf).expect("File error"); + + let mut c = Cursor::new(&buf); + let context = mp4::read_mp4(&mut c, ParseStrictness::Normal).expect("read_mp4 failed"); + + println!("xHE-AAC MP4 file parsed successfully"); + println!("Number of tracks: {}", context.tracks.len()); + + // This file contains a single xHE-AAC audio track at 44.1kHz mono, ~14.6kbps, 3 seconds + assert_eq!(context.tracks.len(), 1, "Expected exactly one track"); + + let track = &context.tracks[0]; + assert_eq!( + track.track_type, + mp4::TrackType::Audio, + "Expected audio track" + ); + + // Check sample description + let stsd = track.stsd.as_ref().expect("expected an stsd"); + assert_eq!( + stsd.descriptions.len(), + 1, + "Expected one sample description" + ); + + let a = match stsd.descriptions.first().expect("expected a SampleEntry") { + mp4::SampleEntry::Audio(ref a) => a, + _ => panic!("expected an AudioSampleEntry"), + }; + + println!("Audio track details:"); + println!(" Codec type: {:?}", a.codec_type); + println!(" Sample rate: {}", a.samplerate); + println!(" Channel count: {}", a.channelcount); + + // The parser should detect this as xHE-AAC + assert_eq!(a.codec_type, mp4::CodecType::XHEAAC); + + // Based on ffprobe: 44.1kHz, 1 channel (mono) + assert_eq!(a.samplerate, 44100.0); + assert_eq!(a.channelcount, 1); + + // Check codec-specific data + match &a.codec_specific { + mp4::AudioCodecSpecific::ES_Descriptor(ref esds) => { + println!(" ESDS present"); + println!(" Audio object type: {:?}", esds.audio_object_type); + println!( + " Extended audio object type: {:?}", + esds.extended_audio_object_type + ); + println!(" Audio sample rate: {:?}", esds.audio_sample_rate); + println!(" Audio channel count: {:?}", esds.audio_channel_count); + + // Should be xHE-AAC with audio object type 42 + assert_eq!(esds.audio_codec, mp4::CodecType::XHEAAC); + assert_eq!(esds.audio_object_type, Some(42)); + + // Verify ESDS matches the container info + if let Some(sample_rate) = esds.audio_sample_rate { + assert_eq!(sample_rate, 44100); + } + if let Some(channel_count) = esds.audio_channel_count { + assert_eq!(channel_count, 1); + } + } + _ => panic!("Expected ES descriptor for xHE-AAC audio"), + } + + println!("xHE-AAC file parsing test completed successfully"); +} diff --git a/mp4parse/tests/sine-3s-xhe-aac-44khz-mono.mp4 b/mp4parse/tests/sine-3s-xhe-aac-44khz-mono.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..44c74babec449bc8b437c1949133ac301e3ec2f1 GIT binary patch literal 6906 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 literal 0 HcmV?d00001