Open tooling, agent-run engineering, and the products built on both.

Your lossless video cut is lying to you

  • #video
  • #ffmpeg
  • #keycut
  • #open-source
  • #keyframes
  • #stream-copy

Ask any video tool to trim a file without re-encoding and it will happily do it. Then check the first frame of what comes back. It is very often not the frame you asked for, and nothing in the output says so.

A stream copy is not allowed to cut where you asked

A stream copy moves compressed packets from one container into another without decoding them. That is what makes it fast and lossless, and it is also the whole constraint. Most frames in a compressed video are not self-contained — they are differences against earlier frames in the same group of pictures. Only the keyframe that opens each group can stand on its own.

A cut that starts mid-group therefore has nothing to start from. ffmpeg resolves this by seeking to the keyframe at or before the requested time and copying from there. No warning, no report of the offset it actually used.

Work the arithmetic on a master encoded with a fixed 60-frame group at 29.97 fps — a keyframe every 2.002 seconds:

  • You ask to start at 903.5 s.
  • The keyframe at or before that is number 451, at 902.902 s.
  • A -ss 903.5 … -c copy hands you a file whose first frame is 902.902 — 0.598 s of footage you deliberately excluded, welded onto the front.

Do that at every join in a multi-segment assembly and the failure compounds. Cut a highlight reel out of a longer master and each piece replays the tail of the piece before it. Worse: if the gap between two kept ranges is a span you removed on purpose, the backward snap resurrects it. The removed content comes back, in the delivered file, silently.

What you get instead

keycut is the cut layer extracted from a production driving-video pipeline, where a day’s footage is concatenated into one master and long-form videos are assembled back out of scored time ranges. It is being published as a standalone library at Back-Road-Creative/keycut.

Same request, same master. keycut snaps the start forward to the first keyframe at or after it — number 452, at 904.904 s. The delivered file starts 1.404 s late and contains not one frame from before your cut.

That is the trade, stated plainly: you can be early or you can be late, and only one of those two is a lie about what the file contains. keycut is always late, by less than one group, and never early.

The pipeline, in order

  • Translate. Segment references arrive as (source_file_index, start_time, end_time) against individual source files. Because the master is itself a stream-copy concat of those files in order, each file’s offset in master time is the sum of the durations before it — so a reference becomes a master-local (start, end) by addition.
  • Merge what cannot be excised. Ranges that overlap, or whose gap is smaller than one group, are merged into a single extraction. A stream copy cannot cleanly remove a sub-group gap; attempting it produces a join that duplicates the gap anyway. keycut keeps the extra footage on purpose rather than producing a sloppy seam. Only forward continuations merge — out-of-order ranges stay separate so the requested output order survives.
  • Probe. ffprobe -select_streams v:0 -show_entries packet=pts_time,flags lists every packet; the ones flagged K are the keyframes. Results are cached per resolved path, mtime and size, so one master is probed once per run.
  • Snap. Each cut start moves forward to the first keyframe at or after it, provided that keyframe still falls inside the range. If no keyframe qualifies, the start is left alone — losing the range entirely would be worse than the slop.
  • Copy, then join. Each aligned range is stream-copied to a scratch file, and the scratch files go through the concat demuxer, also as a stream copy.

The two commands that matter, and the difference is flag order:

1# Aligned stream copy: -ss BEFORE -i is an input seek (fast, keyframe-bound)
2ffmpeg -y -ss 904.904 -to 1120.0 -i master.mp4 -c copy seg_00.mp4
3
4# Fallback: -ss AFTER -i forces a decode seek to the exact time, then re-encodes
5ffmpeg -y -i master.mp4 -ss 903.5 -to 1120.0 <encoder args> -c:a aac -b:a 256k seg_00.mp4

When copying is not safe, it re-encodes that range only

Two situations make a copy unsafe, and both fall back per range rather than per file:

  • The probe failed. With no keyframe list there is no way to align anything, so every range is re-encoded with an accurate seek. Guessing would ship the silent-early-start bug.
  • The copy would snap back over removed content. When a caller supplies a mask marking spans that must not appear, keycut checks where a copy’s start would actually land. If that span contains a masked second, the range is re-encoded from the exact cut instead of copied.

The fallback re-encodes with the same codec, group size, pixel format and colour metadata as the surrounding footage, so the re-encoded piece still joins the copied ones through the concat demuxer. It uses the CPU encoder deliberately: no GPU dependency, deterministic output, and the path is rare enough that the cost is worth paying.

Every failure returns nothing. A failed probe, a failed copy, a failed concat — none of them produce a partial file that looks finished. That is the same instinct as the rest of the work on this site: make the dishonest artefact unrepresentable rather than documenting how to spot it.

Why this is worth a library

Nothing above is exotic. Every piece is a documented behaviour of a widely used tool. The problem is that the correct sequence — translate, merge, probe, snap, copy, concat, fall back — is a few hundred lines that almost nobody writes, because the naive one-liner appears to work. It produces a playable file of approximately the right length. You only find out it was wrong when someone watches a join and sees two seconds of the last shot replayed, or sees the parked car that was supposed to be cut.

Configuration details reflect a production environment at time of writing. Implementation specifics vary based on tooling versions, platform updates, and organizational requirements. Validate approaches against current documentation before deployment.

← Back to Journal