We've all been there. You need to convert a video, so you Google it and find a Stack Overflow answer with a command that looks like someone smashed their keyboard: ffmpeg -i input.mov -c:v libx264 -preset slow -crf 22 -c:a aac -b:a 128k -movflags +faststart output.mp4. You run it. It works. You have absolutely no idea why, and next time you need something slightly different, you're back on Stack Overflow. I did this for years before I finally sat down and figured out what FFmpeg actually wants from me.
The thing is, FFmpeg isn't actually that complicated. It has a consistent mental model that, once you grasp it, makes almost every command readable. The problem is that nobody explains the model — they just throw commands at you. So let's fix that.
Containers vs Codecs: The First Thing to Understand
The single biggest source of confusion with video files is the difference between containers and codecs. A container is the file format — .mp4, .mkv, .webm, .mov. It's a box that holds things. A codec is how the actual video and audio data inside that box is compressed — H.264, H.265, VP9, AV1, AAC, Opus.
An MP4 file can contain H.264 video with AAC audio, or H.265 video with Opus audio, or dozens of other combinations. A MKV file can contain virtually anything. When someone says "convert to MP4," they usually mean "put H.264 video and AAC audio into an MP4 container" — but those are three separate decisions.
# See what's actually inside a file
$ ffprobe -hide_banner video.mkv
Input #0, matroska:
Stream #0:0: Video: hevc (Main), 1920x1080, 23.98 fps
Stream #0:1: Audio: opus, 48000 Hz, stereo
Stream #0:2: Subtitle: subrip
# This file is:
# Container: MKV (Matroska)
# Video codec: HEVC (H.265)
# Audio codec: Opus
# Plus a subtitle stream
Use ffprobe before doing anything. It tells you exactly what you're working with. Half the time, the "conversion" you need is just remuxing — moving the same streams into a different container without re-encoding. That's nearly instant.
The FFmpeg Command Structure
Every FFmpeg command follows the same pattern, and once you see it, you can't unsee it:
ffmpeg [global options] [input options] -i input [output options] output
# The KEY insight: options apply to the NEXT file.
# Options before -i apply to the input.
# Options after -i (before the output filename) apply to the output.
# This matters! These two commands are different:
ffmpeg -ss 00:01:00 -i video.mp4 output.mp4 # Seeks BEFORE decoding (fast)
ffmpeg -i video.mp4 -ss 00:01:00 output.mp4 # Seeks AFTER decoding (slow, precise)
Position matters in FFmpeg. This is the thing that trips up everyone. An option's meaning can change based on whether it appears before or after -i. The -ss example above is the most common case: putting it before -i makes FFmpeg seek directly in the input file (fast but potentially imprecise), while putting it after makes FFmpeg decode everything up to that point and then start outputting (slow but frame-accurate).
Stream Selection: Telling FFmpeg What You Want
Video files often contain multiple streams — video, audio, subtitles, sometimes multiple audio tracks in different languages. FFmpeg uses a stream specifier syntax to let you target specific streams:
-c:v— codec for video streams (v = video)-c:a— codec for audio streams (a = audio)-c:s— codec for subtitle streams (s = subtitle)-c copy— copy ALL streams without re-encoding-c:v copy -c:a aac— copy video as-is, re-encode audio to AAC-map 0:a:1— select the second audio stream from the first input
The -c copy flag is the most important one to know. It tells FFmpeg to copy the stream data directly without re-encoding. This is basically instant and lossless — you're just moving data from one container to another. Anytime you don't need to change the codec, use -c copy.
# Remux MKV to MP4 without re-encoding (instant)
ffmpeg -i input.mkv -c copy output.mp4
# Copy video but re-encode audio (fast — only audio is processed)
ffmpeg -i input.mkv -c:v copy -c:a aac -b:a 192k output.mp4
# Extract just the audio stream
ffmpeg -i video.mp4 -vn -c:a copy audio.aac
# -vn means "no video" — drop the video stream entirely
Practical Recipes You'll Actually Use
Here are the commands I use constantly. Each one is explained so you can modify them for your specific needs.
Compress a Video for the Web
ffmpeg -i raw_video.mov \
-c:v libx264 \
-preset slow \
-crf 23 \
-c:a aac -b:a 128k \
-movflags +faststart \
web_video.mp4
# -c:v libx264 → encode video with H.264
# -preset slow → slower encoding = smaller file (options: ultrafast to veryslow)
# -crf 23 → quality level (18=near lossless, 23=default, 28=low quality)
# -c:a aac → encode audio as AAC
# -b:a 128k → audio bitrate 128 kbps
# -movflags +faststart → move metadata to start of file for streaming
The CRF (Constant Rate Factor) value is the most important quality knob. Lower numbers mean higher quality and bigger files. For most web video, CRF 20-25 is the sweet spot. I typically use 22 for anything that needs to look good and 26 for background/supplementary video where quality matters less.
Trim a Video Without Re-encoding
# Extract from 1:30 to 3:45 — no re-encoding, nearly instant
ffmpeg -ss 00:01:30 -to 00:03:45 -i input.mp4 -c copy trimmed.mp4
# If you need frame-accurate cuts (slower):
ffmpeg -i input.mp4 -ss 00:01:30 -to 00:03:45 \
-c:v libx264 -crf 18 -c:a copy trimmed.mp4
Generate Thumbnails
# Single thumbnail at the 10-second mark
ffmpeg -ss 00:00:10 -i video.mp4 -frames:v 1 thumb.jpg
# One thumbnail every 30 seconds
ffmpeg -i video.mp4 -vf "fps=1/30" thumbs/thumb_%04d.jpg
# Resize to 320px wide, maintain aspect ratio
ffmpeg -ss 00:00:10 -i video.mp4 -frames:v 1 \
-vf "scale=320:-1" thumb_small.jpg
Create a GIF From a Video Clip
# Good quality GIF with palette generation (two-pass)
ffmpeg -ss 00:00:05 -t 3 -i video.mp4 \
-vf "fps=15,scale=480:-1:flags=lanczos,split[s0][s1]; \
[s0]palettegen[p];[s1][p]paletteuse" \
output.gif
# -t 3 → duration: 3 seconds
# fps=15 → 15 frames per second (keep GIFs small)
# scale=480:-1 → resize to 480px wide
# palettegen/use → generate optimal color palette (much better quality)
Understanding Filters: The Power Feature
Filters are where FFmpeg goes from "useful tool" to "absurdly powerful." The -vf flag (video filter) and -af flag (audio filter) let you chain processing operations. Filters are separated by commas for sequential processing, or semicolons for complex filter graphs.
# Chain multiple filters: resize, then add padding for exact dimensions
ffmpeg -i input.mp4 -vf "scale=1280:720:force_original_aspect_ratio=decrease,\
pad=1280:720:(ow-iw)/2:(oh-ih)/2:black" output.mp4
# Normalize audio volume
ffmpeg -i input.mp4 -af "loudnorm" -c:v copy output.mp4
# Speed up a video 2x (with audio pitch correction)
ffmpeg -i input.mp4 -vf "setpts=0.5*PTS" -af "atempo=2.0" fast.mp4
# Add text overlay
ffmpeg -i input.mp4 \
-vf "drawtext=text='DRAFT':fontsize=72:fontcolor=red:x=10:y=10" \
watermarked.mp4
The filter system is deep enough to fill its own article, but the key insight is this: filters process decoded frames. That means you can't use -c copy with filters — the video has to be decoded, filtered, and re-encoded. If your command uses -vf, budget time for a full re-encode.
Hardware Acceleration: Use Your GPU
If you're encoding video regularly, hardware acceleration can speed things up by 5-10x. Modern GPUs have dedicated encoding hardware that FFmpeg can use.
# NVIDIA GPU (NVENC)
ffmpeg -i input.mp4 -c:v h264_nvenc -preset p4 -crf 23 output.mp4
# Apple Silicon (VideoToolbox)
ffmpeg -i input.mp4 -c:v h264_videotoolbox -b:v 5M output.mp4
# Intel Quick Sync (QSV)
ffmpeg -i input.mp4 -c:v h264_qsv -preset medium output.mp4
# Check what's available on your system:
ffmpeg -encoders 2>/dev/null | grep -E '(nvenc|videotoolbox|qsv|vaapi)'
A word of caution: hardware encoders are faster but typically produce slightly larger files at the same visual quality compared to software encoding (libx264). For one-off transcodes where quality matters, software encoding is still better. For batch processing or real-time streaming, hardware acceleration is the clear winner. I use libx264 for final production encodes and NVENC for everything else.
Common Mistakes and How to Avoid Them
- Re-encoding when you don't need to. If you're just trimming, extracting audio, or changing containers, use
-c copy. There's no reason to spend 20 minutes re-encoding a video you're just cutting 30 seconds off of. - Using too low a CRF value. CRF 18 produces massive files with quality improvements most people can't see. Start at 23, go lower only if you see artifacts. For most web content, 23-25 is plenty.
- Forgetting
-movflags +faststart. Without this, MP4 metadata sits at the end of the file, which means browsers can't start playing until the entire file downloads. Always add it for web video. - Not checking the input first. Run
ffprobebefore writing your command. Knowing the input codecs, resolution, and stream layout makes everything easier. - Piping to nowhere. If FFmpeg seems stuck, it might be waiting for you to answer a question (like whether to overwrite an existing file). Add
-yto auto-overwrite, or-nto never overwrite.
The Cheat Sheet
Here's what I keep in a text file on my desktop. These cover about 90% of what I need day to day:
# Probe a file
ffprobe -hide_banner input.mp4
# Remux (change container, no re-encode)
ffmpeg -i input.mkv -c copy output.mp4
# Compress for web
ffmpeg -i input.mov -c:v libx264 -crf 23 -preset medium \
-c:a aac -b:a 128k -movflags +faststart output.mp4
# Extract audio only
ffmpeg -i video.mp4 -vn -c:a copy audio.m4a
# Trim without re-encoding
ffmpeg -ss 00:01:00 -to 00:02:30 -i input.mp4 -c copy clip.mp4
# Resize to 720p
ffmpeg -i input.mp4 -vf "scale=-1:720" -c:a copy output.mp4
# Convert to GIF
ffmpeg -ss 5 -t 3 -i input.mp4 \
-vf "fps=12,scale=400:-1" output.gif
# Concatenate files (same codec)
echo "file 'part1.mp4'" > list.txt
echo "file 'part2.mp4'" >> list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy combined.mp4
FFmpeg isn't a tool you master in a day. But once you understand the mental model — containers hold streams, streams have codecs, options apply to the next file, filters require re-encoding — the documentation starts making sense. You stop copying commands blindly and start composing them intentionally. And honestly, that shift from "this is magic" to "oh, that's what it's doing" is one of the more satisfying moments you can have as a developer.