Extracting the last frame of a video sounds simple, but the naive approach — decoding the entire video to get the final image — is needlessly expensive. For a 2-hour movie, that means decoding 170,000+ frames just to save one.
FFmpeg has a way to skip straight to the end. This article explains exactly how it works and gives you ready-to-use scripts for both Windows and Unix environments.
ffmpeg -sseof -1 -i "input.mp4" -update 1 "output.png"
The Commands
Windows (PowerShell)
# Define paths
$InputVideo = "input.mp4"
$OutputImage = "output.png"
# -sseof -1 : Seek to 1 second before the end of the file
# -update 1 : Overwrite the output file on every frame (keeps the last one)
ffmpeg -y -sseof -1 -i "$InputVideo" -update 1 -vframes 1 "$OutputImage"
macOS / Linux (Bash)
#!/bin/bash
INPUT="input.mp4"
OUTPUT="output.png"
# -sseof -1 : Input seek to 1 second before EOF
# -update 1 : Tell the image2 muxer to overwrite on each frame
ffmpeg -y -sseof -1 -i "$INPUT" -update 1 "$OUTPUT"
How This Works Internally
The command is short, but it exploits specific FFmpeg pipeline behaviors. Here's what's happening under the hood.
1. -sseof -1: Input Seeking from the End of File
FFmpeg's seek options behave differently depending on where in the command they appear.
- Placed before
-i(input seeking): FFmpeg jumps to the specified position in the file before starting to decode. This operates at the demuxer level, meaning the decoder never sees the content before that position. -sseof -1: Thesseofvariant specifies the position relative to the end of the file.-1means "1 second before the end."
The result: for a 2-hour video, FFmpeg reads only the last ~1 second of data. The processing time is essentially constant regardless of file length — milliseconds, not minutes.
2. -update 1: The Overwrite Loop
By default, when FFmpeg outputs to image files, the image2 muxer expects sequential filenames like frame001.png, frame002.png. Pointing it at a single fixed filename either errors out or stops after the first frame.
-update 1 changes this behavior:
- The muxer is told: "each new frame should overwrite the existing file"
- FFmpeg decodes the last ~1 second of video, generating frames sequentially
- Each frame overwrites
output.png - When the stream ends (EOF), whatever was written last is the chronologically final frame of the video — which is exactly what you want
3. Keyframe snapping behavior
Input seeking with -sseof doesn't land on an exact timestamp. It snaps to the nearest keyframe (I-frame) at or before the specified position. Depending on the video's GOP (Group of Pictures) structure, this might mean FFmpeg starts reading from 2–3 seconds before the end rather than exactly 1 second.
This doesn't affect the output — you still get the final frame of the video. But it means the decoder might process more than just 1 second of content. For most use cases this is fine.
4. PNG vs. JPG output
For PNG output, no quality flag is needed — PNG is lossless. For JPG:
ffmpeg -y -sseof -1 -i "$INPUT" -update 1 -q:v 2 "output.jpg"
-q:v for JPEG ranges from 2 (highest quality) to 31 (lowest). Values 2–5 are typically appropriate for thumbnails.
Comparison with Naive Approaches
| Method | What it does | Problem |
|---|---|---|
| Full decode approach (not recommended) | Decode from start, output last frame | Decodes the entire video; time scales linearly with length |
| ffprobe frame count → select | Count frames with ffprobe, then seek | Two operations; ffprobe itself can be slow |
-sseof -1 -update 1 -vframes 1 | Seeks directly to near-EOF, overwrites | Near-constant time regardless of video length |
For a 30-minute video at 30fps, the naive full-decode approach processes 54,000 frames. The -sseof approach processes at most a few hundred. The difference scales linearly with video length.
Batch Processing Scripts
Bash: Process all MP4s in a directory
#!/bin/bash
INPUT_DIR="./videos"
OUTPUT_DIR="./thumbnails"
mkdir -p "$OUTPUT_DIR"
for video in "$INPUT_DIR"/*.mp4; do
filename=$(basename "$video" .mp4)
output="$OUTPUT_DIR/${filename}_last_frame.png"
ffmpeg -y -sseof -1 -i "$video" -update 1 "$output"
echo "Done: $filename"
done
echo "All done. Thumbnails in: $OUTPUT_DIR"
PowerShell: Batch process with error handling
$InputDir = ".\videos"
$OutputDir = ".\thumbnails"
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
$videos = Get-ChildItem -Path $InputDir -Filter "*.mp4"
$total = $videos.Count
$count = 0
foreach ($video in $videos) {
$count++
$filename = $video.BaseName
$output = Join-Path $OutputDir "${filename}_last_frame.png"
Write-Progress -Activity "Extracting last frames" `
-Status "$filename ($count/$total)" `
-PercentComplete (($count / $total) * 100)
ffmpeg -y -sseof -1 -i $video.FullName -update 1 $output 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "OK: $filename"
} else {
Write-Host "FAILED: $filename" -ForegroundColor Red
}
}
Write-Host "Complete. $count files processed."
Summary
Extracting the last frame of a video efficiently comes down to two FFmpeg options working together:
-sseof -1positions the read pointer near the end of the file without decoding anything before it-update 1ensures that as FFmpeg decodes the final seconds, each frame overwrites the previous one — leaving the chronologically last frame as the output file
This approach works in constant time regardless of video length. For a batch job processing thousands of videos, the difference between this method and full decoding can be measured in hours.