pulse00/ffmpeg-bundle
pulse00/ffmpeg-bundle integrates the PHP-FFMpeg library into Symfony applications, providing a simple service-based API for audio and video processing. Generate thumbnails, inspect media metadata, apply filters, and run conversions with FFmpeg.
What 0.6.0 actually gives you: nothing new in the API — it widens the compatibility envelope (php >=5.5, PHP 7, Symfony 3) via PR #8. Treat it as a "you can now install this on a modern-ish stack" release, not a feature release.
Where to look first, in this order:
DependencyInjection/Configuration.php — the whole public surface of the bundle is this config tree (binaries, ffmpeg.threads, ffmpeg.timeout, ffprobe.timeout). Read it before anything else.Resources/config/services.xml — shows you exactly one thing worth knowing: the ffmpeg service is a preconfigured FFMpeg\FFMpeg::create() instance. There is no magic beyond that.Media::open(), filters, formats), not the bundle's.Install:
composer require pulse00/ffmpeg-bundle:^0.6
Configure (Symfony-style, as the bundle expects):
# config/packages/pulse00_ffmpeg.yaml
pulse00_ffmpeg:
binaries:
ffmpeg: /usr/bin/ffmpeg
ffprobe: /usr/bin/ffprobe
ffmpeg:
threads: 4
timeout: 3600
ffprobe:
timeout: 60
In Laravel there is no service provider. You have two options, and the honest answer is that option B is usually correct:
// A) Wire the bundle's service graph by hand
// app/Providers/AppServiceProvider.php
$this->app->singleton(\FFMpeg\FFMpeg::class, fn () => \FFMpeg\FFMpeg::create([
'ffmpeg.binaries' => config('services.ffmpeg.binaries'),
'ffprobe.binaries' => config('services.ffprobe.binaries'),
'timeout' => config('services.ffmpeg.timeout', 3600),
'threads' => config('services.ffmpeg.threads', 4),
]));
// B) Skip the bundle entirely — it is a thin Symfony DI wrapper
// composer require php-ffmpeg/php-ffmpeg
First use case — transcode an uploaded video to a web-safe MP4:
$ffmpeg = app(\FFMpeg\FFMpeg::class);
$video = $ffmpeg->open(storage_path('app/uploads/raw.mov'));
$video
->filters()
->resize(new \FFMpeg\Coordinate\Dimension(1280, 720), \FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET, true)
->synchronize();
$video->save(new \FFMpeg\Format\Video\X264('aac', 'libx264'), storage_path('app/public/out.mp4'));
If that runs once end-to-end, you've validated the binaries, the codecs, and your timeouts. That's the whole smoke test.
Bind it as a singleton, never resolve per-call. FFMpeg::create() shells out to probe the binaries; doing that in a loop is pure waste. Inject the container binding into jobs and commands.
Never transcode inside a request. Every realistic pattern below belongs in a queued job:
class TranscodeVideo implements ShouldQueue
{
public $timeout = 1800; // must be > FFMpeg timeout config
public $tries = 1; // transcodes are not idempotent-cheap
public function __construct(public int $videoId) {}
public function handle(\FFMpeg\FFMpeg $ffmpeg, \FFMpeg\FFProbe $ffprobe): void
{
$video = $this->videoId ? Video::findOrFail($this->videoId) : null;
$in = Storage::disk('local')->path($video->source_path);
$out = Storage::disk('local')->path("transcodes/{$video->id}.mp4");
$ffmpeg->open($in)
->save(new X264('aac', 'libx264'), $out);
$video->update(['transcoded_path' => "transcodes/{$video->id}.mp4"]);
}
}
Probe before you commit. Read duration/resolution first, then pick a target bitrate and a resize strategy. This avoids transcoding a 4K source into a 1280-wide output with the wrong aspect handling.
$format = $ffprobe->format($path);
$duration = (float) $format->get('duration');
$streams = $ffprobe->streams($path);
$videoStream = $streams->videos()->first();
$width = $videoStream->get('width');
Thumbnails are cheap; keep them synchronous-ish or in their own small job.
$ffmpeg->open($path)
->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds($duration * 0.1))
->save(storage_path("app/thumbs/{$id}.jpg"));
Progress reporting requires the save() callback form and a place to persist it (cache/DB). It does not work in a synchronous HTTP handler because the client has gone away:
$format = (new X264('aac', 'libx264'))->setKiloBitrate(1200);
$video->save($format, $out, function ($audio, $video, $percent) use ($id) {
Cache::put("transcode:{$id}", $percent, 3600);
});
Concat / HLS: open each segment as its own Media, then use the concat filter with the appropriate format. This is where php-ffmpeg's abstractions start leaking — you'll often end up dropping to a raw ->save() with a custom format, or shelling out.
Testing: ship a 2–3 second fixture clip in tests/Fixtures/. Faking ffmpeg is not worth it; a real 40 KB clip runs in well under a second.
The Laravel framing is the biggest gotcha. This is a Symfony bundle. It registers pulse00_ffmpeg config, a Pulse00FFMpegBundle, and an ffmpeg service alias. In Laravel you get none of that — no auto-discovery, no config publishing, no facade. Budget for either manual wiring (Pattern A above) or just depending on php-ffmpeg/php-ffmpeg directly. In a fresh Laravel app the bundle buys you almost nothing.
0.6.0 is a constraint-widening release. Expect changes in composer.json only (symfony/symfony widened to allow ^3.0, PHP 7 allowed). Run composer update pulse00/ffmpeg-bundle -W and check composer.lock — the risk isn't this package, it's whatever php-ffmpeg version Composer resolves alongside it. Pin php-ffmpeg/php-ffmpeg explicitly.
Binary paths fail silently-ish. The common failure is a wrong ffmpeg.binaries producing a RuntimeException deep in Symfony\Component\Process with a truncated command line. Debug with:
which ffmpeg ffprobe
ffmpeg -version
and log the full command before execution when you're chasing a failure. If you're on Forge/Vapor/Docker, the binary is usually /usr/bin/ffmpeg — but only if you installed it in the image.
Three timeouts stack up, and only one of them is the bundle's.
pulse00_ffmpeg.ffmpeg.timeout (passed to php-ffmpeg)--timeoutmax_execution_time if you ever run it syncIf the queue worker's timeout is the smallest, your job is killed mid-write and you get a truncated, unplayable MP4 with no exception. Set queue $timeout strictly greater than the ffmpeg timeout, and let ffmpeg be the thing that fails first.
Codec availability is build-dependent. X264('aac', 'libx264') requires an ffmpeg compiled with libx264. On minimal Alpine images you'll get "Unknown encoder 'libx264'" at save() time — not at open() time, which makes it feel like your filters are broken. Verify with:
ffmpeg -codecs | grep -E 'libx264|aac'
Filters are lazy. Nothing happens until save() — but ->synchronize() forces resolution and gives you a much better error location when a filter fails. Use it in development.
Temp disk and memory. php-ffmpeg writes intermediary files to the system temp dir. Containerized workers with a 64 MB /tmp will fail on long inputs. Point sys_temp_dir at a real volume and set a size-based cleanup policy. Long-running save() calls also hold progress state in memory — batch big files, don't fan out 50 at once.
Service decoration is the extension point. If you need custom binary resolution (per-tenant ffmpeg, GPU builds), don't fork the bundle — decorate or replace the ffmpeg service definition in your own container config. In Laravel this is trivial since you own the binding anyway.
Prefer modern php-ffmpeg on greenfield work. 0.6.0 dates from the Symfony 3 era. It's fine for maintenance on an existing Symfony 2.7/3.x app, but the underlying library has moved on in API polish and PHP version support; a new Laravel project should just take the direct dependency and skip the bundle.
How can I help you explore Laravel packages today?