mp4-edit 0.1.1

mp4 read/write library designed with audiobooks in mind
Documentation
use std::{
    fmt::Debug,
    ops::RangeBounds,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

const NANOS_PER_SECOND: u128 = 1_000_000_000;

pub fn scaled_duration(duration: Duration, timescale: u64) -> u64 {
    (duration.as_nanos() * u128::from(timescale) / NANOS_PER_SECOND).min(u128::from(u64::MAX))
        as u64
}

pub fn unscaled_duration(duration: u64, timescale: u64) -> Duration {
    let duration_nanos = (u128::from(duration) * NANOS_PER_SECOND / u128::from(timescale))
        .min(u128::from(u64::MAX)) as u64;
    Duration::from_nanos(duration_nanos)
}

pub fn scaled_duration_range(
    range: impl RangeBounds<Duration>,
    timescale: u64,
) -> impl RangeBounds<u64> + Debug {
    use std::ops::Bound;
    let start = match range.start_bound() {
        Bound::Included(start) => Bound::Included(scaled_duration(*start, timescale)),
        Bound::Excluded(start) => Bound::Excluded(scaled_duration(*start, timescale)),
        Bound::Unbounded => Bound::Unbounded,
    };
    let end = match range.end_bound() {
        Bound::Included(end) => Bound::Included(scaled_duration(*end, timescale)),
        Bound::Excluded(end) => Bound::Excluded(scaled_duration(*end, timescale)),
        Bound::Unbounded => Bound::Unbounded,
    };
    (start, end)
}

pub fn mp4_timestamp(duration: Duration) -> u64 {
    duration.as_secs() + 2_082_844_800
}

pub fn mp4_timestamp_now() -> u64 {
    mp4_timestamp(now())
}

fn now() -> Duration {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("time went backwards")
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::ops::Bound;
    use std::time::Duration;

    fn test_scaled_duration_range<F>(test_case: F)
    where
        F: FnOnce() -> ScaledDurationRangeTestCase,
    {
        let test_case = test_case();

        let range = (test_case.start_bound, test_case.end_bound);
        let scaled_range = scaled_duration_range(range, test_case.timescale);

        match (&test_case.expected_start, scaled_range.start_bound()) {
            (Bound::Included(expected), Bound::Included(actual)) => {
                assert_eq!(
                    *expected, *actual,
                    "Start bound mismatch in {}",
                    test_case.description
                )
            }
            (Bound::Excluded(expected), Bound::Excluded(actual)) => {
                assert_eq!(
                    *expected, *actual,
                    "Start bound mismatch in {}",
                    test_case.description
                )
            }
            (Bound::Unbounded, Bound::Unbounded) => (),
            _ => panic!("Start bound type mismatch in {}", test_case.description),
        }

        match (&test_case.expected_end, scaled_range.end_bound()) {
            (Bound::Included(expected), Bound::Included(actual)) => {
                assert_eq!(
                    *expected, *actual,
                    "End bound mismatch in {}",
                    test_case.description
                )
            }
            (Bound::Excluded(expected), Bound::Excluded(actual)) => {
                assert_eq!(
                    *expected, *actual,
                    "End bound mismatch in {}",
                    test_case.description
                )
            }
            (Bound::Unbounded, Bound::Unbounded) => (),
            _ => panic!("End bound type mismatch in {}", test_case.description),
        }
    }

    struct ScaledDurationRangeTestCase {
        start_bound: Bound<Duration>,
        end_bound: Bound<Duration>,
        timescale: u64,
        expected_start: Bound<u64>,
        expected_end: Bound<u64>,
        description: &'static str,
    }

    macro_rules! test_scaled_duration_range {
        ($($name:ident => $test_case:expr,)*) => {
            $(
                #[test]
                fn $name() {
                    test_scaled_duration_range($test_case);
                }
            )*
        };
    }

    test_scaled_duration_range!(
        included_included => || ScaledDurationRangeTestCase {
            start_bound: Bound::Included(Duration::from_secs(1)),
            end_bound: Bound::Included(Duration::from_secs(3)),
            timescale: 1000,
            expected_start: Bound::Included(1000),
            expected_end: Bound::Included(3000),
            description: "included/included",
        },
        included_excluded => || ScaledDurationRangeTestCase {
            start_bound: Bound::Included(Duration::from_secs(1)),
            end_bound: Bound::Excluded(Duration::from_secs(4)),
            timescale: 44100,
            expected_start: Bound::Included(44100),
            expected_end: Bound::Excluded(176400),
            description: "included/excluded",
        },
        included_unbounded => || ScaledDurationRangeTestCase {
            start_bound: Bound::Included(Duration::from_millis(500)),
            end_bound: Bound::Unbounded,
            timescale: 48000,
            expected_start: Bound::Included(24000),
            expected_end: Bound::Unbounded,
            description: "included/unbounded",
        },
        excluded_included => || ScaledDurationRangeTestCase {
            start_bound: Bound::Excluded(Duration::from_millis(100)),
            end_bound: Bound::Included(Duration::from_secs(2)),
            timescale: 48000,
            expected_start: Bound::Excluded(4800),
            expected_end: Bound::Included(96000),
            description: "excluded/included",
        },
        excluded_excluded => || ScaledDurationRangeTestCase {
            start_bound: Bound::Excluded(Duration::from_secs(1)),
            end_bound: Bound::Excluded(Duration::from_secs(3)),
            timescale: 1000,
            expected_start: Bound::Excluded(1000),
            expected_end: Bound::Excluded(3000),
            description: "excluded/excluded",
        },
        excluded_unbounded => || ScaledDurationRangeTestCase {
            start_bound: Bound::Excluded(Duration::from_millis(250)),
            end_bound: Bound::Unbounded,
            timescale: 8000,
            expected_start: Bound::Excluded(2000),
            expected_end: Bound::Unbounded,
            description: "excluded/unbounded",
        },
        unbounded_included => || ScaledDurationRangeTestCase {
            start_bound: Bound::Unbounded,
            end_bound: Bound::Included(Duration::from_secs(5)),
            timescale: 1000,
            expected_start: Bound::Unbounded,
            expected_end: Bound::Included(5000),
            description: "unbounded/included",
        },
        unbounded_excluded => || ScaledDurationRangeTestCase {
            start_bound: Bound::Unbounded,
            end_bound: Bound::Excluded(Duration::from_secs(3)),
            timescale: 22050,
            expected_start: Bound::Unbounded,
            expected_end: Bound::Excluded(66150),
            description: "unbounded/excluded",
        },
        unbounded_unbounded => || ScaledDurationRangeTestCase {
            start_bound: Bound::Unbounded,
            end_bound: Bound::Unbounded,
            timescale: 1000,
            expected_start: Bound::Unbounded,
            expected_end: Bound::Unbounded,
            description: "unbounded/unbounded",
        },
        zero_timescale => || ScaledDurationRangeTestCase {
            start_bound: Bound::Included(Duration::from_secs(1)),
            end_bound: Bound::Included(Duration::from_secs(2)),
            timescale: 0,
            expected_start: Bound::Included(0),
            expected_end: Bound::Included(0),
            description: "zero timescale",
        },
        large_timescale => || ScaledDurationRangeTestCase {
            start_bound: Bound::Included(Duration::from_millis(1)),
            end_bound: Bound::Included(Duration::from_millis(2)),
            timescale: 1_000_000,
            expected_start: Bound::Included(1000),
            expected_end: Bound::Included(2000),
            description: "large timescale",
        },
        small_timescale => || ScaledDurationRangeTestCase {
            start_bound: Bound::Included(Duration::from_secs(1)),
            end_bound: Bound::Included(Duration::from_secs(2)),
            timescale: 1,
            expected_start: Bound::Included(1),
            expected_end: Bound::Included(2),
            description: "small timescale",
        },
        overflow_protection => || ScaledDurationRangeTestCase {
            start_bound: Bound::Included(Duration::from_secs(u64::MAX)),
            end_bound: Bound::Included(Duration::from_secs(u64::MAX)),
            timescale: 1000,
            expected_start: Bound::Included(u64::MAX),
            expected_end: Bound::Included(u64::MAX),
            description: "overflow protection",
        },
    );
}