summaryrefslogtreecommitdiffstats
path: root/third_party/rust/mp4parse/src/unstable.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/rust/mp4parse/src/unstable.rs
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--third_party/rust/mp4parse/src/unstable.rs546
1 files changed, 546 insertions, 0 deletions
diff --git a/third_party/rust/mp4parse/src/unstable.rs b/third_party/rust/mp4parse/src/unstable.rs
new file mode 100644
index 0000000000..eeb16f8ed8
--- /dev/null
+++ b/third_party/rust/mp4parse/src/unstable.rs
@@ -0,0 +1,546 @@
+// 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<T>(pub T);
+
+impl<T> From<T> for CheckedInteger<T> {
+ fn from(i: T) -> Self {
+ Self(i)
+ }
+}
+
+// Orphan rules prevent a more general implementation, but this suffices
+impl From<CheckedInteger<i64>> for i64 {
+ fn from(checked: CheckedInteger<i64>) -> i64 {
+ checked.0
+ }
+}
+
+impl<T, U: Into<T>> Add<U> for CheckedInteger<T>
+where
+ T: CheckedAdd,
+{
+ type Output = Option<Self>;
+
+ fn add(self, other: U) -> Self::Output {
+ self.0.checked_add(&other.into()).map(Into::into)
+ }
+}
+
+impl<T, U: Into<T>> Sub<U> for CheckedInteger<T>
+where
+ T: CheckedSub,
+{
+ type Output = Option<Self>;
+
+ 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<u64> {
+ type Output = Option<CheckedInteger<i64>>;
+
+ 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<T: std::cmp::PartialEq> PartialEq<T> for CheckedInteger<T> {
+ 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<u64>,
+ /// 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<u64>,
+ /// The time in microseconds when the indexed sample should be displayed.
+ /// Analogous to the concept of presentation time stamp (pts).
+ pub start_composition: CheckedInteger<i64>,
+ /// 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<i64>,
+ /// 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<i64>,
+ /// 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<i64>,
+) -> Option<TryVec<Indice>> {
+ let timescale = match track.timescale {
+ Some(ref t) => TrackTimeScale::<i64>(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::<i64>(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<u32>,
+ cur_offset: i64,
+ ctts_iter: Option<std::slice::Iter<'a, TimeOffset>>,
+ track_id: usize,
+}
+
+impl<'a> Iterator for TimeOffsetIterator<'a> {
+ type Item = i64;
+
+ #[allow(clippy::reversed_empty_ranges)]
+ fn next(&mut self) -> Option<i64> {
+ 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<i64> {
+ match self.next() {
+ Some(v) => TrackScaledTime::<i64>(v as i64, self.track_id),
+ _ => TrackScaledTime::<i64>(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<u32>,
+ 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<u32> {
+ 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<i64> {
+ match self.next() {
+ Some(v) => TrackScaledTime::<i64>(i64::from(v), self.track_id),
+ _ => TrackScaledTime::<i64>(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<SampleToChunk>,
+ stco_offsets: &'a TryVec<u64>,
+) -> 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<u32>,
+ sample_count: u32,
+ stsc_peek_iter: std::iter::Peekable<std::slice::Iter<'a, SampleToChunk>>,
+ 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<u32> 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<u32> {
+ 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<T, S>(numerator: T, denominator: T, scale2: S) -> Option<T>
+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<T>(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<Microseconds<u64>> {
+ 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<T>`
+pub fn track_time_to_us<T>(
+ time: TrackScaledTime<T>,
+ scale: TrackTimeScale<T>,
+) -> Option<Microseconds<T>>
+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::<u64, u64>(17, 3, 1000), Some(5666));
+ let large = 0x4000_0000_0000_0000;
+ assert_eq!(rational_scale::<u64, u64>(large, 2, 2), Some(large));
+ assert_eq!(rational_scale::<u64, u64>(large, 4, 4), Some(large));
+ assert_eq!(rational_scale::<u64, u64>(large, 2, 8), None);
+ assert_eq!(rational_scale::<u64, u64>(large, 8, 4), Some(large / 2));
+ assert_eq!(rational_scale::<u64, u64>(large + 1, 4, 4), Some(large + 1));
+ assert_eq!(rational_scale::<u64, u64>(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))
+ );
+}