How do you detect scene changes?
Threshold the select filter's scene score: gt(scene,0.4) is a fair default. 0.3 fires on camera movement, 0.5+ catches hard cuts only.
Guide · Media
An automatic editor is not one clever algorithm. It is a detector that proposes cut points, a scorer that ranks segments, and a cutter that stitches the winners together — and ffmpeg can do all three from the command line.
The discipline that keeps this fast is doing all the analysis on cheap passes and encoding exactly once, at the end. Every intermediate re-encode costs time and a generation of quality.
Hardcoding 1920x1080 at 30fps is how a pipeline breaks on the first phone clip someone hands you. Read it, branch on it.
ffmpeg's select filter exposes a per-frame scene score between 0 and 1, measuring how different a frame is from its predecessor. Thresholding that score gives you cut candidates.
For unedited footage — an event, a talk, a match — the visual track is often uniform and the interesting moments are audible: a cheer, a laugh, a sudden loudness jump. Audio energy is a crude but effective highlight detector, and it is cheap to compute.
# per-window loudness, one line per measurement ffmpeg -i input.mp4 -af ebur128=framelog=verbose -f null - 2>&1 # volume statistics across the whole file ffmpeg -i input.mp4 -af volumedetect -f null - 2>&1 # detect silence, then treat the gaps between silences as segments ffmpeg -i input.mp4 -af silencedetect=noise=-30dB:d=0.6 -f null - 2>&1Two approaches that work in practice. Peak-relative: compute short-term loudness over one-second windows, then keep windows more than roughly 6 dB above the file's median — those are your spikes. Silence-inverted: use silencedetect to find quiet gaps, treat the audible stretches between them as natural segments, and rank those segments by mean loudness.
Then pad each pick: a highlight starts before the spike. Two to three seconds of lead-in and one to two of tail turns a jarring fragment into a watchable clip.
Stream copy is enormously faster than re-encoding, but it can only cut at keyframes. Put -ss before -i and ffmpeg seeks quickly to the nearest keyframe; put it after, and it decodes up to the exact frame, which is accurate and slow.
The pragmatic compromise: cut with stream copy when the keyframe interval is small enough that a fraction of a second of drift does not matter, and re-encode only the clips where the cut point must be exact. Or force a short keyframe interval when you control the source encode.
The concat demuxer joins files without re-encoding, but only when every input shares the same codec, resolution, frame rate, and audio parameters. Mixed sources need the concat filter, which re-encodes.
# clips.txt file 'clip01.mp4' file 'clip02.mp4' file 'clip03.mp4' ffmpeg -f concat -safe 0 -i clips.txt -c copy reel.mp4If concatenated output plays the first clip and then freezes or desyncs, the inputs did not actually match. Normalise every clip to identical codec, resolution, frame rate, sample rate, and channel layout first, or switch to the concat filter and accept one re-encode.
Vertical delivery from horizontal footage is a framing decision, not a resize. Three options, in ascending order of effort:
| Approach | How it looks | When to use it |
|---|---|---|
| Blurred-background pad | Original centred, blurred enlarged copy behind it | Fast, safe default for talking-head and wide shots |
| Centre crop | Fills the frame, cuts off both sides | Subject reliably centred; loses anything at the edges |
| Tracked crop | Crop window follows the subject | Best result, needs detection per frame and real work |
Finish with loudness normalisation so clips cut from different parts of a recording do not jump in volume. The two-pass loudnorm filter targeting around −14 LUFS is the usual choice for social platforms.
The honest counterpoint: an API gives you transcription, face tracking, and semantic highlight selection that a local ffmpeg pipeline does not have without extra models. If your selection logic genuinely needs to understand speech, run a local transcription model and keep the rest of the pipeline where it is.
This is the architecture behind EventReels — probe, detect, score, cut, assemble, one encode at the end, entirely on the machine.
FAQ
Threshold the select filter's scene score: gt(scene,0.4) is a fair default. 0.3 fires on camera movement, 0.5+ catches hard cuts only.
By audio, not video. Loudness spikes above the median via ebur128, or invert silencedetect and rank the audible segments. Pad the lead-in.
-ss before -i with -c copy. Fast and lossless, but cuts land on keyframes — re-encode only the clips that must be frame-accurate.
The concat demuxer needs identical codec, resolution, frame rate, and audio params. Mismatched inputs play clip one then freeze.
Blurred-background pad is the safe default; centre crop fills the frame but loses the edges; tracked crop looks best and costs real work.
No upload, no per-minute cost while iterating, no footage handed to a third party, deterministic and debuggable. APIs still win on transcription.