access_unit/
mp4.rs

1use crate::AudioType;
2
3/// Returns true if the data starts with a valid MP4 `ftyp` box.
4pub fn is_mp4(data: &[u8]) -> bool {
5    matches!(next_box(data, 0), Some((name, _, _)) if &name == b"ftyp")
6}
7
8/// Quick detection of M4A container via ftyp box.
9/// Works with moov at the end - only needs the first ~32 bytes.
10pub fn is_m4a(data: &[u8]) -> bool {
11    if let Some((name, content, _)) = next_box(data, 0) {
12        if &name != b"ftyp" || content.len() < 8 {
13            return false;
14        }
15        // Check major brand (first 4 bytes of content)
16        let major_brand = &content[0..4];
17        // Check compatible brands (after minor version, every 4 bytes)
18        let compatible_brands = &content[8..];
19
20        // Check if major brand indicates M4A
21        if major_brand == b"M4A " || major_brand == b"M4B " {
22            return true;
23        }
24
25        // Check compatible brands for M4A indicator
26        for chunk in compatible_brands.chunks_exact(4) {
27            if chunk == b"M4A " || chunk == b"M4B " {
28                return true;
29            }
30        }
31    }
32    false
33}
34
35/// Attempts to find the first audio track in the MP4 and map its sample entry to an `AudioType`.
36pub fn detect_audio_track(data: &[u8]) -> Option<AudioType> {
37    if !is_mp4(data) {
38        return None;
39    }
40
41    let moov = find_child(data, *b"moov")?;
42
43    let mut offset = 0;
44    while let Some((name, trak, next_offset)) = next_box(moov, offset) {
45        if &name == b"trak" {
46            if let Some(audio_type) = parse_trak(trak) {
47                return Some(audio_type);
48            }
49        }
50        offset = next_offset;
51    }
52
53    None
54}
55
56fn parse_trak(trak: &[u8]) -> Option<AudioType> {
57    let mdia = find_child(trak, *b"mdia")?;
58    if !is_audio_handler(mdia) {
59        return None;
60    }
61
62    let minf = find_child(mdia, *b"minf")?;
63    let stbl = find_child(minf, *b"stbl")?;
64    let stsd = find_child(stbl, *b"stsd")?;
65
66    parse_stsd(stsd)
67}
68
69fn is_audio_handler(mdia: &[u8]) -> bool {
70    let hdlr = match find_child(mdia, *b"hdlr") {
71        Some(hdlr) => hdlr,
72        None => return false,
73    };
74
75    if hdlr.len() < 12 {
76        return false;
77    }
78
79    // hdlr full box: version/flags (4), pre_defined (4), handler_type (4)
80    &hdlr[8..12] == b"soun"
81}
82
83fn parse_stsd(stsd: &[u8]) -> Option<AudioType> {
84    if stsd.len() < 8 {
85        return None;
86    }
87
88    let entry_count = u32::from_be_bytes(stsd[4..8].try_into().ok()?) as usize;
89    let mut offset = 8;
90
91    for _ in 0..entry_count {
92        let (format, next_offset) = parse_stsd_entry(stsd, offset)?;
93        let audio_type = fourcc_to_audio_type(format);
94        if audio_type != AudioType::Unknown {
95            return Some(audio_type);
96        }
97        offset = next_offset;
98    }
99
100    None
101}
102
103fn parse_stsd_entry(stsd: &[u8], offset: usize) -> Option<([u8; 4], usize)> {
104    if offset + 8 > stsd.len() {
105        return None;
106    }
107
108    let size = u32::from_be_bytes(stsd[offset..offset + 4].try_into().ok()?) as usize;
109    if size < 8 || offset + size > stsd.len() {
110        return None;
111    }
112
113    let mut format = [0u8; 4];
114    format.copy_from_slice(&stsd[offset + 4..offset + 8]);
115
116    Some((format, offset + size))
117}
118
119fn find_child<'a>(data: &'a [u8], target: [u8; 4]) -> Option<&'a [u8]> {
120    let mut offset = 0;
121    while let Some((name, content, next_offset)) = next_box(data, offset) {
122        if name == target {
123            return Some(content);
124        }
125        offset = next_offset;
126    }
127    None
128}
129
130fn next_box<'a>(data: &'a [u8], offset: usize) -> Option<([u8; 4], &'a [u8], usize)> {
131    if offset + 8 > data.len() {
132        return None;
133    }
134
135    let size32 = u32::from_be_bytes(data[offset..offset + 4].try_into().ok()?);
136    let mut header_len = 8usize;
137    let mut size = size32 as u64;
138
139    if size32 == 1 {
140        if offset + 16 > data.len() {
141            return None;
142        }
143        size = u64::from_be_bytes(data[offset + 8..offset + 16].try_into().ok()?);
144        header_len = 16;
145    } else if size32 == 0 {
146        size = (data.len() - offset) as u64;
147    }
148
149    if size < header_len as u64 {
150        return None;
151    }
152
153    let end = offset.checked_add(size as usize)?;
154    if end > data.len() {
155        return None;
156    }
157
158    let mut name = [0u8; 4];
159    name.copy_from_slice(&data[offset + 4..offset + 8]);
160
161    let content_start = offset + header_len;
162    Some((name, &data[content_start..end], end))
163}
164
165fn fourcc_to_audio_type(code: [u8; 4]) -> AudioType {
166    match &code {
167        b"mp4a" => AudioType::AAC,
168        b"fLaC" | b"FLAC" => AudioType::FLAC,
169        b"Opus" | b"opus" => AudioType::Opus,
170        b"mp3 " | b".mp3" => AudioType::MP3,
171        _ => AudioType::Unknown,
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use std::fs;
179
180    fn read(path: &str) -> Vec<u8> {
181        fs::read(path).unwrap_or_else(|err| panic!("read {}: {}", path, err))
182    }
183
184    #[test]
185    fn detects_mp4_container() {
186        let data = read("testdata/mp4/heat.mp4");
187        assert!(is_mp4(&data));
188    }
189
190    #[test]
191    fn extracts_audio_type() {
192        let data = read("testdata/mp4/heat.mp4");
193        assert_eq!(detect_audio_track(&data), Some(AudioType::AAC));
194    }
195}