A video can look perfectly reasonable on a laptop and still be a poor delivery asset. The file may exceed an upload limit, take too long to start on mobile, or consume far more bandwidth than the page around it.
A useful video compress workflow does not begin with the strongest preset. It begins with a delivery constraint: where the file will play, how large it may be, and which details must survive encoding.
This guide builds a repeatable workflow around FFmpeg. It covers source inspection, resolution decisions, constant-quality encoding, target-size encoding, and output validation.
TL;DR
- Inspect the source before changing it.
- Remove unused duration before reducing quality.
- Use CRF when visual quality matters more than an exact file size.
- Use a bitrate budget and two-pass encoding when a hard upload limit matters.
- Test the result in the real playback environment.
Start with the delivery constraint
The same source needs different treatment depending on where it is going. A product demo embedded in documentation has different requirements from a video attached to a support ticket.
| Delivery case | Primary constraint | What to verify |
|---|---|---|
| Web page preview | Startup time and transfer size | Playback on a slower network profile |
| Support or email attachment | Hard file-size limit | Final size with a safety margin |
| UI or code walkthrough | Text readability | Small labels, cursors, and terminal output |
| Social upload | Platform compatibility | Aspect ratio, audio sync, and platform re-encoding |
| Long-term archive | Source fidelity | Keep the original instead of optimizing only for delivery |
Write down the constraint before choosing an encoder setting. “Make it smaller” is not testable. “Keep the file below 95 MB while preserving readable 1080p UI text” is.
Inspect the source with ffprobe
Do not guess what is inside the file. Read its duration, dimensions, frame rate, codecs, and bitrate first:
ffprobe -v error \ -show_entries format=duration,size,bit_rate \ -show_entries stream=index,codec_type,codec_name,width,height,r_frame_rate,bit_rate \ -of json \ input.mp4 The output answers several useful questions:
- Is the source 4K even though it will render in a 900-pixel content column?
- Is a screen recording stored at 60 fps when 30 fps is sufficient for the motion it contains?
- Is an unexpectedly large audio track consuming part of the budget?
- Has the file already been heavily compressed?
Re-encoding an already damaged file cannot restore missing detail. Keep the original and generate each delivery version from that source.
Estimate where the bytes come from
For a quick estimate, file size is mostly a function of total bitrate and duration:
file size (MB) ≈ total bitrate (Mbps) × duration (seconds) ÷ 8 A ten-minute file with a total bitrate of 12 Mbps will be roughly 900 MB before container overhead is considered. That explains why resolution alone is not a reliable predictor of size. Two 1080p files with different bitrates can have very different weights.
Duration is the least destructive variable to reduce. Trim dead air, duplicate takes, loading screens, and long static endings before touching image quality.
Use CRF when quality is the main constraint
For most web previews and documentation videos, an exact byte count is less important than stable visual quality. The Constant Rate Factor mode in libx264 is a practical starting point:
ffmpeg -i input.mp4 \ -c:v libx264 \ -preset slow \ -crf 23 \ -c:a aac \ -b:a 128k \ -movflags +faststart \ output.mp4 The important options are:
-
-crf 23controls the quality target. Lower values retain more detail and usually produce larger files. A practical test range is 18–28. -
-preset slowgives the encoder more time to find efficient representations. Presets change encoding effort, not the requested CRF quality level. -
-b:a 128ksets the audio bitrate. Speech-heavy material may not need the same audio budget as music. -
-movflags +faststartmoves MP4 metadata toward the beginning of the file, which helps progressive web playback start sooner.
Run a short representative segment first. A screen recording with small text may need a lower CRF value than a talking-head clip with a simple background.
ffmpeg -ss 00:02:00 -t 30 -i input.mp4 \ -c:v libx264 -preset slow -crf 23 \ -c:a aac -b:a 128k \ sample.mp4 The command starts two minutes into the source and encodes a 30-second sample. Choose a section containing motion, text, gradients, and the details that matter to the viewer.
Scale only when the playback size justifies it
Keeping 4K pixels for a small documentation embed wastes bandwidth. On the other hand, reducing a UI walkthrough too aggressively can make labels unreadable.
To create a 1080p version while preserving the aspect ratio:
ffmpeg -i input.mp4 \ -vf "scale=-2:1080" \ -c:v libx264 -preset slow -crf 23 \ -c:a aac -b:a 128k \ -movflags +faststart \ output-1080p.mp4 The -2 lets FFmpeg calculate a compatible width from the original aspect ratio. Judge the result at the size users will actually see. Zooming to 400% can reveal artifacts, but it does not tell you whether the delivered experience is acceptable.
Build a bitrate budget for a hard size limit
CRF does not promise a specific output size. When an upload form rejects anything above a fixed limit, calculate a bitrate budget instead:
target total bitrate (kbps) ≈ target size (MB) × 8192 × 0.97 ÷ duration (seconds) The 0.97 factor leaves a small allowance for container overhead. Subtract the planned audio bitrate to estimate the video bitrate.
This small Node.js script makes the calculation repeatable:
// bitrate-budget.mjs const [targetMB, durationSeconds, audioKbps = "128"] = process.argv.slice(2); if (!targetMB || !durationSeconds) { console.error("Usage: node bitrate-budget.mjs <targetMB> <durationSeconds> [audioKbps]"); process.exit(1); } const totalKbps = (Number(targetMB) * 8192 * 0.97) / Number(durationSeconds); const videoKbps = Math.floor(totalKbps - Number(audioKbps)); if (videoKbps <= 0) { throw new Error("The target size is too small for the selected audio bitrate."); } console.log({ targetMB: Number(targetMB), durationSeconds: Number(durationSeconds), audioKbps: Number(audioKbps), videoKbps, }); For a 600-second video targeting 95 MB with 128 kbps audio:
node bitrate-budget.mjs 95 600 128 The calculation produces a video budget of about 1130 kbps. Use that value with two-pass encoding when final size is more important than encoding speed:
# Pass 1: analyze the video stream ffmpeg -y -i input.mp4 \ -c:v libx264 -b:v 1130k -pass 1 \ -an -f mp4 /dev/null # Pass 2: write the final video and audio ffmpeg -i input.mp4 \ -c:v libx264 -b:v 1130k -pass 2 \ -c:a aac -b:a 128k \ -movflags +faststart \ output-95mb.mp4 On Windows, replace /dev/null with NUL. The result can still vary slightly, so leave headroom instead of targeting the platform limit exactly.
Validate more than the final size
An effective video compress pass preserves the information the viewer needs. Check the output in motion, not only through a single thumbnail.
Inspect these areas:
- text, cursor edges, and thin interface lines;
- faces, hair, foliage, and other fine detail;
- gradients, shadows, and dark scenes;
- fast movement and scene changes;
- audio sync at the beginning, middle, and end;
- seeking and startup behavior in the target browser or app.
Run ffprobe on the output as well:
ffprobe -v error \ -show_entries format=duration,size,bit_rate \ -show_entries stream=codec_type,codec_name,width,height,r_frame_rate \ -of json \ output.mp4 For automated pipelines, record the command, source checksum, output size, duration, and encoder version in a manifest. That turns a one-off optimization into a reproducible build step.
Where a browser tool fits
FFmpeg is a better foundation when the process must be scripted, reviewed, or repeated across many files. A browser workflow can still be useful for an occasional comparison when no local media environment is available.
For example, a non-sensitive sample can be processed in VideoCompress and compared with the local FFmpeg output. Judge both versions using the same source segment and the same playback conditions. Confidential recordings and unreleased material should remain in an approved environment.
The tool is only a comparison point here; the workflow and validation criteria remain the same regardless of implementation.
Common mistakes
- Starting with the strongest preset: optimize for the delivery constraint, not the largest reduction percentage.
- Re-encoding the previous output: return to the original source for every experiment to avoid cumulative damage.
- Changing several variables at once: test CRF, scale, frame rate, and audio settings separately when diagnosing quality loss.
- Ignoring short motion samples: a static frame can hide blocking, smearing, and temporal artifacts.
- Targeting the exact upload ceiling: reserve a few megabytes for bitrate variation and container overhead.
- Keeping only the optimized copy: delivery files are not substitutes for an archive-quality original.
Pre-publish checklist
- [ ] The delivery size, playback environment, and quality requirement are written down.
- [ ] The source duration, resolution, frame rate, codecs, and bitrate were inspected.
- [ ] Unused duration was removed before lowering quality.
- [ ] A representative short segment was tested first.
- [ ] The output was reviewed at its actual display size.
- [ ] Text, motion, gradients, and audio sync were checked.
- [ ] The final file has headroom below the upload limit.
- [ ] Browser startup and seeking were tested where relevant.
- [ ] The untouched source remains stored separately.
Final thoughts
A reliable video compress workflow is a constraint-solving process, not a race toward the smallest file. Inspect the source, choose either a quality target or a size budget, test a representative segment, and validate the result where it will actually play.
Once those decisions are explicit, they can move into a script or build pipeline. That is the point where video delivery stops being a collection of guesses and becomes a repeatable engineering task.