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
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