Cut: format matrix, test methods and FFmpeg pitfalls
About the interface in this document. The cut points are found in the mpv player, which a button of the section opens, and are moved into the fields with buttons, or typed as a timecode on the keyboard.
A reference for the CUT section. Everything written here is measured, not
taken from documentation: each cell of the tables is a real attempt to write with
-c copy and to read the result back, each sync number is a measurement by frames
and samples. The numbers were taken on FFmpeg 9.0.1, and taken again on 8.0.1 they
matched cell for cell. Which build goes into the distribution is decided by the installer
from the version of the NVIDIA driver (on most machines this is the 8.x branch), so both
are checked.
The section in detail is in TRIM_EN.md, the decisions are in DECISIONS_EN.md. Here are the facts about trimming; the full set of measurements, including frame rate conversion and the limits of FFmpeg itself, is in MEASUREMENTS_RU.md (Russian).
1. What the section does
CUT copies the streams (-c:v copy) and does not re-encode the picture. From this come
four properties that define everything else:
- the frame rate is taken from the file as it is:
ffprobegives the exact fraction (24000/1001for 23.976 and24/1for a true 24.000), and nothing is guessed; a camera shoots both; - the start of a piece lands on a keyframe (otherwise there is nothing to start the stream with);
- the end is cut by the number of frames, by the exact fraction of the rate;
- the sound is copied as it is, except in two cases (channel selection and PCM in MXF), where it is rewritten sample for sample without a generation loss.
| Mode | What stays | Points |
|---|---|---|
| Remove the start | everything after IN | one (IN) |
| Remove the end | everything before OUT | one (OUT) |
| Keep the middle | the piece between IN and OUT | two |
| Split | all parts, nothing is thrown away; the joins are on keyframes | N points give N + 1 parts, up to 30 points |
The + rows in KEEP and CUT OUT are not new modes but separate runs of the same
mode on the same take, each into its own file (_keep01, _cut01): everything
measured here applies to each of them as to a single cut.
2. Container × video codec
Pairs «video + a sound that the container surely accepts», written with -c copy, then
read back, and the codec compared.
| Video | MP4 | MOV | MKV | MXF | TS | M2TS |
|---|---|---|---|---|---|---|
| H.264 | yes | yes | yes | yes | yes | yes |
| HEVC | yes | yes | yes | no | yes | yes |
| ProRes | no | yes | yes | yes | no | no |
| DNxHD/DNxHR | no | yes | yes | yes | no | no |
| MPEG-2 | yes | yes | yes | yes | yes | yes |
| AV1 | yes | no | yes | no | no | no |
| VP9 | yes | no | yes | no | no | no |
What follows from this in practice:
- ProRes and DNxHR live in MOV, MKV and MXF. MP4 does not take them: an attempt gives «Could not find tag for codec» and a zero-length file, so such a pair is rejected before the run.
- HEVC does not go into MXF. Camera HEVC (Canon, Sony) cannot be moved into MXF without re-encoding; that is a separate operation, not trimming.
- AV1 and VP9 do not go into MOV.
3. Container × audio codec
| Sound | MP4 | MOV | MKV | MXF | TS | M2TS |
|---|---|---|---|---|---|---|
| PCM 16 LE | yes | yes | yes | yes | no | no |
PCM 16 BE (twos, Canon) |
yes | yes | yes | no | no | no |
| PCM 24 LE | yes | yes | yes | yes | no | no |
| PCM 32 float | yes | yes | yes | no | no | no |
| AAC | yes | yes | yes | no | yes | yes |
| AC-3 | yes | yes | yes | no | yes | yes |
| E-AC-3 | yes | yes | yes | no | yes | yes |
| MP3 | yes | yes | yes | no | yes | yes |
| FLAC | yes | no | yes | no | no | no |
| ALAC | yes | yes | yes | no | no | no |
| Opus | yes | no | yes | no | yes | yes |
The main points:
- MXF takes only PCM, and only little-endian. This matters for Canon:
the camera writes
pcm_s16be, and on output to MXF the sound is rewritten intopcm_s16le: the same samples, in the byte order the container requires. - MPEG-TS does not take PCM. Material from an AVCHD card with PCM sound cannot be moved into TS; MOV takes it as it is.
- MOV does not take FLAC or Opus, although MP4 takes both.
3.1. A sound file
A sound file does not copy the stream: it decodes it and cuts on the sample, so the container matrix above does not apply to it. What is written where:
| Button | Codec | Container | Lossless |
|---|---|---|---|
| As is: PCM | the same PCM | wav, w64, aif, aiff, caf, au, snd, mka |
yes |
| As is: FLAC, ALAC, WavPack, TTA | the same | its own or mka |
yes |
| As is: MP3, AAC, Opus, Vorbis, AC-3, E-AC-3 | the same, at the source bitrate | its own or mka |
one generation |
| WAV | PCM at the source bit depth; float for a compressed source | wav, RF64 past 4 GB |
yes |
| FLAC, ALAC | FLAC, ALAC | flac, m4a |
up to 24 bits |
| M4A | AAC | m4a |
no |
| MP3 | LAME | mp3 |
no |
| Opus | libopus, at most 256 kbps | opus |
no |
MP3, M4A, FLAC, ALAC and MKA keep a cover; WAV and Opus do not.
Packet copying (exact cut turned off) is offered for MP3 in mp3, AAC in
m4a and m4b, Vorbis in ogg and oga, Opus in opus, ogg and oga; for
the rest it is not. What the join costs for each codec is in TRIM_EN.md; the
measurements are in MEASUREMENTS_RU.md (Russian), section 12.3.
4. Cut accuracy
4.1 Frames
Checked on eight synthetic sources and one camera source. In every case the number of frames matched the plan exactly.
| Source | Request | Frames | Difference |
|---|---|---|---|
| ProRes 25p | 3 → 9 | 150 | 0 |
| HEVC long-GOP 29.97 | 12 → 30 | 600 | 0 (after topping up by decodable frames) |
| H.264 all-intra 23.976 | 3 → 9 | 144 | 0 |
| 29.97 drop-frame | 5 → 15 | 330 | 0 |
| 59.94 drop-frame | 3 → 9 | 360 | 0 |
| MPEG-TS (start 1.44 s) | 2 → 8 | 300 | 0 |
| 4 PCM channels | 2 → 8 | 150 | 0 |
| Canon HEVC 4:2:2 10-bit | 3 → 10 | 166 | 0 |
| ARRI ProRes 4444 XQ 24.000 | 2 → 8 | 144 | 0 |
| VFR (30 → 17.6) | 4 → 16 | by time | +1.07 s, reported |
4.2 Sound sync
The mark is in both tracks at once: a white frame at the start of each second and a click at the same moment. The deviation is counted against the source, not as an absolute value.
| Container | Change against the source |
|---|---|
| MP4 | 0…2 ms |
| MOV | 0…5 ms |
| MKV | 1…3 ms |
| MXF | 0…5 ms (after the PCM rewrite; before it, 9…18) |
On the camera file the sound of the piece is found in the source word for word: the piece starts 48 samples (1 ms, a fortieth of a frame) after the cut point.
4.3 Metadata
Checked on a file with rotation, two tracks and project tags:
| What | MP4 | MOV | MKV |
|---|---|---|---|
Rotation (rotation=90) |
yes | yes | yes |
Track language (rus, eng) |
yes | yes | yes |
| Project tags (title, artist) | yes | yes | yes |
| Camera tags (make, model, brands) | yes | yes | partly |
Timecode as a tmcd track |
yes | yes | no (a text tag) |
Camera tags are carried by -movflags use_metadata_tags; without it the MP4 muxer
writes its own isomiso2mp41 and loses make, model and a brand like mp42hvc1CAEP.
5. FFmpeg pitfalls found by measurement
Each one took a separate investigation; none is obvious from the documentation.
| No. | Pitfall | Symptom | How it is avoided |
|---|---|---|---|
| 1 | -ss before -i resets the time |
-to after it counts from the new zero |
the tail is passed as a duration/frames, not as an absolute point |
| 2 | round(fps) on fractional rates |
4 minutes at 29.97 become 240.27 s | the rate lives as a Fraction from ffprobe to ffmpeg |
| 3 | Rounding the end «to the nearest» | a frame standing at 29.9966 is lost when 30.0 is requested | the end is rounded up |
| 4 | -t when copying |
an overshoot of 1–3 frames on a stream with B-frames | the tail is set with -frames:v |
| 5 | -frames:v is exact only with an exact -ss |
with -ss off the keyframe the piece is shorter by the gap |
the seek always goes to the computed keyframe |
| 6 | HEVC with a B-pyramid | the muxer writes 2–4 frames fewer than requested | the shortfall is measured and the cut is repeated once with a correction |
| 7 | -avoid_negative_ts make_zero |
the video drifts 0.1 s from the sound | not used |
| 8 | start_time ≠ 0 (MPEG-TS 1.44 s) |
all points shift by this amount | the offset is removed from the keyframes and added back on seek |
| 9 | avg_frame_rate on VFR |
the frame count covers the wrong span (12 s → 16 s) | VFR is detected and cut by time, with a warning |
| 10 | -n on an existing file |
«already exists», exit code 0 | the target is checked before the run |
| 11 | PCM in MXF with -c copy |
384 extra samples at the start (8 ms) | PCM is rewritten sample for sample (LE) |
| 12 | -map_metadata 0 in MP4 |
camera tags and brands are lost | -movflags use_metadata_tags |
| 13 | pan=mono|c0=c1 on mono |
writes silence, exit code 0 | the number of channels is checked before the run |
| 14 | MXF + compressed sound | «Could not write header», code 4294967274 | rejected before the run with an explanation |
| 15 | Packet ≠ frame | the muxer writes 600 packets, the decoder gives 597: the trailing B-frames refer past the cut boundary | the shortfall is caught by the stream duration and topped up by decodable frames |
| 16 | Camera RAW in MXF | codec_name is empty, the sizes are zero, «dimensions not set», a zero-length file |
rejected before the run: FFmpeg has no X-OCN or ARRIRAW decoder |
| 17 | A warning breaks the JSON parsing | stderr is mixed into stdout, and json.loads fails on the line «could not resolve file descriptor strong ref» |
probes are read with separate streams |
6. How all this is measured
The commands that produced the numbers above. Use the same ones for the next check; otherwise the results cannot be compared.
Frames. Count packets instead of decoding: the result is the same, the time is forty times shorter (0.14 s against 5.7 s on four minutes of 59.94).
ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 FILE
Keyframes around a point, without scanning the whole file:
ffprobe -v error -select_streams v:0 -skip_frame nokey -read_intervals "START%+WINDOW" -show_entries frame=pts_time -of csv=p=0 FILE
Sync. You need a source with a mark in both tracks: a white frame and a click at one
moment. Read the video through signalstats with -fps_mode passthrough; otherwise the filter
duplicates frames to fit its own clock and shifts the times:
ffmpeg -v info -i FILE -map 0:v:0 -vf "signalstats,metadata=print:key=lavfi.signalstats.YAVG" -fps_mode passthrough -f null -
The sound is read by samples, not through silencedetect: the detector works in 20 ms
windows, and the values to measure are a few milliseconds.
ffmpeg -v error -i FILE -map 0:a:0 -ac 1 -ar 48000 -f s16le -
Bit-exact check of copying. Find the sound of the piece inside the sound of the source: if it is there word for word, the sound was copied, and the position gives the exact offset of the cut.
Compatibility. Write one second with -c copy into the container and read it
back. Choose the partner stream to suit the container: MXF does not take AAC, TS does not
take PCM, and without this the cell shows the refusal of the partner, not of the codec under test.
Traps of the measurements themselves
Three mistakes that were already made once; they are worth not repeating:
- Comparing «the first flash found» with «the first click found». If one stream opens with a mark and the other does not, the detectors describe different events, and the result is a «107 ms desync» that does not exist.
- Measuring sync with FFmpeg detectors.
blackdetectworks in whole frames,silencedetectin windows; together they give an error of 40 ms, that is, more than the value being measured. - Checking compatibility by the exit code. A file can be written and not read back; TS «accepts» ProRes exactly this way.
6.1. Camera RAW: what the tool cannot do
Two formats that FFmpeg does not open in any version; checked on both 8.0.1 and 9.0.1:
| File | What is inside | What ffprobe sees |
|---|---|---|
| Sony BURANO X-OCN LT | X-OCN in MXF OP1a | a stream without a codec name, width=0; the sound pcm_s24le is read |
ARRI Alexa Mini .mxf |
ARRIRAW in MXF | the same |
An attempt at a plain copy gives «dimensions not set» and a zero-length file, so such sources are rejected before the run with a direct explanation. They can be cut in the vendor's own software (Sony Catalyst, ARRI Reference Tool) or after a transcode.
The same material in MOV is cut fully: ARRI ProRes 4444 XQ (2944×2160, 12-bit, 5 PCM tracks) went through frame for frame, together with the timecode 02:00:11:11 → 02:00:13:11.
7. What was not checked
The list is open, so that nobody (the author included) decides that the matrix is closed:
- cameras other than Canon HEVC and ARRI ProRes: Sony XAVC, Panasonic, BMPCC, GoPro with telemetry, AVCHD straight from the card;
- files longer than an hour (an hour is checked: 50.7 GB, a frame-accurate cut at the
58th minute and in the last seconds; see
MEASUREMENTS_RU.md(Russian)); - running out of disk space in the middle of writing;
- a source on a network path or held by another program;
- two operations at once;
-
96 kHz and 32-bit float sound in real material;
- sound that runs past the last frame to the packet boundary: after the tail is topped up, it moves forward by 0.1 s and this is reported, but it was not checked by ear;
- sound files from a real recorder (poly-WAV, iXML), packet copying of AC-3, E-AC-3, DTS and AAC outside M4A.