Skip to content

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.

CLI wrappers drive the ffmpeg binary as a separate process:

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 require is 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.

  • 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 ffmpeg than 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 ffmpeg process 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.)
  • 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.
pbmedia/laravel-ffmpeg
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 why
try {
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-readable
try {
(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.

  • 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.