Skip to main content

oximedia_container/
sample_table.rs

1#![allow(dead_code)]
2//! ISO Base Media File Format sample table abstractions.
3//!
4//! Models the `stbl` box family (`stts`, `stsc`, `stsz`, `stco`, `stss`)
5//! providing sample-to-chunk, sample-size, chunk-offset, and sync-sample
6//! look-ups required for random access into MP4/MOV containers.
7
8use std::collections::BTreeSet;
9
10/// Time-to-sample entry (`stts` box row).
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct TimeToSampleEntry {
13    /// Number of consecutive samples with the same delta.
14    pub sample_count: u32,
15    /// Duration of each sample in timescale units.
16    pub sample_delta: u32,
17}
18
19/// Sample-to-chunk entry (`stsc` box row).
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct SampleToChunkEntry {
22    /// First chunk number for this run (1-based).
23    pub first_chunk: u32,
24    /// Number of samples per chunk in this run.
25    pub samples_per_chunk: u32,
26    /// Sample description index (1-based).
27    pub sample_description_index: u32,
28}
29
30/// How sample sizes are stored.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum SampleSizeMode {
33    /// All samples share the same size.
34    Uniform(u32),
35    /// Per-sample size table.
36    Variable(Vec<u32>),
37}
38
39/// Complete sample table for one track.
40#[derive(Debug, Clone)]
41pub struct SampleTable {
42    /// Time-to-sample entries from `stts`.
43    pub time_to_sample: Vec<TimeToSampleEntry>,
44    /// Sample-to-chunk entries from `stsc`.
45    pub sample_to_chunk: Vec<SampleToChunkEntry>,
46    /// Per-sample sizes from `stsz`.
47    pub sample_sizes: SampleSizeMode,
48    /// Chunk offsets from `stco` / `co64`.
49    pub chunk_offsets: Vec<u64>,
50    /// Set of sync (key-frame) sample numbers (1-based) from `stss`.
51    /// If `None`, every sample is a sync sample.
52    pub sync_samples: Option<BTreeSet<u32>>,
53    /// Track timescale (ticks per second).
54    pub timescale: u32,
55}
56
57impl SampleTable {
58    /// Creates an empty sample table with the given timescale.
59    #[must_use]
60    pub fn new(timescale: u32) -> Self {
61        Self {
62            time_to_sample: Vec::new(),
63            sample_to_chunk: Vec::new(),
64            sample_sizes: SampleSizeMode::Uniform(0),
65            chunk_offsets: Vec::new(),
66            sync_samples: None,
67            timescale,
68        }
69    }
70
71    /// Returns the total number of samples described by the `stts` entries.
72    #[must_use]
73    pub fn sample_count_from_stts(&self) -> u64 {
74        self.time_to_sample
75            .iter()
76            .map(|e| u64::from(e.sample_count))
77            .sum()
78    }
79
80    /// Returns the total number of samples from the size table.
81    #[must_use]
82    pub fn sample_count(&self) -> u64 {
83        match &self.sample_sizes {
84            SampleSizeMode::Uniform(_) => self.sample_count_from_stts(),
85            SampleSizeMode::Variable(sizes) => sizes.len() as u64,
86        }
87    }
88
89    /// Returns the total media duration in timescale units.
90    #[must_use]
91    pub fn total_duration_ticks(&self) -> u64 {
92        self.time_to_sample
93            .iter()
94            .map(|e| u64::from(e.sample_count) * u64::from(e.sample_delta))
95            .sum()
96    }
97
98    /// Returns the total media duration in seconds.
99    #[must_use]
100    #[allow(clippy::cast_precision_loss)]
101    pub fn duration_seconds(&self) -> f64 {
102        if self.timescale == 0 {
103            return 0.0;
104        }
105        self.total_duration_ticks() as f64 / f64::from(self.timescale)
106    }
107
108    /// Returns the size of the given sample (1-based index).
109    #[must_use]
110    pub fn sample_size(&self, sample_number: u32) -> Option<u32> {
111        if sample_number == 0 {
112            return None;
113        }
114        match &self.sample_sizes {
115            SampleSizeMode::Uniform(sz) => {
116                if u64::from(sample_number) <= self.sample_count() {
117                    Some(*sz)
118                } else {
119                    None
120                }
121            }
122            SampleSizeMode::Variable(sizes) => sizes.get((sample_number - 1) as usize).copied(),
123        }
124    }
125
126    /// Returns `true` if the given sample (1-based) is a sync sample.
127    #[must_use]
128    pub fn is_sync_sample(&self, sample_number: u32) -> bool {
129        match &self.sync_samples {
130            None => true, // all samples are sync
131            Some(set) => set.contains(&sample_number),
132        }
133    }
134
135    /// Finds the nearest sync sample at or before `sample_number` (1-based).
136    #[must_use]
137    pub fn nearest_sync_before(&self, sample_number: u32) -> Option<u32> {
138        match &self.sync_samples {
139            None => Some(sample_number),
140            Some(set) => set.range(..=sample_number).next_back().copied(),
141        }
142    }
143
144    /// Finds the nearest sync sample at or after `sample_number` (1-based).
145    #[must_use]
146    pub fn nearest_sync_after(&self, sample_number: u32) -> Option<u32> {
147        match &self.sync_samples {
148            None => Some(sample_number),
149            Some(set) => set.range(sample_number..).next().copied(),
150        }
151    }
152
153    /// Converts a decode timestamp (in timescale ticks) to a sample number
154    /// (1-based). Returns `None` if the timestamp exceeds the track duration.
155    #[must_use]
156    pub fn sample_at_time(&self, ticks: u64) -> Option<u32> {
157        let mut remaining = ticks;
158        let mut sample_num: u64 = 1;
159
160        for entry in &self.time_to_sample {
161            let run_duration = u64::from(entry.sample_count) * u64::from(entry.sample_delta);
162            if remaining < run_duration {
163                if entry.sample_delta == 0 {
164                    #[allow(clippy::cast_possible_truncation)]
165                    return Some(sample_num as u32);
166                }
167                let offset = remaining / u64::from(entry.sample_delta);
168                #[allow(clippy::cast_possible_truncation)]
169                return Some((sample_num + offset) as u32);
170            }
171            remaining -= run_duration;
172            sample_num += u64::from(entry.sample_count);
173        }
174        None
175    }
176
177    /// Returns the decode timestamp (in ticks) of a given sample (1-based).
178    #[must_use]
179    pub fn sample_time(&self, sample_number: u32) -> Option<u64> {
180        if sample_number == 0 {
181            return None;
182        }
183        let target = u64::from(sample_number);
184        let mut current_sample: u64 = 1;
185        let mut current_time: u64 = 0;
186
187        for entry in &self.time_to_sample {
188            let count = u64::from(entry.sample_count);
189            if target < current_sample + count {
190                let offset = target - current_sample;
191                return Some(current_time + offset * u64::from(entry.sample_delta));
192            }
193            current_time += count * u64::from(entry.sample_delta);
194            current_sample += count;
195        }
196        None
197    }
198
199    /// Total data size of all samples.
200    #[must_use]
201    pub fn total_data_size(&self) -> u64 {
202        match &self.sample_sizes {
203            SampleSizeMode::Uniform(sz) => u64::from(*sz) * self.sample_count(),
204            SampleSizeMode::Variable(sizes) => sizes.iter().map(|&s| u64::from(s)).sum(),
205        }
206    }
207
208    /// Returns the average sample size in bytes.
209    #[must_use]
210    #[allow(clippy::cast_precision_loss)]
211    pub fn average_sample_size(&self) -> f64 {
212        let count = self.sample_count();
213        if count == 0 {
214            return 0.0;
215        }
216        self.total_data_size() as f64 / count as f64
217    }
218
219    /// Number of chunk offsets.
220    #[must_use]
221    pub fn chunk_count(&self) -> usize {
222        self.chunk_offsets.len()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    fn sample_table_30fps() -> SampleTable {
231        let mut st = SampleTable::new(30_000);
232        // 300 samples at delta 1000 → 10 seconds at 30000 timescale
233        st.time_to_sample.push(TimeToSampleEntry {
234            sample_count: 300,
235            sample_delta: 1000,
236        });
237        st.sample_sizes = SampleSizeMode::Uniform(4096);
238        st.chunk_offsets = vec![0, 40960, 81920]; // 3 chunks
239        st.sample_to_chunk.push(SampleToChunkEntry {
240            first_chunk: 1,
241            samples_per_chunk: 100,
242            sample_description_index: 1,
243        });
244        st
245    }
246
247    #[test]
248    fn test_sample_count_from_stts() {
249        let st = sample_table_30fps();
250        assert_eq!(st.sample_count_from_stts(), 300);
251    }
252
253    #[test]
254    fn test_sample_count_uniform() {
255        let st = sample_table_30fps();
256        assert_eq!(st.sample_count(), 300);
257    }
258
259    #[test]
260    fn test_sample_count_variable() {
261        let mut st = SampleTable::new(1000);
262        st.sample_sizes = SampleSizeMode::Variable(vec![100, 200, 300]);
263        assert_eq!(st.sample_count(), 3);
264    }
265
266    #[test]
267    fn test_total_duration_ticks() {
268        let st = sample_table_30fps();
269        assert_eq!(st.total_duration_ticks(), 300_000);
270    }
271
272    #[test]
273    fn test_duration_seconds() {
274        let st = sample_table_30fps();
275        assert!((st.duration_seconds() - 10.0).abs() < 0.001);
276    }
277
278    #[test]
279    fn test_duration_seconds_zero_timescale() {
280        let st = SampleTable::new(0);
281        assert!((st.duration_seconds()).abs() < f64::EPSILON);
282    }
283
284    #[test]
285    fn test_sample_size_uniform() {
286        let st = sample_table_30fps();
287        assert_eq!(st.sample_size(1), Some(4096));
288        assert_eq!(st.sample_size(300), Some(4096));
289        assert_eq!(st.sample_size(301), None);
290        assert_eq!(st.sample_size(0), None);
291    }
292
293    #[test]
294    fn test_sample_size_variable() {
295        let mut st = SampleTable::new(1000);
296        st.sample_sizes = SampleSizeMode::Variable(vec![100, 200, 300]);
297        assert_eq!(st.sample_size(1), Some(100));
298        assert_eq!(st.sample_size(3), Some(300));
299        assert_eq!(st.sample_size(4), None);
300    }
301
302    #[test]
303    fn test_is_sync_sample_all_sync() {
304        let st = sample_table_30fps();
305        assert!(st.is_sync_sample(1));
306        assert!(st.is_sync_sample(150));
307    }
308
309    #[test]
310    fn test_is_sync_sample_selective() {
311        let mut st = sample_table_30fps();
312        let mut syncs = BTreeSet::new();
313        syncs.insert(1);
314        syncs.insert(30);
315        syncs.insert(60);
316        st.sync_samples = Some(syncs);
317
318        assert!(st.is_sync_sample(1));
319        assert!(st.is_sync_sample(30));
320        assert!(!st.is_sync_sample(2));
321    }
322
323    #[test]
324    fn test_nearest_sync_before() {
325        let mut st = sample_table_30fps();
326        let mut syncs = BTreeSet::new();
327        syncs.insert(1);
328        syncs.insert(30);
329        syncs.insert(60);
330        st.sync_samples = Some(syncs);
331
332        assert_eq!(st.nearest_sync_before(29), Some(1));
333        assert_eq!(st.nearest_sync_before(30), Some(30));
334        assert_eq!(st.nearest_sync_before(45), Some(30));
335    }
336
337    #[test]
338    fn test_nearest_sync_after() {
339        let mut st = sample_table_30fps();
340        let mut syncs = BTreeSet::new();
341        syncs.insert(1);
342        syncs.insert(30);
343        syncs.insert(60);
344        st.sync_samples = Some(syncs);
345
346        assert_eq!(st.nearest_sync_after(2), Some(30));
347        assert_eq!(st.nearest_sync_after(30), Some(30));
348        assert_eq!(st.nearest_sync_after(61), None);
349    }
350
351    #[test]
352    fn test_sample_at_time() {
353        let st = sample_table_30fps();
354        // sample 1 is at tick 0, sample 2 at tick 1000, ...
355        assert_eq!(st.sample_at_time(0), Some(1));
356        assert_eq!(st.sample_at_time(999), Some(1));
357        assert_eq!(st.sample_at_time(1000), Some(2));
358        assert_eq!(st.sample_at_time(299_999), Some(300));
359        assert_eq!(st.sample_at_time(300_000), None); // past end
360    }
361
362    #[test]
363    fn test_sample_time() {
364        let st = sample_table_30fps();
365        assert_eq!(st.sample_time(1), Some(0));
366        assert_eq!(st.sample_time(2), Some(1000));
367        assert_eq!(st.sample_time(300), Some(299_000));
368        assert_eq!(st.sample_time(0), None);
369        assert_eq!(st.sample_time(301), None);
370    }
371
372    #[test]
373    fn test_total_data_size() {
374        let st = sample_table_30fps();
375        assert_eq!(st.total_data_size(), 300 * 4096);
376    }
377
378    #[test]
379    fn test_average_sample_size() {
380        let mut st = SampleTable::new(1000);
381        st.sample_sizes = SampleSizeMode::Variable(vec![100, 200, 300]);
382        st.time_to_sample.push(TimeToSampleEntry {
383            sample_count: 3,
384            sample_delta: 1000,
385        });
386        assert!((st.average_sample_size() - 200.0).abs() < f64::EPSILON);
387    }
388
389    #[test]
390    fn test_chunk_count() {
391        let st = sample_table_30fps();
392        assert_eq!(st.chunk_count(), 3);
393    }
394}