Skip to main content

mp4_edit/atom/leaf/
chpl.rs

1use bon::bon;
2use derive_more::Deref;
3use std::{fmt, time::Duration};
4
5use crate::{
6    atom::{util::DebugList, FourCC},
7    parser::ParseAtomData,
8    writer::SerializeAtom,
9    ParseError,
10};
11
12pub const CHPL: FourCC = FourCC::new(b"chpl");
13
14#[derive(Default, Clone, Deref)]
15pub struct ChapterEntries(Vec<ChapterEntry>);
16
17impl ChapterEntries {
18    pub fn into_vec(self) -> Vec<ChapterEntry> {
19        self.0
20    }
21}
22
23impl FromIterator<ChapterEntry> for ChapterEntries {
24    fn from_iter<T: IntoIterator<Item = ChapterEntry>>(iter: T) -> Self {
25        Vec::from_iter(iter).into()
26    }
27}
28
29impl From<Vec<ChapterEntry>> for ChapterEntries {
30    fn from(entries: Vec<ChapterEntry>) -> Self {
31        ChapterEntries(entries)
32    }
33}
34
35impl fmt::Debug for ChapterEntries {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        fmt::Debug::fmt(&DebugList::new(self.0.iter(), 10), f)
38    }
39}
40
41/// Chapter entry containing start time and title
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ChapterEntry {
44    /// Start time of the chapter in 100-nanosecond units
45    pub start_time: u64,
46    /// Chapter title as UTF-8 string
47    pub title: String,
48}
49
50#[bon]
51impl ChapterEntry {
52    #[builder]
53    pub fn new(#[builder(into, finish_fn)] title: String, start_time: Duration) -> Self {
54        // convert to 100-nanosecond units
55        let start_time = (start_time.as_nanos() / 100).min(u128::from(u64::MAX)) as u64;
56        ChapterEntry { start_time, title }
57    }
58}
59
60/// Chapter List Atom - contains chapter information for media
61#[derive(Debug, Clone)]
62pub struct ChapterListAtom {
63    /// Version of the chpl atom format (1)
64    pub version: u8,
65    pub flags: [u8; 3],
66    pub reserved: [u8; 4],
67    /// List of chapter entries
68    pub chapters: ChapterEntries,
69}
70
71impl Default for ChapterListAtom {
72    fn default() -> Self {
73        Self {
74            version: 1,
75            flags: [0u8; 3],
76            reserved: [0u8; 4],
77            chapters: Default::default(),
78        }
79    }
80}
81
82impl ChapterListAtom {
83    pub fn new(chapters: impl Into<ChapterEntries>) -> Self {
84        Self {
85            version: 1,
86            flags: [0u8; 3],
87            reserved: [0u8; 4],
88            chapters: chapters.into(),
89        }
90    }
91
92    pub fn replace_chapters(&mut self, chapters: impl Into<ChapterEntries>) {
93        self.chapters = chapters.into();
94    }
95}
96
97impl ParseAtomData for ChapterListAtom {
98    fn parse_atom_data(atom_type: FourCC, input: &[u8]) -> Result<Self, ParseError> {
99        crate::atom::util::parser::assert_atom_type!(atom_type, CHPL);
100        use crate::atom::util::parser::stream;
101        use winnow::Parser;
102        Ok(parser::parse_chpl_data.parse(stream(input))?)
103    }
104}
105
106impl SerializeAtom for ChapterListAtom {
107    fn atom_type(&self) -> FourCC {
108        CHPL
109    }
110
111    fn into_body_bytes(self) -> Vec<u8> {
112        serializer::serialize_chpl_data(self)
113    }
114}
115
116mod serializer {
117    use crate::atom::chpl::{ChapterEntries, ChapterEntry};
118
119    use super::ChapterListAtom;
120
121    pub fn serialize_chpl_data(atom: ChapterListAtom) -> Vec<u8> {
122        vec![
123            version(atom.version),
124            flags(atom.flags),
125            reserved(atom.reserved),
126            chapters(atom.chapters),
127        ]
128        .into_iter()
129        .flatten()
130        .collect()
131    }
132
133    fn version(version: u8) -> Vec<u8> {
134        vec![version]
135    }
136
137    fn flags(flags: [u8; 3]) -> Vec<u8> {
138        flags.to_vec()
139    }
140
141    fn reserved(reserved: [u8; 4]) -> Vec<u8> {
142        reserved.to_vec()
143    }
144
145    fn chapters(chapters: ChapterEntries) -> Vec<u8> {
146        vec![
147            vec![u8::try_from(chapters.len())
148                .expect("there must be no more than {u8::MAX} chapter entries")],
149            chapters.0.into_iter().flat_map(chapter).collect(),
150        ]
151        .into_iter()
152        .flatten()
153        .collect()
154    }
155
156    fn chapter(chapter: ChapterEntry) -> Vec<u8> {
157        vec![start_time(chapter.start_time), title(chapter.title)]
158            .into_iter()
159            .flatten()
160            .collect()
161    }
162
163    fn start_time(start_time: u64) -> Vec<u8> {
164        start_time.to_be_bytes().to_vec()
165    }
166
167    fn title(title: String) -> Vec<u8> {
168        let title_bytes = title.into_bytes();
169        vec![
170            vec![u8::try_from(title_bytes.len()).expect("title length must not exceed {u8::MAX}")],
171            title_bytes,
172        ]
173        .into_iter()
174        .flatten()
175        .collect()
176    }
177}
178
179mod parser {
180    use winnow::{
181        binary::{be_u64, length_and_then, u8},
182        combinator::{repeat, seq, trace},
183        error::StrContext,
184        token::rest,
185        ModalResult, Parser,
186    };
187
188    use super::ChapterListAtom;
189    use crate::atom::{
190        chpl::{ChapterEntries, ChapterEntry},
191        util::parser::{byte_array, version, Stream},
192    };
193
194    pub fn parse_chpl_data(input: &mut Stream<'_>) -> ModalResult<ChapterListAtom> {
195        trace(
196            "chpl",
197            seq!(ChapterListAtom {
198                version: version.verify(|v| *v == 1),
199                flags: byte_array.context(StrContext::Label("flags")),
200                reserved: byte_array.context(StrContext::Label("reserved")),
201                chapters: chapters.context(StrContext::Label("chapters")),
202            })
203            .context(StrContext::Label("chpl")),
204        )
205        .parse_next(input)
206    }
207
208    fn chapters(input: &mut Stream<'_>) -> ModalResult<ChapterEntries> {
209        trace("chapters", move |input: &mut Stream<'_>| {
210            let chapter_count = u8
211                .context(StrContext::Label("chapter_count"))
212                .parse_next(input)?;
213            repeat(chapter_count as usize, chapter)
214                .map(ChapterEntries)
215                .parse_next(input)
216        })
217        .parse_next(input)
218    }
219
220    fn chapter(input: &mut Stream<'_>) -> ModalResult<ChapterEntry> {
221        trace(
222            "chapter",
223            seq!(ChapterEntry {
224                start_time: be_u64.context(StrContext::Label("start_time")),
225                title: length_and_then(
226                    u8,
227                    rest.try_map(|buf: &[u8]| String::from_utf8(buf.to_vec()))
228                )
229                .context(StrContext::Label("title")),
230                // _: null_bytes, // discard trailing null bytes
231            })
232            .context(StrContext::Label("chapter")),
233        )
234        .parse_next(input)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::atom::test_utils::test_atom_roundtrip;
242
243    /// Test round-trip for all available chpl test data files
244    #[test]
245    fn test_chpl_roundtrip() {
246        test_atom_roundtrip::<ChapterListAtom>(CHPL);
247    }
248}