The launch log
FFmpeg

The keyframe trap: why ffmpeg -c copy cuts your video in the wrong place

Ask for 12.400 and get 12.012 — or a second and a half of black. Both are the same bug, and an edit list decides which one you see.

Umar BhuttaAug 14, 2026 9 min read

You ask for a cut at 00:00:12.400, the command finishes in under a second with no warnings, and the clip starts on the wrong shot. Or it starts black, with audio, for a second and a half. Both are the same bug, and the bug is not in FFmpeg — it is in what a stream copy is physically able to do.

Why a copy cannot start where you want

H.264 and every codec like it store a group of pictures: one I-frame that is complete in itself, then a run of P- and B-frames that describe only the differences from other frames. A P-frame is not a picture. It is instructions for modifying a picture you are assumed to already have. Hand a decoder a P-frame with no I-frame before it and there is nothing to decode against.

-c copy means “move packets, do not decode.” So a stream copy cannot synthesise a new I-frame at your timestamp, and it cannot hand over a P-frame without its I-frame either. Its only legal cut points are the keyframes that already exist in the file. With a two-second keyframe interval, that is a grid roughly 50 frames wide, and your cut goes to the nearest line on it.

Look at your own keyframe grid first

Every decision below depends on where the keyframes actually are, and that is one command:

$ ffprobe -v error -select_streams v -skip_frame nokey \
    -show_entries frame=pts_time -of csv=p=0 input.mp4
0.000000
2.002000
4.004000
6.006000
8.008000
10.010000
12.012000
14.014000

One note if you are copying this from an older answer: the field used to be called pkt_pts_time, and it was renamed to pts_time. On FFmpeg 7 and later the old name silently returns blank lines rather than an error, which looks exactly like a file with no keyframes. If your output is empty, that is usually why.

Those values are from a 29.97 fps file with a two-second GOP, which is why the grid is 2.002 s rather than 2.000 s — a detail that bites anyone computing cut points arithmetically instead of reading them. Screen recordings and streaming-oriented encodes often use a much coarser grid; a 10-second GOP is common, and it turns a “slightly early” cut into a badly wrong one.

The worked example

Requesting 12.400 on that file, with the keyframes above, the nearest usable one is 12.012 — 0.388 s early. Here is what each command actually produces.

-ss before -i: fast input seek

$ ffmpeg -ss 12.400 -i input.mp4 -t 5 -c copy out.mp4

This seeks in the container before demuxing, so it is effectively instant regardless of how far into the file you are. It lands on the keyframe at 12.012. What FFmpeg does next is the part nobody documents in the answers you will find: writing to MP4, it keeps those 0.388 s of pre-roll packets in the file, gives them negative timestamps, flags them for discard, and writes an edit list saying presentation begins at your requested point.

So the frames are in there. Whether anyone sees them depends on the player:

$ ffprobe -v error -select_streams v \
    -show_entries packet=pts_time,flags -of csv=p=0 out.mp4 | head -3
-0.400400,KD_
-0.333667,_D_
-0.266933,_D_

$ ffprobe -v error -ignore_editlist 1 -select_streams v \
    -show_entries packet=pts_time,flags -of csv=p=0 out.mp4 | head -1
0.066733,K__

Anything that honours the edit list starts at 12.400. Anything that ignores it — and plenty of hardware players, embedded players and older tooling do — starts at 12.012, plays your pre-roll, and reports a longer duration than you asked for. That is the source of the maddening “it is right in VLC and wrong on the TV” class of bug report.

The -avoid_negative_ts gotcha

$ ffmpeg -ss 12.400 -i input.mp4 -t 5 -c copy \
    -avoid_negative_ts make_zero out.mp4

This is frequently pasted around as a fix. It is not a fix, it is a different, more honest answer: with no negative timestamps allowed there is no edit list to hide behind, so the output plainly begins on the keyframe at 12.012 and is 0.388 s longer than requested. Every player now agrees with every other player, and they all agree your cut is early.

Where make_zero earns its reputation as a fix is the mirror case — output seek on a copy:

$ ffmpeg -i input.mp4 -ss 12.400 -t 5 -c copy out.mp4

-ss after -i is an output option: FFmpeg demuxes from the start and discards until the timestamp. Combined with -c copy, everything from 12.400 up to the next keyframe at 14.014 has to be thrown away, because none of it is independently decodable — while the output timeline still begins at 12.400. The result is a file whose first video frame sits at 1.614 s. Audio plays from zero, the picture is frozen or black for a second and a half, and the command exited 0. That is the apparent freeze-frame people go hunting timestamp flags for, and it is the reason output seek and stream copy should essentially never appear in the same command.

Re-encode, and the cut is exact

$ ffmpeg -ss 12.400 -i input.mp4 -t 5 \
    -c:v libx264 -crf 18 -preset medium -c:a aac -b:a 128k out.mp4

Input seek is accurate by default in modern FFmpeg: it still seeks fast to 12.012, then decodes and discards forward to 12.400 and starts encoding there, with a fresh I-frame at output timestamp zero. You get the frame you asked for, in every player, with no edit list trickery. You pay encode time and one generation of loss.

Forcing a keyframe where you want to cut

$ ffmpeg -i input.mp4 -force_key_frames 12.400 \
    -c:v libx264 -crf 18 -c:a copy prepared.mp4

This is sometimes proposed as the best of both worlds: put a keyframe at the cut point, then stream-copy against it. It works, and notice what you just did — you re-encoded the entire video to place one keyframe. If your goal was avoiding a re-encode, you have spent it. The keyframe also lands on the next frame boundary at or after the time you named, so at 29.97 fps a request for 12.400 becomes 12.412.

It is genuinely useful in one scenario: you are preparing a master that will be cut many times later, or segmented for HLS, and you want a known keyframe grid. For a one-off trim it is the slowest available path to a frame-exact cut.

Choosing, in one table

Stream copyRe-encode
Cut accuracyNearest earlier keyframe — up to one GOP earlyThe exact frame requested
SpeedSeconds, independent of clip lengthProportional to duration and resolution
QualityBit-identical to the sourceOne generation of loss
Player agreementDepends on edit-list handlingConsistent everywhere
Use it whenRough cuts, long files, the boundary can moveThe cut point matters

Two rules cover almost every case. Put -ss before -i, always — it is fast and it is accurate when re-encoding. And never pair output seek with -c copy. Beyond that it is a single question: does the exact first frame matter? If yes, re-encode once, at a sensible CRF, and stop trimming trims — repeated generations are what actually produces visible damage, not one careful pass.

If you are re-encoding anyway, it is worth knowing what else is cheap to do in the same pass — a delogo box costs you nothing extra once the decode is already happening.

Free tool · runs in your browser

Trim video

Cut a range and see where the keyframes fall, then choose a lossless stream copy or a frame-exact re-encode.

Trim video

More from the launch log