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                urls: vec![Url {
184                    location: "".into(),
185                }],
186            },
187        });
188        expected.push(Iloc {
189            item_locations: vec![ItemLocation {
190                item_id: 3,
191                construction_method: 0,
192                data_reference_index: 0,
193                base_offset: 0,
194                extents: vec![ItemLocationExtent {
195                    item_reference_index: 0,
196                    offset: 200,
197                    length: 100,
198                }],
199            }],
200        });
201        expected.push(Iinf { item_infos: vec![] });
202        expected.push(Iprp {
203            ipco: Ipco { properties: vec![] },
204            ipma: vec![Ipma {
205                item_properties: vec![
206                    PropertyAssociations {
207                        item_id: 1,
208                        associations: vec![
209                            PropertyAssociation {
210                                essential: true,
211                                property_index: 1,
212                            },
213                            PropertyAssociation {
214                                essential: false,
215                                property_index: 2,
216                            },
217                            PropertyAssociation {
218                                essential: false,
219                                property_index: 3,
220                            },
221                            PropertyAssociation {
222                                essential: false,
223                                property_index: 5,
224                            },
225                            PropertyAssociation {
226                                essential: true,
227                                property_index: 4,
228                            },
229                        ],
230                    },
231                    PropertyAssociations {
232                        item_id: 2,
233                        associations: vec![
234                            PropertyAssociation {
235                                essential: true,
236                                property_index: 6,
237                            },
238                            PropertyAssociation {
239                                essential: false,
240                                property_index: 3,
241                            },
242                            PropertyAssociation {
243                                essential: false,
244                                property_index: 7,
245                            },
246                            PropertyAssociation {
247                                essential: true,
248                                property_index: 8,
249                            },
250                            PropertyAssociation {
251                                essential: true,
252                                property_index: 4,
253                            },
254                        ],
255                    },
256                ],
257            }],
258        });
259        expected.push(Iref {
260            references: vec![Reference {
261                reference_type: b"cdsc".into(),
262                from_item_id: 2,
263                to_item_ids: vec![1, 3],
264            }],
265        });
266        expected.push(Idat {
267            data: vec![0x01, 0xFF, 0xFE, 0x03],
268        });
269        expected.push(Ilst::default());
270
271        let mut buf = Vec::new();
272        expected.encode(&mut buf).unwrap();
273
274        let mut buf = buf.as_ref();
275        let output = Meta::decode(&mut buf).unwrap();
276        assert_eq!(output, expected);
277    }
278
279    #[test]
280    fn test_meta_apple_quicktime() {
281        // Test Apple QuickTime format meta box (without FullBox header)
282        // In Apple's spec, meta box is a regular Box, not a FullBox
283        // So it starts directly with hdlr instead of version+flags
284
285        // Manually construct a meta box in Apple format
286        let mut buf = Vec::new();
287
288        // meta box header
289        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size (placeholder, will fix)
290        buf.extend_from_slice(b"meta");
291
292        // hdlr box directly (no version/flags before it)
293        let hdlr = Hdlr {
294            handler: b"mdir".into(),
295            name: "Apple".into(),
296        };
297
298        // Encode hdlr
299        let mut hdlr_buf = Vec::new();
300        hdlr.encode(&mut hdlr_buf).unwrap();
301        buf.extend_from_slice(&hdlr_buf);
302
303        // Add an ilst box
304        let ilst = Ilst::default();
305        let mut ilst_buf = Vec::new();
306        ilst.encode(&mut ilst_buf).unwrap();
307        buf.extend_from_slice(&ilst_buf);
308
309        // Fix the meta box size
310        let size = buf.len() as u32;
311        buf[0..4].copy_from_slice(&size.to_be_bytes());
312
313        // Skip the meta box header (8 bytes) to get to the body
314        let mut cursor = std::io::Cursor::new(&buf[8..]);
315
316        // Decode
317        let decoded = Meta::decode_body(&mut cursor).expect("failed to decode Apple meta box");
318
319        // Verify
320        assert_eq!(decoded.hdlr.handler, FourCC::new(b"mdir"));
321        assert_eq!(decoded.hdlr.name, "Apple");
322        assert_eq!(decoded.items.len(), 1);
323        assert!(decoded.get::<Ilst>().is_some());
324    }
325
326    #[test]
327    fn test_meta_apple_with_ilst() {
328        // Test a more complete Apple-style meta box with ilst containing iTunes metadata
329        let mut buf = Vec::new();
330
331        // meta box header
332        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size (placeholder)
333        buf.extend_from_slice(b"meta");
334
335        // hdlr box (no version/flags)
336        let hdlr = Hdlr {
337            handler: b"mdir".into(),
338            name: "".into(),
339        };
340        let mut hdlr_buf = Vec::new();
341        hdlr.encode(&mut hdlr_buf).unwrap();
342        buf.extend_from_slice(&hdlr_buf);
343
344        // ilst box with some metadata
345        let ilst = Ilst {
346            name: Some(Name("Test Song".into())),
347            year: Some(Year("2025".into())),
348            ..Default::default()
349        };
350
351        let mut ilst_buf = Vec::new();
352        ilst.encode(&mut ilst_buf).unwrap();
353        buf.extend_from_slice(&ilst_buf);
354
355        // Fix the meta box size
356        let size = buf.len() as u32;
357        buf[0..4].copy_from_slice(&size.to_be_bytes());
358
359        // Decode
360        let mut cursor = std::io::Cursor::new(&buf[8..]);
361        let decoded =
362            Meta::decode_body(&mut cursor).expect("failed to decode Apple meta with ilst");
363
364        // Verify
365        assert_eq!(decoded.hdlr.handler, FourCC::new(b"mdir"));
366        let decoded_ilst = decoded.get::<Ilst>().expect("ilst not found");
367        assert_eq!(decoded_ilst.name.as_ref().unwrap().0, "Test Song");
368        assert_eq!(decoded_ilst.year.as_ref().unwrap().0, "2025");
369    }
370
371    #[test]
372    fn test_meta_iso_vs_apple_roundtrip() {
373        // Test that we can decode both ISO (with FullBox) and Apple (without) formats
374        // and our encoder always produces ISO format
375
376        let meta = Meta {
377            hdlr: Hdlr {
378                handler: b"mdir".into(),
379                name: "Handler".into(),
380            },
381            items: vec![],
382        };
383
384        // Encode (produces ISO format with version/flags)
385        let mut encoded = Vec::new();
386        meta.encode(&mut encoded).unwrap();
387
388        // Decode should work
389        let mut cursor = std::io::Cursor::new(&encoded);
390        let decoded = Meta::decode(&mut cursor).expect("failed to decode ISO format");
391        assert_eq!(decoded, meta);
392
393        // Now manually create Apple format (without version/flags)
394        let mut apple_format = Vec::new();
395        apple_format.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size placeholder
396        apple_format.extend_from_slice(b"meta");
397
398        let mut hdlr_buf = Vec::new();
399        meta.hdlr.encode(&mut hdlr_buf).unwrap();
400        apple_format.extend_from_slice(&hdlr_buf);
401
402        let size = apple_format.len() as u32;
403        apple_format[0..4].copy_from_slice(&size.to_be_bytes());
404
405        // Decode Apple format
406        let mut cursor = std::io::Cursor::new(&apple_format);
407        let decoded_apple = Meta::decode(&mut cursor).expect("failed to decode Apple format");
408        assert_eq!(decoded_apple, meta);
409    }
410}