Tennis Evolution

Guides

Why naive ffmpeg concat desyncs a highlight reel

The obvious way to build a highlight reel — cut clips with -c copy, then join them with the concat demuxer — failed in three different ways on a real iPhone recording. This is what went wrong and what the tool does instead.

The symptoms

Why it happens

Stream-copied cuts must start on a keyframe, but the audio packet nearest that keyframe starts a few milliseconds earlier or later. Each clip therefore has slightly different audio and video start times. The concat demuxer stitches timestamps, not samples, so those offsets accumulate, and the first clip's offset (here, a 1.01 s keyframe gap) becomes permanent. HEVC open-GOP streams also reference frames from before the cut, which do not exist in the joined file.

The fix: trim → concat → one encode

ffmpeg -i source.MOV -filter_complex \
 "[0:v]trim=start=A:end=B,setpts=PTS-STARTPTS[v0];[0:a]atrim=start=A:end=B,asetpts=PTS-STARTPTS[a0];... \
  [v0][a0][v1][a1]concat=n=2:v=1:a=1[v][a]" -map "[v]" -map "[a]" \
 -c:v libx265 -crf 19 -color_primaries bt2020 -color_trc arib-std-b67 -colorspace bt2020nc reel.mp4

Every segment is trimmed from the original source with sample-accurate timestamps reset to zero, concatenated in the filter graph, and encoded once. No join ever sees a keyframe boundary, so there is nothing to drift.

Then verify, every time

Tennis Evolution checks each output: clean full decode, video and audio both starting at 0 s, matching durations, non-black first and last frames, non-silent audio. A failing check exits non-zero, so a script or an agent cannot mistake a broken reel for a finished one.

FAQ

Can I still do a lossless join?

Yes, `tennis-evolution concat` exists as an escape hatch, but it reports the timestamp problems instead of hiding them.

Try the rally lab Free vs Pro