You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
join.py on FFmpeg 7.1: a short H.264 clip in MP4/MOV is held 2 frames past its picture (stream starts read differently) #475
Seen in.#473 CI, job test (debian-trixie container, py3.13, ffmpeg 7.1 apt, 3/3), on 1622dea and on 440d6eb.
What happened.test_join_clip_lengths_are_whole_frames_and_samples makes 60 clips of exactly 5 frames from one source (-c:v libx264 -qp 0 -preset ultrafast) and joins them with --transition none. The result reports clip_frames.
The sound in each clip is exactly 5 frames of samples, and with PCM it has no codec padding. So the extra two frames do not come from AAC; an earlier version of this issue blamed the AAC tail, which was wrong.
Likely cause (not confirmed: no 7.1 here).decision.av_clip_length() measures where the sound ends from the clip's first frame as a + a0 - v0 (stream_starts). It then holds the clip to that sound when it ends more than a frame past the picture (narration is never cut). For these H.264 clips in MP4/MOV, 7.1 seems to report a video start about two frames before the audio start (negative CTS or an edit-list difference). The sound then appears to end two frames after the picture, and every clip is lengthened from 5 to 7 frames, 0.2333 s. The rule predates #473; the old millisecond code used the same av_clip_length().
#473 switched the test to FFV1 + PCM in MOV, which has no reordering delay and exact frame times, so the test measures the cut alone.
To do.
On FFmpeg 7.1, print probe() → video.start_time, video.duration, audio.start_time, audio.duration and stream_starts() for one such clip, and compare with 6.1.
Fix stream_starts, or the sound-end computation, so a clip whose sound and picture cover the same frames is not lengthened. That probably means reading the first displayed frame's pts rather than the stream start.
Add a regression test with H.264 clips in MP4 that runs on the 7.1 and 8 jobs.
changed the title [-]join.py on FFmpeg 7.1: a short clip's AAC tail is read as sound past the picture, and the clip is lengthened by 1–2 frames[/-][+]join.py on FFmpeg 7.1: a short H.264 clip in MP4/MOV is held 2 frames past its picture (stream starts read differently)[/+]on Nov 12, 2026
What FFmpeg 7.1 reported for one fixture clip (CI on bab3780). The clip was 5 FFV1 frames + 8000 PCM samples in MOV, written with -fps_mode passthrough from a 30 fps NUT source:
So the stream starts are not the cause. The MOV carried no duration for the last frame, so the video reads one frame short (4 frame-times). probe()'s fps comes from avg_frame_rate = 5 / 0.1333 = 37.5.
join.py then did two things:
It took the first clip's fps as the join's rate. output_rate() only uses r_frame_rate when it agrees with fps, so the join ran at 75/2.
At 37.5 fps it counted the sound, which lasts 0.1667 s, as running more than a frame past the 0.1333 s picture, so it held each clip to ceil(0.1667 × 37.5) = 7 frames.
The earlier H.264/AAC MP4 fixture gave the same 7 frames, so this probably happens to any short clip whose container leaves the last frame's duration out.
#473 writes its fixture with -r <rate> so that each frame carries its duration. The join.py side stays here:
The rate. Prefer the stream's declared r_frame_rate over an avg_frame_rate computed from a short duration, for example when nb_frames / r_frame_rate matches the sound better, or whenever r_frame_rate is a plain rate.
The video length. When it is (nb_frames - 1) / rate, read it as nb_frames / rate.
A test. Five-frame clips without a last-frame duration (-fps_mode passthrough into MOV) should join at the source rate, 5 frames each, on 7.1.
Activity
test(join): the frame-exact join test uses PCM clips; an AAC tail on …
test(join): the frame-exact join test uses FFV1 + PCM clips in MOV
[-]join.py on FFmpeg 7.1: a short clip's AAC tail is read as sound past the picture, and the clip is lengthened by 1–2 frames[/-][+]join.py on FFmpeg 7.1: a short H.264 clip in MP4/MOV is held 2 frames past its picture (stream starts read differently)[/+]test(join): on a clip-length mismatch, print what this FFmpeg reports…
test(join): write each fixture clip at the source rate, not passthrough
kajisho5 commentedon Nov 12, 2026
What FFmpeg 7.1 reported for one fixture clip (CI on bab3780). The clip was 5 FFV1 frames + 8000 PCM samples in MOV, written with
-fps_mode passthroughfrom a 30 fps NUT source:So the stream starts are not the cause. The MOV carried no duration for the last frame, so the video reads one frame short (4 frame-times).
probe()'sfpscomes fromavg_frame_rate= 5 / 0.1333 = 37.5.join.py then did two things:
fpsas the join's rate.output_rate()only usesr_frame_ratewhen it agrees withfps, so the join ran at 75/2.The earlier H.264/AAC MP4 fixture gave the same 7 frames, so this probably happens to any short clip whose container leaves the last frame's duration out.
#473 writes its fixture with
-r <rate>so that each frame carries its duration. The join.py side stays here:r_frame_rateover anavg_frame_ratecomputed from a short duration, for example whennb_frames / r_frame_ratematches the sound better, or wheneverr_frame_rateis a plain rate.(nb_frames - 1) / rate, read it asnb_frames / rate.-fps_mode passthroughinto MOV) should join at the source rate, 5 frames each, on 7.1.Generated by Claude Code
fix(join): a short clip without a last-frame duration is read at its …