filtergraph
v0.2.2
Published
Build FFmpeg filter_complex graphs with named nodes and edges
Maintainers
Readme
filtergraph
Build FFmpeg -filter_complex graphs with named nodes and edges instead of raw pad indices.
Why
FFmpeg filter_complex strings are write-only. A graph with 4 inputs, overlays, wipes, and audio mixing becomes an unreadable wall of [0:v]scale=... references. This library lets you think in named nodes and edges, and generates correct filter_complex output.
Install
npm install filtergraphZero dependencies. ESM only. Node.js 18+.
Quick Start
import { Graph } from 'filtergraph';
const g = new Graph();
// Named inputs (declaration order = ffmpeg -i order)
g.input('clip', 'video.mp4');
g.input('overlay', 'logo.png');
// Filter nodes
g.node('scaled', 'scale', { w: 1920, h: 1080 });
g.node('fmt', 'format', { pix_fmts: 'rgba' });
g.node('comp', 'overlay', { format: 'auto' });
// Edges connect them
g.edge('clip:v', 'scaled');
g.edge('overlay:v', 'fmt');
g.edge('scaled', 'comp:0');
g.edge('fmt', 'comp:1');
g.output('comp', 'outv');
// Generate the filter_complex string
console.log(g.toFilterComplex());
// → [0:v]scale=w=1920:h=1080[scaled];[1:v]format=pix_fmts=rgba[fmt];[scaled][fmt]overlay=format=auto[outv]
// Or generate a full ffmpeg command array
const cmd = g.toCommand('output.mp4');
// → ['-y', '-i', 'video.mp4', '-i', 'logo.png', '-filter_complex', '...', '-map', '[outv]', ...]API
g.input(name, path, flags?)
Add a named file input. Declaration order determines the -i index.
g.input('clip', 'video.mp4');
g.input('title', 'title.png', { loop: 1, t: 3, r: 30 }); // per-input flagsg.source(name, filter, args?)
Add a generated source (no file). Renders inline in the filter_complex.
g.source('silence', 'anullsrc', { r: 48000, cl: 'stereo' });
g.source('black', 'color', { c: 'black', s: '1920x1080', r: 30 });g.node(id, filter, args?)
Add a filter node. Args can be an object, a string (positional), or empty.
g.node('scaled', 'scale', { w: 1920, h: 1080 }); // → scale=w=1920:h=1080
g.node('pts', 'setpts', 'PTS-STARTPTS'); // → setpts=PTS-STARTPTS
g.node('pts', 'setpts', { expr: 'N/30/TB' }); // → setpts=N/30/TB
g.node('audio', 'acopy', {}); // → acopyg.edge(from, to)
Connect a source pad to a destination pad.
g.edge('clip:v', 'scaled'); // input video → filter
g.edge('clip:a', 'audio'); // input audio → filter
g.edge('scaled', 'comp:0'); // filter → multi-input pad 0
g.edge('fmt', 'comp:1'); // filter → multi-input pad 1
g.edge('split:0', 'branch_a'); // multi-output pad → filterInput refs default to :v if no stream type specified.
g.output(nodeId, padName)
Mark a node's output as a final output. Becomes -map [padName] in the command.
g.output('comp', 'outv');
g.output('audio_trim', 'outa');g.toFilterComplex()
Returns the -filter_complex string. Validates the graph first (throws on errors). Automatically optimizes linear chains into comma-separated sequences.
g.toCommand(outputPath, options?)
Returns a complete ffmpeg args array for child_process.spawn.
const cmd = g.toCommand('out.mp4', {
videoCodec: 'libx264', // default
crf: 23, // default
preset: 'fast', // default
pixFmt: 'yuv420p', // default
audioCodec: 'aac', // default
audioBitrate: '128k', // default
audioRate: 48000, // default
audioChannels: 2, // default
extraArgs: ['-profile:v', 'high'],
});
spawn('ffmpeg', cmd);Use { videoCopy: true } and/or { audioCopy: true } for stream copy.
g.toJSON()
Returns a JSON-serializable graph for visualization (e.g. with Cytoscape.js).
const json = g.toJSON();
// → { nodes: [...], edges: [...] }Validation
The graph is validated before generation. Errors include:
Duplicate node id: 'base'Duplicate input name: 'clip'Unknown reference 'missing' in edgeCycle detected: a → b → aNode 'orphan' has no incoming edgesNo outputs definedOutput references unknown node 'typo'Node id 'clip' conflicts with input name
Chain Optimization
Linear sequences of single-in/single-out filters are automatically collapsed:
// What you write:
g.edge('clip:v', 'scaled');
g.edge('scaled', 'cropped');
g.edge('cropped', 'trimmed');
// Generated (optimized):
[0:v]scale=...,crop=...,trim=...[trimmed]
// Not the naive version:
[0:v]scale=...[scaled];[scaled]crop=...[cropped];[cropped]trim=...[trimmed]License
MIT
