Coverage Report

Created: 2025-11-11 07:07

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
107M
    fn from(i: T) -> Self {
17
107M
        Self(i)
18
107M
    }
<mp4parse::unstable::CheckedInteger<i64> as core::convert::From<i64>>::from
Line
Count
Source
16
1.55k
    fn from(i: T) -> Self {
17
1.55k
        Self(i)
18
1.55k
    }
<mp4parse::unstable::CheckedInteger<u64> as core::convert::From<u64>>::from
Line
Count
Source
16
107M
    fn from(i: T) -> Self {
17
107M
        Self(i)
18
107M
    }
19
}
20
21
// Orphan rules prevent a more general implementation, but this suffices
22
impl From<CheckedInteger<i64>> for i64 {
23
368
    fn from(checked: CheckedInteger<i64>) -> i64 {
24
368
        checked.0
25
368
    }
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
107M
    fn add(self, other: U) -> Self::Output {
35
107M
        self.0.checked_add(&other.into()).map(Into::into)
36
107M
    }
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
368
    fn sub(self, other: U) -> Self::Output {
46
368
        self.0.checked_sub(&other.into()).map(Into::into)
47
368
    }
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
15.7k
    fn sub(self, other: Self) -> Self::Output {
56
15.7k
        if self >= other {
57
15.6k
            self.0
58
15.6k
                .checked_sub(other.0)
59
15.6k
                .and_then(|u| i64::try_from(u).ok())
60
15.6k
                .map(CheckedInteger)
61
        } else {
62
39
            other
63
39
                .0
64
39
                .checked_sub(self.0)
65
39
                .and_then(|u| i64::try_from(u).ok())
66
39
                .map(i64::neg)
67
39
                .map(CheckedInteger)
68
        }
69
15.7k
    }
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
107M
    fn eq(&self, other: &T) -> bool {
101
107M
        self.0 == *other
102
107M
    }
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
817
pub fn create_sample_table(
140
817
    track: &Track,
141
817
    track_offset_time: CheckedInteger<i64>,
142
817
) -> Option<TryVec<Indice>> {
143
817
    let (stsc, stco, stsz, stts) = match (&track.stsc, &track.stco, &track.stsz, &track.stts) {
144
417
        (Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
145
400
        _ => return None,
146
    };
147
148
    // According to spec, no sync table means every sample is sync sample.
149
417
    let has_sync_table = track.stss.is_some();
150
151
417
    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
417
    let total_sample_count = sample_to_chunk_iter(&stsc.samples, &stco.offsets)
159
93.0k
        .map(|(_, sample_counts)| sample_counts.to_usize())
160
417
        .try_fold(0usize, usize::checked_add)?;
161
417
    let mut sample_table = TryVec::with_capacity(total_sample_count).ok()?;
162
163
79.9k
    for i in sample_to_chunk_iter(&stsc.samples, &stco.offsets) {
164
79.9k
        let chunk_id = i.0 as usize;
165
79.9k
        let sample_counts = i.1;
166
79.9k
        let mut cur_position = match stco.offsets.get(chunk_id) {
167
79.9k
            Some(&i) => i.into(),
168
8
            _ => return None,
169
        };
170
79.9k
        for _ in 0..sample_counts {
171
107M
            let start_offset = cur_position;
172
107M
            let end_offset = match (stsz.sample_size, sample_size_iter.next()) {
173
37.7k
                (_, Some(t)) => (start_offset + *t)?,
174
107M
                (t, _) if t > 0 => (start_offset + t)?,
175
9
                _ => 0.into(),
176
            };
177
107M
            if end_offset == 0 {
178
11
                return None;
179
107M
            }
180
107M
            cur_position = end_offset;
181
182
107M
            sample_table
183
107M
                .push(Indice {
184
107M
                    start_offset,
185
107M
                    end_offset,
186
107M
                    sync: !has_sync_table,
187
107M
                    ..Default::default()
188
107M
                })
189
107M
                .ok()?;
190
        }
191
    }
192
193
    // Mark the sync sample in sample_table according to 'stss'.
194
385
    if let Some(ref v) = track.stss {
195
2.40k
        for iter in &v.samples {
196
2.34k
            match iter
197
2.34k
                .checked_sub(&1)
198
2.34k
                .and_then(|idx| sample_table.get_mut(idx as usize))
199
            {
200
2.32k
                Some(elem) => elem.sync = true,
201
15
                _ => return None,
202
            }
203
        }
204
311
    }
205
206
370
    let ctts_iter = track.ctts.as_ref().map(|v| v.samples.as_slice().iter());
207
208
370
    let mut ctts_offset_iter = TimeOffsetIterator {
209
370
        cur_sample_range: (0..0),
210
370
        cur_offset: 0,
211
370
        ctts_iter,
212
370
        track_id: track.id,
213
370
    };
214
215
370
    let mut stts_iter = TimeToSampleIterator {
216
370
        cur_sample_count: (0..0),
217
370
        cur_sample_delta: 0,
218
370
        stts_iter: stts.samples.as_slice().iter(),
219
370
        track_id: track.id,
220
370
    };
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
370
    let mut sum_delta = TrackScaledTime::<i64>(0, track.id);
229
107M
    for sample in sample_table.as_mut_slice() {
230
107M
        let decode_time = sum_delta;
231
107M
        sum_delta = (sum_delta + stts_iter.next_delta())?;
232
233
        // ctts_offset is the current sample offset time.
234
107M
        let ctts_offset = ctts_offset_iter.next_offset_time();
235
236
107M
        let start_composition = decode_time + ctts_offset;
237
238
107M
        let end_composition = sum_delta + ctts_offset;
239
240
107M
        let start_decode = decode_time;
241
242
107M
        sample.start_composition = CheckedInteger(track_offset_time.0 + start_composition?.0);
243
107M
        sample.end_composition = CheckedInteger(track_offset_time.0 + end_composition?.0);
244
107M
        sample.start_decode = CheckedInteger(start_decode.0);
245
    }
246
247
    // Correct composition end time due to 'ctts' causes composition time re-ordering.
248
    //
249
    // Composition end time is not in specification. However, gecko needs it, so we need to
250
    // calculate to correct the composition end time.
251
370
    if !sample_table.is_empty() {
252
        // Create an index table refers to sample_table and sorted by start_composisiton time.
253
72
        let mut sort_table = TryVec::with_capacity(sample_table.len()).ok()?;
254
255
107M
        for i in 0..sample_table.len() {
256
107M
            sort_table.push(i).ok()?;
257
        }
258
259
706M
        sort_table.sort_by_key(|i| match sample_table.get(*i) {
260
706M
            Some(v) => v.start_composition,
261
0
            _ => 0.into(),
262
706M
        });
263
264
107M
        for indices in sort_table.windows(2) {
265
107M
            if let [current_index, peek_index] = *indices {
266
107M
                let next_start_composition_time = sample_table[peek_index].start_composition;
267
107M
                let sample = &mut sample_table[current_index];
268
107M
                sample.end_composition = next_start_composition_time;
269
107M
            }
270
        }
271
298
    }
272
273
370
    Some(sample_table)
274
817
}
275
276
// Convert a 'ctts' compact table to full table by iterator,
277
// (sample_with_the_same_offset_count, offset) => (offset), (offset), (offset) ...
278
//
279
// For example:
280
// (2, 10), (4, 9) into (10, 10, 9, 9, 9, 9) by calling next_offset_time().
281
struct TimeOffsetIterator<'a> {
282
    cur_sample_range: std::ops::Range<u32>,
283
    cur_offset: i64,
284
    ctts_iter: Option<std::slice::Iter<'a, TimeOffset>>,
285
    track_id: usize,
286
}
287
288
impl Iterator for TimeOffsetIterator<'_> {
289
    type Item = i64;
290
291
    #[allow(clippy::reversed_empty_ranges)]
292
107M
    fn next(&mut self) -> Option<i64> {
293
107M
        let has_sample = self.cur_sample_range.next().or_else(|| {
294
            // At end of current TimeOffset, find the next TimeOffset.
295
95.8k
            let iter = match self.ctts_iter {
296
75.4k
                Some(ref mut v) => v,
297
20.4k
                _ => return None,
298
            };
299
            let offset_version;
300
75.4k
            self.cur_sample_range = match iter.next() {
301
40.2k
                Some(v) => {
302
40.2k
                    offset_version = v.time_offset;
303
40.2k
                    0..v.sample_count
304
                }
305
                _ => {
306
35.1k
                    offset_version = TimeOffsetVersion::Version0(0);
307
35.1k
                    0..0
308
                }
309
            };
310
311
75.4k
            self.cur_offset = match offset_version {
312
35.1k
                TimeOffsetVersion::Version0(i) => i64::from(i),
313
40.2k
                TimeOffsetVersion::Version1(i) => i64::from(i),
314
            };
315
316
75.4k
            self.cur_sample_range.next()
317
95.8k
        });
318
319
107M
        has_sample.and(Some(self.cur_offset))
320
107M
    }
321
}
322
323
impl TimeOffsetIterator<'_> {
324
107M
    fn next_offset_time(&mut self) -> TrackScaledTime<i64> {
325
107M
        match self.next() {
326
107M
            Some(v) => TrackScaledTime::<i64>(v, self.track_id),
327
55.8k
            _ => TrackScaledTime::<i64>(0, self.track_id),
328
        }
329
107M
    }
330
}
331
332
// Convert 'stts' compact table to full table by iterator,
333
// (sample_count_with_the_same_time, time) => (time, time, time) ... repeats
334
// sample_count_with_the_same_time.
335
//
336
// For example:
337
// (2, 3000), (1, 2999) to (3000, 3000, 2999).
338
struct TimeToSampleIterator<'a> {
339
    cur_sample_count: std::ops::Range<u32>,
340
    cur_sample_delta: u32,
341
    stts_iter: std::slice::Iter<'a, Sample>,
342
    track_id: usize,
343
}
344
345
impl Iterator for TimeToSampleIterator<'_> {
346
    type Item = u32;
347
348
    #[allow(clippy::reversed_empty_ranges)]
349
107M
    fn next(&mut self) -> Option<u32> {
350
107M
        let has_sample = self.cur_sample_count.next().or_else(|| {
351
46.2M
            self.cur_sample_count = match self.stts_iter.next() {
352
575
                Some(v) => {
353
575
                    self.cur_sample_delta = v.sample_delta;
354
575
                    0..v.sample_count
355
                }
356
46.2M
                _ => 0..0,
357
            };
358
359
46.2M
            self.cur_sample_count.next()
360
46.2M
        });
361
362
107M
        has_sample.and(Some(self.cur_sample_delta))
363
107M
    }
364
}
365
366
impl TimeToSampleIterator<'_> {
367
107M
    fn next_delta(&mut self) -> TrackScaledTime<i64> {
368
107M
        match self.next() {
369
60.7M
            Some(v) => TrackScaledTime::<i64>(i64::from(v), self.track_id),
370
46.2M
            _ => TrackScaledTime::<i64>(0, self.track_id),
371
        }
372
107M
    }
373
}
374
375
// Convert 'stco' compact table to full table by iterator.
376
// (start_chunk_num, sample_number) => (start_chunk_num, sample_number),
377
//                                     (start_chunk_num + 1, sample_number),
378
//                                     (start_chunk_num + 2, sample_number),
379
//                                     ...
380
//                                     (next start_chunk_num, next sample_number),
381
//                                     ...
382
//
383
// For example:
384
// (1, 5), (5, 10), (9, 2) => (1, 5), (2, 5), (3, 5), (4, 5), (5, 10), (6, 10),
385
// (7, 10), (8, 10), (9, 2)
386
832
fn sample_to_chunk_iter<'a>(
387
832
    stsc_samples: &'a TryVec<SampleToChunk>,
388
832
    stco_offsets: &'a TryVec<u64>,
389
832
) -> SampleToChunkIterator<'a> {
390
832
    SampleToChunkIterator {
391
832
        chunks: (0..0),
392
832
        sample_count: 0,
393
832
        stsc_peek_iter: stsc_samples.as_slice().iter().peekable(),
394
832
        remain_chunk_count: stco_offsets
395
832
            .len()
396
832
            .try_into()
397
832
            .expect("stco.entry_count is u32"),
398
832
    }
399
832
}
400
401
struct SampleToChunkIterator<'a> {
402
    chunks: std::ops::Range<u32>,
403
    sample_count: u32,
404
    stsc_peek_iter: std::iter::Peekable<std::slice::Iter<'a, SampleToChunk>>,
405
    remain_chunk_count: u32, // total chunk number from 'stco'.
406
}
407
408
impl Iterator for SampleToChunkIterator<'_> {
409
    type Item = (u32, u32);
410
411
173k
    fn next(&mut self) -> Option<(u32, u32)> {
412
173k
        let has_chunk = self.chunks.next().or_else(|| {
413
7.18k
            self.chunks = self.locate();
414
7.18k
            self.remain_chunk_count
415
7.18k
                .checked_sub(
416
7.18k
                    self.chunks
417
7.18k
                        .len()
418
7.18k
                        .try_into()
419
7.18k
                        .expect("len() of a Range<u32> must fit in u32"),
420
                )
421
7.18k
                .and_then(|res| {
422
7.10k
                    self.remain_chunk_count = res;
423
7.10k
                    self.chunks.next()
424
7.10k
                })
425
7.18k
        });
426
427
173k
        has_chunk.map(|id| (id, self.sample_count))
428
173k
    }
429
}
430
431
impl SampleToChunkIterator<'_> {
432
    #[allow(clippy::reversed_empty_ranges)]
433
7.18k
    fn locate(&mut self) -> std::ops::Range<u32> {
434
        loop {
435
8.12k
            return match (self.stsc_peek_iter.next(), self.stsc_peek_iter.peek()) {
436
7.28k
                (Some(next), Some(peek)) if next.first_chunk == peek.first_chunk => {
437
                    // Invalid entry, skip it and will continue searching at
438
                    // next loop iteration.
439
942
                    continue;
440
                }
441
6.34k
                (Some(next), Some(peek)) if next.first_chunk > 0 && peek.first_chunk > 0 => {
442
6.33k
                    self.sample_count = next.samples_per_chunk;
443
6.33k
                    (next.first_chunk - 1)..(peek.first_chunk - 1)
444
                }
445
153
                (Some(next), None) if next.first_chunk > 0 => {
446
153
                    self.sample_count = next.samples_per_chunk;
447
                    // Total chunk number in 'stsc' could be different to 'stco',
448
                    // there could be more chunks at the last 'stsc' record.
449
153
                    match next.first_chunk.checked_add(self.remain_chunk_count) {
450
153
                        Some(r) => (next.first_chunk - 1)..r - 1,
451
0
                        _ => 0..0,
452
                    }
453
                }
454
693
                _ => 0..0,
455
            };
456
        }
457
7.18k
    }
458
}
459
460
/// Calculate numerator * scale / denominator, if possible.
461
///
462
/// Applying the associativity of integer arithmetic, we divide first
463
/// and add the remainder after multiplying each term separately
464
/// to preserve precision while leaving more headroom. That is,
465
/// (n * s) / d is split into floor(n / d) * s + (n % d) * s / d.
466
///
467
/// Return None on overflow or if the denominator is zero.
468
4.17k
pub fn rational_scale<T, S>(numerator: T, denominator: T, scale2: S) -> Option<T>
469
4.17k
where
470
4.17k
    T: PrimInt + Zero,
471
4.17k
    S: PrimInt,
472
{
473
4.17k
    if denominator.is_zero() {
474
0
        return None;
475
4.17k
    }
476
477
4.17k
    let integer = numerator / denominator;
478
4.17k
    let remainder = numerator % denominator;
479
4.17k
    num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) {
480
4.17k
        Some(integer) => remainder
481
4.17k
            .checked_mul(&s)
482
4.17k
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
mp4parse::unstable::rational_scale::<u64, u64>::{closure#0}::{closure#0}
Line
Count
Source
482
4.17k
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
Unexecuted instantiation: mp4parse::unstable::rational_scale::<u64, i32>::{closure#0}::{closure#0}
483
0
        None => None,
484
4.17k
    })
mp4parse::unstable::rational_scale::<u64, u64>::{closure#0}
Line
Count
Source
479
4.17k
    num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) {
480
4.17k
        Some(integer) => remainder
481
4.17k
            .checked_mul(&s)
482
4.17k
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
483
0
        None => None,
484
4.17k
    })
Unexecuted instantiation: mp4parse::unstable::rational_scale::<u64, i32>::{closure#0}
485
4.17k
}
mp4parse::unstable::rational_scale::<u64, u64>
Line
Count
Source
468
4.17k
pub fn rational_scale<T, S>(numerator: T, denominator: T, scale2: S) -> Option<T>
469
4.17k
where
470
4.17k
    T: PrimInt + Zero,
471
4.17k
    S: PrimInt,
472
{
473
4.17k
    if denominator.is_zero() {
474
0
        return None;
475
4.17k
    }
476
477
4.17k
    let integer = numerator / denominator;
478
4.17k
    let remainder = numerator % denominator;
479
4.17k
    num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) {
480
        Some(integer) => remainder
481
            .checked_mul(&s)
482
            .and_then(|remainder| (remainder / denominator).checked_add(&integer)),
483
        None => None,
484
    })
485
4.17k
}
Unexecuted instantiation: mp4parse::unstable::rational_scale::<u64, i32>
486
487
#[derive(Debug, PartialEq, Eq)]
488
pub struct Microseconds<T>(pub T);
489
490
/// Convert `time` in media's global (mvhd) timescale to microseconds,
491
/// using provided `MediaTimeScale`
492
0
pub fn media_time_to_us(time: MediaScaledTime, scale: MediaTimeScale) -> Option<Microseconds<u64>> {
493
0
    let microseconds_per_second = 1_000_000;
494
0
    rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds)
495
0
}
496
497
/// Convert `time` in track's local (mdhd) timescale to microseconds,
498
/// using provided `TrackTimeScale<T>`
499
pub fn track_time_to_us<T>(
500
    time: TrackScaledTime<T>,
501
    scale: TrackTimeScale<T>,
502
) -> Option<Microseconds<T>>
503
where
504
    T: PrimInt + Zero,
505
{
506
    assert_eq!(time.1, scale.1);
507
    let microseconds_per_second = 1_000_000;
508
    rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds)
509
}
510
511
#[test]
512
fn rational_scale_overflow() {
513
    assert_eq!(rational_scale::<u64, u64>(17, 3, 1000), Some(5666));
514
    let large = 0x4000_0000_0000_0000;
515
    assert_eq!(rational_scale::<u64, u64>(large, 2, 2), Some(large));
516
    assert_eq!(rational_scale::<u64, u64>(large, 4, 4), Some(large));
517
    assert_eq!(rational_scale::<u64, u64>(large, 2, 8), None);
518
    assert_eq!(rational_scale::<u64, u64>(large, 8, 4), Some(large / 2));
519
    assert_eq!(rational_scale::<u64, u64>(large + 1, 4, 4), Some(large + 1));
520
    assert_eq!(rational_scale::<u64, u64>(large, 40, 1000), None);
521
}
522
523
#[test]
524
fn media_time_overflow() {
525
    let scale = MediaTimeScale(90000);
526
    let duration = MediaScaledTime(9_007_199_254_710_000);
527
    assert_eq!(
528
        media_time_to_us(duration, scale),
529
        Some(Microseconds(100_079_991_719_000_000u64))
530
    );
531
}
532
533
#[test]
534
fn track_time_overflow() {
535
    let scale = TrackTimeScale(44100u64, 0);
536
    let duration = TrackScaledTime(4_413_527_634_807_900u64, 0);
537
    assert_eq!(
538
        track_time_to_us(duration, scale),
539
        Some(Microseconds(100_079_991_719_000_000u64))
540
    );
541
}