A single FFmpeg command that scales, watermarks, and tiles video side by side — that's not three commands glued together, it's a filter graph at work. filter_complex is the most powerful and most intimidating part of FFmpeg: brackets, pads, chains, a pile of symbols. This guide takes it apart with diagrams, and by the end you'll be writing your own compound commands.
First, Keep Them Straight: Filter Chain vs. Filter Graph
A filter chain is a serial pipeline of filters — the output of one becomes the input of the next, joined by commas:
scale=1280:-1,format=yuv420p,vflip
Data flows through in sequence: scale → convert pixel format → vertical flip.
A filter graph allows multiple chains that can fork, merge, and cross-connect. Semicolons separate chains, and square brackets mark pads:
[0:v]scale=640:480[a];[1:v]scale=640:480[b];[a][b]hstack
One-line summary: use -vf (filter chain) for single-stream serial processing; use -filter_complex (filter graph) for multiple streams, or any time you need to split and recombine. -filter_complex is the superset — a filter chain is just a filter graph with no branches.
Understanding Pads: The Wiring of a Filter Graph
Pads are the key to reading filter graphs. The label inside square brackets is a virtual wire connecting an upstream output to a downstream input:
input0 ──[0:v]──> scale ──[a]──┐
├──> hstack ──> output
input1 ──[1:v]──> scale ──[b]──┘
The corresponding command:
ffmpeg -i left.mp4 -i right.mp4 -filter_complex \
"[0:v]scale=640:480[left];[1:v]scale=640:480[right];[left][right]hstack" \
out.mp4
Piece by piece:
[0:v]: the video stream of input 0 (0is the-iorder;vmeans video — audio isa)scale=640:480[left]: the processed output gets theleftlabel for downstream reference[left][right]hstack: thehstackfilter takes two input pads and joins them horizontally
An unlabeled output is automatically connected to the graph's final output; hstack's output carries no label, so it is the result.
How -vf and -filter_complex Relate
-vf | -filter_complex | |
|---|---|---|
| Input streams | Exactly one | Any number |
| Can reference a 2nd input | No (-i is just a data source) | Yes, [1:v] references it directly |
| Output | Mapped automatically to the output file | Selectable precisely with -map |
| Audio | Not involved | Can be processed in the same graph |
-vf is essentially single-stream syntactic sugar for -filter_complex. But note: don't mix -vf and -filter_complex in the same command — you can easily end up with two filter pipelines that don't know about each other.
Example 1: Scale + Watermark (the Most Common Composite Need)
Add an image watermark to the bottom-right corner while capping the video at 720p:
ffmpeg -i input.mp4 -i logo.png -filter_complex \
"[1:v]scale=120:-1[wm];[0:v][wm]overlay=W-w-20:H-h-20,scale=-2:720" \
out.mp4
Broken down:
[1:v]scale=120:-1[wm]: scale the watermark to 120 pixels wide (-1computes height from the aspect ratio)[0:v][wm]overlay=...: overlay the watermark on the main picture.W/Hare the main picture's dimensions andw/hthe watermark's, soW-w-20:H-h-20means the bottom-right corner with a 20 px margin,scale=-2:720: a chained scale at the tail after overlay —-2auto-computes the width and rounds it to an even number (H.264 requires even dimensions;-1will error out)
While you're at it, memorize the overlay position shorthand: 0:0 is top-left, (W-w)/2:(H-h)/2 is centered, 0:H-h is bottom-left.
Example 2: Multi-Stream Composition and Layout
A 2×2 video wall — scale each input to half width and height, then place them with xstack:
ffmpeg -i a.mp4 -i b.mp4 -i c.mp4 -i d.mp4 -filter_complex \
"[0:v]scale=iw/2:ih/2[a];[1:v]scale=iw/2:ih/2[b];\
[2:v]scale=iw/2:ih/2[c];[3:v]scale=iw/2:ih/2[d];\
[a][b][c][d]xstack=inputs=4:layout=0_0|w0_0|0_h0|w0_h0" \
out.mp4
In layout, w0 means "the width of input 0" (and h0 likewise) — the relative positions of the pads are written directly into the layout expression.
Forking is just as natural: one source producing both a preview thumbnail and the final output:
ffmpeg -i input.mp4 -filter_complex \
"[0:v]split=2[v1][v2];[v1]scale=320:-1[thumb];[v2]scale=1280:-2[main]" \
-map "[thumb]" thumb.jpg -map "[main]" main.mp4
split=2 duplicates one stream into two, each flowing through a different chain, and -map decides which pad gets written to which output file. This is the real power of filter graphs over -vf.
Debugging Tips
Long filter graphs will have bugs. A few habits that save you:
- Verify one segment at a time. Get
[0:v]scale=640:480[v]working before adding the next segment — writing the whole thing and then debugging is the slowest possible approach. - Use
-v errorto cut log noise. FFmpeg's default logging is chatty; error-only output points you straight to the complaining filter. - Error messages name the filter and the pad. The pad label after
Invalid argumentis your breakpoint — trace upstream from there and check its output parameters. - Resolution mismatches are the #1 error source.
hstack/vstackrequire equal heights/widths, and mismatched pixel formats makeoverlaysilently slow — normalize withscale+format=yuv420pbefore joining.
Final Thoughts
The mental model for filter graphs is dataflow programming: each filter is a node, pads are the wires, and what you're designing is a graph, not a command. Once the [x] label wiring clicks, the rest is just looking up filter parameters.
If you'd rather not hand-write them, the Video Watermark and Video to GIF tools wrap the overlay and scale filters from this article in ready-to-use interfaces; to combine parameters freely, run your commands directly in the FFmpeg Command Lab — everything happens locally in your browser.
FAQ
Q: Does filter chain order matter? A: Yes, and it affects file size too. Scaling down first means subsequent filters process less data — but denoising filters usually belong before scaling. Order depends on the job.
Q: What's the difference between -1 and -2 in scale?
A: -1 computes the missing dimension from the aspect ratio; -2 does the same and rounds it to an even number. H.264/H.265 require even dimensions, so in practice you should almost always use -2.
Q: Why is my overlay processing frame by frame and extremely slow?
A: Usually the main video and the watermark have mismatched pixel formats, so FFmpeg performs an implicit format conversion on every frame. Adding an explicit format=yuv420p to both streams eliminates it.