Skip to main content

mp4_edit/atom/leaf/
stts.rs

1use bon::{bon, Builder};
2use derive_more::{Deref, DerefMut};
3
4use rangemap::RangeSet;
5use std::{
6    fmt::{self, Debug},
7    ops::{Bound, Range, RangeBounds, Sub},
8};
9
10use crate::{
11    atom::{util::DebugList, FourCC},
12    parser::ParseAtomData,
13    writer::SerializeAtom,
14    ParseError,
15};
16
17pub const STTS: FourCC = FourCC::new(b"stts");
18
19#[derive(Default, Clone, Deref, DerefMut)]
20pub struct TimeToSampleEntries(Vec<TimeToSampleEntry>);
21
22impl From<Vec<TimeToSampleEntry>> for TimeToSampleEntries {
23    fn from(entries: Vec<TimeToSampleEntry>) -> Self {
24        Self::new(entries)
25    }
26}
27
28impl TimeToSampleEntries {
29    pub fn new(inner: Vec<TimeToSampleEntry>) -> Self {
30        Self(inner)
31    }
32
33    pub fn inner(&self) -> &[TimeToSampleEntry] {
34        &self.0
35    }
36}
37
38impl fmt::Debug for TimeToSampleEntries {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        fmt::Debug::fmt(&DebugList::new(self.0.iter(), 10), f)
41    }
42}
43
44/// Defines duration for a consecutive group of samples
45#[derive(Debug, Clone, PartialEq, Eq, Builder)]
46pub struct TimeToSampleEntry {
47    /// Number of consecutive samples with the same duration
48    pub sample_count: u32,
49    /// Duration of each sample in timescale units (see MDHD atom)
50    pub sample_duration: u32,
51}
52
53/// Time-to-Sample (stts) atom
54#[derive(Default, Debug, Clone)]
55pub struct TimeToSampleAtom {
56    pub version: u8,
57    pub flags: [u8; 3],
58    pub entries: TimeToSampleEntries,
59}
60
61#[bon]
62impl TimeToSampleAtom {
63    #[builder]
64    pub fn new(
65        #[builder(default = 0)] version: u8,
66        #[builder(default = [0u8; 3])] flags: [u8; 3],
67        #[builder(with = FromIterator::from_iter)] entries: Vec<TimeToSampleEntry>,
68    ) -> Self {
69        Self {
70            version,
71            flags,
72            entries: entries.into(),
73        }
74    }
75
76    /// Removes samples contained in the given `trim_duration`, excluding partially matched samples.
77    /// Returns the remaining duration after the trim and indices of removed samples.
78    ///
79    /// # Panics
80    ///
81    /// This method panics if the trim ranges overlap.
82    ///
83    /// WARNING: failing to update other atoms appropriately will cause file corruption.
84    #[cfg(feature = "experimental-trim")]
85    pub(crate) fn trim_duration<R>(&mut self, trim_ranges: &[R]) -> (u64, RangeSet<usize>)
86    where
87        R: RangeBounds<u64> + Debug,
88    {
89        let mut trim_range_index = 0;
90        let mut removed_sample_indices = RangeSet::new();
91        let mut remove_entry_range = RangeSet::new();
92        let mut next_duration_offset = 0u64;
93        let mut next_sample_index = 0usize;
94        let mut total_original_duration = 0u64;
95        let mut total_duration_trimmed = 0u64;
96
97        'entries: for (entry_index, entry) in self.entries.iter_mut().enumerate() {
98            let current_duration_offset = next_duration_offset;
99            let entry_duration = entry.sample_count as u64 * entry.sample_duration as u64;
100            next_duration_offset = current_duration_offset + entry_duration;
101            total_original_duration += entry_duration;
102
103            let current_sample_index = next_sample_index;
104            next_sample_index += entry.sample_count as usize;
105
106            let entry_duration = {
107                let entry_duration_start = current_duration_offset;
108                let entry_duration_end = current_duration_offset
109                    + (entry.sample_count as u64 * entry.sample_duration as u64).saturating_sub(1);
110                entry_duration_start..entry_duration_end + 1
111            };
112
113            'trim_range: for (i, trim_range) in trim_ranges.iter().enumerate() {
114                let (trim_duration, entry_trim_duration) =
115                    match entry_trim_duration(&entry_duration, trim_range) {
116                        Some(m) => m,
117                        None => {
118                            // Entire entry is outside trim range, continue to next trim range
119                            continue 'trim_range;
120                        }
121                    };
122
123                debug_assert!(
124                    i >= trim_range_index,
125                    "invariant: trim ranges must not overlap"
126                );
127                trim_range_index = i;
128
129                // Entire entry is inside trim range
130                if trim_duration.contains(&entry_duration.start)
131                    && trim_duration.contains(&(entry_duration.end - 1))
132                {
133                    remove_entry_range.insert(entry_index..entry_index + 1);
134                    removed_sample_indices.insert(
135                        current_sample_index..current_sample_index + entry.sample_count as usize,
136                    );
137                    total_duration_trimmed += entry_duration.end - entry_duration.start;
138                    continue 'entries;
139                }
140
141                // Partial overlap
142
143                if entry.sample_count == 1 {
144                    // we can't trim anything smaller than a sample
145                    // TODO: return range that can be used in an edit list entry
146                    continue 'entries;
147                }
148
149                let sample_duration = entry.sample_duration as u64;
150
151                let trim_sample_start_index = (current_sample_index as u64
152                    + (entry_trim_duration.start - entry_duration.start).div_ceil(sample_duration))
153                    as usize;
154                let trim_sample_end_index =
155                    match (entry_trim_duration.end - entry_duration.start) / sample_duration {
156                        0 => trim_sample_start_index,
157                        end => current_sample_index + end as usize - 1,
158                    };
159
160                // TODO(fix): when trimming a duration of 0, end_index can end up less than start_index
161                assert!(trim_sample_end_index >= trim_sample_start_index);
162
163                let num_samples_to_remove = trim_sample_end_index + 1 - trim_sample_start_index;
164                if num_samples_to_remove == entry.sample_count as usize {
165                    if entry.sample_count > 1 {
166                        // remove one less samples
167                        if trim_sample_start_index == 0 {
168                            // anchor to start
169                            removed_sample_indices
170                                .insert(trim_sample_start_index..trim_sample_end_index);
171                        } else {
172                            // anchor to end
173                            removed_sample_indices
174                                .insert((trim_sample_start_index + 1)..trim_sample_end_index);
175                        }
176                    }
177                    entry.sample_count = 1;
178                    let trimmed_duration = entry_trim_duration.end - entry_trim_duration.start;
179                    entry.sample_duration -= trimmed_duration as u32;
180                    total_duration_trimmed += trimmed_duration;
181                } else {
182                    removed_sample_indices
183                        .insert(trim_sample_start_index..(trim_sample_end_index + 1));
184
185                    entry.sample_count = entry.sample_count.sub(num_samples_to_remove as u32);
186
187                    total_duration_trimmed += ((trim_sample_end_index as u64 + 1)
188                        * sample_duration)
189                        - (trim_sample_start_index as u64 * sample_duration);
190                }
191            }
192        }
193
194        let mut n_removed = 0;
195        for range in remove_entry_range.into_iter() {
196            let mut range = (range.start - n_removed)..(range.end - n_removed);
197            n_removed += range.len();
198
199            // maybe merge entries before and after the removed ones
200            if range.start > 0 {
201                if let Ok([prev_entry, next_entry]) = self
202                    .entries
203                    .as_mut_slice()
204                    .get_disjoint_mut([range.start - 1, range.end])
205                {
206                    if prev_entry.sample_duration == next_entry.sample_duration {
207                        prev_entry.sample_count += next_entry.sample_count;
208                        range.end += 1;
209                    }
210                }
211            }
212
213            self.entries.drain(range);
214        }
215
216        (
217            total_original_duration - total_duration_trimmed,
218            removed_sample_indices,
219        )
220    }
221}
222
223#[cfg(feature = "experimental-trim")]
224fn entry_trim_duration<'a, R>(
225    entry_range: &Range<u64>,
226    trim_range: &'a R,
227) -> Option<(&'a R, Range<u64>)>
228where
229    R: RangeBounds<u64>,
230{
231    // entry is contained in range
232    if trim_range.contains(&entry_range.start) && trim_range.contains(&(entry_range.end - 1)) {
233        return Some((trim_range, entry_range.clone()));
234    }
235
236    let finite_trim_range = convert_range(entry_range, trim_range);
237
238    // trim range ends too soon to be useful
239    if finite_trim_range.end <= entry_range.start {
240        return None;
241    }
242
243    // trim range is contained in entry
244    if entry_range.contains(&finite_trim_range.start)
245        && finite_trim_range.end > 0
246        && entry_range.contains(&(finite_trim_range.end - 1))
247    {
248        return Some((trim_range, finite_trim_range));
249    }
250
251    // trim range starts inside of entry
252    if finite_trim_range.start >= entry_range.start && finite_trim_range.start < entry_range.end {
253        return Some((trim_range, finite_trim_range.start..entry_range.end));
254    }
255
256    // trim range ends inside of entry
257    if trim_range.contains(&entry_range.start)
258        && finite_trim_range.start < entry_range.start
259        && finite_trim_range.end <= entry_range.end
260    {
261        return Some((trim_range, entry_range.start..finite_trim_range.end));
262    }
263
264    None
265}
266
267#[cfg(feature = "experimental-trim")]
268fn convert_range(reference_range: &Range<u64>, range: &impl RangeBounds<u64>) -> Range<u64> {
269    let start = match range.start_bound() {
270        Bound::Included(start) => *start,
271        Bound::Excluded(start) => *start + 1,
272        Bound::Unbounded => 0,
273    };
274    let end = match range.end_bound() {
275        Bound::Included(end) => *end + 1,
276        Bound::Excluded(end) => *end,
277        Bound::Unbounded => reference_range.end,
278    };
279    start..end
280}
281
282impl<S: time_to_sample_atom_builder::State> TimeToSampleAtomBuilder<S> {
283    pub fn entry(
284        self,
285        entry: impl Into<TimeToSampleEntry>,
286    ) -> TimeToSampleAtomBuilder<time_to_sample_atom_builder::SetEntries<S>>
287    where
288        S::Entries: time_to_sample_atom_builder::IsUnset,
289    {
290        self.entries(vec![entry.into()])
291    }
292}
293
294impl From<Vec<TimeToSampleEntry>> for TimeToSampleAtom {
295    fn from(entries: Vec<TimeToSampleEntry>) -> Self {
296        TimeToSampleAtom {
297            version: 0,
298            flags: [0u8; 3],
299            entries: entries.into(),
300        }
301    }
302}
303
304impl ParseAtomData for TimeToSampleAtom {
305    fn parse_atom_data(atom_type: FourCC, input: &[u8]) -> Result<Self, ParseError> {
306        crate::atom::util::parser::assert_atom_type!(atom_type, STTS);
307        use crate::atom::util::parser::stream;
308        use winnow::Parser;
309        Ok(parser::parse_stts_data.parse(stream(input))?)
310    }
311}
312
313impl SerializeAtom for TimeToSampleAtom {
314    fn atom_type(&self) -> FourCC {
315        STTS
316    }
317
318    fn into_body_bytes(self) -> Vec<u8> {
319        serializer::serialize_mdhd_atom(self)
320    }
321}
322
323mod serializer {
324    use crate::atom::util::serializer::be_u32;
325
326    use super::TimeToSampleAtom;
327
328    pub fn serialize_mdhd_atom(stts: TimeToSampleAtom) -> Vec<u8> {
329        let mut data = Vec::new();
330
331        data.push(stts.version);
332        data.extend(stts.flags);
333        data.extend(be_u32(
334            u32::try_from(stts.entries.len()).expect("stts entries len must fit in u32"),
335        ));
336
337        for entry in stts.entries.0.into_iter() {
338            data.extend(entry.sample_count.to_be_bytes());
339            data.extend(entry.sample_duration.to_be_bytes());
340        }
341
342        data
343    }
344}
345
346mod parser {
347    use winnow::{
348        binary::{be_u32, length_repeat},
349        combinator::{seq, trace},
350        error::StrContext,
351        ModalResult, Parser,
352    };
353
354    use super::{TimeToSampleAtom, TimeToSampleEntries, TimeToSampleEntry};
355    use crate::atom::util::parser::{flags3, version, Stream};
356
357    pub fn parse_stts_data(input: &mut Stream<'_>) -> ModalResult<TimeToSampleAtom> {
358        trace(
359            "stts",
360            seq!(TimeToSampleAtom {
361                version: version,
362                flags: flags3,
363                entries: length_repeat(be_u32, entry)
364                    .map(TimeToSampleEntries)
365                    .context(StrContext::Label("entries")),
366            })
367            .context(StrContext::Label("stts")),
368        )
369        .parse_next(input)
370    }
371
372    fn entry(input: &mut Stream<'_>) -> ModalResult<TimeToSampleEntry> {
373        trace(
374            "entry",
375            seq!(TimeToSampleEntry {
376                sample_count: be_u32.context(StrContext::Label("sample_count")),
377                sample_duration: be_u32.context(StrContext::Label("sample_duration")),
378            })
379            .context(StrContext::Label("entry")),
380        )
381        .parse_next(input)
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::atom::test_utils::test_atom_roundtrip;
389
390    /// Test round-trip for all available stts test data files
391    #[test]
392    fn test_stts_roundtrip() {
393        test_atom_roundtrip::<TimeToSampleAtom>(STTS);
394    }
395}
396
397#[cfg(feature = "experimental-trim")]
398#[cfg(test)]
399mod trim_tests {
400    use std::ops::Bound;
401
402    use super::*;
403
404    struct TrimDurationTestCase {
405        trim_duration: Vec<(Bound<u64>, Bound<u64>)>,
406        expect_removed_samples: Vec<Range<usize>>,
407        expect_removed_duration: u64,
408        expect_entries: Vec<TimeToSampleEntry>,
409    }
410
411    fn test_trim_duration_stts() -> TimeToSampleAtom {
412        TimeToSampleAtom::builder()
413            .entries(vec![
414                // samples  0..1
415                // duration 0..100
416                TimeToSampleEntry {
417                    sample_count: 1,
418                    sample_duration: 100,
419                },
420                // samples 1..5
421                // duration 100..900
422                TimeToSampleEntry {
423                    sample_count: 4,
424                    sample_duration: 200,
425                },
426                // samples 5..9
427                // duration 900..1300
428                TimeToSampleEntry {
429                    sample_count: 4,
430                    sample_duration: 100,
431                },
432            ])
433            .build()
434    }
435
436    fn test_trim_duration<F>(mut stts: TimeToSampleAtom, test_case: F)
437    where
438        F: FnOnce(&TimeToSampleAtom) -> TrimDurationTestCase,
439    {
440        let starting_duration = stts.entries.iter().fold(0, |sum, entry| {
441            sum + (entry.sample_count as u64 * entry.sample_duration as u64)
442        });
443
444        let test_case = test_case(&stts);
445
446        let (actual_remaining_duration, actual_removed_samples) =
447            stts.trim_duration(&test_case.trim_duration);
448
449        assert_eq!(
450            actual_removed_samples,
451            RangeSet::from_iter(test_case.expect_removed_samples.into_iter()),
452            "removed sample indices don't match what's expected"
453        );
454
455        let calculated_remaining_duration = stts.entries.iter().fold(0, |sum, entry| {
456            sum + (entry.sample_count as u64 * entry.sample_duration as u64)
457        });
458        assert_eq!(
459            actual_remaining_duration, calculated_remaining_duration,
460            "remaining duration doesn't add up correctly"
461        );
462
463        let actual_removed_duration = starting_duration.saturating_sub(actual_remaining_duration);
464        assert_eq!(
465            actual_removed_duration, test_case.expect_removed_duration,
466            "removed duration doesn't match what's expected; started with {starting_duration} and ended up with {actual_remaining_duration}"
467        );
468
469        assert_eq!(
470            stts.entries.0, test_case.expect_entries,
471            "time to sample entries don't match what's expected"
472        )
473    }
474
475    macro_rules! test_trim_duration {
476        ($($name:ident $(($stts:expr))? => $test_case:expr,)*) => {
477            $(
478                #[test]
479                fn $name() {
480                    let stts = test_trim_duration!(@get_stts $($stts)?);
481                    test_trim_duration(stts, $test_case);
482                }
483            )*
484        };
485
486        (@get_stts $stts:expr) => { $stts };
487        (@get_stts) => { test_trim_duration_stts() };
488    }
489
490    test_trim_duration!(
491        trim_unbounded_to_0_excluded_from_start => |stts| TrimDurationTestCase {
492            trim_duration: vec![(Bound::Unbounded, Bound::Excluded(0))],
493            expect_removed_samples: vec![],
494            expect_removed_duration: 0,
495            expect_entries: stts.entries.iter().cloned().collect::<Vec<_>>(),
496        },
497        trim_unbounded_to_0_included_from_start_trim_nothing => |stts| {
498            // 1s is less than 1 sample, so nothing should get trimmed
499            let expected_entries = stts.entries.0.clone();
500            TrimDurationTestCase {
501                trim_duration: vec![(Bound::Unbounded, Bound::Included(0))],
502                expect_removed_samples: vec![],
503                expect_removed_duration: 0,
504                expect_entries: expected_entries,
505            }
506        },
507        trim_unbounded_to_0_included_from_start_trim_sample ({
508            TimeToSampleAtom::builder().entry(
509                TimeToSampleEntry {
510                    sample_count: 100,
511                    sample_duration: 1,
512                },
513            ).build()
514        }) => |stts| {
515            // 1s is 1 sample, so we should trim 1 sample
516            let mut expected_entries = stts.entries.0.clone();
517            expected_entries[0].sample_count -= 1;
518            TrimDurationTestCase {
519                trim_duration: vec![(Bound::Unbounded, Bound::Included(0))],
520                expect_removed_samples: vec![0..1],
521                expect_removed_duration: 1,
522                expect_entries: expected_entries,
523            }
524        },
525        trim_first_entry_unbounded_start => |stts| TrimDurationTestCase {
526            trim_duration: vec![(Bound::Unbounded, Bound::Excluded(100))],
527            expect_removed_samples: vec![0..1],
528            expect_removed_duration: 100,
529            expect_entries: stts.entries[1..].to_vec(),
530        },
531        trim_first_entry_included_start => |stts| TrimDurationTestCase {
532            trim_duration: vec![(Bound::Included(0), Bound::Excluded(100))],
533            expect_removed_samples: vec![0..1],
534            expect_removed_duration: 100,
535            expect_entries: stts.entries[1..].to_vec(),
536        },
537        trim_last_sample_unbounded_end => |stts| {
538            let mut expect_entries = stts.entries.clone().0;
539            expect_entries.last_mut().unwrap().sample_count = 3;
540            TrimDurationTestCase {
541                trim_duration: vec![(Bound::Included(1_200), Bound::Unbounded)],
542                expect_removed_duration: 100,
543                expect_removed_samples: vec![8..9],
544                expect_entries,
545            }
546        },
547        trim_last_three_samples_unbounded_end => |stts| {
548            let mut expect_entries = stts.entries.clone().0;
549            expect_entries.last_mut().unwrap().sample_count = 1;
550            TrimDurationTestCase {
551                trim_duration: vec![(Bound::Included(1_000), Bound::Unbounded)],
552                expect_removed_duration: 300,
553                expect_removed_samples: vec![6..9],
554                expect_entries,
555            }
556        },
557        trim_last_sample_included_end => |stts| {
558            let mut expect_entries = stts.entries.clone().0;
559            expect_entries.last_mut().unwrap().sample_count = 3;
560            TrimDurationTestCase {
561                trim_duration: vec![(Bound::Included(1_200), Bound::Included(1_300 - 1))],
562                expect_removed_duration: 100,
563                expect_removed_samples: vec![8..9],
564                expect_entries,
565            }
566        },
567        trim_middle_entry_excluded_end => |_| TrimDurationTestCase {
568            trim_duration: vec![(Bound::Included(100), Bound::Excluded(900))],
569            expect_removed_duration: 800,
570            expect_removed_samples: vec![1..5],
571            expect_entries: vec![
572                TimeToSampleEntry {
573                    sample_count: 5,
574                    sample_duration: 100,
575                },
576            ],
577        },
578        trim_middle_entry_excluded_start => |_| TrimDurationTestCase {
579            trim_duration: vec![(Bound::Excluded(99), Bound::Excluded(900))],
580            expect_removed_duration: 800,
581            expect_removed_samples: vec![1..5],
582            expect_entries: vec![
583                TimeToSampleEntry {
584                    sample_count: 5,
585                    sample_duration: 100,
586                },
587            ],
588        },
589        trim_middle_entry_excluded_start_included_end => |_| TrimDurationTestCase {
590            trim_duration: vec![(Bound::Excluded(99), Bound::Included(899))],
591            expect_removed_duration: 800,
592            expect_removed_samples: vec![1..5],
593            expect_entries: vec![
594                TimeToSampleEntry {
595                    sample_count: 5,
596                    sample_duration: 100,
597                },
598            ],
599        },
600        trim_middle_samples => |stts| TrimDurationTestCase {
601            // entry 1 samples:
602            //  sample index 1 starts at 100 (not trimmed)
603            //  sample index 2 starts at 300 (trimmed)
604            //  sample index 3 starts at 500 (trimmed)
605            //  sample index 4 starts at 700 (not trimmed)
606            trim_duration: vec![(Bound::Included(300), Bound::Excluded(700))],
607            expect_removed_duration: 400,
608            expect_removed_samples: vec![2..4],
609            expect_entries: vec![
610                stts.entries[0].clone(),
611                TimeToSampleEntry {
612                    sample_count: 2,
613                    sample_duration: 200,
614                },
615                stts.entries[2].clone(),
616            ],
617        },
618        trim_middle_samples_partial => |stts| TrimDurationTestCase {
619            // partially matching samples should be left intact
620            // entry 1 samples:
621            //  sample index 1 starts at 100 (partially matched, not trimmed)
622            //  sample index 2 starts at 300 (trimmed)
623            //  sample index 3 starts at 500 (trimmed)
624            //  sample index 4 starts at 700 (partially matched, not trimmed)
625            trim_duration: vec![(Bound::Included(240), Bound::Excluded(850))],
626            expect_removed_duration: 400,
627            expect_removed_samples: vec![2..4],
628            expect_entries: vec![
629                stts.entries[0].clone(),
630                TimeToSampleEntry {
631                    sample_count: 2,
632                    sample_duration: 200,
633                },
634                stts.entries[2].clone(),
635            ],
636        },
637        trim_everything => |_| TrimDurationTestCase {
638            trim_duration: vec![(Bound::Unbounded, Bound::Unbounded)],
639            expect_removed_duration: 1_300,
640            expect_removed_samples: vec![0..9],
641            expect_entries: Vec::new(),
642        },
643        trim_middle_from_large_entry ({
644            TimeToSampleAtom::builder().entry(
645                // samples  0..10
646                // duration 0..10_000
647                //
648                // sample 0 => 0
649                // sample 1 => 10_000
650                // sample 2 => 20_000 (trimmed)
651                // sample 3 => 30_000 (trimmed)
652                // sample 4 => 40_000 (trimmed)
653                // sample 5 => 50_000
654                // ...
655                TimeToSampleEntry {
656                    sample_count: 10,
657                    sample_duration: 10_000,
658                },
659            ).build()
660        }) => |stts| TrimDurationTestCase {
661            trim_duration: vec![(Bound::Excluded(19_999), Bound::Included(50_000))],
662            expect_removed_duration: 30_000,
663            expect_removed_samples: vec![2..5],
664            expect_entries: stts.entries.iter().cloned().map(|mut entry| {
665                entry.sample_count = 7;
666                entry
667            }).collect::<Vec<_>>(),
668        },
669        trim_start_and_end => |stts| {
670            let mut expect_entries = stts.entries[1..].to_vec();
671            expect_entries.last_mut().unwrap().sample_count = 1;
672            TrimDurationTestCase {
673                trim_duration: vec![
674                    (Bound::Included(0), Bound::Excluded(100)),
675                    (Bound::Included(1_000), Bound::Excluded(1_300)),
676                ],
677                expect_removed_duration: 100 + 300,
678                expect_removed_samples: vec![0..1, 6..9],
679                expect_entries,
680            }
681        },
682        trim_start_and_end_single_entry ({
683            TimeToSampleAtom::builder().entry(
684                TimeToSampleEntry {
685                    sample_count: 100,
686                    sample_duration: 1,
687                },
688            ).build()
689        }) => |stts| {
690            let mut expect_entry = stts.entries[0].clone();
691            expect_entry.sample_count -= 20 + 20;
692            TrimDurationTestCase {
693                trim_duration: vec![
694                    (Bound::Unbounded, Bound::Excluded(20)),
695                    (Bound::Included(80), Bound::Unbounded),
696                ],
697                expect_removed_duration: 20 + 20,
698                expect_removed_samples: vec![0..20, 80..100],
699                expect_entries: vec![expect_entry],
700            }
701        },
702        trim_start_end_single_sample ({
703            TimeToSampleAtom::builder().entry(
704                TimeToSampleEntry {
705                    sample_count: 1,
706                    sample_duration: 100,
707                },
708            ).build()
709        }) => |stts| {
710            let expect_entry = stts.entries[0].clone();
711            TrimDurationTestCase {
712                trim_duration: vec![
713                    (Bound::Included(50), Bound::Included(100)),
714                ],
715                expect_removed_duration: 0,
716                expect_removed_samples: vec![],
717                expect_entries: vec![expect_entry],
718            }
719        },
720    );
721}