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}