From 37e349c80cb11b475e46eed163edef891a82108d Mon Sep 17 00:00:00 2001 From: Paul Adenot Date: Fri, 16 Oct 2026 16:00:14 +0200 Subject: [PATCH] Fix edit timescale in sample tables Convert edit durations from the movie timescale to the track timescale before applying composition offsets. Reject missing timescales and unrepresentable offsets instead of ignoring the edit. Add regression coverage for scale conversion and invalid values, and correct the bipbop sample table expectations to include its scaled empty edit. Fixes bug 2082877. --- mp4parse_capi/src/lib.rs | 87 +++++++++++++++++++++++- mp4parse_capi/tests/test_sample_table.rs | 18 ++--- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/mp4parse_capi/src/lib.rs b/mp4parse_capi/src/lib.rs index 1cd733cc..823e9d22 100644 --- a/mp4parse_capi/src/lib.rs +++ b/mp4parse_capi/src/lib.rs @@ -1727,9 +1727,19 @@ fn get_indice_table( _ => None, }; - let empty_duration: Option> = match &track.empty_duration { - &Some(e) => i64::try_from(e.0).ok().map(Into::into), - _ => None, + let empty_duration: Option> = match track.empty_duration { + Some(duration) => { + let movie_timescale = context.timescale.ok_or(Mp4parseStatus::Invalid)?; + let track_timescale = track.timescale.ok_or(Mp4parseStatus::Invalid)?; + let duration = rational_scale(duration.0, movie_timescale.0, track_timescale.0) + .ok_or(Mp4parseStatus::Invalid)?; + Some( + i64::try_from(duration) + .map_err(|_| Mp4parseStatus::Invalid)? + .into(), + ) + } + None => None, }; // Find the track start offset time from 'elst'. @@ -2376,3 +2386,74 @@ fn parse_no_timescale() { assert_eq!(rv, Mp4parseStatus::Invalid); }; } + +#[cfg(test)] +mod edit_list_tests { + use super::*; + use mp4parse::{MediaScaledTime, MediaTimeScale, TrackScaledTime, TrackTimeScale}; + + fn context() -> MediaContext { + let mut file = std::fs::File::open("tests/video_colr_nclx_hdr10.mp4").unwrap(); + let mut context = mp4parse::read_mp4(&mut file, ParseStrictness::Normal).unwrap(); + context.tracks[0].ctts = None; + context.tracks[0].media_time = Some(TrackScaledTime(0, 0)); + context + } + + #[test] + fn empty_edit_uses_movie_timescale() { + for (movie_scale, track_scale, duration, media_time, expected) in [ + (57600, 60, 960, 0, 1), + (60, 57600, 960, 0, 921600), + (60, 60, 960, 0, 960), + (57600, 60, 960, 2, -1), + (4, 1, u64::MAX - 3, 0, (u64::MAX / 4) as i64), + ] { + let mut context = context(); + context.timescale = Some(MediaTimeScale(movie_scale)); + let track = &mut context.tracks[0]; + track.timescale = Some(TrackTimeScale(track_scale, 0)); + track.empty_duration = Some(MediaScaledTime(duration)); + track.media_time = Some(TrackScaledTime(media_time, 0)); + let track_id = track.track_id.unwrap(); + let mut cache = TryHashMap::default(); + let mut indices = Mp4parseByteData::default(); + assert_eq!( + get_indice_table(&context, &mut cache, track_id, &mut indices), + Ok(()) + ); + assert!(indices.length > 0); + assert!(!indices.indices.is_null()); + let first = unsafe { &*indices.indices }; + assert_eq!(first.start_composition.0, expected); + assert_eq!(first.start_decode.0, 0); + } + } + + #[test] + fn invalid_empty_edit_timescale() { + for (movie_scale, track_scale, duration) in [ + (None, Some(60), 960), + (Some(57600), None, 960), + (Some(0), Some(60), 960), + (Some(1), Some(2), u64::MAX), + (Some(1), Some(1), u64::MAX), + ] { + let mut context = context(); + context.timescale = movie_scale.map(MediaTimeScale); + let track = &mut context.tracks[0]; + track.timescale = track_scale.map(|scale| TrackTimeScale(scale, 0)); + track.empty_duration = Some(MediaScaledTime(duration)); + let track_id = track.track_id.unwrap(); + assert_eq!( + get_indice_table( + &context, + &mut TryHashMap::default(), + track_id, + &mut Mp4parseByteData::default() + ), + Err(Mp4parseStatus::Invalid) + ); + } + } +} diff --git a/mp4parse_capi/tests/test_sample_table.rs b/mp4parse_capi/tests/test_sample_table.rs index 49037a3d..2e357b7f 100644 --- a/mp4parse_capi/tests/test_sample_table.rs +++ b/mp4parse_capi/tests/test_sample_table.rs @@ -81,20 +81,20 @@ fn parse_sample_table() { rv = mp4parse_get_indice_table(parser, track_info.track_id, &mut indice); assert_eq!(rv, Mp4parseStatus::Ok); - // Compare the last few data from stagefright. + // The 95-tick empty edit at 1000 Hz adds 8550 ticks at 90000 Hz. let video_indice_291 = Indice { start_offset: 280_226.into(), end_offset: 280_855.into(), - start_composition: 876995.into(), - end_composition: 879996.into(), + start_composition: 885450.into(), + end_composition: 888451.into(), start_decode: 873900.into(), sync: false, }; let video_indice_292 = Indice { start_offset: 280_855.into(), end_offset: 281_297.into(), - start_composition: 873996.into(), - end_composition: 876995.into(), + start_composition: 882451.into(), + end_composition: 885450.into(), start_decode: 873901.into(), sync: false, }; @@ -105,16 +105,16 @@ fn parse_sample_table() { let video_indice_295 = Indice { start_offset: 282_391.into(), end_offset: 283_032.into(), - start_composition: 888995.into(), - end_composition: 888996.into(), + start_composition: 897450.into(), + end_composition: 897451.into(), start_decode: 885900.into(), sync: false, }; let video_indice_296 = Indice { start_offset: 283_092.into(), end_offset: 283_526.into(), - start_composition: 885996.into(), - end_composition: 888995.into(), + start_composition: 894451.into(), + end_composition: 897450.into(), start_decode: 885901.into(), sync: false, };