A native PHP extension that binds the FFmpeg libraries (libavformat, libavcodec,
libavfilter, libavutil, libswscale, libswresample) directly — no shell_exec,
no CLI string-building, no FFI. Written in pure C against the Zend API.
Language-level media access for PHP: open and probe media, build filter graphs, generate streams from nothing, and transcode or remux — with a real object model and typed exceptions instead of parsing stderr.
📚 Docs & guides: https://ext-ffmpeg.com
Attribution. This project is built on FFmpeg and would not exist without it. It is an independent binding and is not affiliated with, endorsed by, or an official part of the FFmpeg project. FFmpeg is a trademark of Fabrice Bellard. If ext-ffmpeg is useful to you, please also support FFmpeg.
Licensing is not finalized. Released builds statically bundle FFmpeg, so the distributed binary is GPL by construction; a license must be chosen and a
LICENSEadded before any public release. See ADR 0005 — Licensing & trademark and ADR 0006 — Linking & distribution.
use FFmpeg\Media;
use FFmpeg\MediaEncoder;
use FFmpeg\Codec\{VideoCodec, AudioCodec};
use FFmpeg\Filter\Scale;
$media = Media::open('input.mov'); // eager probe
echo $media->duration, "s\n"; // typed, readonly properties
$v = $media->videoStream();
echo "{$v->width}x{$v->height} {$v->codec}\n";
(new MediaEncoder())
->addVideo($v->apply(new Scale(1280, 720)), VideoCodec::H264, crf: 20, preset: 'medium')
->addAudio($media->audioStream(), AudioCodec::AAC, bitrate: 128_000)
->save('output.mp4');Every FFmpeg failure surfaces as a typed FFmpeg\Exception\* carrying ->operation and
->avError — never a bare false. More recipes: https://ext-ffmpeg.com/guide/examples/.
Pre-release, and further along than "early". Implemented and tested today:
- Core object model —
Media::open, typed stream views (VideoStream/AudioStream), theFFmpeg\Exception\*hierarchy, andMediaEncoder(addVideo/addAudio/save) with re-encode, stream-copy/remux, and per-stream encode options (crf/preset/bitrate). Native lifecycle is tied to PHP's GC. - Filter system — an immutable stream-node DAG lowered into one
AVFilterGraphatsave(): single- and multi-input filters (overlay,amix), multi-output fan-out (split/asplit), audio graphs, per-filter output-metadata derivation, and source filters viaVideoStream::generate()/AudioStream::generate(). - Typed filter catalog — a maintainer-time code generator introspects the bundled FFmpeg and emits 475 typed filter classes + 395 enums into a companion Composer package (see Packages).
- A documentation website at https://ext-ffmpeg.com with per-filter pages and a live ffmpeg.wasm playground.
Still ahead: subtitles / multi-track muxing, per-frame PHP callables inside filter
expressions, and finalizing licensing before going public. See docs/PRD.md and
docs/filter-system-design.md.
Pre-release. Building from source works today; PIE and prebuilt binaries are the intended distribution and aren't published yet. Full guide: https://ext-ffmpeg.com/guide/installation/.
Requirements: PHP ≥ 8.4 and FFmpeg ≥ 7.0 shared libraries (libavformat,
libavcodec, libavutil, libswscale, libswresample) — brew install ffmpeg or
apt install libavformat-dev libavcodec-dev libavutil-dev libswscale-dev libswresample-dev.
From source (today):
git clone https://github.com/artisan-build/ext-ffmpeg
cd ext-ffmpeg/ext
phpize && ./configure && make && sudo make install
# ./configure --with-ffmpeg=<prefix> if FFmpeg isn't on your pkg-config pathThen add extension=ffmpeg to your php.ini and verify with php -m | grep ffmpeg.
Then add the companion packages for the typed filters and IDE support:
composer require artisan-build/ext-ffmpeg-filters # the typed filter classes (autoloaded)
composer require --dev artisan-build/ext-ffmpeg-stubs # IDE stubs for the core .so classesPIE (once published): pie install artisan-build/ext-ffmpeg — the repo is already
PIE-ready (root composer.json, build-path: ext).
The extension ships as a hybrid two-package model, both version-locked in lockstep
with the extension and the bundled FFmpeg (see packages/):
| Package | What | How it's used |
|---|---|---|
ext-ffmpeg-filters |
The 475 concrete, typed filter classes (Scale, Overlay, Volume, …) + enums, generated from FFmpeg metadata |
composer require — real classes, PSR-4 autoloaded, instantiated at runtime |
ext-ffmpeg-stubs |
Declaration-only IDE stubs for the core .so classes (Media, streams, MediaEncoder, the Filter base + axis interfaces, codec enums, exceptions) |
require-dev — indexed by your editor, never autoloaded; synced from the canonical ext/src/**/*.stub.php |
The catalog lives in exactly one place (the filters package), so there's no duplicate
declaration. See docs/filter-system-design.md.
This repo doubles as the multi-repo dev workspace. The extension source lives in ext/;
the two large upstreams are cloned in as gitignored siblings so all three can be built
together.
ext-ffmpeg/ ← workspace root (orchestration Makefile + manifest.toml)
├── ext/ ← the extension: config.m4, ffmpeg.c (thin MINIT), src/<concern>/, tests/
│ └── src/ ← one concern per dir: media/ stream/ codec/ encoder/ exception/ filter/
├── packages/ ← co-located Composer packages (ext-ffmpeg-filters, ext-ffmpeg-stubs)
├── codegen/ ← typed filter-catalog generator (introspect.c → filters.json → generate.php)
├── site/ ← the docs website (Astro + Starlight) → ext-ffmpeg.com
├── docs/ ← PRD, ADRs (decisions/), contributing guides, design notes, lessons/
├── scripts/ ← bootstrap + helper scripts
├── art/ ← brand assets
├── php-src/ ← clone of ProjektGopher/php-src (gitignored fork)
└── ffmpeg/ ← clone of ProjektGopher/FFmpeg (gitignored fork)
php-src and ffmpeg are forks under the ProjektGopher org, each with the real
project as an upstream remote, pinned in manifest.toml (php PHP-8.5, ffmpeg
n7.1.1). We build against our own from-source FFmpeg (Homebrew's 8.0 dropped the
text-rendering libs) and our own debug + ASAN/UBSAN php-src (turns segfaults into
stack traces). Local patches (e.g. a future custom AVFilter) live on a patch branch and
rebase onto upstream tags.
make bootstrap # clone the php-src + ffmpeg forks at pinned refs (manifest.toml)
make ffmpeg # build FFmpeg from source into ffmpeg/_install (text-rendering libs on)
make php # build + install php-src (DEBUG) into php-src/_install (the dev workhorse)
make ext # phpize-build the extension (in ext/) against the dev php + dev ffmpeg
make smoke # probe the fixture and exercise an exception
make test # run the .phpt suite against the dev phpThe dev PHP is a --enable-debug build: Zend's debug memory manager does red-zone bounds
checks and reports leaks at shutdown. make php-asan layers ASAN/UBSAN on top — run that
php with USE_ZEND_ALLOC=0 ASAN_OPTIONS=detect_leaks=0 (LSan is unavailable on
macOS/arm64).
Other targets (make help for the full list):
make stubs # regenerate src/**/*_arginfo.h from the .stub.php files (commit them)
make clangd # unified compile_commands.json (needs `bear`) for cross-repo go-to-def
make clean # remove extension build artifacts
# filter catalog (maintainer)
make filter-introspect # dump bundled FFmpeg's filter metadata → codegen/filters.json
make filter-catalog # generate the typed classes + doc facts from filters.json
make filter-catalog-test # unit + golden-snapshot tests for the generator (no extension needed)
make filter-catalog-check# drift check: regenerate and assert no git diff
make stubs-package # sync core .stub.php → packages/ext-ffmpeg-stubs (declaration-only)
# website
make site-setup # install deps + park a local Herd domain (https://ext-ffmpeg.test)
make site-dev # Astro dev server (HMR)
make site # build the static site → site/dist
make deploy # build + deploy to Cloudflare Pages (maintainers)make stubs and the catalog generator run with the dev php (no Homebrew/Herd php
needed); generated *_arginfo.h and the catalog are committed, so make ext never needs
the codegen toolchain. See docs/contributing/building.md and codegen/README.md.
- PHP-CPP added an ABI dependency (
libphpcpp.so) that itself needed patching for PHP 8.5, plusextern "C"friction. The original PoC used it; dropped. - FFI isn't universally available and has per-call overhead unsuitable for frame-by-frame work.
- Rust (ext-php-rs) is viable, but C aligns with php-src, FFmpeg, and OpenCV/MediaPipe (all C-API), and with the PHP Foundation audience.
See ADR 0002 — Foundation, naming & scope.
- https://ext-ffmpeg.com — the canonical usage docs (guide, per-class reference, filter catalog, live playground).
docs/PRD.md— the long-range product vision.docs/filter-system-design.md— the filter wave's design, implementation log, and gaps.docs/decisions/— the ADRs (no private-struct access, naming/scope, v1 API, project structure, licensing/trademark, linking/distribution).docs/contributing/— how to build, the repo structure, adding a class, authoring a filter page, and the website.docs/ffmpeg-notes.md— language-agnostic FFmpeg gotchas.docs/lessons/— short engineering postmortems (a segfault, a stale make dep, an arginfo type-degradation).docs/AVEXPR_DEEP_DIVE.md,docs/CALLABLE_EVALUATION.md— per-frame callable research.- Upstream FFmpeg patch proposals live on the site under /upstream.
Built on FFmpeg. An Artisan Build project.
