halfsize

halfsize / FFmpeg target size

FFmpeg: compress to an exact target size

-crf asks for quality and lets the file size land wherever it wants. When a form says 20 MB, you need the opposite: a bitrate budget, two passes, and a check that the result actually fits.

Why -crf does not hit a size

Constant Rate Factor targets a perceptual quality level. A talking-head clip and a fireworks clip at the same CRF can differ by 4× in size, because the encoder spends bits where the picture needs them. That is the right behaviour for archiving and the wrong behaviour for an upload form with a hard cap.

You can guess, encode, check the size, and encode again. That loop is what most people mean by "just use HandBrake". It works, but it costs an encode per guess and it never tells you what the picture lost.

The bitrate budget

Video encoders think in bits per second. A file-size target converts to a bitrate:

video_kbps = (target_bytes × 8 ÷ seconds) − audio_kbps − container_overhead

A 60-second clip at 20 MB (20,000,000 bytes) is 2.67 Mbps total. Remove 128 kbps of AAC audio and roughly 1.5% for MP4 container overhead, and you have about 2.46 Mbps for the video stream.

That number is the whole trick. Everything else is bookkeeping: rotation metadata, HDR, variable frame rate, whether the source audio is already low bitrate and re-encoding it would waste bits.

Two-pass x264

Average bitrate mode in a single pass has to guess how complex the next scene will be. Two-pass fixes that: the first pass analyses the whole clip, the second spends the budget where it matters.

$ ffmpeg -y -i input.mp4 -c:v libx264 -b:v 2460k \
    -pass 1 -passlogfile ffmpeg2pass -an -f mp4 /dev/null
$ ffmpeg -i input.mp4 -c:v libx264 -b:v 2460k \
    -pass 2 -passlogfile ffmpeg2pass -c:a aac -b:a 128k \
    -movflags +faststart output.mp4

On Windows, replace /dev/null with NUL. Delete the ffmpeg2pass* log files afterwards; they are only needed between the two passes.

Why the result still misses by a few percent

Three things bend the number:

Landing 1–3% under the cap is the correct outcome. 19.45 MB is accepted everywhere. 20.01 MB is rejected by the same form.

Verify the size and the quality

$ ffprobe -v error -show_entries format=duration,size \
    -of default=noprint_wrappers=1 output.mp4

$ ffmpeg -i output.mp4 -i input.mp4 -lavfi libvmaf -f null -

VMAF scores perceptual similarity from 0 to 100. Above 90 reads as “looks the same”. 80–90 is “slight softening”. 70–80 is visible loss. A size target without this number is a guess with extra steps.

What halfsize automates

$ npx halfsize input.mp4 --to 20mb

  target     20.0 MB
  plan       H.264  1280x720  2.46 Mbps  audio 128 kbps  2-pass
  scale      1920x1080 → 1280x720
  output     input.20mb.mp4

  136 MB  →  19.45 MB   7.0x smaller   2.7% under target
  VMAF 88.3   SSIM 0.971   slight softening

Measured results

60-second 1080p30 source at 18 Mbps, default settings, one laptop:

TargetOutputResolutionVideo bitrateVMAF
20 MB19.45 MB1280×7202.46 Mbps88.3
16 MB15.59 MB960×5401.94 Mbps84.8
10 MB9.77 MB960×5401.24 Mbps82.9
2 MB1.99 MB960×5400.25 Mbps79.9

FAQ

Is two-pass always better than CRF?

For a hard size cap, yes: it is the only mode that spends the budget predictably. For quality-first work, CRF is faster and usually better, because it does not waste bits on scenes that do not need them.

Can I just use -fs 20M?

-fs stops writing when the file reaches the limit, which can truncate mid-GOP. The file may be the right size and unplayable at the end. It also gives you no control over quality distribution.

Does H.265 help?

Usually 30–50% better compression at the same quality, so you can keep more resolution. The catch is playback: older Windows machines, many browsers, and some editors cannot decode it. For a file that must play anywhere, H.264 is still the safe answer.

Why is my 10-second clip over the target?

Short clips give the rate controller less to average over, and container overhead is a larger fraction. Add 5% margin, or set a slightly lower bitrate by hand.

How exact is “exact”?

Typically 1–3% under. The encoder cannot promise a byte count before it finishes, so a small margin is deliberate.

Guides by destination