1use std::{fmt::Debug, ops::RangeBounds, time::Duration};
2
3use bon::bon;
4
5use crate::{
6 atom::{
7 atom_ref::{self, unwrap_atom_data},
8 hdlr::HandlerType,
9 mdhd::MDHD,
10 mvhd::MVHD,
11 AtomHeader, MovieHeaderAtom, TrakAtomRef, TrakAtomRefMut, UserDataAtomRefMut, TRAK, UDTA,
12 },
13 Atom, AtomData, FourCC,
14};
15
16pub const MOOV: FourCC = FourCC::new(b"moov");
17
18#[derive(Debug, Clone, Copy)]
19pub struct MoovAtomRef<'a>(pub(crate) atom_ref::AtomRef<'a>);
20
21impl<'a> MoovAtomRef<'a> {
22 pub fn children(&self) -> impl Iterator<Item = &'a Atom> {
23 self.0.children()
24 }
25
26 pub fn header(&self) -> Option<&'a MovieHeaderAtom> {
27 let atom = self.children().find(|a| a.header.atom_type == MVHD)?;
28 match atom.data.as_ref()? {
29 AtomData::MovieHeader(data) => Some(data),
30 _ => None,
31 }
32 }
33
34 pub fn into_tracks_iter(self) -> impl Iterator<Item = TrakAtomRef<'a>> {
36 self.children()
37 .filter(|a| a.header.atom_type == TRAK)
38 .map(TrakAtomRef::new)
39 }
40
41 pub fn into_audio_track_iter(self) -> impl Iterator<Item = TrakAtomRef<'a>> {
43 self.into_tracks_iter().filter(|trak| {
44 matches!(
45 trak.media()
46 .handler_reference()
47 .map(|hdlr| &hdlr.handler_type),
48 Some(HandlerType::Audio)
49 )
50 })
51 }
52
53 pub fn next_track_id(&self) -> u32 {
57 self.children()
58 .filter(|a| a.header.atom_type == TRAK)
59 .map(TrakAtomRef::new)
60 .fold(1, |id, trak| {
61 (trak.track_id().unwrap_or_default() + 1).max(id)
62 })
63 }
64}
65
66#[derive(Debug)]
67pub struct MoovAtomRefMut<'a>(pub(crate) atom_ref::AtomRefMut<'a>);
68
69impl<'a> MoovAtomRefMut<'a> {
70 pub fn as_ref(&self) -> MoovAtomRef<'_> {
71 MoovAtomRef(self.0.as_ref())
72 }
73
74 pub fn into_ref(self) -> MoovAtomRef<'a> {
75 MoovAtomRef(self.0.into_ref())
76 }
77
78 pub fn children(&mut self) -> impl Iterator<Item = &'_ mut Atom> {
79 self.0.children()
80 }
81
82 pub fn header(&mut self) -> &'_ mut MovieHeaderAtom {
84 unwrap_atom_data!(
85 self.0.find_or_insert_child(MVHD).call(),
86 AtomData::MovieHeader,
87 )
88 }
89
90 pub fn user_data(&mut self) -> UserDataAtomRefMut<'_> {
92 UserDataAtomRefMut(
93 self.0
94 .find_or_insert_child(UDTA)
95 .insert_after(vec![TRAK, MVHD])
96 .call(),
97 )
98 }
99
100 pub fn tracks(&mut self) -> impl Iterator<Item = TrakAtomRefMut<'_>> {
101 self.0
102 .children()
103 .filter(|a| a.header.atom_type == TRAK)
104 .map(TrakAtomRefMut::new)
105 }
106
107 pub fn audio_tracks(&mut self) -> impl Iterator<Item = TrakAtomRefMut<'_>> {
109 self.tracks().filter(|trak| {
110 matches!(
111 trak.as_ref()
112 .media()
113 .handler_reference()
114 .map(|hdlr| &hdlr.handler_type),
115 Some(HandlerType::Audio)
116 )
117 })
118 }
119
120 pub fn tracks_retain<P>(&mut self, mut pred: P) -> &mut Self
122 where
123 P: FnMut(TrakAtomRef) -> bool,
124 {
125 self.0
126 .0
127 .children
128 .retain(|a| a.header.atom_type != TRAK || pred(TrakAtomRef::new(a)));
129 self
130 }
131}
132
133#[cfg(feature = "experimental-trim")]
134#[bon]
135impl<'a> MoovAtomRefMut<'a> {
136 #[builder(finish_fn(name = "trim"), builder_type = TrimDuration)]
142 pub fn trim_duration(
143 &mut self,
144 from_start: Option<Duration>,
145 from_end: Option<Duration>,
146 ) -> &mut Self {
147 use std::ops::Bound;
148 let start_duration = from_start.map(|d| (Bound::Unbounded, Bound::Included(d)));
149 let end_duration = from_end.map(|d| {
150 let d = self.header().duration().saturating_sub(d);
151 (Bound::Included(d), Bound::Unbounded)
152 });
153 let trim_ranges = vec![start_duration, end_duration]
154 .into_iter()
155 .flatten()
156 .collect::<Vec<_>>();
157 self.trim_duration_ranges(&trim_ranges)
158 }
159
160 #[builder(finish_fn(name = "retain"), builder_type = RetainDuration)]
166 pub fn retain_duration(
167 &mut self,
168 from_offset: Option<Duration>,
169 duration: Duration,
170 ) -> &mut Self {
171 use std::ops::Bound;
172 let trim_ranges = vec![
173 (
174 Bound::Unbounded,
175 Bound::Excluded(from_offset.unwrap_or_default()),
176 ),
177 (
178 Bound::Included(from_offset.unwrap_or_default() + duration),
179 Bound::Unbounded,
180 ),
181 ];
182 self.trim_duration_ranges(&trim_ranges)
183 }
184
185 fn trim_duration_ranges<R>(&mut self, trim_ranges: &[R]) -> &mut Self
186 where
187 R: RangeBounds<Duration> + Clone + Debug,
188 {
189 let movie_timescale = u64::from(self.header().timescale);
190 let mut remaining_audio_duration = None;
191 let remaining_duration = self
192 .tracks()
193 .map(|mut trak| {
194 let handler_type = trak
195 .as_ref()
196 .media()
197 .handler_reference()
198 .map(|hdlr| hdlr.handler_type.clone());
199 let remaining_duration = trak.trim_duration(movie_timescale, trim_ranges);
200 if let Some(HandlerType::Audio) = handler_type {
201 if remaining_audio_duration.is_none() {
202 remaining_audio_duration = Some(remaining_duration);
203 }
204 }
205 remaining_duration
206 })
207 .max();
208 if let Some(remaining_duration) = remaining_audio_duration.or(remaining_duration) {
210 self.header().update_duration(|_| remaining_duration);
211 }
212 self
213 }
214}
215
216#[cfg(feature = "experimental-trim")]
217#[bon]
218impl<'a, 'b, S: trim_duration::State> TrimDuration<'a, 'b, S> {
219 #[builder(finish_fn(name = "trim"), builder_type = TrimDurationRanges)]
220 fn ranges<R>(
221 self,
222 #[builder(start_fn)] ranges: impl IntoIterator<Item = R>,
223 ) -> &'b mut MoovAtomRefMut<'a>
224 where
225 R: RangeBounds<Duration> + Clone + Debug,
226 S::FromEnd: trim_duration::IsUnset,
227 S::FromStart: trim_duration::IsUnset,
228 {
229 self.self_receiver
230 .trim_duration_ranges(&ranges.into_iter().collect::<Vec<_>>())
231 }
232}
233
234#[bon]
235impl<'a> MoovAtomRefMut<'a> {
236 #[builder]
238 pub fn add_track(
239 &mut self,
240 #[builder(default = Vec::new())] children: Vec<Atom>,
241 ) -> TrakAtomRefMut<'_> {
242 let trak = Atom::builder()
243 .header(AtomHeader::new(*TRAK))
244 .children(children)
245 .build();
246 let index = self.0.get_insert_position().after(vec![TRAK, MDHD]).call();
247 TrakAtomRefMut(self.0.insert_child(index, trak))
248 }
249}
250
251#[cfg(feature = "experimental-trim")]
252#[cfg(test)]
253mod trim_tests {
254 use std::ops::Bound;
255 use std::time::Duration;
256
257 use bon::Builder;
258
259 use crate::{
260 atom::{
261 container::MOOV,
262 ftyp::{FileTypeAtom, FTYP},
263 hdlr::{HandlerReferenceAtom, HandlerType},
264 mvhd::{MovieHeaderAtom, MVHD},
265 stsc::SampleToChunkEntry,
266 trak::trim_tests::{
267 create_test_track, create_test_track_builder, CreateTestTrackBuilder,
268 },
269 util::scaled_duration,
270 Atom, AtomHeader,
271 },
272 parser::Metadata,
273 FourCC,
274 };
275
276 #[bon::builder(finish_fn(name = "build"))]
277 fn create_test_metadata(
278 #[builder(field)] tracks: Vec<Atom>,
279 #[builder(getter)] movie_timescale: u32,
280 #[builder(getter)] duration: Duration,
281 ) -> Metadata {
282 let atoms = vec![
283 Atom::builder()
284 .header(AtomHeader::new(*FTYP))
285 .data(
286 FileTypeAtom::builder()
287 .major_brand(*b"isom")
288 .minor_version(512)
289 .compatible_brands(
290 vec![*b"isom", *b"iso2", *b"mp41"]
291 .into_iter()
292 .map(FourCC::from)
293 .collect::<Vec<_>>(),
294 )
295 .build(),
296 )
297 .build(),
298 Atom::builder()
299 .header(AtomHeader::new(*MOOV))
300 .children(Vec::from_iter(
301 std::iter::once(
302 Atom::builder()
304 .header(AtomHeader::new(*MVHD))
305 .data(
306 MovieHeaderAtom::builder()
307 .timescale(movie_timescale)
308 .duration(scaled_duration(duration, movie_timescale as u64))
309 .next_track_id(2)
310 .build(),
311 )
312 .build(),
313 )
314 .chain(tracks.into_iter()),
315 ))
316 .build(),
317 ];
318
319 Metadata::new(atoms.into())
320 }
321
322 impl<S> CreateTestMetadataBuilder<S>
323 where
324 S: create_test_metadata_builder::State,
325 S::MovieTimescale: create_test_metadata_builder::IsSet,
326 S::Duration: create_test_metadata_builder::IsSet,
327 {
328 fn track<CTBS>(mut self, track: CreateTestTrackBuilder<CTBS>) -> Self
329 where
330 CTBS: create_test_track_builder::State,
331 CTBS::MovieTimescale: create_test_track_builder::IsUnset,
332 CTBS::MediaTimescale: create_test_track_builder::IsSet,
333 CTBS::Duration: create_test_track_builder::IsUnset,
334 {
335 self.tracks.push(
336 track
337 .movie_timescale(*self.get_movie_timescale())
338 .duration(self.get_duration().clone())
339 .build(),
340 );
341 self
342 }
343 }
344
345 fn test_moov_trim_duration(mut metadata: Metadata, test_case: TrimDurationTestCase) {
346 let movie_timescale = test_case.movie_timescale;
347 let media_timescale = test_case.media_timescale;
348
349 metadata
351 .moov_mut()
352 .trim_duration()
353 .ranges(
354 test_case
355 .ranges
356 .into_iter()
357 .map(|r| (r.start_bound, r.end_bound))
358 .collect::<Vec<_>>(),
359 )
360 .trim();
361
362 let new_movie_duration = metadata.moov().header().map(|h| h.duration).unwrap_or(0);
364 let expected_movie_duration = scaled_duration(
365 test_case.expected_remaining_duration,
366 movie_timescale as u64,
367 );
368 assert_eq!(
369 new_movie_duration, expected_movie_duration,
370 "Movie duration should match expected",
371 );
372
373 let new_track_duration = metadata
375 .moov()
376 .into_tracks_iter()
377 .next()
378 .and_then(|t| t.header().map(|h| h.duration))
379 .unwrap_or(0);
380 let expected_track_duration = scaled_duration(
381 test_case.expected_remaining_duration,
382 movie_timescale as u64,
383 );
384 assert_eq!(
385 new_track_duration, expected_track_duration,
386 "Track duration should match expected",
387 );
388
389 let new_media_duration = metadata
391 .moov()
392 .into_tracks_iter()
393 .next()
394 .map(|t| t.media().header().map(|h| h.duration).unwrap_or(0))
395 .unwrap_or(0);
396 let expected_media_duration = scaled_duration(
397 test_case.expected_remaining_duration,
398 media_timescale as u64,
399 );
400 assert_eq!(
401 new_media_duration, expected_media_duration,
402 "Media duration should match expected",
403 );
404
405 let track = metadata.moov().into_tracks_iter().next().unwrap();
407 let stbl = track.media().media_information().sample_table();
408
409 let stts = stbl
411 .time_to_sample()
412 .expect("Time-to-sample atom should exist");
413 let stsc = stbl
414 .sample_to_chunk()
415 .expect("Sample-to-chunk atom should exist");
416 let stsz = stbl.sample_size().expect("Sample-size atom should exist");
417 let stco = stbl.chunk_offset().expect("Chunk-offset atom should exist");
418
419 let total_samples = stsz.sample_count() as u32;
421 if test_case.expected_remaining_duration != Duration::ZERO {
422 assert!(total_samples > 0, "Sample table should have samples",);
423 }
424
425 let stts_total_samples: u32 = stts.entries.iter().map(|entry| entry.sample_count).sum();
427 assert_eq!(
428 stts_total_samples, total_samples,
429 "Time-to-sample total samples should match sample size count",
430 );
431
432 let chunk_count = stco.chunk_count() as u32;
434 assert!(chunk_count > 0, "Should have at least one chunk",);
435
436 for entry in stsc.entries.iter() {
438 assert!(
439 entry.first_chunk >= 1 && entry.first_chunk <= chunk_count,
440 "Sample-to-chunk first_chunk {} should be between 1 and {}",
441 entry.first_chunk,
442 chunk_count,
443 );
444 assert!(
445 entry.samples_per_chunk > 0,
446 "Sample-to-chunk samples_per_chunk should be > 0",
447 );
448 }
449
450 let total_duration: u64 = stts
452 .entries
453 .iter()
454 .map(|entry| entry.sample_count as u64 * entry.sample_duration as u64)
455 .sum();
456 let expected_duration_scaled = scaled_duration(
457 test_case.expected_remaining_duration,
458 media_timescale as u64,
459 );
460
461 assert_eq!(
462 total_duration, expected_duration_scaled,
463 "Sample table total duration should match the expected duration",
464 );
465 }
466
467 #[derive(Debug, Builder)]
468 struct TrimDurationRange {
469 start_bound: Bound<Duration>,
470 end_bound: Bound<Duration>,
471 }
472
473 #[derive(Debug)]
474 struct TrimDurationTestCase {
475 movie_timescale: u32,
476 media_timescale: u32,
477 original_duration: Duration,
478 ranges: Vec<TrimDurationRange>,
479 expected_remaining_duration: Duration,
480 }
481
482 #[bon::bon]
483 impl TrimDurationTestCase {
484 #[builder]
485 pub fn new(
486 #[builder(field)] ranges: Vec<TrimDurationRange>,
487 #[builder(default = 1_000)] movie_timescale: u32,
488 #[builder(default = 10_000)] media_timescale: u32,
489 original_duration: Duration,
490 expected_remaining_duration: Duration,
491 ) -> Self {
492 assert!(
493 ranges.len() > 0,
494 "test case must include at least one range"
495 );
496
497 Self {
498 movie_timescale,
499 media_timescale,
500 original_duration,
501 ranges,
502 expected_remaining_duration,
503 }
504 }
505 }
506
507 impl<S> TrimDurationTestCaseBuilder<S>
508 where
509 S: trim_duration_test_case_builder::State,
510 {
511 fn range(mut self, range: TrimDurationRange) -> Self {
512 self.ranges.push(range);
513 self
514 }
515 }
516
517 macro_rules! test_moov_trim_duration {
518 ($(
519 $name:ident {
520 $(
521 @tracks( $($track:expr),*, ),
522 )?
523 $($field:ident: $value:expr),+$(,)?
524 } $(,)?
525 )* $(,)?) => {
526 $(
527 test_moov_trim_duration!(@single $name {
528 $(
529 @tracks( $($track),*, ),
530 )?
531 $($field: $value),+
532 });
533 )*
534 };
535
536 (@single $name:ident {
537 $($field:ident: $value:expr),+$(,)?
538 } $(,)?) => {
539 test_moov_trim_duration!(@single $name {
540 @tracks(
541 |media_timescale| create_test_track().media_timescale(media_timescale),
542 ),
543 $($field: $value),+
544 });
545 };
546
547 (@single $name:ident {
548 @tracks($($track:expr),+,),
549 $($field:ident: $value:expr),+
550 } $(,)?) => {
551 test_moov_trim_duration!(@fn_def $name {
552 @tracks($($track),+),
553 $($field: $value),+
554 });
555 };
556
557 (@fn_def $name:ident {
558 @tracks($($track:expr),+),
559 $($field:ident: $value:expr),+$(,)?
560 } $(,)?) => {
561 #[test]
562 fn $name() {
563 let movie_timescale = 1_000;
564 let media_timescale = 10_000;
565
566 let test_case = TrimDurationTestCase::builder().
567 $($field($value)).+.
568 build();
569
570 let metadata = create_test_metadata()
572 .movie_timescale(movie_timescale)
573 .duration(test_case.original_duration).
574 $(
575 track(
576 ($track)(media_timescale)
577 )
578 ).+.
579 build();
580
581 test_moov_trim_duration(metadata, test_case);
582 }
583 };
584 }
585
586 test_moov_trim_duration!(
587 trim_start_2_seconds {
588 original_duration: Duration::from_secs(10),
589 range: TrimDurationRange::builder()
590 .start_bound(Bound::Included(Duration::ZERO))
591 .end_bound(Bound::Included(Duration::from_secs(2)))
592 .build(),
593 expected_remaining_duration: Duration::from_secs(8),
594 }
595 trim_end_2_seconds {
596 original_duration: Duration::from_secs(10),
597 range: TrimDurationRange::builder()
598 .start_bound(Bound::Included(Duration::from_secs(8)))
599 .end_bound(Bound::Included(Duration::from_secs(10)))
600 .build(),
601 expected_remaining_duration: Duration::from_secs(8),
602 }
603 trim_middle_2_seconds {
604 original_duration: Duration::from_secs(10),
605 range: TrimDurationRange::builder()
606 .start_bound(Bound::Included(Duration::from_secs(4)))
607 .end_bound(Bound::Included(Duration::from_secs(6)))
608 .build(),
609 expected_remaining_duration: Duration::from_secs(8),
610 }
611 trim_middle_included_start_2_seconds {
612 original_duration: Duration::from_secs(10),
613 range: TrimDurationRange::builder()
614 .start_bound(Bound::Included(Duration::from_secs(2)))
615 .end_bound(Bound::Included(Duration::from_secs(4)))
616 .build(),
617 expected_remaining_duration: Duration::from_secs(8),
618 }
619 trim_middle_excluded_start_2_seconds {
620 original_duration: Duration::from_millis(10_000),
621 range: TrimDurationRange::builder()
622 .start_bound(Bound::Excluded(Duration::from_millis(1_999)))
623 .end_bound(Bound::Included(Duration::from_millis(4_000)))
624 .build(),
625 expected_remaining_duration: Duration::from_millis(8_000),
626 }
627 trim_middle_excluded_end_2_seconds {
628 original_duration: Duration::from_secs(10),
629 range: TrimDurationRange::builder()
630 .start_bound(Bound::Included(Duration::from_secs(1)))
631 .end_bound(Bound::Excluded(Duration::from_secs(3)))
632 .build(),
633 expected_remaining_duration: Duration::from_secs(8),
634 }
635 trim_start_unbounded_5_seconds {
636 original_duration: Duration::from_secs(10),
637 range: TrimDurationRange::builder()
638 .start_bound(Bound::Unbounded)
639 .end_bound(Bound::Included(Duration::from_secs(5)))
640 .build(),
641 expected_remaining_duration: Duration::from_secs(5),
642 }
643 trim_end_unbounded_6_seconds {
644 original_duration: Duration::from_secs(100),
645 range: TrimDurationRange::builder()
646 .start_bound(Bound::Included(Duration::from_secs(94)))
647 .end_bound(Bound::Unbounded)
648 .build(),
649 expected_remaining_duration: Duration::from_secs(94),
650 }
651 trim_start_and_end_20_seconds {
652 original_duration: Duration::from_secs(100),
653 range: TrimDurationRange::builder()
654 .start_bound(Bound::Unbounded)
655 .end_bound(Bound::Excluded(Duration::from_secs(20)))
656 .build(),
657 range: TrimDurationRange::builder()
658 .start_bound(Bound::Included(Duration::from_secs(80)))
659 .end_bound(Bound::Unbounded)
660 .build(),
661 expected_remaining_duration: Duration::from_secs(60),
662 }
663 trim_first_and_last_chunk {
664 @tracks(
665 |media_timescale| create_test_track().stsc_entries(vec![
666 SampleToChunkEntry::builder()
668 .first_chunk(1)
669 .samples_per_chunk(20)
670 .sample_description_index(1)
671 .build(),
672 SampleToChunkEntry::builder()
673 .first_chunk(2)
674 .samples_per_chunk(60)
675 .sample_description_index(2)
676 .build(),
677 SampleToChunkEntry::builder()
678 .first_chunk(3)
679 .samples_per_chunk(20)
680 .sample_description_index(3)
681 .build(),
682 ]).media_timescale(media_timescale),
683 ),
684 original_duration: Duration::from_secs(100),
685 range: TrimDurationRange::builder()
686 .start_bound(Bound::Unbounded)
687 .end_bound(Bound::Excluded(Duration::from_secs(20)))
688 .build(),
689 range: TrimDurationRange::builder()
690 .start_bound(Bound::Included(Duration::from_secs(80)))
691 .end_bound(Bound::Unbounded)
692 .build(),
693 expected_remaining_duration: Duration::from_secs(60),
694 }
695 trim_first_and_20s_multi_track {
696 @tracks(
697 |media_timescale| create_test_track().stsc_entries(vec![
698 SampleToChunkEntry::builder()
700 .first_chunk(1)
701 .samples_per_chunk(20)
702 .sample_description_index(1)
703 .build(),
704 SampleToChunkEntry::builder()
705 .first_chunk(2)
706 .samples_per_chunk(60)
707 .sample_description_index(2)
708 .build(),
709 SampleToChunkEntry::builder()
710 .first_chunk(3)
711 .samples_per_chunk(20)
712 .sample_description_index(3)
713 .build(),
714 ]).media_timescale(media_timescale),
715 |_| create_test_track().handler_reference(
716 HandlerReferenceAtom::builder()
717 .handler_type(HandlerType::Text).build(),
718 ).media_timescale(666_666),
719 ),
720 original_duration: Duration::from_secs(100),
721 range: TrimDurationRange::builder()
722 .start_bound(Bound::Unbounded)
723 .end_bound(Bound::Excluded(Duration::from_secs(20)))
724 .build(),
725 range: TrimDurationRange::builder()
726 .start_bound(Bound::Included(Duration::from_secs(80)))
727 .end_bound(Bound::Unbounded)
728 .build(),
729 expected_remaining_duration: Duration::from_secs(60),
730 }
731 );
733}