▸ Basics
Simplest conversion
ffmpeg -i input.mov output.mp4 # container + codec inferred from output extension
Overwrite / never-overwrite output
ffmpeg -y -i input.mov output.mp4 # -y: always overwrite ffmpeg -n -i input.mov output.mp4 # -n: never overwrite, exit if it exists
Container vs. codec — the mental model
Key idea: the file extension (
.mp4, .mkv, .webm) picks the container (a box format for streams + metadata); -c:v/-c:a pick the codec (how the bits inside are actually encoded). Most containers can hold several codecs — that's why "convert to mp4" is ambiguous until you also say which codec.Quiet / verbose output
ffmpeg -v quiet -stats -i input.mp4 output.mp4 # silence banner, keep progress ffmpeg -hide_banner -i input.mp4 output.mp4 # drop the build/config banner only
Multiple inputs
ffmpeg -i video.mp4 -i audio.aac -c copy muxed.mp4
▸ Codecs & containers
Pick a video / audio codec explicitly
ffmpeg -i in.mov -c:v libx264 -c:a aac out.mp4 ffmpeg -i in.mov -c:v libvpx-vp9 -c:a libopus out.webm
Stream copy — remux without re-encoding
ffmpeg -i in.mkv -c copy out.mp4 # fast, lossless — only works if codecs are container-compatible
Tip:
-c copy is near-instant because no pixels are decoded — it's just repackaging streams. It fails if the target container can't hold the source codec (e.g. some subtitle formats into .mp4).Common container / codec pairings
| Container | Typical video | Typical audio |
|---|---|---|
.mp4 | h264, hevc | aac |
.webm | vp9, av1 | opus |
.mkv | almost anything | almost anything |
.mov | h264, prores | aac, pcm |
List available encoders / decoders
ffmpeg -encoders | grep 264 ffmpeg -decoders | grep vp9
▸ Transcoding & quality
Constant Rate Factor (CRF) — quality-targeted, variable bitrate
ffmpeg -i in.mp4 -c:v libx264 -crf 23 -preset medium out.mp4 # CRF: 0 = lossless, 18 ≈ visually lossless, 23 = default, 51 = worst. Lower = bigger + better.
Encoder presets — speed vs. compression efficiency
ffmpeg -i in.mp4 -c:v libx264 -preset veryslow -crf 20 out.mp4 # ultrafast → superfast → veryfast → faster → fast → medium → slow → slower → veryslow
Target bitrate (one-pass)
ffmpeg -i in.mp4 -c:v libx264 -b:v 2M -maxrate 2.5M -bufsize 4M out.mp4
Two-pass encoding — best bitrate-targeted quality
ffmpeg -y -i in.mp4 -c:v libx264 -b:v 2M -pass 1 -an -f null /dev/null ffmpeg -i in.mp4 -c:v libx264 -b:v 2M -pass 2 -c:a aac out.mp4 # pass 1 writes ffmpeg2pass-0.log (stats only, no output file needed); pass 2 reads it
Which to use? CRF for "make it look good, size doesn't matter." Two-pass bitrate for "must fit in exactly N MB" (upload caps, fixed-size streaming targets).
▸ Resize & crop
Scale to a fixed width, auto height
ffmpeg -i in.mp4 -vf "scale=1280:-2" out.mp4 # -2 keeps height even (required by most codecs); -1 doesn't guarantee that
Crop
ffmpeg -i in.mp4 -vf "crop=640:360:0:0" out.mp4 # crop=out_w:out_h:x:y — x,y is the top-left corner of the crop rectangle
Pad (letterbox / add borders)
ffmpeg -i in.mp4 -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2" out.mp4 # scale-to-fit then pad to exact target size, centered
Rotate / flip
ffmpeg -i in.mp4 -vf "transpose=1" out.mp4 # 90° clockwise; 0=CCW, 2=CCW+flip, 3=CW+flip ffmpeg -i in.mp4 -vf "hflip" out.mp4
▸ Trim & concatenate
Fast trim (input seek — before -i)
ffmpeg -ss 00:00:10 -i in.mp4 -t 30 -c copy out.mp4 # -ss before -i seeks the demuxer directly — fast, but may snap to the nearest keyframe with -c copy
Accurate trim (output seek — after -i)
ffmpeg -i in.mp4 -ss 00:00:10 -t 30 out.mp4 # -ss after -i decodes from the start and seeks precisely — slower, frame-accurate, requires re-encode
-to vs. -t
ffmpeg -ss 10 -to 40 -i in.mp4 out.mp4 # -to is an absolute timestamp in the source ffmpeg -ss 10 -t 30 -i in.mp4 out.mp4 # -t is a duration relative to -ss
Concatenate — same codec/container (concat demuxer)
# list.txt: file 'part1.mp4' file 'part2.mp4' file 'part3.mp4' ffmpeg -f concat -safe 0 -i list.txt -c copy joined.mp4 # -safe 0 allows absolute/relative paths outside the list file's directory
Concatenate — different codecs (concat filter, re-encodes)
ffmpeg -i part1.mp4 -i part2.mp4 \ -filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]" \ -map "[v]" -map "[a]" joined.mp4
▸ Filters & filter graphs
Simple filter chains — -vf / -af
ffmpeg -i in.mp4 -vf "scale=1280:-2,eq=brightness=0.05" out.mp4 # comma chains filters left-to-right through a single linear stream
Filter graphs — -filter_complex
ffmpeg -i in.mp4 -filter_complex "split[s0][s1];[s0]scale=640:-2[small];[s1]scale=1920:-2[large]" \ -map "[small]" small.mp4 -map "[large]" large.mp4 # needed whenever a graph has multiple inputs/outputs or branches — -vf can't express that
Labeled nodes & semicolons
Reading a graph:
; separates independent chains, , chains filters in sequence, [label] names a pad so a later chain can consume it. split[s0][s1] duplicates one input into two labeled outputs.Overlay (picture-in-picture / watermark)
ffmpeg -i main.mp4 -i logo.png \ -filter_complex "overlay=W-w-10:H-h-10" out.mp4 # W/H = main video size, w/h = overlay size — bottom-right corner with 10px margin
Drawtext (burn in a timestamp / label)
ffmpeg -i in.mp4 -vf "drawtext=text='%{pts\\:hms}':x=10:y=10:fontsize=24:fontcolor=white" out.mp4 # requires libfreetype — not compiled into every build; not run locally, syntax from ffmpeg.org's drawtext docs
▸ Audio
Extract audio only
ffmpeg -i in.mp4 -vn -c:a copy audio.aac ffmpeg -i in.mp4 -vn -c:a libmp3lame -q:a 2 audio.mp3
Replace / mux in a different audio track
ffmpeg -i video.mp4 -i new_audio.aac \ -map 0:v -map 1:a -c:v copy -c:a copy -shortest out.mp4 # -shortest stops output at the shorter of the two streams
Loudness normalization — loudnorm (EBU R128)
# pass 1: measure ffmpeg -i in.mp4 -af "loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json" -f null - # reads back measured_I / measured_TP / measured_LRA / measured_thresh from the JSON it prints # pass 2: apply, feeding the measured_* values back in ffmpeg -i in.mp4 \ -af "loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-21.8:measured_TP=-17.7:measured_LRA=0.1:measured_thresh=-31.8:linear=true" \ out.mp4
Why two passes: single-pass
loudnorm normalizes dynamically as it goes and can drift; feeding the first pass's measured values back in as a second pass produces a linear, more accurate correction.Resample / remap channels
ffmpeg -i in.wav -ar 48000 -ac 2 out.wav # -ar sample rate, -ac channel count
▸ Subtitles
Not run locally: this build lacks
libass, so subtitles below is documentation-only — syntax sourced from ffmpeg.org's filters reference, not executed. (drawtext, covered in the Filters section above, has the same caveat for its own reason — missing libfreetype.) Everything else on this page was run against a real file.Burn in (hardcode into the picture)
ffmpeg -i in.mp4 -vf "subtitles=movie.srt" out.mp4 # irreversible — viewers can't turn it off. Watch path escaping on Windows (colons in drive letters).
Soft-mux (selectable track, no re-encode of video)
ffmpeg -i in.mp4 -i subs.srt \ -map 0 -map 1 -c copy -c:s mov_text out.mp4 # .mp4/.mov need the mov_text subtitle codec; .mkv accepts srt/ass natively with -c:s copy
Extract a subtitle track
ffmpeg -i in.mkv -map 0:s:0 out.srt
▸ Thumbnails & GIFs
Single-frame thumbnail
ffmpeg -i in.mp4 -ss 00:00:05 -frames:v 1 thumb.jpg
Frames at a fixed interval
ffmpeg -i in.mp4 -vf "fps=1" frame_%03d.jpg # one frame per second
High-quality GIF — two-pass palette
# two commands (writes an intermediate palette.png): ffmpeg -i in.mp4 -vf "fps=15,scale=480:-1:flags=lanczos,palettegen" palette.png ffmpeg -i in.mp4 -i palette.png -lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse" out.gif # one command, same result, via filter_complex + split: ffmpeg -i in.mp4 \ -filter_complex "fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \ out.gif
Why not just
ffmpeg -i in.mp4 out.gif? Direct GIF encoding quantizes to a fixed generic 256-color palette per frame and looks noticeably banded. palettegen/paletteuse derives one optimized palette for the whole clip first.▸ Streaming (HLS / DASH / RTMP)
Segment into an HLS playlist
ffmpeg -i in.mp4 -c:v libx264 -c:a aac \ -hls_time 6 -hls_list_size 0 \ -hls_segment_filename "segment_%03d.ts" index.m3u8 # -hls_time: target segment length (sec); -hls_list_size 0: keep every segment in the playlist (VOD, not live)
Segment into DASH
ffmpeg -i in.mp4 -c:v libx264 -c:a aac \ -f dash manifest.mpd
Push to an RTMP endpoint (live streaming)
ffmpeg -re -i in.mp4 -c:v libx264 -preset veryfast -c:a aac \ -f flv rtmp://live.example.com/app/stream_key # -re: read input at native frame rate instead of as fast as possible — required for live push
Not run: this needs a live RTMP endpoint to push to — no server was on hand to test against. HLS and DASH above were both verified by actually segmenting a file.
▸ Hardware acceleration
Verified on: VideoToolbox commands below were run on Apple Silicon for this cheatsheet. NVENC and VAAPI are documentation-only — sourced from NVIDIA's and ffmpeg.org's official docs, not executed (this machine has neither an Nvidia GPU nor a VAAPI-capable Linux/Intel setup).
VideoToolbox (macOS)
ffmpeg -i in.mp4 -c:v h264_videotoolbox -b:v 1M out.mp4
NVENC (Nvidia)
ffmpeg -vsync 0 -hwaccel cuda -hwaccel_output_format cuda -i in.mp4 \ -c:v h264_nvenc -b:v 5M -c:a copy out.mp4
VAAPI (Linux / Intel & AMD)
ffmpeg -init_hw_device vaapi=hw:/dev/dri/renderD128 -i in.mp4 \ -vf "format=nv12,hwupload" -c:v h264_vaapi -qp 25 out.mp4
List available hwaccels on this build
ffmpeg -hwaccels
▸ ffprobe basics
Full stream + format dump
ffprobe -v error -show_format -show_streams in.mp4
Just the fields you want
ffprobe -v error \ -show_entries stream=codec_type,codec_name,width,height \ -of default=noprint_wrappers=1 in.mp4
Duration only
ffprobe -v error -show_entries format=duration -of csv=p=0 in.mp4
-v error vs. default verbosity
Always add
-v error when scripting against ffprobe output — the default log level otherwise mixes informational banner text into stdout/stderr and breaks naive parsing.▸ ffprobe + JSON
Structured output for scripting
ffprobe -v error -of json \ -show_entries "format=duration:stream=width,height,codec_name" \ in.mp4
Pipe into jq
ffprobe -v error -of json -show_entries format=duration in.mp4 | jq -r .format.duration
Resolution as a single string
RES=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 in.mp4) echo "$RES" # → 1920x1080
Codec name only
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 in.mp4
▸ Batch & scripting
Convert every file in a directory
for f in *.mov; do ffmpeg -i "$f" -c:v libx264 -crf 23 "${f%.mov}.mp4" done
Parallel batch with xargs
ls *.mov | xargs -P 4 -I{} ffmpeg -i {} -c:v libx264 -crf 23 {}.mp4 # -P 4: four ffmpeg processes at once — tune to core count / thermal headroom
Watch-folder pattern
fswatch -0 ./incoming | while read -d "" f; do ffmpeg -i "$f" -c:v libx264 "./done/$(basename "$f").mp4" done # fswatch (macOS/Linux) triggers on new files; swap for inotifywait on Linux-only boxes
Not run:
fswatch isn't installed on the machine this was written on — the ffmpeg invocation inside the loop uses the same transcode pattern verified elsewhere on this page, but the loop itself wasn't executed.▸ Real-world recipes
Compress for Slack / email attachment limits
ffmpeg -i in.mov -vf "scale=1280:-2" -c:v libx264 -crf 28 -preset fast \ -c:a aac -b:a 96k compressed.mp4
Screen recording → shareable mp4
ffmpeg -i screen_recording.mov -vf "fps=30,scale=1920:-2" \ -c:v libx264 -crf 20 -pix_fmt yuv420p shareable.mp4 # -pix_fmt yuv420p: some source formats (e.g. ProRes) default to a pixel format QuickTime/most players can't read
Contact-sheet montage from stills
ffmpeg -i in.mp4 -vf "select='not(mod(n\\,150))',scale=320:-1,tile=4x4" -frames:v 1 contact_sheet.jpg # samples every 150th frame, tiles 16 of them into a 4×4 grid
Health-check a media file in a script
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$1" 2>/dev/null) if [ -z "$dur" ]; then echo "unreadable or corrupt: $1" >&2 exit 1 fi
Strip audio entirely (mute a clip)
ffmpeg -i in.mp4 -an -c:v copy muted.mp4