Skip to main content

mp4_atom/meta/
mod.rs

1mod idat;
2mod iinf;
3mod iloc;
4mod ilst;
5mod iprp;
6mod iref;
7mod pitm;
8mod properties;
9
10pub use idat::*;
11pub use iinf::*;
12pub use iloc::*;
13pub use ilst::*;
14pub use iprp::*;
15pub use iref::*;
16pub use pitm::*;
17pub use properties::*;
18
19use crate::*;
20
21// MetaBox, ISO/IEC 14496:2022 Secion 8.11.1
22// Its like a container box, but derived from FullBox
23
24// TODO: add ItemProtectionBox, IPMPControlBox
25
26#[derive(Debug, Clone, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct Meta {
29    pub hdlr: Hdlr,
30    pub items: Vec<Any>,
31}
32
33impl Eq for Meta {}
34
35macro_rules! meta_atom {
36    ($($atom:ident),*,) => {
37        /// A trait to signify that this is a common meta atom.
38        pub trait MetaAtom: AnyAtom {}
39
40        $(impl MetaAtom for $atom {})*
41    };
42}
43
44meta_atom! {
45        Pitm,
46        Dinf,
47        Iloc,
48        Iinf,
49        Iprp,
50        Iref,
51        Idat,
52        Ilst,
53}
54
55// Implement helpers to make it easier to get these atoms.
56impl Meta {
57    /// A helper to get a common meta atom from the items vec.
58    pub fn get<T: MetaAtom>(&self) -> Option<&T> {
59        self.items.iter().find_map(T::from_any_ref)
60    }
61
62    /// A helper to get a common meta atom from the items vec.
63    pub fn get_mut<T: MetaAtom>(&mut self) -> Option<&mut T> {
64        self.items.iter_mut().find_map(T::from_any_mut)
65    }
66
67    /// A helper to insert a common meta atom into the items vec.
68    pub fn push<T: MetaAtom>(&mut self, atom: T) {
69        self.items.push(atom.into_any());
70    }
71
72    /// A helper to remove a common meta atom from the items vec.
73    ///
74    /// This removes the first instance of the atom from the vec.
75    pub fn remove<T: MetaAtom>(&mut self) -> Option<T> {
76        let pos = self.items.iter().position(|a| T::from_any_ref(a).is_some());
77        if let Some(pos) = pos {
78            Some(T::from_any(self.items.remove(pos)).unwrap())
79        } else {
80            None
81        }
82    }
83}
84
85impl Atom for Meta {
86    const KIND: FourCC = FourCC::new(b"meta");
87    fn decode_body<B: Buf>(buf: &mut B) -> Result<Self> {
88        // are we a full box?
89        // In Apple's QuickTime specification, the MetaBox is a regular Box.
90        // In ISO 14496-12, MetaBox extends FullBox.
91
92        if buf.remaining() < 8 {
93            return Err(Error::OutOfBounds);
94        }
95
96        if buf.slice(8)[4..8] == *b"hdlr".as_ref() {
97            // Apple QuickTime specification
98            tracing::trace!("meta box without fullbox header");
99        } else {
100            // ISO 14496-12
101            let _version_and_flags = u32::decode(buf)?; // version & flags
102        }
103
104        let hdlr = Hdlr::decode(buf)?;
105        let mut items = Vec::new();
106        while let Some(atom) = Any::decode_maybe(buf)? {
107            items.push(atom);
108        }
109        skip_trailing_padding(buf);
110
111        Ok(Self { hdlr, items })
112    }
113
114    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
115        0u32.encode(buf)?; // version & flags
116        self.hdlr.encode(buf)?;
117        for atom in &self.items {
118            atom.encode(buf)?;
119        }
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_meta_empty() {
130        let expected = Meta {
131            hdlr: Hdlr {
132                handler: b"fake".into(),
133                name: "".into(),
134            },
135            items: Vec::new(),
136        };
137        let mut buf = Vec::new();
138        expected.encode(&mut buf).unwrap();
139
140        let mut buf = buf.as_ref();
141        let output = Meta::decode(&mut buf).unwrap();
142        assert_eq!(output, expected);
143    }
144
145    // The hand-written `meta` decode loop must also tolerate a sub-header
146    // padding remainder after its child boxes.
147    #[test]
148    fn test_meta_trailing_padding() {
149        let meta = Meta {
150            hdlr: Hdlr {
151                handler: b"mdir".into(),
152                name: "".into(),
153            },
154            items: Vec::new(),
155        };
156        let mut buf = Vec::new();
157        meta.encode(&mut buf).unwrap();
158        buf.extend_from_slice(&[0, 0, 0, 0]);
159        let size = (buf.len() as u32).to_be_bytes();
160        buf[0..4].copy_from_slice(&size);
161
162        let decoded =
163            Meta::decode(&mut buf.as_slice()).expect("trailing padding must be tolerated");
164        assert_eq!(decoded, meta);
165    }
166
167    #[test]
168    fn test_meta_mdir() {
169        let mut expected = Meta {
170            hdlr: Hdlr {
171                handler: b"mdir".into(),
172                name: "".into(),
173            },
174            items: Vec::new(),
175        };
176
177        expected.push(Pitm { item_id: 3 });
178        expected.push(Dinf {
179            dref: Dref {
180                urls: vec![Url {
181                    location: "".into(),
182                }],
183            },
184        });
185        expected.push(Iloc {
186            item_locations: vec![ItemLocation {
187                item_id: 3,
188                construction_method: 0,
189                data_reference_index: 0,
190                base_offset: 0,
191                extents: vec![ItemLocationExtent {
192                    item_reference_index: 0,
193                    offset: 200,
194                    length: 100,
195                }],
196            }],
197        });
198        expected.push(Iinf { item_infos: vec![] });
199        expected.push(Iprp {
200            ipco: Ipco { properties: vec![] },
201            ipma: vec![Ipma {
202                item_properties: vec![
203                    PropertyAssociations {
204                        item_id: 1,
205                        associations: vec![
206                            PropertyAssociation {
207                                essential: true,
208                                property_index: 1,
209                            },
210                            PropertyAssociation {
211                                essential: false,
212                                property_index: 2,
213                            },
214                            PropertyAssociation {
215                                essential: false,
216                                property_index: 3,
217                            },
218                            PropertyAssociation {
219                                essential: false,
220                                property_index: 5,
221                            },
222                            PropertyAssociation {
223                                essential: true,
224                                property_index: 4,
225                            },
226                        ],
227                    },
228                    PropertyAssociations {
229                        item_id: 2,
230                        associations: vec![
231                            PropertyAssociation {
232                                essential: true,
233                                property_index: 6,
234                            },
235                            PropertyAssociation {
236                                essential: false,
237                                property_index: 3,
238                            },
239                            PropertyAssociation {
240                                essential: false,
241                                property_index: 7,
242                            },
243                            PropertyAssociation {
244                                essential: true,
245                                property_index: 8,
246                            },
247                            PropertyAssociation {
248                                essential: true,
249                                property_index: 4,
250                            },
251                        ],
252                    },
253                ],
254            }],
255        });
256        expected.push(Iref {
257            references: vec![Reference {
258                reference_type: b"cdsc".into(),
259                from_item_id: 2,
260                to_item_ids: vec![1, 3],
261            }],
262        });
263        expected.push(Idat {
264            data: vec![0x01, 0xFF, 0xFE, 0x03],
265        });
266        expected.push(Ilst::default());
267
268        let mut buf = Vec::new();
269        expected.encode(&mut buf).unwrap();
270
271        let mut buf = buf.as_ref();
272        let output = Meta::decode(&mut buf).unwrap();
273        assert_eq!(output, expected);
274    }
275
276    #[test]
277    fn test_meta_apple_quicktime() {
278        // Test Apple QuickTime format meta box (without FullBox header)
279        // In Apple's spec, meta box is a regular Box, not a FullBox
280        // So it starts directly with hdlr instead of version+flags
281
282        // Manually construct a meta box in Apple format
283        let mut buf = Vec::new();
284
285        // meta box header
286        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size (placeholder, will fix)
287        buf.extend_from_slice(b"meta");
288
289        // hdlr box directly (no version/flags before it)
290        let hdlr = Hdlr {
291            handler: b"mdir".into(),
292            name: "Apple".into(),
293        };
294
295        // Encode hdlr
296        let mut hdlr_buf = Vec::new();
297        hdlr.encode(&mut hdlr_buf).unwrap();
298        buf.extend_from_slice(&hdlr_buf);
299
300        // Add an ilst box
301        let ilst = Ilst::default();
302        let mut ilst_buf = Vec::new();
303        ilst.encode(&mut ilst_buf).unwrap();
304        buf.extend_from_slice(&ilst_buf);
305
306        // Fix the meta box size
307        let size = buf.len() as u32;
308        buf[0..4].copy_from_slice(&size.to_be_bytes());
309
310        // Skip the meta box header (8 bytes) to get to the body
311        let mut cursor = std::io::Cursor::new(&buf[8..]);
312
313        // Decode
314        let decoded = Meta::decode_body(&mut cursor).expect("failed to decode Apple meta box");
315
316        // Verify
317        assert_eq!(decoded.hdlr.handler, FourCC::new(b"mdir"));
318        assert_eq!(decoded.hdlr.name, "Apple");
319        assert_eq!(decoded.items.len(), 1);
320        assert!(decoded.get::<Ilst>().is_some());
321    }
322
323    #[test]
324    fn test_meta_apple_with_ilst() {
325        // Test a more complete Apple-style meta box with ilst containing iTunes metadata
326        let mut buf = Vec::new();
327
328        // meta box header
329        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size (placeholder)
330        buf.extend_from_slice(b"meta");
331
332        // hdlr box (no version/flags)
333        let hdlr = Hdlr {
334            handler: b"mdir".into(),
335            name: "".into(),
336        };
337        let mut hdlr_buf = Vec::new();
338        hdlr.encode(&mut hdlr_buf).unwrap();
339        buf.extend_from_slice(&hdlr_buf);
340
341        // ilst box with some metadata
342        let ilst = Ilst {
343            name: Some(Name("Test Song".into())),
344            year: Some(Year("2025".into())),
345            ..Default::default()
346        };
347
348        let mut ilst_buf = Vec::new();
349        ilst.encode(&mut ilst_buf).unwrap();
350        buf.extend_from_slice(&ilst_buf);
351
352        // Fix the meta box size
353        let size = buf.len() as u32;
354        buf[0..4].copy_from_slice(&size.to_be_bytes());
355
356        // Decode
357        let mut cursor = std::io::Cursor::new(&buf[8..]);
358        let decoded =
359            Meta::decode_body(&mut cursor).expect("failed to decode Apple meta with ilst");
360
361        // Verify
362        assert_eq!(decoded.hdlr.handler, FourCC::new(b"mdir"));
363        let decoded_ilst = decoded.get::<Ilst>().expect("ilst not found");
364        assert_eq!(decoded_ilst.name.as_ref().unwrap().0, "Test Song");
365        assert_eq!(decoded_ilst.year.as_ref().unwrap().0, "2025");
366    }
367
368    #[test]
369    fn test_meta_iso_vs_apple_roundtrip() {
370        // Test that we can decode both ISO (with FullBox) and Apple (without) formats
371        // and our encoder always produces ISO format
372
373        let meta = Meta {
374            hdlr: Hdlr {
375                handler: b"mdir".into(),
376                name: "Handler".into(),
377            },
378            items: vec![],
379        };
380
381        // Encode (produces ISO format with version/flags)
382        let mut encoded = Vec::new();
383        meta.encode(&mut encoded).unwrap();
384
385        // Decode should work
386        let mut cursor = std::io::Cursor::new(&encoded);
387        let decoded = Meta::decode(&mut cursor).expect("failed to decode ISO format");
388        assert_eq!(decoded, meta);
389
390        // Now manually create Apple format (without version/flags)
391        let mut apple_format = Vec::new();
392        apple_format.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size placeholder
393        apple_format.extend_from_slice(b"meta");
394
395        let mut hdlr_buf = Vec::new();
396        meta.hdlr.encode(&mut hdlr_buf).unwrap();
397        apple_format.extend_from_slice(&hdlr_buf);
398
399        let size = apple_format.len() as u32;
400        apple_format[0..4].copy_from_slice(&size.to_be_bytes());
401
402        // Decode Apple format
403        let mut cursor = std::io::Cursor::new(&apple_format);
404        let decoded_apple = Meta::decode(&mut cursor).expect("failed to decode Apple format");
405        assert_eq!(decoded_apple, meta);
406    }
407}