1use std::time::Duration;
2
3use bon::bon;
4
5use crate::atom::{
6 container::{DINF, EDTS, MDIA, MINF, STBL, TRAK},
7 dref::DataReferenceEntry,
8 elst::{EditEntry, ELST},
9 gmin::GMIN,
10 hdlr::{HandlerReferenceAtom, HandlerType},
11 mdhd::{LanguageCode, MediaHeaderAtom},
12 stco_co64::ChunkOffsetAtom,
13 stsc::{SampleToChunkAtom, SampleToChunkEntry},
14 stsd::{
15 SampleDescriptionTableAtom, SampleEntry, SampleEntryData, SampleEntryType, TextSampleEntry,
16 },
17 stsz::SampleSizeAtom,
18 stts::{TimeToSampleAtom, TimeToSampleEntry},
19 text::TEXT,
20 tkhd::TrackHeaderAtom,
21 util::{mp4_timestamp_now, scaled_duration},
22 Atom, AtomHeader, BaseMediaInfoAtom, DataReferenceAtom, EditListAtom, TextMediaInfoAtom, GMHD,
23};
24use crate::writer::SerializeAtom;
25
26#[derive(Debug, Clone)]
28pub struct InputChapter {
29 pub title: String,
30 pub offset_ms: u64,
31 pub duration_ms: u64,
32}
33
34#[bon]
35impl InputChapter {
36 #[builder]
37 pub fn new(
38 #[builder(into, finish_fn)] title: String,
39 start_offset: Duration,
40 duration: Duration,
41 ) -> Self {
42 Self {
43 title,
44 offset_ms: start_offset.as_millis() as u64,
45 duration_ms: duration.as_millis() as u64,
46 }
47 }
48}
49
50pub struct ChapterTrack {
52 language: LanguageCode,
53 timescale: u32,
54 track_id: u32,
55 creation_time: u64,
56 modification_time: u64,
57 sample_data: Vec<Vec<u8>>,
58 sample_durations: Vec<u32>,
59 sample_sizes: Vec<u32>, media_duration: u64,
61 movie_duration: u64,
62 handler_name: String,
63}
64
65#[bon]
66impl ChapterTrack {
67 #[builder]
68 pub fn new(
69 #[builder(finish_fn, into)] chapters: Vec<InputChapter>,
70 #[builder(default = LanguageCode::Undetermined)] language: LanguageCode,
71 #[builder(default = 44100)] timescale: u32,
72 #[builder(default = 600)] movie_timescale: u32,
73 #[builder(default = 2)] track_id: u32,
74 #[builder(into)] total_duration: Duration,
75 #[builder(default = mp4_timestamp_now())] creation_time: u64,
76 #[builder(default = mp4_timestamp_now())] modification_time: u64,
77 #[builder(default = "Apple Text Media Handler".to_string(), into)] handler_name: String,
78 ) -> Self {
79 let (sample_data, sample_durations, sample_sizes) =
80 Self::create_samples_durations_and_sizes(&chapters, total_duration, timescale);
81
82 Self {
83 language,
84 timescale,
85 track_id,
86 creation_time,
87 modification_time,
88 sample_data,
89 sample_durations,
90 sample_sizes,
91 media_duration: scaled_duration(total_duration, u64::from(timescale)),
93 movie_duration: scaled_duration(total_duration, u64::from(movie_timescale)),
95 handler_name,
96 }
97 }
98
99 fn create_samples_durations_and_sizes(
102 chapters: &[InputChapter],
103 total_duration: Duration,
104 timescale: u32,
105 ) -> (Vec<Vec<u8>>, Vec<u32>, Vec<u32>) {
106 if chapters.is_empty() {
107 return (vec![], vec![], vec![]);
108 }
109
110 let total_duration_ms = total_duration.as_millis() as u64;
111 let mut samples = Vec::new();
112 let mut durations = Vec::new();
113 let mut sizes = Vec::new();
114
115 for (i, chapter) in chapters.iter().enumerate() {
116 let chapter_marker = Self::create_variable_chapter_marker(&chapter.title);
118 let sample_size = chapter_marker.len() as u32;
119
120 samples.push(chapter_marker);
121 sizes.push(sample_size);
122
123 let chapter_end_ms = if i + 1 < chapters.len() {
125 chapters[i + 1].offset_ms
127 } else {
128 total_duration_ms
130 };
131
132 let actual_duration_ms = chapter_end_ms - chapter.offset_ms;
134 let chapter_duration_seconds = actual_duration_ms as f64 / 1000.0;
135 let chapter_duration_scaled = (chapter_duration_seconds * f64::from(timescale)) as u32;
136
137 durations.push(chapter_duration_scaled);
138 }
139
140 (samples, durations, sizes)
141 }
142
143 fn create_variable_chapter_marker(title: &str) -> Vec<u8> {
145 let title_bytes = title.as_bytes();
151 let text_len = title_bytes.len() as u16;
152
153 let total_size = 2 + title_bytes.len();
155 let mut data = Vec::with_capacity(total_size);
156
157 data.extend_from_slice(&text_len.to_be_bytes());
159
160 data.extend_from_slice(title_bytes);
162
163 data
164 }
165
166 pub fn sample_bytes(&self) -> Vec<u8> {
168 let mut result = Vec::new();
169 for sample in &self.sample_data {
170 result.extend_from_slice(sample);
171 }
172 result
173 }
174
175 pub fn individual_samples(&self) -> &[Vec<u8>] {
177 &self.sample_data
178 }
179
180 pub fn sample_sizes(&self) -> &[u32] {
182 &self.sample_sizes
183 }
184
185 pub fn total_sample_size(&self) -> usize {
187 self.sample_sizes.iter().map(|&s| s as usize).sum()
188 }
189
190 pub fn sample_count(&self) -> u32 {
192 self.sample_data.len() as u32
193 }
194
195 pub fn has_uniform_sample_size(&self) -> bool {
197 if self.sample_sizes.is_empty() {
198 return true;
199 }
200 let first_size = self.sample_sizes[0];
201 self.sample_sizes.iter().all(|&size| size == first_size)
202 }
203
204 pub fn uniform_sample_size(&self) -> Option<u32> {
206 if self.has_uniform_sample_size() {
207 self.sample_sizes.first().copied()
208 } else {
209 None
210 }
211 }
212
213 pub fn create_trak_atom(&self, chunk_offset: u64) -> Atom {
215 let track_header = self.create_track_header();
216 let edit_list = self.create_edit_list_atom();
217 let media_atom = self.create_media_atom(chunk_offset);
218
219 Atom::builder()
220 .header(AtomHeader::new(*TRAK))
221 .children(vec![track_header, edit_list, media_atom])
222 .build()
223 }
224
225 fn create_track_header(&self) -> Atom {
226 let tkhd = TrackHeaderAtom {
227 version: 0,
228 flags: [0, 0, 2], creation_time: self.creation_time,
230 modification_time: self.modification_time,
231 track_id: self.track_id,
232 duration: self.movie_duration,
233 layer: 0,
234 alternate_group: 0,
235 volume: 0.0, matrix: None, width: 0.0, height: 0.0,
239 };
240
241 Atom::builder()
242 .header(AtomHeader::new(tkhd.atom_type()))
243 .data(tkhd)
244 .build()
245 }
246
247 fn create_edit_list_atom(&self) -> Atom {
248 Atom::builder()
249 .header(AtomHeader::new(*EDTS))
250 .children(vec![Atom::builder()
251 .header(AtomHeader::new(*ELST))
252 .data(EditListAtom::new(vec![EditEntry {
253 segment_duration: self.movie_duration,
254 media_time: 0,
255 media_rate: 1.0,
256 }]))
257 .build()])
258 .build()
259 }
260
261 fn create_media_atom(&self, chunk_offset: u64) -> Atom {
262 let mdhd = self.create_media_header();
263 let hdlr = self.create_handler_reference();
264 let minf = self.create_media_information(chunk_offset);
265
266 Atom::builder()
267 .header(AtomHeader::new(*MDIA))
268 .children(vec![mdhd, hdlr, minf])
269 .build()
270 }
271
272 fn create_media_header(&self) -> Atom {
273 let mdhd = MediaHeaderAtom::builder()
274 .creation_time(self.creation_time)
275 .modification_time(self.modification_time)
276 .timescale(self.timescale)
277 .duration(self.media_duration)
278 .language(self.language)
279 .build();
280
281 Atom::builder()
282 .header(AtomHeader::new(mdhd.atom_type()))
283 .data(mdhd)
284 .build()
285 }
286
287 fn create_handler_reference(&self) -> Atom {
288 let hdlr = HandlerReferenceAtom::builder()
289 .handler_type(HandlerType::Text)
290 .name(&self.handler_name)
291 .build();
292
293 Atom::builder()
294 .header(AtomHeader::new(hdlr.atom_type()))
295 .data(hdlr)
296 .build()
297 }
298
299 fn create_media_information(&self, chunk_offset: u64) -> Atom {
300 let stbl = self.create_sample_table(chunk_offset);
301
302 let dref = DataReferenceAtom::builder()
304 .entry(
305 DataReferenceEntry::builder()
306 .url(String::new())
307 .flags(
308 [0, 0, 1], )
310 .build(),
311 )
312 .build();
313
314 let dinf = Atom::builder()
315 .header(AtomHeader::new(*DINF))
316 .children(vec![Atom::builder()
317 .header(AtomHeader::new(dref.atom_type()))
318 .data(dref)
319 .build()])
320 .build();
321
322 Atom::builder()
323 .header(AtomHeader::new(*MINF))
324 .children(vec![
325 Atom::builder()
326 .header(AtomHeader::new(*GMHD))
327 .children(vec![
328 Atom::builder()
329 .header(AtomHeader::new(*GMIN))
330 .data(BaseMediaInfoAtom::default())
331 .build(),
332 Atom::builder()
333 .header(AtomHeader::new(*TEXT))
334 .data(TextMediaInfoAtom::default())
335 .build(),
336 ])
337 .build(),
338 dinf,
339 stbl,
340 ])
341 .build()
342 }
343
344 fn create_sample_table(&self, chunk_offset: u64) -> Atom {
345 let stsd = self.create_sample_description();
346 let stts = self.create_time_to_sample();
347 let stsc = self.create_sample_to_chunk();
348 let stsz = self.create_sample_size();
349 let stco = self.create_chunk_offset(chunk_offset);
350
351 Atom::builder()
352 .header(AtomHeader::new(*STBL))
353 .children(vec![stsd, stts, stsc, stsz, stco])
354 .build()
355 }
356
357 fn create_sample_description(&self) -> Atom {
358 let text_sample_entry = SampleEntry {
360 entry_type: SampleEntryType::Text,
361 data_reference_index: 1,
362 data: SampleEntryData::Text(TextSampleEntry::builder().font_name("Sarif").build()),
363 };
364
365 let stsd = SampleDescriptionTableAtom::from(vec![text_sample_entry]);
366
367 Atom::builder()
368 .header(AtomHeader::new(stsd.atom_type()))
369 .data(stsd)
370 .build()
371 }
372
373 fn create_time_to_sample(&self) -> Atom {
374 let entries: Vec<TimeToSampleEntry> = self
377 .sample_durations
378 .iter()
379 .map(|&duration| TimeToSampleEntry {
380 sample_count: 1, sample_duration: duration,
382 })
383 .collect();
384
385 let stts = TimeToSampleAtom::from(entries);
386
387 Atom::builder()
388 .header(AtomHeader::new(stts.atom_type()))
389 .data(stts)
390 .build()
391 }
392
393 fn create_sample_to_chunk(&self) -> Atom {
394 let stsc = SampleToChunkAtom::from(vec![SampleToChunkEntry {
396 first_chunk: 1,
397 samples_per_chunk: self.sample_count(),
398 sample_description_index: 1,
399 }]);
400
401 Atom::builder()
402 .header(AtomHeader::new(stsc.atom_type()))
403 .data(stsc)
404 .build()
405 }
406
407 fn create_sample_size(&self) -> Atom {
408 let stsz = if let Some(uniform_size) = self.uniform_sample_size() {
409 SampleSizeAtom::builder()
410 .sample_size(uniform_size)
411 .sample_count(self.sample_count())
412 .build()
413 } else {
414 SampleSizeAtom::builder()
415 .entry_sizes(self.sample_sizes.clone())
416 .build()
417 };
418
419 Atom::builder()
420 .header(AtomHeader::new(stsz.atom_type()))
421 .data(stsz)
422 .build()
423 }
424
425 fn create_chunk_offset(&self, chunk_offset: u64) -> Atom {
426 let stco = ChunkOffsetAtom::builder()
428 .chunk_offset(chunk_offset)
429 .build();
430
431 Atom::builder()
432 .header(AtomHeader::new(stco.atom_type()))
433 .data(stco)
434 .build()
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 #[test]
442 fn test_chapter_track_builder() {
443 let chapters = vec![
444 InputChapter {
445 title: "Opening Credits".to_string(),
446 offset_ms: 0,
447 duration_ms: 19758,
448 },
449 InputChapter {
450 title: "Dedication".to_string(),
451 offset_ms: 19758,
452 duration_ms: 4510,
453 },
454 InputChapter {
455 title: "Epigraph".to_string(),
456 offset_ms: 24268,
457 duration_ms: 12364,
458 },
459 ];
460
461 let last_chapter = chapters.last().unwrap();
463 let total_duration =
464 Duration::from_millis(last_chapter.offset_ms + last_chapter.duration_ms);
465
466 let track = ChapterTrack::builder()
467 .track_id(2)
468 .timescale(44100) .movie_timescale(600) .total_duration(total_duration)
471 .language(LanguageCode::Undetermined)
472 .build(chapters);
473
474 assert_eq!(track.sample_count(), 3);
476
477 let samples = track.individual_samples();
479 assert_eq!(samples.len(), 3);
480
481 assert_eq!(samples[0].len(), 17);
486 assert_eq!(samples[1].len(), 12);
487 assert_eq!(samples[2].len(), 10);
488
489 assert_eq!(track.sample_durations.len(), 3);
491
492 println!(
493 "Generated {} chapter samples with durations: {:?}",
494 track.sample_count(),
495 track.sample_durations
496 );
497 }
498}