Skip to main content

mp4_atom/meta/
mod.rs

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