mp4_edit/atom/leaf/
chpl.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ChapterEntry {
44 pub start_time: u64,
46 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 let start_time = (start_time.as_nanos() / 100).min(u128::from(u64::MAX)) as u64;
56 ChapterEntry { start_time, title }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct ChapterListAtom {
63 pub version: u8,
65 pub flags: [u8; 3],
66 pub reserved: [u8; 4],
67 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 })
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]
245 fn test_chpl_roundtrip() {
246 test_atom_roundtrip::<ChapterListAtom>(CHPL);
247 }
248}