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.mp4
Prefer no terminal?
Use the Video Compressor in your browser
Open Video Compressor →

What each flag does

-b:vTarget video bitrate. Computed from desired size and duration: bitrate_kbps ≈ size_MB × 8000 / duration_seconds.
-pass 1First pass: analyze the video to build a bitrate distribution map. No output written.
-pass 2Second pass: encode using the map for optimal bit allocation.
-anNo 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.mp4

Close 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 arithmeticTarget 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 qualityUse this only when a hard cap exists. If you just want it smaller, CRF gives a better file for the same effort.
Leave roomContainer 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.

Related recipes