Part of the Meicut Engineering series — how we build an in-browser media toolkit.
ffmpeg -i in.mp4 -ss 60 -c copy -t 30 out.mp4 is everywhere. So is -i in.mp4 -c:v libx264 .... And so is -filter_complex "...concat...". Each one is presented as the way to trim a video. In practice there is no single best way: three mechanisms, three different trade-offs, and the right one depends entirely on your constraints. This post lays out all three, says which we chose and why under our constraints, and is open to better answers.
Approach 1: -c copy — the lossless shortcut, and its price
What it does: it tells ffmpeg not to decode or re-encode anything. It copies compressed packets and rewrites container metadata. For MP4/MOV, that includes the timestamps and possibly an edit list that tells players where to start.
What you get:
- Bit-identical output (no pixels touched)
- Orders of magnitude faster than any encode, since the bottleneck is I/O, not CPU
What it costs:
-
Not frame-accurate. A stream copy cannot cut between encoded frames; the cut snaps to the nearest keyframe. Your start point is accurate only to within one group-of-pictures (GOP): typically a fraction of a second to a couple of seconds, depending on
-g/gop_size. - B-frame and IDR hazards. If the cut lands mid-GOP on a source with B-frames, the first output packet may not be an independently-decodable IDR frame, and a player can glitch or show garbage at the head.
-
Seek-position-dependent timestamp metadata. With output-side seeking (
-ssafter-i), copy-cut depends on the timestamp metadata the muxer writes, whose player support is inconsistent. Input-side seeking (-ssbefore-i) is the more robust copy-cut form, but by construction it lands on a keyframe. Either way, you trade precision for speed. - Codec/container compatibility. The compressed stream has to be legal inside the target container; exotic codecs or non-standard pairings won't stream-copy.
Where it shines: dropping an intro/outro, extracting one coarse segment, batch-trimming files with consistent parameters, when "second-precision, zero-loss, near-instant" beats "frame-perfect." It is the engine behind most "instant / lossless trim" buttons, and it is a useful trade, just one that gives up frame accuracy.
Approach 2: re-encode with input seeking — frame-accurate and portable
What it does: -ss before -i (input seeking) locates the region quickly at the demux level, then a full re-encode produces the segment.
What you get:
-
Frame-accurate, but only because we re-encode. Input seeking alone does not buy you frame precision. With
-c copy, input seeking still lands on a keyframe. The precision comes from decoding from the nearest keyframe and then starting to write exactly at the requested timestamp. That chain holds only on the re-encode path, which is the boundary from Approach 1. - Container-portable and filter-capable. You can re-target resolution, codec, bitrate, and apply effects (fades, etc.) in the same pass.
- Clean output. No edit-list or mid-GOP artifacts to worry about.
What it costs:
- Slower, by definition: you encode every output frame.
- A second-generation quality loss, even if mild at a reasonable CRF (CRF 23 is our default preset family, as discussed in Part 1: Video Compression at Product Scale).
- Needs a working encode pipeline (codec, preset tuning, compatibility flags like
-pix_fmt yuv420p).
Where it shines: any time the cut point must be precise: a transition, a single-frame edit, trimming then merging, or any transform on top of the trim.
Approach 3: filter-graph splicing — one pass, multiple segments
What it does: a single -filter_complex pass trims every segment (trim/atrim), resets timestamps (setpts=PTS-STARTPTS), and concatenates them (concat), all in one ffmpeg process.
What you get:
- One process, no intermediate files. Nothing to write, clean up, or keep in sync.
- Frame-accurate, timestamp-continuous joins at the filter level (frame-accurate, not "sample-accurate", which is an audio term). Fades compose naturally into the same graph.
What it costs:
-
The whole input is decoded to feed the trims. Because
trimruns in the filter layer, ffmpeg decodes the input once and streams it through every segment's trim. The decode cost is O(file length) for all segments combined, not per-segment. A deep start in a long file still wastes CPU relative to a demux-level seek, but it is not multiplied by the segment count. - A complex graph (multi-input, transitions) is more to get wrong and harder to debug.
One adjacent mechanism worth naming separately, because it is for merging rather than trimming: the concat demuxer (-f concat) joins pre-existing files that already share stream parameters. It is fast and avoids re-encoding, but it demands parameter consistency. Resolution and framerate mismatches are the obvious breakers; the subtler killer is timebase mismatch, which ffprobe does not surface visibly and which tends to show up later as progressive audio/video drift rather than a hard failure.
A comparison, side by side
| Re-encode? | Frame accuracy | Speed | Quality | Best for | |
|---|---|---|---|---|---|
| copy-cut | No | keyframe-level (seconds) | fastest | bit-identical | coarse trims, intro/outro removal, consistent batches |
| input-seek + re-encode | Yes | frame-level | slower (you encode) | second-generation, CRF-controlled | precise cuts, cross-container, add effects |
| filter-graph splicing | Yes | frame-level | full decode | second-generation, clean joins | multi-segment, fades, one-process workflow |
Speed: filter-graph splicing depends on segment count and file length; against a single re-encode it is comparable.
Our choice, and why — not a universal answer
Under our constraints, frame accuracy, multi-segment joins, fades, and cross-container output are all non-negotiable product requirements, on both a browser-local path and a server path. So we currently do:
-
Single-segment trim: input seeking (
-ssbefore-i) + re-encode. Fast to locate, frame-accurate to cut, portable output. -
Multi-segment trim: one
-filter_complexpass (trim/atrim+setpts+concat). Single process, clean joins, fades included.
We did not pick -c copy as the default. Not because copy-cut is wrong; it is genuinely the right tool for a large class of "quick, coarse, lossless" trims. The framing is: in a product that promises frame-accurate cuts, a fast path that quietly snaps your cut point to a keyframe is a bug; in a coarse-trim workflow, that same snapping is a feature. Our product is the former, so copy-cut cannot be our default.
We also own the cost: re-encoding is slower and introduces a second generation; the quality side is usually a non-issue, because CRF 23's second-generation loss is invisible to the eye in most consumer scenarios. The speed side is the real one. On a long video, the encode step can take from seconds to minutes depending on the device and whether hardware encoding is available. That is exactly why we reserve the copy-cut fast path as a candidate for the future, rather than claiming one approach fits everyone.
An invitation, not a conclusion
Trimming a video is not a single correct command. It is a series of trade-offs between precision, speed, losslessness, storage, and the transforms you need to compose. Our implementation serves a specific product: precise, multi-segment cuts. If your use case differs, say you only need second-precision and care most about zero loss and speed, copy-cut may well be your right answer. I'd like to hear which approach you reach for and why; we are still evaluating where a copy-cut fast path might be worth adding alongside our re-encode default.
The trimmer behind this decision is part of the online toolkit at meicut.com — cut video — runs in the browser, no install required; files auto-delete after processing.
Meicut Engineering series:
- Part 1: Video Compression at Product Scale
- Part 2: Trimming Videos Is a Trade-off — copy-cut, re-encode, or filter-graph
- Part 3: "Convert Format" Is a Misnomer — containers vs codecs
- Part 4: Subtitles: soft vs burned, and the font problem
Top comments (0)