Skip to main content

mp4_edit/atom/leaf/
stsc.rs

1use bon::{bon, Builder};
2use derive_more::{Deref, DerefMut};
3
4use std::fmt;
5
6use crate::{
7    atom::{util::DebugList, FourCC},
8    parser::ParseAtomData,
9    writer::SerializeAtom,
10    ParseError,
11};
12
13#[cfg(feature = "experimental-trim")]
14use {
15    crate::atom::stco_co64::ChunkOffsetOperationUnresolved,
16    rangemap::RangeSet,
17    std::{iter::Peekable, ops::Range, slice},
18};
19
20pub const STSC: FourCC = FourCC::new(b"stsc");
21
22#[derive(Default, Clone, Deref, DerefMut)]
23pub struct SampleToChunkEntries(Vec<SampleToChunkEntry>);
24
25impl SampleToChunkEntries {
26    pub fn inner(&self) -> &[SampleToChunkEntry] {
27        &self.0
28    }
29
30    #[cfg(feature = "experimental-trim")]
31    fn expanded_iter(&self, total_chunks: usize) -> ExpandedSampleToChunkEntryIter<'_> {
32        ExpandedSampleToChunkEntryIter::new(total_chunks, &self.0)
33    }
34}
35
36impl From<Vec<SampleToChunkEntry>> for SampleToChunkEntries {
37    fn from(inner: Vec<SampleToChunkEntry>) -> Self {
38        Self(inner)
39    }
40}
41
42impl fmt::Debug for SampleToChunkEntries {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        fmt::Debug::fmt(&DebugList::new(self.0.iter(), 10), f)
45    }
46}
47
48#[derive(Debug, Clone)]
49#[cfg(feature = "experimental-trim")]
50struct ExpandedSampleToChunkEntry {
51    pub chunk_index: usize,
52    pub sample_indices: Range<usize>,
53    pub samples_per_chunk: u32,
54    pub sample_description_index: u32,
55}
56
57/// Iterates over [`SampleToChunkEntry`]s expanded to a single entry per chunk.
58#[cfg(feature = "experimental-trim")]
59struct ExpandedSampleToChunkEntryIter<'a> {
60    total_chunks: usize,
61    next_sample_index: usize,
62    current_entry: Option<(&'a SampleToChunkEntry, usize, usize)>,
63    iter: Peekable<slice::Iter<'a, SampleToChunkEntry>>,
64}
65
66#[cfg(feature = "experimental-trim")]
67impl<'a> ExpandedSampleToChunkEntryIter<'a> {
68    fn new(total_chunks: usize, entries: &'a [SampleToChunkEntry]) -> Self {
69        let iter = entries.iter().peekable();
70        Self {
71            total_chunks,
72            next_sample_index: 0,
73            current_entry: None,
74            iter,
75        }
76    }
77}
78
79#[cfg(feature = "experimental-trim")]
80impl<'a> Iterator for ExpandedSampleToChunkEntryIter<'a> {
81    type Item = ExpandedSampleToChunkEntry;
82
83    fn next(&mut self) -> Option<Self::Item> {
84        let (entry, chunk_index, chunk_count) = self.current_entry.take().or_else(|| {
85            let entry = self.iter.next()?;
86            let chunk_index = entry.first_chunk as usize - 1;
87            let chunk_count = match self.iter.peek() {
88                Some(next_entry) => (next_entry.first_chunk - entry.first_chunk) as usize,
89                None => self.total_chunks - chunk_index,
90            };
91            Some((entry, chunk_index, chunk_count))
92        })?;
93
94        let first_sample_index = self.next_sample_index;
95        self.next_sample_index += entry.samples_per_chunk as usize;
96
97        if chunk_count > 1 {
98            self.current_entry = Some((entry, chunk_index + 1, chunk_count - 1));
99        }
100
101        let sample_count = entry.samples_per_chunk as usize;
102        let last_sample_index = first_sample_index + sample_count.saturating_sub(1);
103        let sample_indices = first_sample_index..last_sample_index + 1;
104
105        // probably best not to use the builder in a large loop
106        Some(ExpandedSampleToChunkEntry {
107            chunk_index,
108            sample_indices,
109            samples_per_chunk: entry.samples_per_chunk,
110            sample_description_index: entry.sample_description_index,
111        })
112    }
113
114    fn size_hint(&self) -> (usize, Option<usize>) {
115        (self.total_chunks, Some(self.total_chunks))
116    }
117}
118
119/// Sample-to-Chunk entry - maps samples to chunks
120#[derive(Debug, Clone, PartialEq, Eq, Builder)]
121pub struct SampleToChunkEntry {
122    /// First chunk number (1-based) that uses this entry
123    pub first_chunk: u32,
124    /// Number of samples in each chunk
125    pub samples_per_chunk: u32,
126    /// Sample description index (1-based, references stsd atom)
127    pub sample_description_index: u32,
128}
129
130/// Sample-to-Chunk Atom - contains sample-to-chunk mapping table
131#[derive(Default, Debug, Clone)]
132pub struct SampleToChunkAtom {
133    /// Version of the stsc atom format (0)
134    pub version: u8,
135    /// Flags for the stsc atom (usually all zeros)
136    pub flags: [u8; 3],
137    /// List of sample-to-chunk entries
138    pub entries: SampleToChunkEntries,
139}
140
141#[bon]
142impl SampleToChunkAtom {
143    #[builder]
144    pub fn new(
145        #[builder(default = 0)] version: u8,
146        #[builder(default = [0u8; 3])] flags: [u8; 3],
147        #[builder(with = FromIterator::from_iter)] entries: Vec<SampleToChunkEntry>,
148    ) -> Self {
149        Self {
150            version,
151            flags,
152            entries: entries.into(),
153        }
154    }
155
156    /// Removes sample indices from the sample-to-chunk mapping table,
157    /// and returns the indices (starting from zero) of any chunks which are now empty (and should be removed)
158    ///
159    /// TODO: return a list of operations to apply to chunk offsets
160    /// - [x] Remove(RangeSet of chunk indices to remove)
161    /// - [ ] Insert(chunk index, sample index range) (will need to be cross referenced with sample_sizes)
162    /// - [ ] ShiftLeft(chunk index, sample index range) (ditto '')
163    ///
164    /// `sample_indices_to_remove` must contain contiguous sample indices as a single range,
165    /// multiple ranges must not overlap.
166    #[cfg(feature = "experimental-trim")]
167    pub(crate) fn remove_sample_indices(
168        &mut self,
169        sample_indices_to_remove: &RangeSet<usize>,
170        total_chunks: usize,
171    ) -> Vec<ChunkOffsetOperationUnresolved> {
172        let mut chunk_ops = Vec::new();
173
174        let mut num_removed_chunks = 0usize;
175        let mut num_inserted_chunks = 0usize;
176
177        struct Context<'a> {
178            sample_indices_to_remove: &'a RangeSet<usize>,
179
180            chunk_ops: &'a mut Vec<ChunkOffsetOperationUnresolved>,
181
182            num_removed_chunks: &'a mut usize,
183            num_inserted_chunks: &'a mut usize,
184
185            next_entries: &'a mut Vec<SampleToChunkEntry>,
186        }
187
188        impl<'a> Context<'a> {
189            pub fn process_entry(&mut self, entry: ExpandedSampleToChunkEntry) {
190                // get only the sample indices that overlap with this entry
191                if let Some(sample_indices_to_remove) =
192                    entry_samples_to_remove(&entry.sample_indices, self.sample_indices_to_remove)
193                        .first()
194                {
195                    if sample_indices_to_remove.len() >= entry.sample_indices.len() {
196                        // sample indices to remove fully includes this entry
197                        self.remove_chunk_offset(entry.chunk_index);
198                        *self.num_removed_chunks += 1;
199                    } else {
200                        self.process_entry_partial_match(sample_indices_to_remove, entry);
201                    }
202                } else {
203                    // no samples/chunks to remove for this entry
204                    match self.next_entries.last() {
205                        Some(prev_entry)
206                            if prev_entry.samples_per_chunk == entry.samples_per_chunk
207                                && prev_entry.sample_description_index
208                                    == entry.sample_description_index =>
209                        {
210                            // redundand with prev entry
211                        }
212                        _ => {
213                            self.insert_or_update_chunk_entry(SampleToChunkEntry {
214                                first_chunk: (entry.chunk_index + 1) as u32,
215                                samples_per_chunk: entry.samples_per_chunk,
216                                sample_description_index: entry.sample_description_index,
217                            });
218                        }
219                    }
220                }
221            }
222
223            fn process_entry_partial_match(
224                &mut self,
225                sample_indices_to_remove: &Range<usize>,
226                entry: ExpandedSampleToChunkEntry,
227            ) {
228                if sample_indices_to_remove.start == entry.sample_indices.start {
229                    /*
230                     * process trim start
231                     *
232                     * e.g.
233                     * ------------------------------------------------------------------
234                     * | 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 20 |
235                     * | ^-----------------^  ^---------------------------------------^ |
236                     * |     trim range                   chunk 0 (remainder)           |
237                     * | |- offset --------->|  (+= size(trim...))                      |
238                     * ------------------------------------------------------------------
239                     */
240
241                    // chunk offset increases by the size of the removed samples
242                    self.shift_chunk_offset_right(
243                        entry.chunk_index,
244                        sample_indices_to_remove.clone(),
245                    );
246
247                    // chunk sample count decreases by n removed samples
248                    self.insert_or_update_chunk_entry(SampleToChunkEntry {
249                        first_chunk: entry.chunk_index as u32 + 1,
250                        samples_per_chunk: entry.samples_per_chunk
251                            - sample_indices_to_remove.len() as u32,
252                        sample_description_index: entry.sample_description_index,
253                    });
254
255                    // process any additional trim ranges (e.g. middle and/or end)
256                    self.process_entry(ExpandedSampleToChunkEntry {
257                        chunk_index: entry.chunk_index,
258                        sample_indices: sample_indices_to_remove.end..entry.sample_indices.end,
259                        samples_per_chunk: entry.samples_per_chunk
260                            - sample_indices_to_remove.len() as u32,
261                        sample_description_index: entry.sample_description_index,
262                    });
263                } else if sample_indices_to_remove.end == entry.sample_indices.end {
264                    /*
265                     * process trim end
266                     *
267                     * e.g.
268                     * ------------------------------------------------------------------
269                     * | 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 20 |
270                     * | ^------------------------------^ ^---------------------------^ |
271                     * |       chunk 0 (remainder)                 trim range           |
272                     * ------------------------------------------------------------------
273                     */
274
275                    // chunk sample count decreases by n removed samples
276                    self.insert_or_update_chunk_entry(SampleToChunkEntry {
277                        first_chunk: entry.chunk_index as u32 + 1,
278                        samples_per_chunk: entry.samples_per_chunk
279                            - sample_indices_to_remove.len() as u32,
280                        sample_description_index: entry.sample_description_index,
281                    });
282
283                    // since we reached the end of the chunk/entry, and trim ranges are processed in order,
284                    // there are no additional matches to be had
285                } else {
286                    /*
287                     * process trim middle
288                     *
289                     * e.g.
290                     * ------------------------------------------------------------------
291                     * | 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 20 |
292                     * | ^------------^ ^------------------^ ^------------------------^ |
293                     * |    chunk 0    |    trim range      |      new chunk 1          |
294                     * | |- offsetA    +   size(trim...)    = offsetB (chunk 1)         |
295                     * ------------------------------------------------------------------
296                     */
297
298                    // insert a new chunk after the current one,
299                    // whose offset is the existing offset + size of removed samples
300                    self.insert_chunk_offset(
301                        entry.chunk_index + 1,
302                        sample_indices_to_remove.clone(),
303                    );
304
305                    // insert or update entry for the current chunk
306                    self.insert_or_update_chunk_entry(SampleToChunkEntry {
307                        first_chunk: entry.chunk_index as u32 + 1,
308                        samples_per_chunk: (entry.sample_indices.len()
309                            - (sample_indices_to_remove.start..entry.sample_indices.end).len())
310                            as u32,
311                        sample_description_index: entry.sample_description_index,
312                    });
313
314                    // insert or update entry for the new chunk
315                    self.insert_or_update_chunk_entry(SampleToChunkEntry {
316                        first_chunk: entry.chunk_index as u32 + 2,
317                        samples_per_chunk: (entry.sample_indices.len()
318                            - (entry.sample_indices.start..sample_indices_to_remove.end).len())
319                            as u32,
320                        sample_description_index: entry.sample_description_index,
321                    });
322
323                    // increment the counter after we've inserted the entry
324                    *self.num_inserted_chunks += 1;
325
326                    // process any additional trim ranges on the new chunk (e.g. middle and/or end)
327                    self.process_entry(ExpandedSampleToChunkEntry {
328                        chunk_index: entry.chunk_index + 1,
329                        sample_indices: sample_indices_to_remove.end..entry.sample_indices.end,
330                        samples_per_chunk: entry.samples_per_chunk,
331                        sample_description_index: entry.sample_description_index,
332                    });
333                }
334            }
335
336            fn adjusted_chunk_index(&self, chunk_index: usize) -> usize {
337                chunk_index + *self.num_inserted_chunks - *self.num_removed_chunks
338            }
339
340            fn insert_or_update_chunk_entry(&mut self, mut entry: SampleToChunkEntry) {
341                entry.first_chunk = self.adjusted_chunk_index(entry.first_chunk as usize) as u32;
342                match self.next_entries.last_mut() {
343                    Some(prev_entry) if prev_entry.first_chunk == entry.first_chunk => {
344                        *prev_entry = entry
345                    }
346                    _ => {
347                        self.next_entries.push(entry);
348                    }
349                }
350            }
351
352            /// increase chunk offset by the size of a removed sample range
353            fn shift_chunk_offset_right(
354                &mut self,
355                chunk_index: usize,
356                removed_sample_indices: Range<usize>,
357            ) {
358                self.chunk_ops
359                    .push(ChunkOffsetOperationUnresolved::ShiftRight {
360                        chunk_index_unadjusted: chunk_index,
361                        chunk_index: self.adjusted_chunk_index(chunk_index),
362                        sample_indices: removed_sample_indices,
363                    });
364            }
365
366            fn insert_chunk_offset(
367                &mut self,
368                chunk_index: usize,
369                removed_sample_indices: Range<usize>,
370            ) {
371                self.chunk_ops.push(ChunkOffsetOperationUnresolved::Insert {
372                    chunk_index_unadjusted: chunk_index,
373                    chunk_index: self.adjusted_chunk_index(chunk_index),
374                    sample_indices: removed_sample_indices,
375                });
376            }
377
378            fn remove_chunk_offset(&mut self, chunk_index: usize) {
379                let chunk_index = self.adjusted_chunk_index(chunk_index);
380                match self.chunk_ops.last_mut() {
381                    Some(ChunkOffsetOperationUnresolved::Remove(prev_op))
382                        if prev_op.start == chunk_index =>
383                    {
384                        // either merge with the previous remove range
385                        prev_op.end += 1;
386                    }
387                    _ => {
388                        // or insert a new remove range
389                        self.chunk_ops.push(ChunkOffsetOperationUnresolved::Remove(
390                            chunk_index..chunk_index + 1,
391                        ));
392                    }
393                }
394            }
395        }
396
397        let num_sample_ranges_to_remove = sample_indices_to_remove.iter().count();
398        let prev_len = self.entries.len();
399        self.entries = SampleToChunkEntries(self.entries.expanded_iter(total_chunks).fold(
400            // TODO: evaluate the actual worst-case additional entries
401            Vec::with_capacity(prev_len + (num_sample_ranges_to_remove * 4)),
402            |mut next_entries, entry| {
403                let mut ctx = Context {
404                    sample_indices_to_remove,
405
406                    chunk_ops: &mut chunk_ops,
407
408                    num_removed_chunks: &mut num_removed_chunks,
409                    num_inserted_chunks: &mut num_inserted_chunks,
410
411                    next_entries: &mut next_entries,
412                };
413
414                ctx.process_entry(entry);
415
416                next_entries
417            },
418        ));
419        self.entries.shrink_to_fit();
420
421        chunk_ops
422    }
423}
424
425#[cfg(feature = "experimental-trim")]
426fn entry_samples_to_remove(
427    entry_sample_indices: &Range<usize>,
428    sample_indices_to_remove: &RangeSet<usize>,
429) -> RangeSet<usize> {
430    let mut entry_samples_to_remove = RangeSet::new();
431
432    for range in sample_indices_to_remove.overlapping(entry_sample_indices) {
433        let range =
434            range.start.max(entry_sample_indices.start)..range.end.min(entry_sample_indices.end);
435        entry_samples_to_remove.insert(range);
436    }
437
438    entry_samples_to_remove
439}
440
441impl<S: sample_to_chunk_atom_builder::State> SampleToChunkAtomBuilder<S> {
442    pub fn entry(
443        self,
444        entry: impl Into<SampleToChunkEntry>,
445    ) -> SampleToChunkAtomBuilder<sample_to_chunk_atom_builder::SetEntries<S>>
446    where
447        S::Entries: sample_to_chunk_atom_builder::IsUnset,
448    {
449        self.entries(vec![entry.into()])
450    }
451}
452
453impl From<Vec<SampleToChunkEntry>> for SampleToChunkAtom {
454    fn from(entries: Vec<SampleToChunkEntry>) -> Self {
455        SampleToChunkAtom {
456            version: 0,
457            flags: [0u8; 3],
458            entries: entries.into(),
459        }
460    }
461}
462
463impl ParseAtomData for SampleToChunkAtom {
464    fn parse_atom_data(atom_type: FourCC, input: &[u8]) -> Result<Self, ParseError> {
465        crate::atom::util::parser::assert_atom_type!(atom_type, STSC);
466        use crate::atom::util::parser::stream;
467        use winnow::Parser;
468        Ok(parser::parse_stsc_data.parse(stream(input))?)
469    }
470}
471
472impl SerializeAtom for SampleToChunkAtom {
473    fn atom_type(&self) -> FourCC {
474        STSC
475    }
476
477    fn into_body_bytes(self) -> Vec<u8> {
478        serializer::serialize_stsc_atom(self)
479    }
480}
481
482mod serializer {
483    use crate::atom::util::serializer::be_u32;
484
485    use super::SampleToChunkAtom;
486
487    pub fn serialize_stsc_atom(stsc: SampleToChunkAtom) -> Vec<u8> {
488        let mut data = Vec::new();
489
490        data.push(stsc.version);
491        data.extend(stsc.flags);
492
493        data.extend(be_u32(
494            stsc.entries
495                .len()
496                .try_into()
497                .expect("entries len should fit in u32"),
498        ));
499
500        for entry in stsc.entries.iter() {
501            data.extend(entry.first_chunk.to_be_bytes());
502            data.extend(entry.samples_per_chunk.to_be_bytes());
503            data.extend(entry.sample_description_index.to_be_bytes());
504        }
505
506        data
507    }
508}
509
510mod parser {
511    use winnow::{
512        binary::{be_u32, length_repeat},
513        combinator::{seq, trace},
514        error::{StrContext, StrContextValue},
515        ModalResult, Parser,
516    };
517
518    use super::{SampleToChunkAtom, SampleToChunkEntries, SampleToChunkEntry};
519    use crate::atom::util::parser::{flags3, version, Stream};
520
521    pub fn parse_stsc_data(input: &mut Stream<'_>) -> ModalResult<SampleToChunkAtom> {
522        trace(
523            "stsc",
524            seq!(SampleToChunkAtom {
525                version: version,
526                flags: flags3,
527                entries: length_repeat(be_u32, entry)
528                    .map(SampleToChunkEntries)
529                    .context(StrContext::Label("entries")),
530            })
531            .context(StrContext::Label("stsc")),
532        )
533        .parse_next(input)
534    }
535
536    fn entry(input: &mut Stream<'_>) -> ModalResult<SampleToChunkEntry> {
537        trace(
538            "entry",
539            seq!(SampleToChunkEntry {
540                first_chunk: be_u32
541                    .verify(|first_chunk| *first_chunk > 0)
542                    .context(StrContext::Label("first_chunk"))
543                    .context(StrContext::Expected(StrContextValue::Description(
544                        "1-based index"
545                    ))),
546                samples_per_chunk: be_u32
547                    .verify(|samples_per_chunk| *samples_per_chunk > 0)
548                    .context(StrContext::Label("samples_per_chunk"))
549                    .context(StrContext::Expected(StrContextValue::Description(
550                        "sample count > 0"
551                    ))),
552                sample_description_index: be_u32
553                    .context(StrContext::Label("sample_description_index"))
554                    .context(StrContext::Expected(StrContextValue::Description(
555                        "1-based index"
556                    ))),
557            })
558            .context(StrContext::Label("entry")),
559        )
560        .parse_next(input)
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use crate::atom::test_utils::test_atom_roundtrip;
568
569    /// Test round-trip for all available stco/co64 test data files
570    #[test]
571    fn test_stsc_roundtrip() {
572        test_atom_roundtrip::<SampleToChunkAtom>(STSC);
573    }
574}
575
576#[cfg(feature = "experimental-trim")]
577#[cfg(test)]
578mod trim_tests {
579    use super::*;
580
581    struct EntrySamplesToRemoveTestCase {
582        entry_start_sample_index: usize,
583        entry_end_sample_index: usize,
584        sample_indices_to_remove: Vec<Range<usize>>,
585        expected_entry_samples_to_remove: Vec<Range<usize>>,
586    }
587
588    fn test_entry_samples_to_remove(tc: EntrySamplesToRemoveTestCase) {
589        let sample_indices_to_remove = RangeSet::from_iter(tc.sample_indices_to_remove.into_iter());
590        let entry_sample_indices = tc.entry_start_sample_index..tc.entry_end_sample_index + 1;
591        let actual_entry_samples_to_remove =
592            entry_samples_to_remove(&entry_sample_indices, &sample_indices_to_remove);
593        let expected_entry_samples_to_remove =
594            RangeSet::from_iter(tc.expected_entry_samples_to_remove.into_iter());
595        assert_eq!(
596            actual_entry_samples_to_remove,
597            expected_entry_samples_to_remove
598        );
599    }
600
601    mod test_entry_samples_to_remove {
602        use super::*;
603
604        macro_rules! test_entry_samples_to_remove {
605            ($(
606                $name:ident => {
607                    $( $field:ident: $value:expr ),+ $(,)?
608                },
609            )*) => {
610                $(
611                    #[test]
612                    fn $name() {
613                        test_entry_samples_to_remove!(@inner $( $field: $value ),+);
614                    }
615                )*
616            };
617
618            (@inner $( $field:ident: $value:expr ),+) => {
619                let tc = EntrySamplesToRemoveTestCase {
620                    $( $field: $value ),+,
621                };
622
623                test_entry_samples_to_remove(tc);
624            };
625        }
626
627        test_entry_samples_to_remove!(
628            entry_contained_in_single_range => {
629                entry_start_sample_index: 800,
630                entry_end_sample_index: 1200,
631                sample_indices_to_remove: vec![300..2000],
632                expected_entry_samples_to_remove: vec![800..1201],
633            },
634            entry_contained_in_multiple_ranges => {
635                entry_start_sample_index: 800,
636                entry_end_sample_index: 1200,
637                sample_indices_to_remove: vec![300..900, 1000..2000],
638                expected_entry_samples_to_remove: vec![800..900, 1000..1201],
639            },
640            entry_starts_in_single_range => {
641                entry_start_sample_index: 800,
642                entry_end_sample_index: 1200,
643                sample_indices_to_remove: vec![1000..2000],
644                expected_entry_samples_to_remove: vec![1000..1201],
645            },
646            entry_ends_in_single_range => {
647                entry_start_sample_index: 800,
648                entry_end_sample_index: 1200,
649                sample_indices_to_remove: vec![100..1000],
650                expected_entry_samples_to_remove: vec![800..1000],
651            },
652            single_range_contained_in_entry => {
653                entry_start_sample_index: 800,
654                entry_end_sample_index: 1200,
655                sample_indices_to_remove: vec![900..1000],
656                expected_entry_samples_to_remove: vec![900..1000],
657            },
658        );
659    }
660
661    #[derive(Builder)]
662    struct RemoveSampleIndicesTestCase {
663        #[builder(default = 20)]
664        total_chunks: usize,
665        sample_indices_to_remove: Vec<Range<usize>>,
666        expected_removed_chunk_indices: Vec<Range<usize>>,
667        expected_entries: Vec<SampleToChunkEntry>,
668    }
669
670    fn test_remove_sample_indices_default_stsc() -> SampleToChunkAtom {
671        SampleToChunkAtom::builder()
672            .entries(vec![
673                // chunks   0..1  (1)
674                // samples 0..10 (10)
675                SampleToChunkEntry {
676                    first_chunk: 1,
677                    samples_per_chunk: 10,
678                    sample_description_index: 1,
679                },
680                // chunks    1..3  (2)
681                // samples 10..50 (40)
682                SampleToChunkEntry {
683                    first_chunk: 2,
684                    samples_per_chunk: 20,
685                    sample_description_index: 1,
686                },
687                // chunks     3..9  (6)
688                // samples 50..110 (60)
689                SampleToChunkEntry {
690                    first_chunk: 4,
691                    samples_per_chunk: 10,
692                    sample_description_index: 1,
693                },
694                // total chunks = 20
695                // chunks     9..20  (11)
696                // samples 110..220 (100)
697                SampleToChunkEntry {
698                    first_chunk: 10,
699                    samples_per_chunk: 10,
700                    // NOTE: this is different to test this entry won't be merged
701                    sample_description_index: 2,
702                },
703            ])
704            .build()
705    }
706
707    fn test_remove_sample_indices<F>(mut stsc: SampleToChunkAtom, test_case: F)
708    where
709        F: FnOnce(&SampleToChunkAtom) -> RemoveSampleIndicesTestCase,
710    {
711        let test_case = test_case(&stsc);
712        let total_chunks = test_case.total_chunks;
713        let sample_indices_to_remove =
714            RangeSet::from_iter(test_case.sample_indices_to_remove.into_iter());
715        let actual_chunk_offset_ops =
716            stsc.remove_sample_indices(&sample_indices_to_remove, total_chunks);
717
718        // TODO: add assertions for all the ops
719        let actual_removed_chunk_indices = actual_chunk_offset_ops
720            .iter()
721            .filter_map(|op| match op {
722                ChunkOffsetOperationUnresolved::Remove(chunk_offsets) => Some(chunk_offsets),
723                _ => None,
724            })
725            .cloned()
726            // chunk ops take into account previous operations having been applied
727            // adjust ranges to be in terms of input chunk indices (easier to reason about in the test case)
728            .scan(0usize, |n_removed, range| {
729                let range = (range.start + *n_removed)..(range.end + *n_removed);
730                *n_removed += range.len();
731                Some(range)
732            })
733            .collect::<Vec<_>>();
734
735        assert_eq!(
736            actual_removed_chunk_indices, test_case.expected_removed_chunk_indices,
737            "removed chunk indices don't match what's expected",
738        );
739
740        stsc.entries
741            .iter()
742            .zip(test_case.expected_entries.iter())
743            .enumerate()
744            .for_each(|(index, (actual, expected))| {
745                assert_eq!(
746                    actual, expected,
747                    "sample to chunk entries[{index}] doesn't match what's expected\n{:#?}",
748                    stsc.entries,
749                );
750            });
751
752        assert_eq!(
753            stsc.entries.0.len(),
754            test_case.expected_entries.len(),
755            "expected {} sample to chunk entries, got {}: {:#?}",
756            test_case.expected_entries.len(),
757            stsc.entries.len(),
758            stsc.entries,
759        );
760    }
761
762    macro_rules! test_remove_sample_indices {
763        ($($name:ident $($stsc:expr)? => $test_case:expr,)*) => {
764            $(
765                #[test]
766                fn $name() {
767                    test_remove_sample_indices!(@inner $($stsc)? => $test_case);
768                }
769            )*
770        };
771        (@inner => $test_case:expr) => {
772            test_remove_sample_indices!(@inner test_remove_sample_indices_default_stsc() => $test_case);
773        };
774        (@inner $stsc:expr => $test_case:expr) => {
775            test_remove_sample_indices($stsc, $test_case);
776        };
777    }
778
779    // TODO: test inserted and adjusted chunk offsets in addition to removed offsets
780    mod test_remove_sample_indices {
781        use super::*;
782
783        test_remove_sample_indices!(
784            remove_first_entry => |stsc| RemoveSampleIndicesTestCase::builder().
785                sample_indices_to_remove(vec![0..10]).
786                expected_removed_chunk_indices(vec![0..1]).
787                expected_entries(stsc.entries[1..].iter().cloned().map(|mut entry| {
788                    entry.first_chunk -= 1;
789                    entry
790                }).collect::<Vec<_>>()).build(),
791            remove_first_sample_from_first_entry => |stsc| {
792                let mut expected_entries = stsc.entries.0.clone();
793                expected_entries[0].samples_per_chunk -= 1;
794                RemoveSampleIndicesTestCase::builder().
795                    sample_indices_to_remove(vec![0..1]).
796                    expected_removed_chunk_indices(vec![]).
797                    expected_entries(expected_entries).build()
798            },
799            remove_last_sample_from_first_entry => |stsc| {
800                let mut expected_entries = stsc.entries.0.clone();
801                // there's just a single chunk in the first entry
802                expected_entries[0].samples_per_chunk -= 1;
803                RemoveSampleIndicesTestCase::builder().
804                    sample_indices_to_remove(vec![9..10]).
805                    expected_removed_chunk_indices(vec![]).
806                    expected_entries(expected_entries).build()
807            },
808            remove_sample_from_second_entry => |stsc| {
809                let mut expected_entries = stsc.entries.0.clone();
810                let mut inserted_entry = expected_entries[1].clone();
811                expected_entries[1].first_chunk += 1;
812                inserted_entry.samples_per_chunk -= 1;
813                expected_entries.insert(1, inserted_entry);
814                RemoveSampleIndicesTestCase::builder().
815                    sample_indices_to_remove(vec![10..11]).
816                    expected_removed_chunk_indices(vec![]).
817                    expected_entries(expected_entries).build()
818            },
819            remove_first_chunk_from_second_entry => |stsc| {
820                let mut expected_entries = stsc.entries.0.clone();
821                expected_entries.iter_mut().skip(2).for_each(|entry| entry.first_chunk -= 1);
822                RemoveSampleIndicesTestCase::builder().
823                    sample_indices_to_remove(vec![10..30]).
824                    expected_removed_chunk_indices(vec![1..2]).
825                    expected_entries(expected_entries).build()
826            },
827            remove_five_samples_from_last_entry_middle => |stsc| {
828                RemoveSampleIndicesTestCase::builder().
829                    sample_indices_to_remove(vec![151..156]).
830                    expected_removed_chunk_indices(vec![]).
831                    // we're removing 5 samples from chunk index 13
832                    // so the last entry (starting at chunk index 10) should be split into 4
833                    expected_entries(vec![
834                        stsc.entries[0].clone(),
835                        stsc.entries[1].clone(),
836                        stsc.entries[2].clone(),
837                        // 1. unaffected chunks:
838                        // chunks     9..13  (4)
839                        // samples 110..150 (40)
840                        SampleToChunkEntry {
841                            first_chunk: 10,
842                            samples_per_chunk: 10,
843                            sample_description_index: 2,
844                        },
845                        // 2. chunk with samples removed from middle:
846                        // this is the left side of the trim range (1 sample remaining)
847                        // chunks    13..14 (1)
848                        // samples 150..151 (1)
849                        SampleToChunkEntry {
850                            first_chunk: 14,
851                            samples_per_chunk: 1,
852                            sample_description_index: 2,
853                        },
854                        // 3. chunk with samples removed from middle:
855                        // this is the right side of the trim range where we've inserted a new chunk
856                        // chunks    13..14 (1) -> 14..15
857                        // samples 156..160 (4)
858                        SampleToChunkEntry {
859                            first_chunk: 15,
860                            samples_per_chunk: 4,
861                            sample_description_index: 2,
862                        },
863                        // 4. remaining unaffected chunks:
864                        // total chunks = 20 (0 chunks removed)
865                        // chunks     14..19 (5) -> 15..20
866                        // samples 155..205 (50) (5 samples removed)
867                        SampleToChunkEntry {
868                            first_chunk: 16,
869                            samples_per_chunk: 10,
870                            sample_description_index: 2,
871                        },
872                    ]).build()
873            },
874            remove_fifteen_samples_from_last_entry_middle => |stsc| {
875                RemoveSampleIndicesTestCase::builder().
876                    sample_indices_to_remove(vec![150..165]).
877                    expected_removed_chunk_indices(vec![13..14]).
878                    // we're removing 15 samples starting from chunk index 13,
879                    // which will remove chunk index 13, and 5 samples from chunk index 14
880                    // so the last entry (starting at chunk index 10) should be split into 3
881                    expected_entries(vec![
882                        stsc.entries[0].clone(),
883                        stsc.entries[1].clone(),
884                        stsc.entries[2].clone(),
885                        // 1. unaffected chunks:
886                        // chunks     9..13  (4)
887                        // samples 110..150 (40)
888                        SampleToChunkEntry {
889                            first_chunk: 10,
890                            samples_per_chunk: 10,
891                            sample_description_index: 2,
892                        },
893                        // 2. chunk 13..4 removed
894                        // 3. chunk with samples removed:
895                        // chunks    14..15 (1)
896                        // samples 150..155 (5)
897                        SampleToChunkEntry {
898                            first_chunk: 14,
899                            samples_per_chunk: 5,
900                            sample_description_index: 2,
901                        },
902                        // 4. remaining unaffected chunks:
903                        // total chunks = 19 (1 chunk removed)
904                        // chunks    14..18  (4)
905                        // samples 160..210 (40) (15 samples removed)
906                        SampleToChunkEntry {
907                            first_chunk: 15,
908                            samples_per_chunk: 10,
909                            sample_description_index: 2,
910                        },
911                    ]).build()
912            },
913            remove_second_entry_merge_first_and_third => |stsc| RemoveSampleIndicesTestCase::builder().
914                sample_indices_to_remove(vec![10..50]).
915                expected_removed_chunk_indices(vec![1..3]).
916                expected_entries(vec![
917                    stsc.entries.first().cloned().unwrap(),
918                    stsc.entries.last().cloned().map(|mut entry| {
919                        entry.first_chunk -= 2;
920                        entry
921                    }).unwrap(),
922                ]).build(),
923            remove_second_and_third_entry_no_merge => |stsc| RemoveSampleIndicesTestCase::builder().
924                sample_indices_to_remove(vec![10..110]).
925                expected_removed_chunk_indices(vec![1..9]).
926                expected_entries(vec![
927                    stsc.entries.first().cloned().unwrap(),
928                    stsc.entries.last().cloned().map(|mut entry| {
929                        entry.first_chunk -= 8;
930                        entry
931                    }).unwrap(),
932                ]).build(),
933            remove_first_and_last_entry => |stsc| RemoveSampleIndicesTestCase::builder().
934                sample_indices_to_remove(vec![0..10, 110..220]).
935                expected_removed_chunk_indices(vec![0..1, 9..20]).
936                expected_entries(vec![
937                    stsc.entries.get(1).cloned().map(|mut entry| {
938                        entry.first_chunk = 1;
939                        entry
940                    }).unwrap(),
941                    stsc.entries.get(2).cloned().map(|mut entry| {
942                        entry.first_chunk = 3;
943                        entry
944                    }).unwrap(),
945                ]).build(),
946
947            remove_last_chunk_single_entry {
948                SampleToChunkAtom::builder().
949                    entry(SampleToChunkEntry::builder().
950                        first_chunk(1).
951                        samples_per_chunk(2).
952                        sample_description_index(1).
953                        build(),
954                    ).build()
955            } => |stsc| RemoveSampleIndicesTestCase::builder().
956                sample_indices_to_remove(vec![38..40]).
957                expected_removed_chunk_indices(vec![19..20]).
958                expected_entries(vec![
959                    stsc.entries.first().cloned().unwrap(),
960                ]).build(),
961
962            remove_multiple_ranges_from_single_entry {
963                SampleToChunkAtom::builder().
964                    entry(SampleToChunkEntry::builder().
965                        first_chunk(1).
966                        samples_per_chunk(1).
967                        sample_description_index(1).
968                        build(),
969                    ).build()
970            } => |stsc| RemoveSampleIndicesTestCase::builder().
971                total_chunks(100).
972                sample_indices_to_remove(vec![20..41, 60..81]).
973                expected_removed_chunk_indices(vec![20..41, 60..81]).
974                expected_entries(vec![
975                    stsc.entries.first().cloned().unwrap(),
976                ]).build(),
977
978            remove_mid_second_chunk_to_mid_last_chunk => |stsc| {
979                RemoveSampleIndicesTestCase::builder().
980                    sample_indices_to_remove(vec![15..215]).
981                    expected_removed_chunk_indices(vec![2..19]).
982                    expected_entries(vec![
983                        // first entry is unchanged
984                        stsc.entries[0].clone(),
985                        // samples 10..15 remain intact
986                        SampleToChunkEntry {
987                            first_chunk: 2,
988                            samples_per_chunk: 5,
989                            sample_description_index: 1,
990                        },
991                        // samples 215..220 remain intact
992                        SampleToChunkEntry {
993                            first_chunk: 3,
994                            samples_per_chunk: 5,
995                            sample_description_index: 2,
996                        },
997                    ]).build()
998            },
999
1000            remove_mid_fifth_chunk_to_mid_last_chunk => |stsc| {
1001                RemoveSampleIndicesTestCase::builder().
1002                    sample_indices_to_remove(vec![65..215]).
1003                    expected_removed_chunk_indices(vec![5..19]).
1004                    expected_entries(vec![
1005                        // first two entries are unchanged
1006                        stsc.entries[0].clone(),
1007                        stsc.entries[1].clone(),
1008                        // 4th chunk remains unchanged
1009                        SampleToChunkEntry {
1010                            first_chunk: 4,
1011                            samples_per_chunk: 10,
1012                            sample_description_index: 1,
1013                        },
1014                        // samples 60..65 remain intact
1015                        SampleToChunkEntry {
1016                            first_chunk: 5,
1017                            samples_per_chunk: 5,
1018                            sample_description_index: 1,
1019                        },
1020                        // samples 215..220 remain intact
1021                        SampleToChunkEntry {
1022                            first_chunk: 6,
1023                            samples_per_chunk: 5,
1024                            sample_description_index: 2,
1025                        },
1026                    ]).build()
1027            },
1028        );
1029    }
1030}