Detect scene changes with FFmpeg
FFmpeg scores how different each frame is from the previous one. Frames above a threshold are the cuts. The test file has hard cuts at 3, 6 and 9 s and both methods below found exactly those.
Verified with FFmpeg 9.0.1 on 26 August 2026. Search intent: ffmpeg scene detection.
ffmpeg -i input.mp4 -vf "select='gt(scene,0.4)',showinfo" -f null -select keeps only frames whose scene score is above 0.4 and showinfo prints one line per kept frame. The pts_time values in those lines are the cut timestamps: 3, 6 and 9 on the test file.
Flags, explained
- select='gt(scene,0.4)'
- Pass a frame only when its scene score (0 to 1) is greater than 0.4. Typical cuts score above 0.3.
- showinfo
- Log one line per frame with pts_time and other frame details.
- -f null -
- Run the filters without writing a video.
- metadata=print:file=
- Write the frame metadata (score and timestamp) to a file.
- scdet=threshold=10
- Scene change detector with its own scale (0 to 100); 10 is the default.
- -fps_mode vfr
- Variable frame rate output so only selected frames are written. Replaces -vsync vfr from older releases.
Variants
Write a clean list with the scores
ffmpeg -i input.mp4 -vf "select='gt(scene,0.4)',metadata=print:file=scenes.txt" -f null -scenes.txt gets one block per cut with pts_time and lavfi.scene_score (0.727, 0.688 and 0.828 for the three cuts). Easier to parse than the showinfo log.
The scdet filter
ffmpeg -i input.mp4 -vf "scdet=threshold=10" -f null -A dedicated detector with a 0 to 100 score. It logged lavfi.scd.score 28.4, 26.9 and 32.3 at lavfi.scd.time 3, 6 and 9. Add metadata=print:key=lavfi.scd.time:file=scdet.txt after it to get the times in a file.
Save one thumbnail per scene
ffmpeg -i input.mp4 -vf "select='gt(scene,0.4)'" -fps_mode vfr scene-%03d.pngEach frame that starts a new scene is written as a PNG: scene-001.png to scene-003.png here. -fps_mode vfr stops FFmpeg from duplicating frames to keep a constant rate.
Looser threshold
ffmpeg -i input.mp4 -vf "select='gt(scene,0.1)',showinfo" -f null -Catches softer transitions and more false positives on fast motion. On the synthetic file it still returned the same three cuts; on real footage expect more.
Pitfalls we hit
- The select scene score runs from 0 to 1, scdet from 0 to 100. A threshold of 0.4 in one is not 40 in the other; the same three cuts scored 0.69 to 0.83 and 26.9 to 32.3.
- Dissolves and fades spread the change over many frames, each with a low score, so they are not reported as one cut. Lower the threshold or use scdet with a lower value and merge nearby hits.
- The expression must be quoted: the shell removes the inner quotes and the comma inside gt(scene,0.4) then breaks the filter graph.
- Log lines go to stderr. Pipe with 2>&1 or use the metadata file variant.
- Fast motion, flashes and camera shake also score high. Check the thumbnails before trusting the list.
How we verified it
Run with FFmpeg 9.0.1 on 26 August 2026 against a 12 s, 1280x720, 30 fps H.264 file made of four different 3 s test patterns. select with threshold 0.4 printed pts_time 3, 6 and 9 with scene scores 0.727, 0.688 and 0.828. scdet with threshold 10 logged scores 28.4, 26.9 and 32.3 at 3, 6 and 9 s. The thumbnail variant wrote scene-001.png to scene-003.png. Threshold 0.1 returned the same three frames.
The same edit as a typed operation
detect_scenes returns the cut timestamps as JSON for one threshold value, and split_video_by_scenes renders one MP4 per scene in the same job, so an agent never parses filter logs or builds a segment list by hand.
{
"inputs": [{ "id": "main", "url": "https://example.com/video.mp4" }],
"options": { "threshold": 0.3 }
}FAQ
What threshold should I use for scene detection?
Start at 0.3 to 0.4 with the select method. Raise it if fast motion produces false cuts, lower it to 0.2 for subtle edits. Always verify with a few thumbnails.
How do I split the video at the detected cuts?
Take the pts_time values and pass them to the segment muxer with -segment_times, or cut each range with -ss and -to. Cuts with -c copy only land on keyframes, so re-encode for exact boundaries.
Should I use select with the scene score or scdet?
Both found the same cuts here. select with the scene score is available everywhere and composes with other filters; scdet is purpose-built, logs its own timestamps and lets you tune the score range.
Why are fades and dissolves missed?
Each frame of a fade differs only slightly from the previous one, so no single frame crosses the threshold. Lower the threshold and group consecutive hits, or detect the fade with a brightness check instead.
Related commands
- ffmpeg split video into partsSplit a video into parts with FFmpegRead
- ffmpeg trim videoTrim a video with FFmpegRead
- ffmpeg video to gifConvert a video to GIF with FFmpegRead