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
110        Ok(Self { hdlr, items })
111    }
112
113    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
114        0u32.encode(buf)?; // version & flags
115        self.hdlr.encode(buf)?;
116        for atom in &self.items {
117            atom.encode(buf)?;
118        }
119        Ok(())
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_meta_empty() {
129        let expected = Meta {
130            hdlr: Hdlr {
131                handler: b"fake".into(),
132                name: "".into(),
133            },
134            items: Vec::new(),
135        };
136        let mut buf = Vec::new();
137        expected.encode(&mut buf).unwrap();
138
139        let mut buf = buf.as_ref();
140        let output = Meta::decode(&mut buf).unwrap();
141        assert_eq!(output, expected);
142    }
143
144    #[test]
145    fn test_meta_mdir() {
146        let mut expected = Meta {
147            hdlr: Hdlr {
148                handler: b"mdir".into(),
149                name: "".into(),
150            },
151            items: Vec::new(),
152        };
153
154        expected.push(Pitm { item_id: 3 });
155        expected.push(Dinf {
156            dref: Dref {
157                urls: vec![Url {
158                    location: "".into(),
159                }],
160            },
161        });
162        expected.push(Iloc {
163            item_locations: vec![ItemLocation {
164                item_id: 3,
165                construction_method: 0,
166                data_reference_index: 0,
167                base_offset: 0,
168                extents: vec![ItemLocationExtent {
169                    item_reference_index: 0,
170                    offset: 200,
171                    length: 100,
172                }],
173            }],
174        });
175        expected.push(Iinf { item_infos: vec![] });
176        expected.push(Iprp {
177            ipco: Ipco { properties: vec![] },
178            ipma: vec![Ipma {
179                item_properties: vec![
180                    PropertyAssociations {
181                        item_id: 1,
182                        associations: vec![
183                            PropertyAssociation {
184                                essential: true,
185                                property_index: 1,
186                            },
187                            PropertyAssociation {
188                                essential: false,
189                                property_index: 2,
190                            },
191                            PropertyAssociation {
192                                essential: false,
193                                property_index: 3,
194                            },
195                            PropertyAssociation {
196                                essential: false,
197                                property_index: 5,
198                            },
199                            PropertyAssociation {
200                                essential: true,
201                                property_index: 4,
202                            },
203                        ],
204                    },
205                    PropertyAssociations {
206                        item_id: 2,
207                        associations: vec![
208                            PropertyAssociation {
209                                essential: true,
210                                property_index: 6,
211                            },
212                            PropertyAssociation {
213                                essential: false,
214                                property_index: 3,
215                            },
216                            PropertyAssociation {
217                                essential: false,
218                                property_index: 7,
219                            },
220                            PropertyAssociation {
221                                essential: true,
222                                property_index: 8,
223                            },
224                            PropertyAssociation {
225                                essential: true,
226                                property_index: 4,
227                            },
228                        ],
229                    },
230                ],
231            }],
232        });
233        expected.push(Iref {
234            references: vec![Reference {
235                reference_type: b"cdsc".into(),
236                from_item_id: 2,
237                to_item_ids: vec![1, 3],
238            }],
239        });
240        expected.push(Idat {
241            data: vec![0x01, 0xFF, 0xFE, 0x03],
242        });
243        expected.push(Ilst::default());
244
245        let mut buf = Vec::new();
246        expected.encode(&mut buf).unwrap();
247
248        let mut buf = buf.as_ref();
249        let output = Meta::decode(&mut buf).unwrap();
250        assert_eq!(output, expected);
251    }
252
253    #[test]
254    fn test_meta_apple_quicktime() {
255        // Test Apple QuickTime format meta box (without FullBox header)
256        // In Apple's spec, meta box is a regular Box, not a FullBox
257        // So it starts directly with hdlr instead of version+flags
258
259        // Manually construct a meta box in Apple format
260        let mut buf = Vec::new();
261
262        // meta box header
263        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size (placeholder, will fix)
264        buf.extend_from_slice(b"meta");
265
266        // hdlr box directly (no version/flags before it)
267        let hdlr = Hdlr {
268            handler: b"mdir".into(),
269            name: "Apple".into(),
270        };
271
272        // Encode hdlr
273        let mut hdlr_buf = Vec::new();
274        hdlr.encode(&mut hdlr_buf).unwrap();
275        buf.extend_from_slice(&hdlr_buf);
276
277        // Add an ilst box
278        let ilst = Ilst::default();
279        let mut ilst_buf = Vec::new();
280        ilst.encode(&mut ilst_buf).unwrap();
281        buf.extend_from_slice(&ilst_buf);
282
283        // Fix the meta box size
284        let size = buf.len() as u32;
285        buf[0..4].copy_from_slice(&size.to_be_bytes());
286
287        // Skip the meta box header (8 bytes) to get to the body
288        let mut cursor = std::io::Cursor::new(&buf[8..]);
289
290        // Decode
291        let decoded = Meta::decode_body(&mut cursor).expect("failed to decode Apple meta box");
292
293        // Verify
294        assert_eq!(decoded.hdlr.handler, FourCC::new(b"mdir"));
295        assert_eq!(decoded.hdlr.name, "Apple");
296        assert_eq!(decoded.items.len(), 1);
297        assert!(decoded.get::<Ilst>().is_some());
298    }
299
300    #[test]
301    fn test_meta_apple_with_ilst() {
302        // Test a more complete Apple-style meta box with ilst containing iTunes metadata
303        let mut buf = Vec::new();
304
305        // meta box header
306        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size (placeholder)
307        buf.extend_from_slice(b"meta");
308
309        // hdlr box (no version/flags)
310        let hdlr = Hdlr {
311            handler: b"mdir".into(),
312            name: "".into(),
313        };
314        let mut hdlr_buf = Vec::new();
315        hdlr.encode(&mut hdlr_buf).unwrap();
316        buf.extend_from_slice(&hdlr_buf);
317
318        // ilst box with some metadata
319        let ilst = Ilst {
320            name: Some(Name("Test Song".into())),
321            year: Some(Year("2025".into())),
322            ..Default::default()
323        };
324
325        let mut ilst_buf = Vec::new();
326        ilst.encode(&mut ilst_buf).unwrap();
327        buf.extend_from_slice(&ilst_buf);
328
329        // Fix the meta box size
330        let size = buf.len() as u32;
331        buf[0..4].copy_from_slice(&size.to_be_bytes());
332
333        // Decode
334        let mut cursor = std::io::Cursor::new(&buf[8..]);
335        let decoded =
336            Meta::decode_body(&mut cursor).expect("failed to decode Apple meta with ilst");
337
338        // Verify
339        assert_eq!(decoded.hdlr.handler, FourCC::new(b"mdir"));
340        let decoded_ilst = decoded.get::<Ilst>().expect("ilst not found");
341        assert_eq!(decoded_ilst.name.as_ref().unwrap().0, "Test Song");
342        assert_eq!(decoded_ilst.year.as_ref().unwrap().0, "2025");
343    }
344
345    #[test]
346    fn test_meta_iso_vs_apple_roundtrip() {
347        // Test that we can decode both ISO (with FullBox) and Apple (without) formats
348        // and our encoder always produces ISO format
349
350        let meta = Meta {
351            hdlr: Hdlr {
352                handler: b"mdir".into(),
353                name: "Handler".into(),
354            },
355            items: vec![],
356        };
357
358        // Encode (produces ISO format with version/flags)
359        let mut encoded = Vec::new();
360        meta.encode(&mut encoded).unwrap();
361
362        // Decode should work
363        let mut cursor = std::io::Cursor::new(&encoded);
364        let decoded = Meta::decode(&mut cursor).expect("failed to decode ISO format");
365        assert_eq!(decoded, meta);
366
367        // Now manually create Apple format (without version/flags)
368        let mut apple_format = Vec::new();
369        apple_format.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // size placeholder
370        apple_format.extend_from_slice(b"meta");
371
372        let mut hdlr_buf = Vec::new();
373        meta.hdlr.encode(&mut hdlr_buf).unwrap();
374        apple_format.extend_from_slice(&hdlr_buf);
375
376        let size = apple_format.len() as u32;
377        apple_format[0..4].copy_from_slice(&size.to_be_bytes());
378
379        // Decode Apple format
380        let mut cursor = std::io::Cursor::new(&apple_format);
381        let decoded_apple = Meta::decode(&mut cursor).expect("failed to decode Apple format");
382        assert_eq!(decoded_apple, meta);
383    }
384}