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}