Skip to main content

vsd_mp4/
parser.rs

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