1use crate::AudioType;
2
3pub fn is_mp4(data: &[u8]) -> bool {
5 matches!(next_box(data, 0), Some((name, _, _)) if &name == b"ftyp")
6}
7
8pub 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 let major_brand = &content[0..4];
17 let compatible_brands = &content[8..];
19
20 if major_brand == b"M4A " || major_brand == b"M4B " {
22 return true;
23 }
24
25 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
35pub 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[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}