ffmpeg / ffprobe cheatsheet

Comprehensive reference for transcoding, filtering, streaming, and inspecting media — Rose Pine theme

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

ContainerTypical videoTypical audio
.mp4h264, hevcaac
.webmvp9, av1opus
.mkvalmost anythingalmost anything
.movh264, proresaac, 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