vsd_mp4/parser.rs
1/*
2 REFERENCES
3 ----------
4
5 1. https://github.com/shaka-project/shaka-player/blob/7098f43f70119226bca2e5583833aaf27b498e33/lib/util/mp4_box_parsers.js
6 2. https://github.com/shaka-project/shaka-player/blob/7098f43f70119226bca2e5583833aaf27b498e33/externs/shaka/mp4_parser.js
7
8*/
9
10use crate::{Error, Reader};
11use std::{collections::HashMap, sync::Arc};
12
13/// `Result` type returned when parsing an mp4 file.
14pub type HandlerResult = Result<(), Error>;
15/// Callback type for parsing an mp4 file.
16pub type CallbackType = Arc<dyn Fn(ParsedBox) -> HandlerResult>;
17
18/// Mp4 file parser.
19#[derive(Clone, Default)]
20pub struct Mp4Parser {
21 pub headers: HashMap<usize, BoxType>,
22 pub box_definitions: HashMap<usize, CallbackType>,
23 pub done: bool,
24}
25
26impl Mp4Parser {
27 /// Declare a box type as a Basic Box.
28 pub fn basic_box(mut self, _type: &str, definition: CallbackType) -> Self {
29 let type_code = type_from_string(_type);
30 self.headers.insert(type_code, BoxType::BasicBox);
31 self.box_definitions.insert(type_code, definition);
32 self
33 }
34
35 /// Declare a box type as a Full Box.
36 pub fn full_box(mut self, _type: &str, definition: CallbackType) -> Self {
37 let type_code = type_from_string(_type);
38 self.headers.insert(type_code, BoxType::FullBox);
39 self.box_definitions.insert(type_code, definition);
40 self
41 }
42
43 /// Stop parsing. Useful for extracting information from partial segments and
44 /// avoiding an out-of-bounds error once you find what you are looking for.
45 pub fn stop(&mut self) {
46 self.done = true;
47 }
48
49 /// Parse the given data using the added callbacks.
50 ///
51 /// # Arguments
52 ///
53 /// - `partial_okay` - If true, allow reading partial payloads
54 /// from some boxes. If the goal is a child box, we can sometimes find it
55 /// without enough data to find all child boxes.
56 /// - `stop_on_partial` - If true, stop reading if an incomplete
57 /// box is detected.
58 pub fn parse(
59 &mut self,
60 data: &[u8],
61 partial_okay: bool,
62 stop_on_partial: bool,
63 ) -> HandlerResult {
64 let mut reader = Reader::new(data, false);
65
66 self.done = false;
67
68 while reader.has_more_data() && !self.done {
69 self.parse_next(0, &mut reader, partial_okay, stop_on_partial)?;
70 }
71
72 Ok(())
73 }
74
75 /// Parse the next box on the current level.
76 ///
77 /// # Arguments
78 ///
79 /// - `abs_start` - The absolute start position in the original
80 /// byte array.
81 /// - `partial_okay` - If true, allow reading partial payloads
82 /// from some boxes. If the goal is a child box, we can sometimes find it
83 /// without enough data to find all child boxes.
84 /// - `stop_on_partial` - If true, stop reading if an incomplete
85 /// box is detected.
86 fn parse_next(
87 &mut self,
88 abs_start: u64,
89 reader: &mut Reader,
90 partial_okay: bool,
91 stop_on_partial: bool,
92 ) -> HandlerResult {
93 let start = reader.get_position();
94
95 // size(4 bytes) + type(4 bytes) = 8 bytes
96 if stop_on_partial && start + 8 > reader.get_length() {
97 self.done = true;
98 return Ok(());
99 }
100
101 let mut size = reader
102 .read_u32()
103 .map_err(|_| Error::new_read("box size (u32)."))? as u64;
104 let _type = reader
105 .read_u32()
106 .map_err(|_| Error::new_read("box type (u32)."))? as usize;
107 let name = type_to_string(_type)
108 .map_err(|_| Error::new_decode(format!("{_type} (u32) to string.")))?;
109 let mut has_64_bit_size = false;
110 // println!("Parsing MP4 box {}", name);
111
112 match size {
113 0 => size = reader.get_length() - start,
114 1 => {
115 if stop_on_partial && reader.get_position() + 8 > reader.get_length() {
116 self.done = true;
117 return Ok(());
118 }
119 size = reader
120 .read_u64()
121 .map_err(|_| Error::new_read("box size (u64)."))?;
122 has_64_bit_size = true;
123 }
124 _ => (),
125 }
126
127 let box_definition = self.box_definitions.get(&_type);
128
129 if let Some(box_definition) = box_definition {
130 let mut version = None;
131 let mut flags = None;
132
133 if *self.headers.get(&_type).unwrap() == BoxType::FullBox {
134 if stop_on_partial && reader.get_position() + 4 > reader.get_length() {
135 self.done = true;
136 return Ok(());
137 }
138
139 let version_and_flags = reader
140 .read_u32()
141 .map_err(|_| Error::new_read("box version and flags (u32)."))?;
142 version = Some(version_and_flags >> 24);
143 flags = Some(version_and_flags & 0xFFFFFF);
144 }
145
146 // Read the whole payload so that the current level can be safely read
147 // regardless of how the payload is parsed.
148 let mut end = start + size;
149
150 if partial_okay && end > reader.get_length() {
151 // For partial reads, truncate the payload if we must.
152 end = reader.get_length();
153 }
154
155 if stop_on_partial && end > reader.get_length() {
156 self.done = true;
157 return Ok(());
158 }
159
160 let payload_size = end - reader.get_position();
161 let payload = if payload_size > 0 {
162 reader
163 .read_bytes_u8(payload_size as usize)
164 .map_err(|_| Error::new_read(format!("box payload ({payload_size} bytes).")))?
165 } else {
166 Vec::with_capacity(0)
167 };
168
169 let payload_reader = Reader::new(&payload, false);
170
171 let _box = ParsedBox {
172 name,
173 parser: self.clone(),
174 partial_okay,
175 stop_on_partial,
176 version,
177 flags,
178 reader: payload_reader,
179 size: size as usize,
180 start: start + abs_start,
181 has_64_bit_size,
182 };
183
184 box_definition(_box)?;
185 } else {
186 // Move the read head to be at the end of the box.
187 // If the box is longer than the remaining parts of the file, e.g. the
188 // mp4 is improperly formatted, or this was a partial range request that
189 // ended in the middle of a box, just skip to the end.
190 let skip_length = (start + size - reader.get_position())
191 .min(reader.get_length() - reader.get_position());
192 reader
193 .skip(skip_length)
194 .map_err(|_| Error::new_read(format!("{skip_length} bytes.")))?;
195 }
196
197 Ok(())
198 }
199}
200
201// CALLBACKS
202
203/// A callback that tells the Mp4 parser to treat the body of a box as a series
204/// of boxes. The number of boxes is limited by the size of the parent box.
205pub fn children(mut _box: ParsedBox) -> HandlerResult {
206 // The "reader" starts at the payload, so we need to add the header to the
207 // start position. The header size varies.
208 let header_size = _box.header_size();
209
210 while _box.reader.has_more_data() && !_box.parser.done {
211 _box.parser.parse_next(
212 _box.start + header_size,
213 &mut _box.reader,
214 _box.partial_okay,
215 _box.stop_on_partial,
216 )?;
217 }
218
219 Ok(())
220}
221
222/// A callback that tells the Mp4 parser to treat the body of a box as a sample
223/// description. A sample description box has a fixed number of children. The
224/// number of children is represented by a 4 byte unsigned integer. Each child
225/// is a box.
226pub fn sample_description(mut _box: ParsedBox) -> HandlerResult {
227 // The "reader" starts at the payload, so we need to add the header to the
228 // start position. The header size varies.
229 let header_size = _box.header_size();
230 let count = _box
231 .reader
232 .read_u32()
233 .map_err(|_| Error::new_read("sample description count (u32)."))?;
234
235 for _ in 0..count {
236 _box.parser.parse_next(
237 _box.start + header_size,
238 &mut _box.reader,
239 _box.partial_okay,
240 _box.stop_on_partial,
241 )?;
242
243 if _box.parser.done {
244 break;
245 }
246 }
247
248 Ok(())
249}
250
251/// A callback that tells the Mp4 parser to treat the body of a box as a visual
252/// sample entry. A visual sample entry has some fixed-sized fields
253/// describing the video codec parameters, followed by an arbitrary number of
254/// appended children. Each child is a box.
255pub fn visual_sample_entry(mut _box: ParsedBox) -> HandlerResult {
256 // The "reader" starts at the payload, so we need to add the header to the
257 // start position. The header size varies.
258 let header_size = _box.header_size();
259
260 // Skip 6 reserved bytes.
261 // Skip 2-byte data reference index.
262 // Skip 16 more reserved bytes.
263 // Skip 4 bytes for width/height.
264 // Skip 8 bytes for horizontal/vertical resolution.
265 // Skip 4 more reserved bytes (0)
266 // Skip 2-byte frame count.
267 // Skip 32-byte compressor name (length byte, then name, then 0-padding).
268 // Skip 2-byte depth.
269 // Skip 2 more reserved bytes (0xff)
270 // 78 bytes total.
271 // See also https://github.com/shaka-project/shaka-packager/blob/d5ca6e84/packager/media/formats/mp4/box_definitions.cc#L1544
272 _box.reader
273 .skip(78)
274 .map_err(|_| Error::new_read("visual sample entry reserved 78 bytes."))?;
275
276 while _box.reader.has_more_data() && !_box.parser.done {
277 _box.parser.parse_next(
278 _box.start + header_size,
279 &mut _box.reader,
280 _box.partial_okay,
281 _box.stop_on_partial,
282 )?;
283 }
284
285 Ok(())
286}
287
288/// A callback that tells the Mp4 parser to treat the body of a box as a audio
289/// sample entry. A audio sample entry has some fixed-sized fields
290/// describing the audio codec parameters, followed by an arbitrary number of
291/// ppended children. Each child is a box.
292pub fn audio_sample_entry(mut _box: ParsedBox) -> HandlerResult {
293 // The "reader" starts at the payload, so we need to add the header to the
294 // start position. The header size varies.
295 let header_size = _box.header_size();
296
297 // 6 bytes reserved
298 // 2 bytes data reference index
299 _box.reader
300 .skip(8)
301 .map_err(|_| Error::new_read("audio sample entry reserved (6+2 bytes)."))?;
302
303 // 2 bytes version
304 let version = _box
305 .reader
306 .read_u16()
307 .map_err(|_| Error::new_read("audio sample entry version (u16)."))?;
308 // 2 bytes revision (0, could be ignored)
309 // 4 bytes reserved
310 _box.reader
311 .skip(6)
312 .map_err(|_| Error::new_read("audio sample entry reserved (2+4 bytes)."))?;
313
314 if version == 2 {
315 // 16 bytes hard-coded values with no comments
316 // 8 bytes sample rate
317 // 4 bytes channel count
318 // 4 bytes hard-coded values with no comments
319 // 4 bytes bits per sample
320 // 4 bytes lpcm flags
321 // 4 bytes sample size
322 // 4 bytes samples per packet
323 _box.reader
324 .skip(48)
325 .map_err(|_| Error::new_read("audio sample entry reserved (48 bytes)."))?;
326 } else {
327 // 2 bytes channel count
328 // 2 bytes bits per sample
329 // 2 bytes compression ID
330 // 2 bytes packet size
331 // 2 bytes sample rate
332 // 2 byte reserved
333 _box.reader
334 .skip(12)
335 .map_err(|_| Error::new_read("audio sample entry reserved (12 bytes)."))?;
336 }
337
338 if version == 1 {
339 // 4 bytes samples per packet
340 // 4 bytes bytes per packet
341 // 4 bytes bytes per frame
342 // 4 bytes bytes per sample
343 _box.reader
344 .skip(16)
345 .map_err(|_| Error::new_read("audio sample entry reserved (16 bytes)."))?;
346 }
347
348 while _box.reader.has_more_data() && !_box.parser.done {
349 _box.parser.parse_next(
350 _box.start + header_size,
351 &mut _box.reader,
352 _box.partial_okay,
353 _box.stop_on_partial,
354 )?;
355 }
356
357 Ok(())
358}
359
360/// Create a callback that tells the Mp4 parser to treat the body of a box as a
361/// binary blob and to parse the body's contents using the provided callback.
362#[allow(clippy::arc_with_non_send_sync)]
363pub fn alldata(callback: Arc<dyn Fn(Vec<u8>) -> HandlerResult>) -> CallbackType {
364 Arc::new(move |mut _box| {
365 let all = _box.reader.get_length() - _box.reader.get_position();
366 callback(
367 _box.reader
368 .read_bytes_u8(all as usize)
369 .map_err(|_| Error::new_read(format!("all data {all} bytes.")))?,
370 )
371 })
372}
373
374// UTILS
375
376/// Convert an ascii string name to the integer type for a box.
377/// The name must be four characters long.
378pub fn type_from_string(name: &str) -> usize {
379 assert!(name.len() == 4, "MP4 box names must be 4 characters long");
380
381 let mut code = 0;
382
383 for chr in name.chars() {
384 code = (code << 8) | chr as usize;
385 }
386
387 code
388}
389
390/// Convert an integer type from a box into an ascii string name.
391/// Useful for debugging.
392pub fn type_to_string(_type: usize) -> Result<String, std::string::FromUtf8Error> {
393 String::from_utf8(vec![
394 ((_type >> 24) & 0xff) as u8,
395 ((_type >> 16) & 0xff) as u8,
396 ((_type >> 8) & 0xff) as u8,
397 (_type & 0xff) as u8,
398 ])
399}
400
401/// An enum used to track the type of box so that the correct values can be
402/// read from the header.
403#[derive(Clone, PartialEq)]
404pub enum BoxType {
405 BasicBox,
406 FullBox,
407}
408
409/// Parsed mp4 box.
410#[derive(Clone, Default)]
411pub struct ParsedBox {
412 /// The box name, a 4-character string (fourcc).
413 pub name: String,
414 /// The parser that parsed this box. The parser can be used to parse child
415 /// boxes where the configuration of the current parser is needed to parsed
416 /// other boxes.
417 pub parser: Mp4Parser,
418 /// If true, allows reading partial payloads from some boxes. If the goal is a
419 /// child box, we can sometimes find it without enough data to find all child
420 /// boxes. This property allows the partialOkay flag from parse() to be
421 /// propagated through methods like children().
422 pub partial_okay: bool,
423 /// If true, stop reading if an incomplete box is detected.
424 pub stop_on_partial: bool,
425 /// The start of this box (before the header) in the original buffer. This
426 /// start position is the absolute position.
427 pub start: u64, // i64
428 /// The size of this box (including the header).
429 pub size: usize,
430 /// The version for a full box, null for basic boxes.
431 pub version: Option<u32>,
432 /// The flags for a full box, null for basic boxes.
433 pub flags: Option<u32>,
434 /// The reader for this box is only for this box. Reading or not reading to
435 /// the end will have no affect on the parser reading other sibling boxes.
436 pub reader: Reader,
437 /// If true, the box header had a 64-bit size field. This affects the offsets
438 /// of other fields.
439 pub has_64_bit_size: bool,
440}
441
442impl ParsedBox {
443 /// Find the header size of the box.
444 /// Useful for modifying boxes in place or finding the exact offset of a field.
445 pub fn header_size(&self) -> u64 {
446 let basic_header_size = 8;
447 let _64_bit_field_size = if self.has_64_bit_size { 8 } else { 0 };
448 let version_and_flags_size = if self.flags.is_some() { 4 } else { 0 };
449 basic_header_size + _64_bit_field_size + version_and_flags_size
450 }
451}