Alternatives
There are good, mature tools for FFmpeg in PHP already. This page is an honest take on the landscape and when each approach is the right one — including the cases where you should not reach for ext-ffmpeg.
Two camps
Section titled “Two camps”CLI wrappers drive the ffmpeg binary as a separate process:
- php-ffmpeg/php-ffmpeg — the foundational library; builds commands and shells out.
- pbmedia/laravel-ffmpeg (ProtoneMedia) — a fluent, disk-aware Laravel layer over it.
- projektgopher/laravel-ffmpeg-tools — builds filter/expression strings (tweening, easing) for the above. It pushes the CLI approach impressively far.
Native binding calls the FFmpeg libraries (libav*) in-process:
- ext-ffmpeg (this project) — a compiled extension; no separate binary, typed API.
When the CLI wrappers are the better choice
Section titled “When the CLI wrappers are the better choice”Genuinely often. Reach for them when:
- You need it today. They’re stable and battle-tested; this isn’t.
composer requireis the whole install. No extension to compile or load — works on shared hosting and anywhere PHP runs.- Your host already has
ffmpeg(or you can install it) and you’re happy owning it. - Your filters are static or pre-computable — a watermark, a fixed crop, a thumbnail, a straight transcode. The CLI’s huge, current feature set is right there.
For “transcode this / add a logo / make a thumbnail,” these are a great answer. Use them.
When ext-ffmpeg starts to earn its keep
Section titled “When ext-ffmpeg starts to earn its keep”- You can’t manage a system binary. On managed platforms like Laravel Cloud you may load a custom extension but can’t install your own binaries. A statically-bundled extension is self-contained — it just works there.
- No version-matching. With a CLI wrapper, your code might need a filter or codec that
only exists in a newer
ffmpegthan the host has — a silent drift between your app and a binary you don’t control. ext-ffmpeg bundles the FFmpeg it was built against, so the capability set ships with the package. - Real error handling. Typed exceptions carrying
->operation+->avError, and failures caught early by the type system — instead of catching a generic “process failed” and grepping a stderr dump. - Cleaner code for complex graphs — see the capstone example.
- The one a CLI wrapper can never do: run your PHP inside the pipeline. A separate
ffmpegprocess can’t call back into your PHP per frame. An in-process extension can — per-frame callables with live runtime values (timestamp, frame number, rendered text size, …). That’s the north-star feature, and it’s why this exists. (Planned; the safe mechanism is a custom AVFilter — see ADR 0001.)
Honest trade-offs against us
Section titled “Honest trade-offs against us”- Pre-release; smaller feature surface today than the full CLI (growing toward complete coverage).
- Installing a PHP extension is a bigger ask than
composer require(PIE / prebuilt binaries aim to make it one step). - The bundled, GPL-licensed binary is large — see Installation.
Side by side
Section titled “Side by side”Transcode — honestly, a wash
Section titled “Transcode — honestly, a wash”FFMpeg::fromDisk('videos')->open('in.mov') ->export()->inFormat(new \FFMpeg\Format\Video\X264) ->save('out.mp4');// ext-ffmpeg(new MediaEncoder()) ->addVideo($media->videoStream(), VideoCodec::H264) ->addAudio($media->audioStream(), AudioCodec::AAC) ->save('out.mp4');Both are clean. Pick the wrapper today unless you specifically want the deploy or error-handling benefits below.
”Did it fail, and why?” — ext-ffmpeg wins
Section titled “”Did it fail, and why?” — ext-ffmpeg wins”// CLI wrapper: one generic failure; you parse a string to learn whytry { FFMpeg::fromDisk('videos')->open('in.mov') ->export()->inFormat(new \FFMpeg\Format\Video\X264)->save('out.mp4');} catch (\ProtoneMedia\LaravelFFMpeg\Exporters\EncodingException $e) { $log = $e->getErrorOutput(); // a wall of text to grep ($e->getCommand() for the command)}// ext-ffmpeg: typed, specific, machine-readabletry { (new MediaEncoder())->addVideo($media->videoStream(), VideoCodec::H264)->save('out.mp4');} catch (\FFmpeg\Exception\EncoderNotFoundException $e) { // libx264 not built in — tell the user exactly that} catch (\FFmpeg\Exception\EncodingException $e) { echo $e->operation, ': ', $e->avError; // which call failed, and the decoded reason}Animated text — the ceiling the CLI can’t pass
Section titled “Animated text — the ceiling the CLI can’t pass”// projektgopher/laravel-ffmpeg-tools: a Tween becomes a pre-computed STRING.// Your PHP runs ONCE, at build time; ffmpeg then evaluates the string itself.$x = (new Tween())->from(0)->to(100)->duration(Timing::seconds(2))->ease(Ease::OutElastic);// → "x='if(lt(t,1),0,...elastic expression...)'"// ext-ffmpeg (planned): a PHP closure runs on EVERY frame, with live values.$media->videoStream()->drawText( text: 'Hello', x: fn($f) => $f->t < 1 ? 0 : (int) (100 * easeOutElastic(($f->t - 1) / 2)), y: fn($f) => ($f->height - $f->textHeight) / 2, // needs the *rendered* text size — only known at render time);The string approach is clever and works for math you can express up front. But it can’t make a decision per frame, read the rendered text dimensions, hit your database, or react to external state — because the PHP isn’t there when ffmpeg runs. The extension’s closure is. That’s the difference no wrapper can close.
Bottom line
Section titled “Bottom line”- Static jobs, ship today, any host,
composer require→ php-ffmpeg / pbmedia/laravel-ffmpeg (+ projektgopher/laravel-ffmpeg-tools for filter strings). Great tools; use them. - Can’t manage a binary (Laravel Cloud), want typed errors, or need real per-frame PHP logic → that’s exactly the gap ext-ffmpeg exists to fill.
They aren’t mutually exclusive — use the wrappers for the simple 90% and ext-ffmpeg where you hit their ceiling.
An Artisan Build project.
Built on FFmpeg — an independent binding, not affiliated with or endorsed by the FFmpeg project. Support FFmpeg.