MpegFlowBlogBack to home
← Topics·Encoding

FFmpeg filter_complex patterns — branching, merging, and multi-output graphs

Practical guide to FFmpeg filter_complex graphs — split for branching, concat/hstack/overlay for merging, multi-input multi-output patterns, common pipeline use cases.

ByMpegFlow Engineering Team·Encoding
·May 9, 2026·8 min read·1,577 words
In this topic
  1. What filter_complex is
  2. The labeling system
  3. Common patterns
  4. Audio + video joint processing
  5. Performance considerations
  6. Hardware acceleration with filter_complex
  7. Debugging filter_complex
  8. Common filter_complex bugs
  9. Operational considerations
  10. Real-world production examples
  11. What MpegFlow does with filter_complex

ffmpeg's filter_complex is the powerful but cryptic tool that handles everything beyond linear filter chains — multi-input processing, branching graphs, multi-output pipelines, audio/video joint processing. Mastering filter_complex unlocks single-pass production pipelines that would otherwise require multiple ffmpeg invocations and intermediate files. This page is the engineering reference for practical filter_complex patterns.

#What filter_complex is

filter_complex enables filter graphs more sophisticated than the linear chain -vf and -af provide:

  • Multiple inputs — combine multiple source files in one pipeline.
  • Multiple outputs — produce several derived outputs from one pipeline.
  • Branching — split a stream into multiple paths.
  • Merging — combine multiple streams into one output.
  • Cross-stream processing — operate on audio and video together.

The syntax uses square brackets to label streams between filters:

-filter_complex "[0:v]split=2[v1][v2]; [v1]scale=1920:1080[full]; [v2]scale=320:180[thumb]"

Three filters in sequence: split produces two outputs labeled [v1] and [v2]; each is then scaled separately.

#The labeling system

Square brackets identify streams:

  • [INDEX:STREAM_TYPE] — input streams. [0:v] is video stream of input 0; [1:a] is audio of input 1.
  • [NAME] — intermediate streams within the graph. [v1], [full], [mixed], etc. — any name you choose.
  • [INDEX:STREAM] — alternative input notation. [0:0] is stream 0 of input 0.

Streams flow between filters. Each filter takes labeled inputs and produces labeled outputs.

The graph is described as a series of filter operations separated by semicolons:

-filter_complex "
  [0:v]filter1[temp1];
  [temp1]filter2[temp2];
  [temp2]filter3[output]
"

Each line declares a filter, its inputs, and its outputs.

#Common patterns

Pattern 1: Split video for multiple outputs

ffmpeg -i input.mp4 -filter_complex \
  "[0:v]split=2[full][small]; \
   [full]scale=1920:1080[v_full]; \
   [small]scale=640:360[v_small]" \
  -map "[v_full]" -c:v libx264 -crf 22 full.mp4 \
  -map "[v_small]" -c:v libx264 -crf 22 small.mp4

The split filter creates two copies; each goes to a different scaling filter; each is mapped to a separate output. One ffmpeg invocation produces two different-sized files.

Pattern 2: Picture-in-picture overlay

ffmpeg -i main.mp4 -i pip.mp4 -filter_complex \
  "[0:v][1:v]overlay=W-w-10:H-h-10[output]" \
  -map "[output]" -c:v libx264 -crf 22 output.mp4

Overlays input 1 (PiP) onto input 0 (main video) with 10-pixel margin from bottom-right.

Pattern 3: Side-by-side comparison

ffmpeg -i source.mp4 -i encoded.mp4 -filter_complex \
  "[0:v]scale=960:540[left]; \
   [1:v]scale=960:540[right]; \
   [left][right]hstack[output]" \
  -map "[output]" -c:v libx264 -crf 18 comparison.mp4

Useful for visual quality comparison — encode A on left, encode B on right, scrub to identify differences.

Pattern 4: Audio mixing

ffmpeg -i background_music.mp3 -i voice.wav -filter_complex \
  "[0:a]volume=0.3[bg]; \
   [1:a]volume=1.0[voice]; \
   [bg][voice]amix=inputs=2:duration=longest[mixed]" \
  -map "[mixed]" -c:a aac mixed.aac

Reduces background music volume; keeps voice at full; mixes them. Common pattern for narrated content.

Pattern 5: Watermark/logo overlay with timing

ffmpeg -i video.mp4 -i logo.png -filter_complex \
  "[1:v]scale=200:80[scaled_logo]; \
   [0:v][scaled_logo]overlay=10:10:enable='between(t,0,5)+gte(t,30)'[output]" \
  -map "[output]" -map 0:a -c:v libx264 -c:a copy output.mp4

Overlays scaled logo at top-left, visible from 0-5 seconds and after 30 seconds. The enable expression controls when the overlay appears.

Pattern 6: Multiple ABR variants in one pass

ffmpeg -i master.mp4 -filter_complex \
  "[0:v]split=4[v1][v2][v3][v4]; \
   [v1]scale=1920:1080[v_1080]; \
   [v2]scale=1280:720[v_720]; \
   [v3]scale=854:480[v_480]; \
   [v4]scale=640:360[v_360]" \
  -map "[v_1080]" -c:v libx264 -crf 22 -b:v 4M out_1080.mp4 \
  -map "[v_720]" -c:v libx264 -crf 22 -b:v 2.5M out_720.mp4 \
  -map "[v_480]" -c:v libx264 -crf 22 -b:v 1.5M out_480.mp4 \
  -map "[v_360]" -c:v libx264 -crf 22 -b:v 800k out_360.mp4

One source decode, four parallel scales, four parallel encodes. Significantly faster than four separate ffmpeg invocations because the source is decoded once.

Pattern 7: Concatenate clips

ffmpeg -i clip1.mp4 -i clip2.mp4 -i clip3.mp4 -filter_complex \
  "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" -c:v libx264 -c:a aac concatenated.mp4

Concatenates three clips with their respective audio. Useful for editorial assembly.

#Audio + video joint processing

filter_complex handles audio and video together when needed:

-filter_complex \
  "[0:v]scale=1920:1080[v]; \
   [0:a]aresample=48000:async=1[a]" \
  -map "[v]" -map "[a]"

Scales video; resamples audio to 48 kHz with sync correction. Both go to output.

For more sophisticated audio processing (compression, EQ, normalization) alongside video:

-filter_complex \
  "[0:v]scale=1920:1080,format=yuv420p[v]; \
   [0:a]highpass=f=80,acompressor,loudnorm[a]"

Removes low frequencies, applies compression, then loudness normalization.

#Performance considerations

filter_complex performance varies:

  • Linear chains (single filter sequence) are typically fastest.
  • Splits + parallel processing parallelize well across CPU cores.
  • Multi-output encodes can saturate the source decoder if it's the bottleneck.
  • Cross-stream operations (concat, mix) are typically fast.

For pipelines with heavy filter graphs, profile to find bottlenecks. Sometimes it's worth splitting one ffmpeg invocation into multiple sequential invocations with intermediate files; sometimes the parallelism wins.

#Hardware acceleration with filter_complex

Hardware-accelerated filters (scale_cuda, scale_qsv, scale_vaapi) work in filter_complex but require the data to stay on GPU:

ffmpeg -hwaccel cuda -hwaccel_output_format cuda \
  -i input.mp4 \
  -filter_complex \
  "[0:v]split=2[v1][v2]; \
   [v1]scale_cuda=1920:1080[v_full]; \
   [v2]scale_cuda=640:360[v_small]" \
  -map "[v_full]" -c:v h264_nvenc -preset p5 full.mp4 \
  -map "[v_small]" -c:v h264_nvenc -preset p5 small.mp4

The split happens on GPU; both scales happen on GPU; both encodes happen on GPU. Data never leaves GPU memory.

For software filters mixed with hardware, transitions between GPU and CPU memory cost throughput. Either keep everything on GPU or everything on CPU; mixed pipelines are slow.

#Debugging filter_complex

Filter graphs can be cryptic. Debugging tips:

Verbose logging:

ffmpeg -loglevel verbose -i input.mp4 -filter_complex "..." ...

Verbose output shows filter graph construction and processing details.

Filter graph dump:

ffmpeg -i input.mp4 -filter_complex "..." -filter_complex_threads 1 -dump output.mp4

Some ffmpeg versions support graph dumping; check your version.

Smaller test cases:

When a complex graph fails, simplify until it works, then add back complexity. Isolate the failing filter.

Filter help:

ffmpeg -h filter=overlay

Shows filter-specific help with options and examples.

#Common filter_complex bugs

Bug 1: Stream index errors.

[0:v] references the first video stream of the first input. If the input has no video stream, this fails. Verify input streams with ffprobe before constructing the graph.

Bug 2: Unmatched labels.

A filter outputs [temp] but no later filter uses [temp]. Or a filter expects [temp] but no earlier filter produces it. Match labels carefully.

Bug 3: Missing semicolons.

Filter declarations are separated by ;. Forget one and the parser misinterprets.

Bug 4: Memory location mismatch.

Mixing CPU and GPU filters without explicit hwupload/hwdownload causes errors or silent CPU fallback.

Bug 5: Output format compatibility.

Some filters output specific pixel formats; downstream filters may need conversion. Add format=yuv420p (or appropriate) where needed.

#Operational considerations

Things that matter for filter_complex in production:

  • Test on representative content — graph behavior may differ for unusual content (very tall vs wide videos, unusual frame rates).
  • Performance benchmarking — graph performance can be unintuitive; benchmark before committing.
  • Maintainability — complex graphs become hard to understand. Document the graph structure.
  • ffmpeg version pinning — graph behavior occasionally changes between versions.
  • Graceful degradation — if filter_complex fails, what's the fallback? Plan error handling.

#Real-world production examples

Example 1: ABR ladder with watermark

ffmpeg -i master.mp4 -i logo.png -filter_complex \
  "[1:v]scale=120:60[logo]; \
   [0:v]split=3[v1][v2][v3]; \
   [v1]scale=1920:1080[full]; \
   [v2]scale=1280:720[hd]; \
   [v3]scale=854:480[sd]; \
   [full][logo]overlay=W-w-20:20[full_wm]; \
   [hd][logo]overlay=W-w-15:15[hd_wm]; \
   [sd][logo]overlay=W-w-10:10[sd_wm]" \
  -map "[full_wm]" -map 0:a -c:v libx264 -crf 22 -c:a aac full.mp4 \
  -map "[hd_wm]" -map 0:a -c:v libx264 -crf 22 -c:a aac hd.mp4 \
  -map "[sd_wm]" -map 0:a -c:v libx264 -crf 22 -c:a aac sd.mp4

Three ladder rungs with watermark applied at appropriate scale per rung.

Example 2: Per-shot dynamic processing

For content with scene changes that need per-segment processing:

ffmpeg -i input.mp4 -filter_complex \
  "[0:v]select='gt(scene,0.4)',showinfo[scene_info]; \
   [0:v]split=2[v1][v2]; \
   [v1]scale=1920:1080[full]; \
   [v2]scale=640:360[thumb]" \
  -map "[full]" -c:v libx264 -crf 22 full.mp4 \
  -map "[thumb]" -vf "fps=1" thumbnails/thumb_%03d.jpg

Generates main video plus periodic thumbnails plus scene-detect log.

Example 3: HDR-to-SDR with audio passthrough

ffmpeg -i hdr.mp4 -filter_complex \
  "[0:v]zscale=t=linear:npl=100,format=gbrpf32le, \
        zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p[sdr_v]" \
  -map "[sdr_v]" -map 0:a \
  -c:v libx264 -crf 22 -c:a copy sdr.mp4

Tone-maps HDR to SDR; passes audio through unchanged.

#What MpegFlow does with filter_complex

MpegFlow's DAG runtime expresses filter graphs as parameters on FfmpegExecutor stages. Common operations are typed first-class — FilterParams::Deinterlace (yadif/bwdif), FilterParams::Thumbnail, FilterParams::Overlay for watermark/logo work — so most production graphs don't require hand-authored filter_complex strings; the runtime materializes the right invocation from the typed parameters. The partitioner persists each stage to job_stages with explicit dependency tracking; per-stage retry handles transient failures.

For customers needing graphs the typed parameters don't cover (specific composition patterns, time-windowed overlays, custom color processing pipelines), the workflow YAML supports passing through a custom filter_complex string at the FfmpegExecutor stage. That custom string is validated against the worker image's FFmpeg-build capability before promotion to production.

For ABR ladder generation specifically, the partitioner emits parallel sibling rendition stages rather than a single filter_complex split — the DAG runtime expresses fan-out at the orchestration layer, not inside one FFmpeg invocation, which keeps per-rendition retry, per-rendition cancellation, and rendition-level partial-success reporting clean.

The strict-broker security model handles filter_complex work like any pipeline payload — workers carry no ambient credentials; content access flows through short-lived presigned URLs scoped per stage; access is disposed on completion.

The general guidance: filter_complex is powerful but cryptic. Master a few common patterns (split, overlay, concat, hstack); build production graphs from those primitives. Don't try to construct from scratch under time pressure; reference proven patterns. Once you have a vocabulary of working filter graphs, building new ones becomes mechanical rather than artistic. Investment in the vocabulary pays off across the entire pipeline lifetime — the same fundamental graph patterns appear in ABR ladder generation, watermarking, color conversion, transcoding, and editorial workflows.

Tags
  • FFmpeg
  • filter-complex
  • encoding
  • filters
  • filter-graph
See also

Related topics and reading

  • FFmpeg — the multimedia framework that runs nearly all video infrastructure
  • Watermarking and overlays — burning logos, tags, and identifiers into video for streaming
  • ffprobe stream inspection — extracting media info for pipeline automation
Building on this?

Join the MpegFlow beta.

We're shipping the encoder MVP this quarter. If you're wrangling encoding in production, the beta is built for you — no card, no console waiting.

Join the beta More encoding
© 2026 MpegFlow, Inc. · Trust & complianceAll systems nominal·StatusPrivacy