FFmpeg recipe
FFmpeg: Compress an MP4 to a Target File Size
When you must hit a specific size (e.g. 25 MB for email), two-pass bitrate encoding is the way. Calculate target bitrate, then run two passes.
Command
# duration=60s, target=24MB → bitrate ≈ 24*8/60 = 3200 kbps total
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 3000k -pass 1 -an -f mp4 /dev/null && \
ffmpeg -i input.mp4 -c:v libx264 -b:v 3000k -pass 2 -c:a aac -b:a 128k -y output.mp4Prefer no terminal?
Use the Video Compressor in your browser
What each flag does
| -b:v | Target video bitrate. Computed from desired size and duration: bitrate_kbps ≈ size_MB × 8000 / duration_seconds. |
|---|---|
| -pass 1 | First pass: analyze the video to build a bitrate distribution map. No output written. |
| -pass 2 | Second pass: encode using the map for optimal bit allocation. |
| -an | No audio in pass 1 to save time. |
Notes & gotchas
- Two-pass is ~2x slower than single-pass CRF but hits target sizes accurately.
- On Windows, use NUL instead of /dev/null in pass 1.
Check it worked
ls -lh output.mp4Close to the size you targeted, usually within a few percent. A large overshoot means the audio bitrate was not subtracted from the budget.
What to change
| The bitrate is arithmetic | Target size in bits, minus the audio, divided by the duration in seconds. Get that wrong and two-pass hits the wrong number precisely. |
|---|---|
| Two passes for a size, CRF for a quality | Use this only when a hard cap exists. If you just want it smaller, CRF gives a better file for the same effort. |
| Leave room | Container overhead is real. Aim a few percent under the cap rather than exactly at it. |
If it fails
The result overshoots the target size.
Why: The bitrate calculation counted only video and ignored the audio track and container overhead.
Fix: Subtract the audio bitrate from the budget before dividing by duration, and leave two or three percent for the container.
Quality is uneven, with some passages much worse than others.
Why: Single-pass bitrate targeting has to guess how much to spend before it has seen the whole file.
Fix: Use two-pass: run with -pass 1 -f null /dev/null first, then the same command with -pass 2.