Coverage Report

Created: 2026-07-16 07:23

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mp4parse-rust/mp4parse/src/unstable.rs
Line
Count
Source
1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
use num_traits::{CheckedAdd, CheckedSub, PrimInt, Zero};
5
use std::ops::{Add, Neg, Sub};
6
7
use super::*;
8
9
/// A zero-overhead wrapper around integer types for the sake of always
10
/// requiring checked arithmetic
11
#[repr(transparent)]
12
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
13
pub struct CheckedInteger<T>(pub T);
14
15
impl<T> From<T> for CheckedInteger<T> {
16
189M
    fn from(i: T) -> Self {
17
189M
        Self(i)
18
189M
    }
<mp4parse::unstable::CheckedInteger<i64> as core::convert::From<i64>>::from
Line
Count
Source
16
2.17k
    fn from(i: T) -> Self {
17
2.17k
        Self(i)
18
2.17k
    }
<mp4parse::unstable::CheckedInteger<u64> as core::convert::From<u64>>::from
Line
Count
Source
16
189M
    fn from(i: T) -> Self {
17
189M
        Self(i)
18
189M
    }
19
}
20
21
// Orphan rules prevent a more general implementation, but this suffices
22
impl From<CheckedInteger<i64>> for i64 {
23
397
    fn from(checked: CheckedInteger<i64>) -> i64 {
24
397
        checked.0
25
397
    }
26
}
27
28
impl<T, U: Into<T>> Add<U> for CheckedInteger<T>
29
where
30
    T: CheckedAdd,
31
{
32
    type Output = Option<Self>;
33
34
189M
    fn add(self, other: U) -> Self::Output {
35
189M
        self.0.checked_add(&other.into()).map(Into::into)
36
189M
    }
37
}
38
39
impl<T, U: Into<T>> Sub<U> for CheckedInteger<T>
40
where
41
    T: CheckedSub,
42
{
43
    type Output = Option<Self>;
44
45
397
    fn sub(self, other: U) -> Self::Output {
46
397
        self.0.checked_sub(&other.into()).map(Into::into)
47
397
    }
<mp4parse::unstable::CheckedInteger<i64> as core::ops::arith::Sub>::sub
Line
Count
Source
45
397
    fn sub(self, other: U) -> Self::Output {
46
397
        self.0.checked_sub(&other.into()).map(Into::into)
47
397
    }
Unexecuted instantiation: <mp4parse::unstable::CheckedInteger<_> as core::ops::arith::Sub<_>>::sub
48
}
49
50
/// Implement subtraction of checked `u64`s returning i64
51
// This is necessary for handling Mp4parseTrackInfo::media_time gracefully
52
impl Sub for CheckedInteger<u64> {
53
    type Output = Option<CheckedInteger<i64>>;
54
55
32.0k
    fn sub(self, other: Self) -> Self::Output {
56
32.0k
        if self >= other {
57
31.9k
            self.0
58
31.9k
                .checked_sub(other.0)
59
31.9k
                .and_then(|u| i64::try_from(u).ok())
60
31.9k
                .map(CheckedInteger)
61
        } else {
62
81
            other
63
81
                .0
64
81
                .checked_sub(self.0)
65
81
                .and_then(|u| i64::try_from(u).ok())
66
81
                .map(i64::neg)
67
81
                .map(CheckedInteger)
68
        }
69
32.0k
    }
70
}
71
72
#[test]
73
fn u64_subtraction_returning_i64() {
74
    // self > other
75
    assert_eq!(
76
        CheckedInteger(2u64) - CheckedInteger(1u64),
77
        Some(CheckedInteger(1i64))
78
    );
79
80
    // self == other
81
    assert_eq!(
82
        CheckedInteger(1u64) - CheckedInteger(1u64),
83
        Some(CheckedInteger(0i64))
84
    );
85
86
    // difference too large to store in i64
87
    assert_eq!(CheckedInteger(u64::MAX) - CheckedInteger(1u64), None);
88
89
    // self < other
90
    assert_eq!(
91
        CheckedInteger(1u64) - CheckedInteger(2u64),
92
        Some(CheckedInteger(-1i64))
93
    );
94
95
    // difference not representable due to overflow
96
    assert_eq!(CheckedInteger(1u64) - CheckedInteger(u64::MAX), None);
97
}
98
99
impl<T: std::cmp::PartialEq> PartialEq<T> for CheckedInteger<T> {
100
189M
    fn eq(&self, other: &T) -> bool {
101
189M
        self.0 == *other
102
189M
    }
103
}
104
105
/// Provides the following information about a sample in the source file:
106
/// sample data offset (start and end), composition time in microseconds
107
/// (start and end) and whether it is a sync sample
108
#[repr(C)]
109
#[derive(Default, Debug, PartialEq, Eq)]
110
pub struct Indice {
111
    /// The byte offset in the file where the indexed sample begins.
112
    pub start_offset: CheckedInteger<u64>,
113
    /// The byte offset in the file where the indexed sample ends. This is
114
    /// equivalent to `start_offset` + the length in bytes of the indexed
115
    /// sample. Typically this will be the `start_offset` of the next sample
116
    /// in the file.
117
    pub end_offset: CheckedInteger<u64>,
118
    /// The time in ticks when the indexed sample should be displayed.
119
    /// Analogous to the concept of presentation time stamp (pts).
120
    pub start_composition: CheckedInteger<i64>,
121
    /// The time in ticks when the indexed sample should stop being
122
    /// displayed. Typically this would be the `start_composition` time of the
123
    /// next sample if samples were ordered by composition time.
124
    pub end_composition: CheckedInteger<i64>,
125
    /// The time in ticks that the indexed sample should be decoded at.
126
    /// Analogous to the concept of decode time stamp (dts).
127
    pub start_decode: CheckedInteger<i64>,
128
    /// Set if the indexed sample is a sync sample. The meaning of sync is
129
    /// somewhat codec specific, but essentially amounts to if the sample is a
130
    /// key frame.
131
    pub sync: bool,
132
}
133
134
/// Create a vector of `Indice`s with the information about track samples.
135
/// It uses `stsc`, `stco`, `stsz` and `stts` boxes to construct a list of
136
/// every sample in the file and provides offsets which can be used to read
137
/// raw sample data from the file.
138
#[allow(clippy::reversed_empty_ranges)]
139
1.38k
pub fn create_sample_table(
140
1.38k
    track: &Track,
141
1.38k
    track_offset_time: CheckedInteger<i64>,
142
1.38k
) -> Option<TryVec<Indice>> {
143
1.38k
    let (stsc, stco, stsz, stts) = match (&track.stsc, &track.stco, &track.stsz, &track.stts) {
144
580
        (Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
145
802
        _ => return None,
146
    };
147
148
    // According to spec, no sync table means every sample is sync sample.
149
580
    let has_sync_table = track.stss.is_some();
150
151
580
    let mut sample_size_iter = stsz.sample_sizes.iter();
152
153
    // Get 'stsc' iterator for (chunk_id, chunk_sample_count) and calculate the sample
154
    // offset address.
155
156
    // With large numbers of samples, the cost of many allocations dominates,
157
    // so it's worth iterating twice to allocate sample_table just once.
158
580
    let total_sample_count = sample_to_chunk_iter(&stsc.samples, &stco.offsets)
159
87.3k
        .map(|(_, sample_counts)| sample_counts.to_usize())
160
580
        .try_fold(0usize, usize::checked_add)?;
161
580
    let mut sample_table = TryVec::with_capacity(total_sample_count).ok()?;
162
163
81.5k
    for i in sample_to_chunk_iter(&stsc.samples, &stco.offsets) {
164
81.5k
        let chunk_id = i.0 as usize;
165
81.5k
        let sample_counts = i.1;
166
81.5k
        let mut cur_position = match stco.offsets.get(chunk_id) {
167
81.5k
            Some(&i) => i.into(),
168
12
            _ => return None,
169
        };
170
81.5k
        for _ in 0..sample_counts {
171
189M
            let start_offset = cur_position;
172
189M
            let end_offset = match (stsz.sample_size, sample_size_iter.next()) {
173
20.0k
                (_, Some(t)) => (start_offset + *t)?,
174
189M
                (t, _) if t > 0 => (start_offset + t)?,
175
1
                _ => 0.into(),
176
            };
177
189M
            if end_offset == 0 {
178
4
                return None;
179
189M
            }
180
189M
            cur_position = end_offset;
181
182
189M
            sample_table
183
189M
                .push(Indice {
184
189M
                    start_offset,
185
189M
                    end_offset,
186
189M
                    sync: !has_sync_table,
187
189M
                    ..Default::default()
188
189M
                })
189
189M
                .ok()?;
190
        }
191
    }
192
193
    // Mark the sync sample in sample_table according to 'stss'.
194
537
    if let Some(ref v) = track.stss {
195
3.01k
        for iter in &v.samples {
196
2.93k
            match iter
197
2.93k
                .checked_sub(&1)
198
2.93k
                .and_then(|idx| sample_table.get_mut(idx as usize))
199
            {
200
2.92k
                Some(elem) => elem.sync = true,
201
7
                _ => return None,
202
            }
203
        }
204
449
    }
205
206
530
    let ctts_iter = track.ctts.as_ref().map(|v| v.samples.as_slice().iter());
207
208
530
    let mut ctts_offset_iter = TimeOffsetIterator {
209
530
        cur_sample_range: (0..0),
210
530
        cur_offset: 0,
211
530
        ctts_iter,
212
530
        track_id: track.id,
213
530
    };
214
215
530
    let mut stts_iter = TimeToSampleIterator {
216
530
        cur_sample_count: (0..0),
217
530
        cur_sample_delta: 0,
218
530
        stts_iter: stts.samples.as_slice().iter(),
219
530
        track_id: track.id,
220
530
    };
221
222
    // sum_delta is the sum of stts_iter delta.
223
    // According to spec:
224
    //      decode time => DT(n) = DT(n-1) + STTS(n)
225
    //      composition time => CT(n) = DT(n) + CTTS(n)
226
    // Note:
227
    //      composition time needs to add the track offset time from 'elst' table.
228
530
    let mut sum_delta = TrackScaledTime::<i64>(0, track.id);
229
189M
    for sample in sample_table.as_mut_slice() {
230
189M
        let decode_time = sum_delta;
231
189M
        sum_delta = (sum_delta + stts_iter.next_delta())?;
232
233
        // ctts_offset is the current sample offset time.
234
189M
        let ctts_offset = ctts_offset_iter.next_offset_time();
235
236
189M
        let start_composition = decode_time + ctts_offset;
237
238
189M
        let end_composition = sum_delta + ctts_offset;
239
240
189M
        let start_decode = decode_time;
241
242
189M
        let start_composition_val: i64 = start_composition?.0;
243
189M
        let end_composition_val: i64 = end_composition?.0;
244
245
189M
        let track_offset: i64 = track_offset_time.0;
246
247
189M
        sample.start_composition = CheckedInteger(track_offset.checked_add(start_composition_val)?);
248
189M
        sample.end_composition = CheckedInteger(track_offset.checked_add(end_composition_val)?);
249
189M
        sample.start_decode = CheckedInteger(start_decode.0);
250
    }
251
252
    // Correct composition end time due to 'ctts' causes composition time re-ordering.
253
    //
254
    // Composition end time is not in specification. However, gecko needs it, so we need to
255
    // calculate to correct the composition end time.
256
530
    if !sample_table.is_empty() {
257
        // Create an index table refers to sample_table and sorted by start_composisiton time.
258
115
        let mut sort_table = TryVec::with_capacity(sample_table.len()).ok()?;
259
260
189M
        for i in 0..sample_table.len() {
261
189M
            sort_table.push(i).ok()?;
262
        }
263
264
1.03G
        sort_table.sort_by_key(|i| match sample_table.get(*i) {
265
1.03G
            Some(v) => v.start_composition,
266
0
            _ => 0.into(),
267
1.03G
        });
268
269
189M
        for indices in sort_table.windows(2) {
270
189M
            if let [current_index, peek_index] = *indices {
271
189M
                let next_start_composition_time = sample_table[peek_index].start_composition;
272
189M
                let sample = &mut sample_table[current_index];
273
189M
                sample.end_composition = next_start_composition_time;
274
189M
            }
275
        }
276
415
    }
277
278
530
    Some(sample_table)
279
1.38k
}
280
281
// Convert a 'ctts' compact table to full table by iterator,
282
// (sample_with_the_same_offset_count, offset) => (offset), (offset), (offset) ...
283
//
284
// For example:
285
// (2, 10), (4, 9) into (10, 10, 9, 9, 9, 9) by calling next_offset_time().
286
struct TimeOffsetIterator<'a> {
287
    cur_sample_range: std::ops::Range<u32>,
288
    cur_offset: i64,
289
    ctts_iter: Option<std::slice::Iter<'a, TimeOffset>>,
290
    track_id: usize,
291
}
292
293
impl Iterator for TimeOffsetIterator<'_> {
294
    type Item = i64;
295
296
    #[allow(clippy::reversed_empty_ranges)]
297
189M
    fn next(&mut self) -> Option<i64> {
298
189M
        let has_sample = self.cur_sample_range.next().or_else(|| {
299
            // At end of current TimeOffset, find the next TimeOffset.
300
13.1M
            let iter = match self.ctts_iter {
301
3.21M
                Some(ref mut v) => v,
302
9.98M
                _ => return None,
303
            };
304
            let offset_version;
305
3.21M
            self.cur_sample_range = match iter.next() {
306
52.9k
                Some(v) => {
307
52.9k
                    offset_version = v.time_offset;
308
52.9k
                    0..v.sample_count
309
                }
310
                _ => {
311
3.15M
                    offset_version = TimeOffsetVersion::Version0(0);
312
3.15M
                    0..0
313
                }
314
            };
315
316
3.21M
            self.cur_offset = match offset_version {
317
3.15M
                TimeOffsetVersion::Version0(i) => i64::from(i),
318
52.9k
                TimeOffsetVersion::Version1(i) => i64::from(i),
319
            };
320
321
3.21M
            self.cur_sample_range.next()
322
13.1M
        });
323
324
189M
        has_sample.and(Some(self.cur_offset))
325
189M
    }
326
}
327
328
impl TimeOffsetIterator<'_> {
329
189M
    fn next_offset_time(&mut self) -> TrackScaledTime<i64> {
330
189M
        match self.next() {
331
176M
            Some(v) => TrackScaledTime::<i64>(v, self.track_id),
332
13.1M
            _ => TrackScaledTime::<i64>(0, self.track_id),
333
        }
334
189M
    }
335
}
336
337
// Convert 'stts' compact table to full table by iterator,
338
// (sample_count_with_the_same_time, time) => (time, time, time) ... repeats
339
// sample_count_with_the_same_time.
340
//
341
// For example:
342
// (2, 3000), (1, 2999) to (3000, 3000, 2999).
343
struct TimeToSampleIterator<'a> {
344
    cur_sample_count: std::ops::Range<u32>,
345
    cur_sample_delta: u32,
346
    stts_iter: std::slice::Iter<'a, Sample>,
347
    track_id: usize,
348
}
349
350
impl Iterator for TimeToSampleIterator<'_> {
351
    type Item = u32;
352
353
    #[allow(clippy::reversed_empty_ranges)]
354
189M
    fn next(&mut self) -> Option<u32> {
355
189M
        let has_sample = self.cur_sample_count.next().or_else(|| {
356
49.6M
            self.cur_sample_count = match self.stts_iter.next() {
357
1.70k
                Some(v) => {
358
1.70k
                    self.cur_sample_delta = v.sample_delta;
359
1.70k
                    0..v.sample_count
360
                }
361
49.6M
                _ => 0..0,
362
            };
363
364
49.6M
            self.cur_sample_count.next()
365
49.6M
        });
366
367
189M
        has_sample.and(Some(self.cur_sample_delta))
368
189M
    }
369
}
370
371
impl TimeToSampleIterator<'_> {
372
189M
    fn next_delta(&mut self) -> TrackScaledTime<i64> {
373
189M
        match self.next() {
374
139M
            Some(v) => TrackScaledTime::<i64>(i64::from(v), self.track_id),
375
49.6M
            _ => TrackScaledTime::<i64>(0, self.track_id),
376
        }
377
189M
    }
378
}
379
380
// Convert 'stco' compact table to full table by iterator.
381
// (start_chunk_num, sample_number) => (start_chunk_num, sample_number),
382
//                                     (start_chunk_num + 1, sample_number),
383
//                                     (start_chunk_num + 2, sample_number),
384
//                                     ...
385
//                                     (next start_chunk_num, next sample_number),
386
//                                     ...
387
//
388
// For example:
389
// (1, 5), (5, 10), (9, 2) => (1, 5), (2, 5), (3, 5), (4, 5), (5, 10), (6, 10),
390
// (7, 10), (8, 10), (9, 2)
391
1.16k
fn sample_to_chunk_iter<'a>(
392
1.16k
    stsc_samples: &'a TryVec<SampleToChunk>,
393
1.16k
    stco_offsets: &'a TryVec<u64>,
394
1.16k
) -> SampleToChunkIterator<'a> {
395
1.16k
    SampleToChunkIterator {
396
1.16k
        chunks: (0..0),
397
1.16k
        sample_count: 0,
398
1.16k
        stsc_peek_iter: stsc_samples.as_slice().iter().peekable(),
399
1.16k
        remain_chunk_count: stco_offsets
400
1.16k
            .len()
401
1.16k
            .try_into()
402
1.16k
            .expect("stco.entry_count is u32"),
403
1.16k
    }
404
1.16k
}
405
406
struct SampleToChunkIterator<'a> {
407
    chunks: std::ops::Range<u32>,
408
    sample_count: u32,
409
    stsc_peek_iter: std::iter::Peekable<std::slice::Iter<'a, SampleToChunk>>,
410
    remain_chunk_count: u32, // total chunk number from 'stco'.
411
}
412
413
impl Iterator for SampleToChunkIterator<'_> {
414
    type Item = (u32, u32);
415
416
169k
    fn next(&mut self) -> Option<(u32, u32)> {
417
169k
        let has_chunk = self.chunks.next().or_else(|| {
418
8.48k
            self.chunks = self.locate();
419
8.48k
            self.remain_chunk_count
420
8.48k
                .checked_sub(
421
8.48k
                    self.chunks
422
8.48k
                        .len()
423
8.48k
                        .try_into()
424
8.48k
                        .expect("len() of a Range<u32> must fit in u32"),
425
                )
426
8.48k
                .and_then(|res| {
427
8.34k
                    self.remain_chunk_count = res;
428
8.34k
                    self.chunks.next()
429
8.34k
                })
430
8.48k
        });
431
432
169k
        has_chunk.map(|id| (id, self.sample_count))
433
169k
    }
434
}
435
436
impl SampleToChunkIterator<'_> {
437
    #[allow(clippy::reversed_empty_ranges)]
438
8.48k
    fn locate(&mut self) -> std::ops::Range<u32> {
439
        loop {
440
9.61k
            return match (self.stsc_peek_iter.next(), self.stsc_peek_iter.peek()) {
441
8.51k
                (Some(next), Some(peek)) if next.first_chunk == peek.first_chunk => {
442
                    // Invalid entry, skip it and will continue searching at
443
                    // next loop iteration.
444
1.12k
                    continue;
445
                }
446
7.39k
                (Some(next), Some(peek)) if next.first_chunk > 0 && peek.first_chunk > 0 => {
447
7.38k
                    self.sample_count = next.samples_per_chunk;
448
7.38k
                    (next.first_chunk - 1)..(peek.first_chunk - 1)
449
                }
450
148
                (Some(next), None) if next.first_chunk > 0 => {
451
148
                    self.sample_count = next.samples_per_chunk;
452
                    // Total chunk number in 'stsc' could be different to 'stco',
453
                    // there could be more chunks at the last 'stsc' record.
454
148
                    match next.first_chunk.checked_add(self.remain_chunk_count) {
455
148
                        Some(r) => (next.first_chunk - 1)..r - 1,
456
0
                        _ => 0..0,
457
                    }
458
                }
459
954
                _ => 0..0,
460
            };
461
        }
462
8.48k
    }
463
}
464
465
/// Calculate numerator * scale / denominator, if possible.
466
///
467
/// Applying the associativity of integer arithmetic, we divide first
468
/// and add the remainder after multiplying each term separately
469
/// to preserve precision while leaving more headroom. That is,
470
/// (n * s) / d is split into floor(n / d) * s + (n % d) * s / d.
471
///
472
/// Return None on overflow or if the denominator is zero.
473
2.53k
pub fn rational_scale<T, S>(numerator: T, denominator: T, scale2: S) -> Option<T>
474
2.53k
where
475
2.53k
    T: PrimInt + Zero,
476
2.53k
    S: PrimInt,
477
{
478
2.53k
    if denominator.is_zero() {
479
0
        return None;
480
2.53k
    }
481
482
2.53k
    let integer = numerator / denominator;
483
2.53k
    let remainder = numerator % denominator;
484
2.53k
    num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) {
485
2.53k
        Some(integer) => remainder
486
2.53k
            .checked_mul(&s)
487
2.53k
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
mp4parse::unstable::rational_scale::<u64, u64>::{closure#0}::{closure#0}
Line
Count
Source
487
2.53k
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
Unexecuted instantiation: mp4parse::unstable::rational_scale::<u64, i32>::{closure#0}::{closure#0}
488
0
        None => None,
489
2.53k
    })
mp4parse::unstable::rational_scale::<u64, u64>::{closure#0}
Line
Count
Source
484
2.53k
    num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) {
485
2.53k
        Some(integer) => remainder
486
2.53k
            .checked_mul(&s)
487
2.53k
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
488
0
        None => None,
489
2.53k
    })
Unexecuted instantiation: mp4parse::unstable::rational_scale::<u64, i32>::{closure#0}
490
2.53k
}
mp4parse::unstable::rational_scale::<u64, u64>
Line
Count
Source
473
2.53k
pub fn rational_scale<T, S>(numerator: T, denominator: T, scale2: S) -> Option<T>
474
2.53k
where
475
2.53k
    T: PrimInt + Zero,
476
2.53k
    S: PrimInt,
477
{
478
2.53k
    if denominator.is_zero() {
479
0
        return None;
480
2.53k
    }
481
482
2.53k
    let integer = numerator / denominator;
483
2.53k
    let remainder = numerator % denominator;
484
2.53k
    num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) {
485
        Some(integer) => remainder
486
            .checked_mul(&s)
487
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
488
        None => None,
489
    })
490
2.53k
}
Unexecuted instantiation: mp4parse::unstable::rational_scale::<u64, i32>
491
492
#[derive(Debug, PartialEq, Eq)]
493
pub struct Microseconds<T>(pub T);
494
495
/// Convert `time` in media's global (mvhd) timescale to microseconds,
496
/// using provided `MediaTimeScale`
497
0
pub fn media_time_to_us(time: MediaScaledTime, scale: MediaTimeScale) -> Option<Microseconds<u64>> {
498
0
    let microseconds_per_second = 1_000_000;
499
0
    rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds)
500
0
}
501
502
/// Convert `time` in track's local (mdhd) timescale to microseconds,
503
/// using provided `TrackTimeScale<T>`
504
0
pub fn track_time_to_us<T>(
505
0
    time: TrackScaledTime<T>,
506
0
    scale: TrackTimeScale<T>,
507
0
) -> Option<Microseconds<T>>
508
0
where
509
0
    T: PrimInt + Zero,
510
{
511
0
    assert_eq!(time.1, scale.1);
512
0
    let microseconds_per_second = 1_000_000;
513
0
    rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds)
514
0
}
515
516
#[test]
517
fn rational_scale_overflow() {
518
    assert_eq!(rational_scale::<u64, u64>(17, 3, 1000), Some(5666));
519
    let large = 0x4000_0000_0000_0000;
520
    assert_eq!(rational_scale::<u64, u64>(large, 2, 2), Some(large));
521
    assert_eq!(rational_scale::<u64, u64>(large, 4, 4), Some(large));
522
    assert_eq!(rational_scale::<u64, u64>(large, 2, 8), None);
523
    assert_eq!(rational_scale::<u64, u64>(large, 8, 4), Some(large / 2));
524
    assert_eq!(rational_scale::<u64, u64>(large + 1, 4, 4), Some(large + 1));
525
    assert_eq!(rational_scale::<u64, u64>(large, 40, 1000), None);
526
}
527
528
#[test]
529
fn media_time_overflow() {
530
    let scale = MediaTimeScale(90000);
531
    let duration = MediaScaledTime(9_007_199_254_710_000);
532
    assert_eq!(
533
        media_time_to_us(duration, scale),
534
        Some(Microseconds(100_079_991_719_000_000u64))
535
    );
536
}
537
538
#[test]
539
fn track_time_overflow() {
540
    let scale = TrackTimeScale(44100u64, 0);
541
    let duration = TrackScaledTime(4_413_527_634_807_900u64, 0);
542
    assert_eq!(
543
        track_time_to_us(duration, scale),
544
        Some(Microseconds(100_079_991_719_000_000u64))
545
    );
546
}