Import: an MP4 lasts as long as its video edit list shows (#2592)

The reader reports a track's media duration (mdhd). For H.264 with
B-frames that runs past the last shown frame by the encoder delay the
edit list trims, so a 75-frame 25 fps clip imported as 76 frames / 3.04 s.
The footage's duration now follows the video track's edit list when it
is shorter (never longer; a delayed start keeps the reader's duration),
as the frame rate is already corrected from the header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-30 05:54:16 +07:00
co-authored by Claude Opus 5.5
parent 77bea50f0b
commit 4bdb68a812
+48
View File
@@ -23,6 +23,7 @@ pub fn probe(path: impl AsRef<Path>) -> Result<Footage> {
let turn = header.as_deref().map_or(Orientation::NoTransforms, video_turn);
turn_footage(&mut f, turn);
correct_rate(&mut f, header.as_deref().and_then(video_rate));
trim_to_edits(&mut f, header.as_deref().and_then(video_shown));
return Ok(f);
}
let bytes = crate::read_regular_file(path).map_err(|e| MediaError::Io(format!("{}: {e}", path.display())))?;
@@ -107,6 +108,7 @@ pub fn probe_bytes(path: &str, bytes: Arc<[u8]>) -> Result<Footage> {
let name = Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| path.to_string());
let turn = video_turn(&bytes);
let rate = video_rate(&bytes);
let shown = video_shown(&bytes);
// A WAV's cue points and XMP markers become the footage's markers.
let markers = crate::wav_markers::read(&bytes);
let src = filmcraft_codecs::open_bytes(&name, bytes)?;
@@ -114,9 +116,37 @@ pub fn probe_bytes(path: &str, bytes: Arc<[u8]>) -> Result<Footage> {
f.markers = markers;
turn_footage(&mut f, turn);
correct_rate(&mut f, rate);
trim_to_edits(&mut f, shown);
Ok(f)
}
/// A movie lasts as long as its video edit list shows (#2592): the reader reports the media
/// duration (`mdhd`), which for H.264 with B-frames runs past the last shown frame by the
/// encoder delay the edit list trims (75 frames at 25 fps read as 3.04 s). Never lengthens.
fn trim_to_edits(f: &mut Footage, shown: Option<Tick>) {
if f.kind == FootageKind::Video
&& let Some(shown) = shown.filter(|s| *s > Tick::ZERO && *s < f.duration)
{
f.duration = shown;
}
}
/// How long the video track's edit list shows its media (`elst` segments, movie timescale):
/// `None` without edits, or with an empty edit (a delayed start keeps the reader's duration).
fn video_shown(bytes: &[u8]) -> Option<Tick> {
let file = filmcraft_isobmff::open(bytes).ok()?;
let t = file.tracks.iter().find(|t| t.kind == filmcraft_isobmff::TrackKind::Video && !t.samples.is_empty())?;
edits_shown(&t.edits, file.timescale)
}
fn edits_shown(edits: &[filmcraft_isobmff::Edit], timescale: u32) -> Option<Tick> {
if edits.is_empty() || timescale == 0 || edits.iter().any(|e| e.media_time < 0) {
return None;
}
let units = edits.iter().try_fold(0u64, |acc, e| acc.checked_add(e.segment_duration))?;
Some(Tick::from_units(i64::try_from(units).ok()?, i64::from(timescale)))
}
/// A movie's frame rate as its frame durations average to ([`video_rate`]), where the reader's
/// own estimate misreads it.
fn correct_rate(f: &mut Footage, rate: Option<FrameRate>) {
@@ -322,6 +352,24 @@ fn averaged_rate(timescale: u32, durations: impl Iterator<Item = u32>) -> Option
mod tests {
use super::*;
/// #2592: an encoder-delayed H.264 track's edit list shows 75 frames at 25 fps (3 s at a
/// 1000 timescale), though its media runs a frame longer; empty edits and no edits leave it.
#[test]
fn the_edit_list_trims_a_movie_to_the_frames_it_shows() {
let edit = |segment_duration, media_time| filmcraft_isobmff::Edit { segment_duration, media_time, media_rate: 0x0001_0000 };
let shown = edits_shown(&[edit(3000, 1024)], 1000);
assert_eq!(shown, Some(Tick::from_units(3, 1)));
let mut f = Footage { kind: FootageKind::Video, duration: Tick::from_units(76, 25), ..Footage::default() };
trim_to_edits(&mut f, shown);
assert_eq!(f.duration, Tick::from_units(75, 25));
// Never longer than the reader's duration, and only for video.
trim_to_edits(&mut f, Some(Tick::from_units(4, 1)));
assert_eq!(f.duration, Tick::from_units(75, 25));
assert_eq!(edits_shown(&[edit(1000, -1), edit(3000, 0)], 1000), None);
assert_eq!(edits_shown(&[], 1000), None);
assert_eq!(edits_shown(&[edit(3000, 0)], 0), None);
}
/// Durations (in 1/`timescale` s) of `n` frames at `rate` whose times are rounded to the timescale.
fn rounded(rate: FrameRate, timescale: i64, n: i64) -> Vec<u32> {
let at = |k: i64| (2 * k * timescale * rate.den + rate.num) / (2 * rate.num);