// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use num_traits::{CheckedAdd, CheckedSub, PrimInt, Zero}; use std::ops::{Add, Neg, Sub}; use super::*; /// A zero-overhead wrapper around integer types for the sake of always /// requiring checked arithmetic #[repr(transparent)] #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct CheckedInteger(pub T); impl From for CheckedInteger { fn from(i: T) -> Self { Self(i) } } // Orphan rules prevent a more general implementation, but this suffices impl From> for i64 { fn from(checked: CheckedInteger) -> i64 { checked.0 } } impl> Add for CheckedInteger where T: CheckedAdd, { type Output = Option; fn add(self, other: U) -> Self::Output { self.0.checked_add(&other.into()).map(Into::into) } } impl> Sub for CheckedInteger where T: CheckedSub, { type Output = Option; fn sub(self, other: U) -> Self::Output { self.0.checked_sub(&other.into()).map(Into::into) } } /// Implement subtraction of checked `u64`s returning i64 // This is necessary for handling Mp4parseTrackInfo::media_time gracefully impl Sub for CheckedInteger { type Output = Option>; fn sub(self, other: Self) -> Self::Output { if self >= other { self.0 .checked_sub(other.0) .and_then(|u| i64::try_from(u).ok()) .map(CheckedInteger) } else { other .0 .checked_sub(self.0) .and_then(|u| i64::try_from(u).ok()) .map(i64::neg) .map(CheckedInteger) } } } #[test] fn u64_subtraction_returning_i64() { // self > other assert_eq!( CheckedInteger(2u64) - CheckedInteger(1u64), Some(CheckedInteger(1i64)) ); // self == other assert_eq!( CheckedInteger(1u64) - CheckedInteger(1u64), Some(CheckedInteger(0i64)) ); // difference too large to store in i64 assert_eq!(CheckedInteger(u64::MAX) - CheckedInteger(1u64), None); // self < other assert_eq!( CheckedInteger(1u64) - CheckedInteger(2u64), Some(CheckedInteger(-1i64)) ); // difference not representable due to overflow assert_eq!(CheckedInteger(1u64) - CheckedInteger(u64::MAX), None); } impl PartialEq for CheckedInteger { fn eq(&self, other: &T) -> bool { self.0 == *other } } /// Provides the following information about a sample in the source file: /// sample data offset (start and end), composition time in microseconds /// (start and end) and whether it is a sync sample #[repr(C)] #[derive(Default, Debug, PartialEq)] pub struct Indice { /// The byte offset in the file where the indexed sample begins. pub start_offset: CheckedInteger, /// The byte offset in the file where the indexed sample ends. This is /// equivalent to `start_offset` + the length in bytes of the indexed /// sample. Typically this will be the `start_offset` of the next sample /// in the file. pub end_offset: CheckedInteger, /// The time in microseconds when the indexed sample should be displayed. /// Analogous to the concept of presentation time stamp (pts). pub start_composition: CheckedInteger, /// The time in microseconds when the indexed sample should stop being /// displayed. Typically this would be the `start_composition` time of the /// next sample if samples were ordered by composition time. pub end_composition: CheckedInteger, /// The time in microseconds that the indexed sample should be decoded at. /// Analogous to the concept of decode time stamp (dts). pub start_decode: CheckedInteger, /// Set if the indexed sample is a sync sample. The meaning of sync is /// somewhat codec specific, but essentially amounts to if the sample is a /// key frame. pub sync: bool, } /// Create a vector of `Indice`s with the information about track samples. /// It uses `stsc`, `stco`, `stsz` and `stts` boxes to construct a list of /// every sample in the file and provides offsets which can be used to read /// raw sample data from the file. #[allow(clippy::reversed_empty_ranges)] pub fn create_sample_table( track: &Track, track_offset_time: CheckedInteger, ) -> Option> { let timescale = match track.timescale { Some(ref t) => TrackTimeScale::(t.0 as i64, t.1), _ => return None, }; let (stsc, stco, stsz, stts) = match (&track.stsc, &track.stco, &track.stsz, &track.stts) { (&Some(ref a), &Some(ref b), &Some(ref c), &Some(ref d)) => (a, b, c, d), _ => return None, }; // According to spec, no sync table means every sample is sync sample. let has_sync_table = matches!(track.stss, Some(_)); let mut sample_size_iter = stsz.sample_sizes.iter(); // Get 'stsc' iterator for (chunk_id, chunk_sample_count) and calculate the sample // offset address. // With large numbers of samples, the cost of many allocations dominates, // so it's worth iterating twice to allocate sample_table just once. let total_sample_count = sample_to_chunk_iter(&stsc.samples, &stco.offsets) .map(|(_, sample_counts)| sample_counts.to_usize()) .try_fold(0usize, usize::checked_add)?; let mut sample_table = TryVec::with_capacity(total_sample_count).ok()?; for i in sample_to_chunk_iter(&stsc.samples, &stco.offsets) { let chunk_id = i.0 as usize; let sample_counts = i.1; let mut cur_position = match stco.offsets.get(chunk_id) { Some(&i) => i.into(), _ => return None, }; for _ in 0..sample_counts { let start_offset = cur_position; let end_offset = match (stsz.sample_size, sample_size_iter.next()) { (_, Some(t)) => (start_offset + *t)?, (t, _) if t > 0 => (start_offset + t)?, _ => 0.into(), }; if end_offset == 0 { return None; } cur_position = end_offset; sample_table .push(Indice { start_offset, end_offset, sync: !has_sync_table, ..Default::default() }) .ok()?; } } // Mark the sync sample in sample_table according to 'stss'. if let Some(ref v) = track.stss { for iter in &v.samples { match iter .checked_sub(&1) .and_then(|idx| sample_table.get_mut(idx as usize)) { Some(elem) => elem.sync = true, _ => return None, } } } let ctts_iter = track.ctts.as_ref().map(|v| v.samples.as_slice().iter()); let mut ctts_offset_iter = TimeOffsetIterator { cur_sample_range: (0..0), cur_offset: 0, ctts_iter, track_id: track.id, }; let mut stts_iter = TimeToSampleIterator { cur_sample_count: (0..0), cur_sample_delta: 0, stts_iter: stts.samples.as_slice().iter(), track_id: track.id, }; // sum_delta is the sum of stts_iter delta. // According to spec: // decode time => DT(n) = DT(n-1) + STTS(n) // composition time => CT(n) = DT(n) + CTTS(n) // Note: // composition time needs to add the track offset time from 'elst' table. let mut sum_delta = TrackScaledTime::(0, track.id); for sample in sample_table.as_mut_slice() { let decode_time = sum_delta; sum_delta = (sum_delta + stts_iter.next_delta())?; // ctts_offset is the current sample offset time. let ctts_offset = ctts_offset_iter.next_offset_time(); let start_composition = track_time_to_us((decode_time + ctts_offset)?, timescale)?.0; let end_composition = track_time_to_us((sum_delta + ctts_offset)?, timescale)?.0; let start_decode = track_time_to_us(decode_time, timescale)?.0; sample.start_composition = (track_offset_time + start_composition)?; sample.end_composition = (track_offset_time + end_composition)?; sample.start_decode = start_decode.into(); } // Correct composition end time due to 'ctts' causes composition time re-ordering. // // Composition end time is not in specification. However, gecko needs it, so we need to // calculate to correct the composition end time. if !sample_table.is_empty() { // Create an index table refers to sample_table and sorted by start_composisiton time. let mut sort_table = TryVec::with_capacity(sample_table.len()).ok()?; for i in 0..sample_table.len() { sort_table.push(i).ok()?; } sort_table.sort_by_key(|i| match sample_table.get(*i) { Some(v) => v.start_composition, _ => 0.into(), }); for indices in sort_table.windows(2) { if let [current_index, peek_index] = *indices { let next_start_composition_time = sample_table[peek_index].start_composition; let sample = &mut sample_table[current_index]; sample.end_composition = next_start_composition_time; } } } Some(sample_table) } // Convert a 'ctts' compact table to full table by iterator, // (sample_with_the_same_offset_count, offset) => (offset), (offset), (offset) ... // // For example: // (2, 10), (4, 9) into (10, 10, 9, 9, 9, 9) by calling next_offset_time(). struct TimeOffsetIterator<'a> { cur_sample_range: std::ops::Range, cur_offset: i64, ctts_iter: Option>, track_id: usize, } impl<'a> Iterator for TimeOffsetIterator<'a> { type Item = i64; #[allow(clippy::reversed_empty_ranges)] fn next(&mut self) -> Option { let has_sample = self.cur_sample_range.next().or_else(|| { // At end of current TimeOffset, find the next TimeOffset. let iter = match self.ctts_iter { Some(ref mut v) => v, _ => return None, }; let offset_version; self.cur_sample_range = match iter.next() { Some(v) => { offset_version = v.time_offset; 0..v.sample_count } _ => { offset_version = TimeOffsetVersion::Version0(0); 0..0 } }; self.cur_offset = match offset_version { TimeOffsetVersion::Version0(i) => i64::from(i), TimeOffsetVersion::Version1(i) => i64::from(i), }; self.cur_sample_range.next() }); has_sample.and(Some(self.cur_offset)) } } impl<'a> TimeOffsetIterator<'a> { fn next_offset_time(&mut self) -> TrackScaledTime { match self.next() { Some(v) => TrackScaledTime::(v as i64, self.track_id), _ => TrackScaledTime::(0, self.track_id), } } } // Convert 'stts' compact table to full table by iterator, // (sample_count_with_the_same_time, time) => (time, time, time) ... repeats // sample_count_with_the_same_time. // // For example: // (2, 3000), (1, 2999) to (3000, 3000, 2999). struct TimeToSampleIterator<'a> { cur_sample_count: std::ops::Range, cur_sample_delta: u32, stts_iter: std::slice::Iter<'a, Sample>, track_id: usize, } impl<'a> Iterator for TimeToSampleIterator<'a> { type Item = u32; #[allow(clippy::reversed_empty_ranges)] fn next(&mut self) -> Option { let has_sample = self.cur_sample_count.next().or_else(|| { self.cur_sample_count = match self.stts_iter.next() { Some(v) => { self.cur_sample_delta = v.sample_delta; 0..v.sample_count } _ => 0..0, }; self.cur_sample_count.next() }); has_sample.and(Some(self.cur_sample_delta)) } } impl<'a> TimeToSampleIterator<'a> { fn next_delta(&mut self) -> TrackScaledTime { match self.next() { Some(v) => TrackScaledTime::(i64::from(v), self.track_id), _ => TrackScaledTime::(0, self.track_id), } } } // Convert 'stco' compact table to full table by iterator. // (start_chunk_num, sample_number) => (start_chunk_num, sample_number), // (start_chunk_num + 1, sample_number), // (start_chunk_num + 2, sample_number), // ... // (next start_chunk_num, next sample_number), // ... // // For example: // (1, 5), (5, 10), (9, 2) => (1, 5), (2, 5), (3, 5), (4, 5), (5, 10), (6, 10), // (7, 10), (8, 10), (9, 2) fn sample_to_chunk_iter<'a>( stsc_samples: &'a TryVec, stco_offsets: &'a TryVec, ) -> SampleToChunkIterator<'a> { SampleToChunkIterator { chunks: (0..0), sample_count: 0, stsc_peek_iter: stsc_samples.as_slice().iter().peekable(), remain_chunk_count: stco_offsets .len() .try_into() .expect("stco.entry_count is u32"), } } struct SampleToChunkIterator<'a> { chunks: std::ops::Range, sample_count: u32, stsc_peek_iter: std::iter::Peekable>, remain_chunk_count: u32, // total chunk number from 'stco'. } impl<'a> Iterator for SampleToChunkIterator<'a> { type Item = (u32, u32); fn next(&mut self) -> Option<(u32, u32)> { let has_chunk = self.chunks.next().or_else(|| { self.chunks = self.locate(); self.remain_chunk_count .checked_sub( self.chunks .len() .try_into() .expect("len() of a Range must fit in u32"), ) .and_then(|res| { self.remain_chunk_count = res; self.chunks.next() }) }); has_chunk.map(|id| (id, self.sample_count)) } } impl<'a> SampleToChunkIterator<'a> { #[allow(clippy::reversed_empty_ranges)] fn locate(&mut self) -> std::ops::Range { loop { return match (self.stsc_peek_iter.next(), self.stsc_peek_iter.peek()) { (Some(next), Some(peek)) if next.first_chunk == peek.first_chunk => { // Invalid entry, skip it and will continue searching at // next loop iteration. continue; } (Some(next), Some(peek)) if next.first_chunk > 0 && peek.first_chunk > 0 => { self.sample_count = next.samples_per_chunk; (next.first_chunk - 1)..(peek.first_chunk - 1) } (Some(next), None) if next.first_chunk > 0 => { self.sample_count = next.samples_per_chunk; // Total chunk number in 'stsc' could be different to 'stco', // there could be more chunks at the last 'stsc' record. match next.first_chunk.checked_add(self.remain_chunk_count) { Some(r) => (next.first_chunk - 1)..r - 1, _ => 0..0, } } _ => 0..0, }; } } } /// Calculate numerator * scale / denominator, if possible. /// /// Applying the associativity of integer arithmetic, we divide first /// and add the remainder after multiplying each term separately /// to preserve precision while leaving more headroom. That is, /// (n * s) / d is split into floor(n / d) * s + (n % d) * s / d. /// /// Return None on overflow or if the denominator is zero. fn rational_scale(numerator: T, denominator: T, scale2: S) -> Option where T: PrimInt + Zero, S: PrimInt, { if denominator.is_zero() { return None; } let integer = numerator / denominator; let remainder = numerator % denominator; num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) { Some(integer) => remainder .checked_mul(&s) .and_then(|remainder| (remainder / denominator).checked_add(&integer)), None => None, }) } #[derive(Debug, PartialEq)] pub struct Microseconds(pub T); /// Convert `time` in media's global (mvhd) timescale to microseconds, /// using provided `MediaTimeScale` pub fn media_time_to_us(time: MediaScaledTime, scale: MediaTimeScale) -> Option> { let microseconds_per_second = 1_000_000; rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds) } /// Convert `time` in track's local (mdhd) timescale to microseconds, /// using provided `TrackTimeScale` pub fn track_time_to_us( time: TrackScaledTime, scale: TrackTimeScale, ) -> Option> where T: PrimInt + Zero, { assert_eq!(time.1, scale.1); let microseconds_per_second = 1_000_000; rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds) } #[test] fn rational_scale_overflow() { assert_eq!(rational_scale::(17, 3, 1000), Some(5666)); let large = 0x4000_0000_0000_0000; assert_eq!(rational_scale::(large, 2, 2), Some(large)); assert_eq!(rational_scale::(large, 4, 4), Some(large)); assert_eq!(rational_scale::(large, 2, 8), None); assert_eq!(rational_scale::(large, 8, 4), Some(large / 2)); assert_eq!(rational_scale::(large + 1, 4, 4), Some(large + 1)); assert_eq!(rational_scale::(large, 40, 1000), None); } #[test] fn media_time_overflow() { let scale = MediaTimeScale(90000); let duration = MediaScaledTime(9_007_199_254_710_000); assert_eq!( media_time_to_us(duration, scale), Some(Microseconds(100_079_991_719_000_000u64)) ); } #[test] fn track_time_overflow() { let scale = TrackTimeScale(44100u64, 0); let duration = TrackScaledTime(4_413_527_634_807_900u64, 0); assert_eq!( track_time_to_us(duration, scale), Some(Microseconds(100_079_991_719_000_000u64)) ); }