Skip to content
mishalkisPublic

About

Build ffmpeg filter graphs without writing them by hand.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

ffgraph

Build ffmpeg filter graphs without writing them by hand.

CI Python License

Reframe a clip, key out a green screen, put text on it, burn in subtitles. Four operations that turn into a -filter_complex string nobody wants to maintain by hand.

A 16:9 source frame beside the same frame reframed to 9:16, with a blurred background, a chromakeyed banner, a caption and burned-in subtitles

That picture is generated by examples/make_demo.py from nothing but ffmpeg test patterns, and this is the code behind the right-hand frame:

from ffgraph import Clip

(
    Clip.open("source.mp4")
    .reframe(1080, 1920)
    .overlay("banner.mp4", chromakey="0x00B140", width=980, y=180, loop=True)
    .text("Part 2: it's here, [100%]", font="DejaVuSans.ttf", size=58, y="h*0.62", box=True)
    .subtitles("subs.ass", fonts_dir="/usr/share/fonts")
    .render("out.mp4")
)

Why

The filters are documented. The ways they quietly ruin an afternoon are not.

  • A looped overlay without shortest=1 never ends. -stream_loop -1 makes the banner infinite, and ffmpeg will cheerfully encode until the disk fills up. Here the two are set together or not at all.
  • drawtext can accept a caption and draw nothing. An apostrophe or a percent sign is enough, with no warning and exit code zero.
  • Blurring a background at full size costs four times what it needs to. It is about to be blurred beyond recognition, so it is done at half size.
  • A mistyped label is a broken graph. Threading [v0] into [v1] into [v2] by hand is where the afternoon actually goes.

ffgraph builds the graph, and command() hands it back unrun — so when ffmpeg does complain, you can see exactly what it was given:

>>> Clip.open("in.mp4").reframe(1080, 1920).command("out.mp4")
['ffmpeg', '-y', '-i', 'in.mp4', '-filter_complex',
 '[0:v]split=2[_bg][_fg];'
 '[_bg]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,'
 'gblur=sigma=8,scale=1080:1920:flags=bilinear[_bgd];'
 '[_fg]scale=1080:1920:force_original_aspect_ratio=decrease:flags=lanczos[_fgd];'
 '[_bgd][_fgd]overlay=(W-w)/2:(H-h)/2,setsar=1[v0]', '-map', '[v0]', ...]

Install

pip install git+https://github.com/mishalkis/ffgraph

No dependencies beyond the standard library — ffmpeg does the work. Python 3.10 or newer, with ffmpeg on PATH.

The four operations

clip.reframe(1080, 1920, blur_background=True, focus_x=None)
clip.overlay(path, chromakey=None, similarity=0.15, width=None, x="center", y=0, loop=False)
clip.text(text, font=None, size=64, x="center", y=64, start=None, end=None, box=False)
clip.subtitles(path, fonts_dir=None)

Each returns a new Clip, so a base can be reused:

base = Clip.open("talk.mp4").reframe(1080, 1920)
base.text("Part 1", end=3).render("part1.mp4")
base.text("Part 2", end=3).render("part2.mp4")

reframe fits the whole picture into the new shape and fills the bands left over with a blurred copy of itself, so nothing is cropped away. Pass blur_background=False to crop to the frame instead, and focus_x (0..1) to say which part of the picture the crop should keep.

overlay puts another file on top, optionally keying a colour out of it first. similarity is how close a pixel must be to count as the key colour — raise it if fringes survive, but not far: at 0.3 a black box on a green background vanishes along with the green.

text takes the caption as you want it read. Positions accept "center", a pixel count, or any ffmpeg expression such as "h*0.62".

subtitles burns an ASS/SSA file in through libass. Point fonts_dir at the fonts it needs; a container rarely has them installed.

Notes on ffmpeg

Captions travel as files. ffmpeg's escaping for inline text= cannot express an apostrophe: quoted or bare, backslashed or doubled, no form I could find both parses and draws. Written to a file and read back with textfile=, a caption is simply the bytes you wrote. The tests check pixels rather than exit codes, because that is where this failure hides.

expansion=none is always set. Otherwise drawtext reads the caption as a date format, and 100% renders as an empty frame.

Paths are quoted, not escaped. A directory called season 1, part 2 is fine. A path containing an apostrophe is not supported, for the reason above.

Development

pip install -e ".[dev]"
pytest

Tests that run ffmpeg skip themselves when it is missing. The rest check the generated graph as a string and need nothing installed.

License

MIT

About

Build ffmpeg filter graphs without writing them by hand.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages