Learn how to build complex filtergraphs with -filter_complex: labeling streams, duplicating video with split, stacking with overlay, xfade transitions, mixing audio, and producing many outputs.

In episode 16 you saw a long filter chain — zscale, tonemap, and friends — working in one line. That's a filtergraph in its simplest form: a one-way pipe. Episode 17 is the peak of everything you've learned: how to build a genuinely complex filtergraph — multiple inputs, several branches, and many outputs at once.
If a simple filter is a single production line, a complex filtergraph is a factory: raw material comes in through many doors, is processed through several lines that can branch and rejoin, and finished products exit through multiple gates. This episode covers the tools to build that factory — labels, split, overlay, xfade, amix, and amerge — along with the rules that keep all frame flows safe.
Every complex filtergraph is built on one small concept that determines everything: labels. A label is a name attached to a frame stream so other filters can reference it. FFmpeg auto-labels input streams with a combination of input number and stream type, then our filters attach their own labels to their outputs.
The simplest example — one input, one filter, one labeled output:
ffmpeg -i input.mp4 -filter_complex "[0:v]hflip[v]" -map "[v]" output.mp4Here the video from the first input is taken via its automatic label, processed by hflip, and the result is labeled v. The -map command then selects that labeled stream as the output. The structure label-followed-by-filter-followed-by-label — exactly like the pattern written in the example above — is the basic "sentence" of all complex filtergraphs.
The crucial difference between -vf and -filter_complex:
-vf (alias -filter:v) is a shortcut for a one-input one-output graph, applied to all output video streams.-filter_complex is a full graph that can consume many inputs and produce many outputs, with labels as its communication tool. It's always accompanied by -map to choose where each graph output flows.The rule of thumb: for one simple path, use -vf. Once there are two inputs, two outputs, or branching, move to -filter_complex.
Important
Without -map referencing the graph's result labels, FFmpeg won't automatically use the filtergraph output — except when there's a single "obvious" video output. The -filter_complex + explicit -map rule avoids the most common mistakes: unused filter results, or the wrong stream getting encoded. Always write -map for every output you want from the graph.
The split filter copies one stream into several identical copies — not duplicating the file, only duplicating the frame route in memory. It's the foundation of almost every filtergraph producing more than one output.
The classic example is picture-in-picture: the main video plays, and its shrunken copy is pasted in the corner as a preview:
ffmpeg -i input.mp4 -filter_complex "[0:v]split[main][pip];[pip]scale=320:180[pips];[main][pips]overlay=main_w-overlay_w-10:10[v]" -map "[v]" -map 0:a output.mp4Notice three things in one line:
split breaks the input into two copies labeled main and pip, scale shrinks the PIP copy to 320x180, then overlay pastes it on top of the main video.overlay consumes two inputs — the background video and the stacked video — then produces one labeled output v. The main_w-overlay_w-10:10 argument places the preview 10 pixels from the right edge and 10 pixels from the top edge.-map 0:a, because this filtergraph only touches video.The overlay filter is the standard way to paste a logo, watermark, image, or animated text over video. Its nature of consuming two streams and producing one makes it a perfect example of how labels connect graph branches.
Joining two clips with concat (episode 12) produces an instant cut. For smooth transitions — fade, slide, or wipe — FFmpeg has the xfade filter. It computes the overlap between the end of the first clip and the start of the second, then applies a transition curve over the specified duration:
ffmpeg -i clip1.mp4 -i clip2.mp4 -filter_complex "[0:v][1:v]xfade=transition=fade:duration=1:offset=3[v]" -map "[v]" output.mp4transition=fade picks the transition type. Available choices are many: fade, slideleft, smoothleft, wipeleft, circleopen, dissolve, and dozens more. Just swap the transition name.duration=1 means the transition lasts 1 second.offset=3 is the time point (in seconds, measured on the first clip's timeline) when the transition starts. With offset 3 and duration 1, the first clip plays fully for 3 seconds, then its last second overlaps with the start of the second clip.One trap to watch: xfade expects both inputs to have the same resolution and framerate. If not, the result will error or look weird — normalize them with scale and fps in front of xfade. You'll also usually want to fade in at the start of the first clip and fade out at the end of the second so the transition feels complete, not just at the joint.
Tip
The maximum offset value is the first clip's duration minus the transition duration. If the offset is too large, FFmpeg will error. Easy math: for a 5-second clip and a 1-second transition, the maximum offset is 4. Use `ffprobe -show_entries format=duration -of default=nw=1 clip.mp4{:bash}` to confirm each clip's duration before building the graph.
Filtergraphs aren't only about video. The two most-used audio filters in complex graphs are amix and amerge, and they're easy to confuse:
amix mixes several audio streams into one stream with combined amplitude — like blending two voices into one track. Great for layering background music under narration.amerge combines channels from several streams into one multi-channel stream — like joining two mono tracks into left-right stereo. The output channel count equals the total input channels.Example of amix — music and vocal sound mixed into one track:
ffmpeg -i music.mp3 -i voice.mp3 -filter_complex "[0:a][1:a]amix=inputs=2:duration=longest:normalize=0[a]" -map "[a]" mix.mp3inputs=2 tells the number of inputs, duration=longest makes the output as long as the longest input (alternatives: shortest or first), and normalize=0 disables the automatic volume reduction — without it, two sources at full volume would be "halved" by amix, resulting in quieter output than you'd expect.
Example of amerge — two mono tracks into stereo:
ffmpeg -i left.wav -i right.wav -filter_complex "[0:a][1:a]amerge=inputs=2[a]" -map "[a]" stereo.wavThe left source occupies the left channel and the right source the right channel. Remember the direction: amix mixes amplitude (n tracks into 1 track with all sounds), amerge stacks channels (n tracks into 1 track with more channels).
The true strength of -filter_complex appears when one graph produces many products. The split + multiple -map combination lets one input be processed once and then emitted as several files with different codecs:
ffmpeg -i input.mp4 -filter_complex "[0:v]split[v1][v2]" \
-map "[v1]" -c:v libx264 -b:v 4M out-h264.mp4 \
-map "[v2]" -c:v libx265 -b:v 2M out-hevc.mkvThe input is decoded once, then split provides two copies of the video stream. The first copy is encoded with libx264, the second with libx265 — all in one command and one decode pass. Without split, you'd have to run FFmpeg twice and decode the input twice.
Also note that each output can have its own encoder settings, because each starts from a different label. This is the same pattern used in episode 14 to build several HLS variants in one command — the difference here is there's no HLS muxer, just regular files.
Tip
For additional audio output from the same graph, just add -map 0:a to the desired output — e.g., in the example above, insert -map 0:a in the H.264 output arguments so that file also carries audio, while the HEVC output is left video-only. Every -map must reference a label or stream that actually exists; if not, FFmpeg will abort the process.
| Filter | Function | Input Count | Output Count |
|---|---|---|---|
split | Duplicates a frame stream | 1 | 2+ |
overlay | Stacks one video on top of another | 2 | 1 |
xfade | Transition between two clips | 2 | 1 |
concat | Joins sequential clips (re-encode) | 2+ | 1 |
amix | Mixes amplitudes of several audios | 2+ | 1 |
amerge | Combines channels of several audios | 2+ | 1 |
Warning
Complex filtergraphs are prime candidates for commands that "look right but come out wrong". Get into the habit of testing long graphs with -t 5 (limit duration to 5 seconds) and cheap temporary output. Verifying small parts first saves hours of debugging; don't wait until the whole video has finished encoding to realize the transition is in the wrong place.
In episode 17 you've reached the peak of FFmpeg syntax: understanding that -filter_complex is a labeled graph connecting many inputs and outputs, using split to duplicate streams, overlay to stack, xfade for transitions, amix and amerge for audio mixing, and -map to route graph results to many files at once.
Key takeaways:
-vf for a single path; -filter_complex + -map for branching graphs.split duplicates frame routes; overlay stacks two streams; xfade blends two clips.amix mixes amplitude, amerge stacks channels — don't swap them.-t 5 before running the full process.With filtergraphs, you can already make almost every video effect in modern editing apps — from watermark overlays and picture-in-picture to transitions between clips. This is the boundary of what can be done with filters. The next step goes beyond filters: shrinking size and raising quality with a new generation of codecs.
In the next episode 18 we'll cover AV1 & Modern Codecs — the next-generation codec used by YouTube and Netflix, how to encode AV1 with libaom and SVT-AV1, and when it's worth replacing H.264 and HEVC. Keep your enthusiasm up!