Skip to main content

mp4_atom/moov/udta/
chpl.rs

1use crate::*;
2
3/// One chapter entry inside a [`Chpl`].
4#[derive(Debug, Clone, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct ChplEntry {
7    /// Start time, in 100ns units, relative to the start of the presentation.
8    pub start: u64,
9    pub title: String,
10}
11
12/// Nero-style Chapter List Box (`chpl`).
13///
14/// Not part of the ISO/QuickTime spec, but a de facto standard written and read
15/// by e.g. Nero, `ffmpeg`'s `mov` muxer/demuxer, and MP4Box, as an alternative
16/// to the QuickTime `chap` track reference + text track convention.
17///
18/// Version 0 and version 1 only differ in an extra 4-byte reserved field
19/// present after the FullBox header in version 1 (absent in version 0); it
20/// carries no known meaning in either case, so it's not exposed here. This
21/// always encodes as version 1, matching the common convention used by
22/// `ffmpeg`'s `mov` muxer and MP4Box.
23#[derive(Debug, Clone, PartialEq, Eq, Default)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Chpl {
26    pub entries: Vec<ChplEntry>,
27}
28
29impl Atom for Chpl {
30    const KIND: FourCC = FourCC::new(b"chpl");
31
32    fn decode_body<B: Buf>(buf: &mut B) -> Result<Self> {
33        let version = u8::decode(buf)?;
34        <[u8; 3]>::decode(buf)?; // flags
35        if version != 0 {
36            <[u8; 4]>::decode(buf)?; // reserved, version 1 only
37        }
38
39        let count = u8::decode(buf)?;
40        let mut entries = Vec::with_capacity(count as usize);
41        for _ in 0..count {
42            let start = u64::decode(buf)?;
43
44            let title = decode_length_prefixed_utf8_string::<u8>(buf)?;
45
46            entries.push(ChplEntry { start, title });
47        }
48
49        Ok(Self { entries })
50    }
51
52    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
53        1u8.encode(buf)?; // version
54        [0u8; 3].encode(buf)?; // flags
55        [0u8; 4].encode(buf)?; // reserved
56
57        let count: u8 = self
58            .entries
59            .len()
60            .try_into()
61            .map_err(|_| Error::TooLarge(Self::KIND))?;
62        count.encode(buf)?;
63
64        for entry in &self.entries {
65            entry.start.encode(buf)?;
66
67            encode_length_prefixed_string::<u8>(buf, &entry.title, Self::KIND)?;
68        }
69
70        Ok(())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_chpl_roundtrip() {
80        let expected = Chpl {
81            entries: vec![
82                ChplEntry {
83                    start: 0,
84                    title: "Intro".into(),
85                },
86                ChplEntry {
87                    start: 30_000_000,
88                    title: "Chapter 2".into(),
89                },
90            ],
91        };
92
93        let mut buf = Vec::new();
94        expected.encode(&mut buf).unwrap();
95
96        let decoded = Chpl::decode(&mut buf.as_slice()).expect("failed to decode chpl");
97        assert_eq!(decoded, expected);
98    }
99
100    #[test]
101    fn test_chpl_empty() {
102        let expected = Chpl { entries: vec![] };
103
104        let mut buf = Vec::new();
105        expected.encode(&mut buf).unwrap();
106
107        let decoded = Chpl::decode(&mut buf.as_slice()).expect("failed to decode chpl");
108        assert_eq!(decoded, expected);
109    }
110
111    #[test]
112    fn test_chpl_version0_no_reserved_field() {
113        // Per ffmpeg's mov.c (mov_read_chpl): version 0 omits the 4-byte
114        // reserved field that version 1 has after the FullBox header.
115        let expected = Chpl {
116            entries: vec![
117                ChplEntry {
118                    start: 0,
119                    title: "Intro".into(),
120                },
121                ChplEntry {
122                    start: 30_000_000,
123                    title: "Chapter 2".into(),
124                },
125            ],
126        };
127
128        let mut buf = Vec::new();
129        0u8.encode(&mut buf).unwrap(); // version 0
130        [0u8; 3].encode(&mut buf).unwrap(); // flags
131        let count: u8 = expected.entries.len().try_into().unwrap();
132        count.encode(&mut buf).unwrap();
133        for entry in &expected.entries {
134            entry.start.encode(&mut buf).unwrap();
135            let title_bytes = entry.title.as_bytes();
136            let title_len: u8 = title_bytes.len().try_into().unwrap();
137            title_len.encode(&mut buf).unwrap();
138            buf.extend_from_slice(title_bytes);
139        }
140
141        let decoded = Chpl::decode_body(&mut buf.as_slice()).expect("failed to decode chpl");
142        assert_eq!(decoded, expected);
143    }
144}