All posts
FFmpegEngineering15 Sep 2026 · 4 min read

Explaining an FFmpeg command as a graph

JPJean Perez

ffmpeg -i in.mp4 -filter_complex "[0:v]scale=1280:720[bg];[1:v]scale=200:100[pip];[bg][pip]overlay=10:10[out]" -map "[out]" out.mp4 reads like a single long instruction. It's actually a graph written out flat, and the flattening is what makes it hard to read.

Every FFmpeg command has the same three kinds of node. Inputs, one per -i, each carrying however many streams the source file has. Filters, each one taking a labeled input and producing a labeled output, written between square brackets like [bg] and [pip]. Outputs, each fed by a -map that names which labeled stream lands in which output file. The command above has two inputs, three filter nodes, and one output. It's a small directed graph with six nodes and five edges, just spelled out as a semicolon-separated string instead of drawn as boxes and arrows.

Reading a filtergraph as a graph

[0:v] means the video stream of input 0. scale=1280:720[bg] means feed it into a scale filter and label the result bg. The semicolon starts a new filter chain rather than continuing the same one, which is the difference between filters applied in sequence to one stream and separate branches that get combined later. [bg][pip]overlay=10:10[out] takes two labeled streams as input to one filter, overlay, and produces a third label, out. That's a fan-in: two edges converging on one node. A split filter does the opposite, fan-out, one stream duplicated into two labeled copies so it can feed two different branches without being consumed twice.

None of this is exotic. It's the same graph vocabulary as any node-based tool: sources, processing nodes with labeled ports, sinks. FFmpeg just doesn't give you the boxes and arrows. It gives you the text serialization of the graph and expects you to hold the shape of it in your head while reading punctuation.

That's also where mistakes live. -map is what actually decides which labeled stream reaches which output; leave it out and FFmpeg falls back to its own default stream selection, which picks streams by its own rules rather than by what the filtergraph routed. A video-only filter chain applied to a video-and-audio input can still produce an output with audio in it. The graph never routed any audio; FFmpeg's default selection added it because nothing said otherwise. The command runs and the file plays. The actual routing only shows up if you compare what the graph drew against what ffprobe says the output contains.

Turning the string back into the graph

A flat string is a bad format for catching that kind of mismatch, because the eye reads it left to right and the graph it describes isn't linear. Rendering the same structure as an actual diagram, boxes for the nodes and arrows for the labeled connections between them, turns "did I route this correctly" from a parsing exercise into something you can look at.

BeemMeUp has three tools built around that idea, and they split cleanly by which side of the command they're reading.

visualize_graph takes a command or a graph definition and renders the structure described above: inputs, filter nodes with their labeled ports, outputs, drawn as a graph instead of left as a string. explain_command reads the same command and walks through it in plain language, flag by flag, so the reader doesn't need to already know what -vf or -map does. Both work on the input side, on the command that's about to run.

inspect_manifest does the same job for streaming output. An HLS or DASH encode produces a manifest as well as media: an .m3u8 or .mpd that describes the variant streams and the segment layout. Reading one by eye means holding its structure in your head, the same way a filtergraph string does, and inspect_manifest parses it back into that structure.

All three read and explain. None of them run an encode. Every tool call that would actually execute FFmpeg hands that job to whichever engine is live in your own browser tab or desktop app, the same engines that already run the command once you've built it. Nothing runs on the server, which matters for a straightforward reason: a hosted service has no access to your files or your GPU, no matter how the request reaches it. Reaching these tools from outside the app at all goes through a short authentication step first, so a connecting client is tied to a real signed-in account before it can ask for anything.

What reading the graph is for

The value of any of this is upstream of the tools themselves. A filtergraph is a real graph whether or not anything renders it, and the FFmpeg documentation describes each filter and option on its own, never the shape a particular command makes out of them. Understanding a command you didn't write, or debugging one you did, means reconstructing that shape from the string. A diagram or a plain-language walkthrough is just a faster way to do the reconstruction a careful reading would eventually do anyway.