Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ffmpeg Bundle Laravel Package

fmonts/ffmpeg-bundle

A Symfony bundle that integrates FFmpeg into your PHP applications via the PHP-FFMpeg library, making it easy to transcode video and audio, extract thumbnails, read media metadata, and apply filters or watermarks through a simple, service-based API.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to internalize: this is a Symfony bundle, not a Laravel package. There is no service provider, no vendor:publish, no facade. In a Laravel codebase you will get ~90% of the value by installing the underlying library and hand-wiring the same three config values the bundle wires for you. Treat fmonts/ffmpeg-bundle as reference documentation for how to configure PHP-FFMpeg correctly, and copy that wiring into a service provider.

If you actually are on Symfony (6/7/8 — 0.8.7 added Symfony 8 support and fixed the Symfony 7.4 config deprecation):

composer require fmonts/ffmpeg-bundle

Enable it in config/bundles.php, then configure binaries. Keys mirror PHP-FFMpeg's option names but are nested — confirm against DependencyInjection/Configuration.php in the installed version rather than trusting the README:

# config/packages/fmonts_ffmpeg.yaml
fmonts_ffmpeg:
    binary: /usr/bin/ffmpeg
    threads: 12
    timeout: 3600
    ffprobe:
        binary: /usr/bin/ffprobe

Inject the fmonts_ffmpeg service (alias FFMpeg\FFMpeg yourself if autowiring isn't registered in your version).

If you're on Laravel (the common case): skip the bundle, install the library, and bind it as a singleton so you get one configured instance per process instead of re-creating it per call.

composer require php-ffmpeg/php-ffmpeg
# and on the host/container: apt-get install -y ffmpeg   (or alpine: apk add ffmpeg)
// config/ffmpeg.php
return [
    'ffmpeg_binaries'  => env('FFMPEG_BINARIES', '/usr/bin/ffmpeg'),
    'ffprobe_binaries' => env('FFPROBE_BINARIES', '/usr/bin/ffprobe'),
    'threads'          => (int) env('FFMPEG_THREADS', 4),
    'timeout'          => (int) env('FFMPEG_TIMEOUT', 3600),
];
// app/Providers/FFmpegServiceProvider.php
public function register(): void
{
    $this->app->singleton(FFMpeg::class, fn () => FFMpeg::create([
        'ffmpeg.binaries'  => config('ffmpeg.ffmpeg_binaries'),
        'ffprobe.binaries' => config('ffmpeg.ffprobe_binaries'),
        'ffmpeg.threads'   => config('ffmpeg.threads'),
        'timeout'          => config('ffmpeg.timeout'),
    ]));

    $this->app->singleton(FFProbe::class, fn () => FFProbe::create([
        'ffprobe.binaries' => config('ffmpeg.ffprobe_binaries'),
        'timeout'          => config('ffmpeg.timeout'),
    ]));
}

First use case — the one you'll write on day one: a user uploads a phone video, you normalize it to a web-safe H.264/AAC MP4 at 720p and generate a poster frame.

$video = app(FFMpeg::class)->open($request->file('video')->getRealPath());

$video->filters()->resize(new Dimension(1280, 720), 'width')->synchronize();

$format = new X264('aac', 'libx264');
$format->setKiloBitrate(1200)->setAudioKiloBitrate(128)->setAudioChannels(2);

$video->save($format, storage_path('app/public/videos/'.$name.'.mp4'));

$video->frame(TimeCode::fromSeconds(3))
      ->save(storage_path('app/public/videos/'.$name.'.jpg'));

If binaries are wrong you'll get FFMpeg\Exception\ExecutableNotFoundException immediately on create() — that's your smoke test, and it's worth asserting in a boot check.

Implementation Patterns

Never transcode in the request cycle. Encoding is minutes-long, CPU-bound, and will blow past max_execution_time and PHP-FPM request_terminate_timeout. Put it in a queued job, on a dedicated queue and a dedicated worker pool.

class TranscodeVideo implements ShouldQueue
{
    public $timeout = 3600;
    public $tries = 1;          // never blindly retry an expensive encode
    public $deleteWhenMissingModels = true;

    public function __construct(public int $videoId) {}

    public function handle(FFMpeg $ffmpeg): void
    {
        $video = Video::findOrFail($this->videoId);
        $tmp = tempnam(sys_get_temp_dir(), 'src_');

        // pull the original down to local disk — FFmpeg needs a real path
        file_put_contents($tmp, Storage::disk('s3')->get($video->source_path));

        $media = $ffmpeg->open($tmp);
        // ... encode ...
        $media->save($format, $tmpOut);

        Storage::disk('s3')->put($video->output_path, fopen($tmpOut, 'r'));
        @unlink($tmp); @unlink($tmpOut);
    }
}

Run the worker with php artisan queue:work --queue=video --timeout=3600 --memory=512.

Wrap it behind your own interface. FFmpeg output is nondeterministic and slow; you want a seam for tests. Define VideoTranscoder with transcode(string $in, string $out, array $opts): void and let the FFmpeg implementation be one binding. In tests, bind a fake that copies the input file. This also saves you when you eventually swap php-ffmpeg for a raw Process call or a cloud transcoder.

Always probe before encoding. Users upload rotated, variable-framerate, 4K, or audio-only files. Use FFProbe to branch:

$probe = app(FFProbe::class);
$format = $probe->format($path);           // duration, bit_rate, format_name
$streams = $probe->streams($path);
$hasVideo = count($streams->videos()) > 0;
$duration = (float) $format->get('duration');

setKiloBitrate() with a fixed value on a 4K source will look terrible; scale the bitrate off $format->get('bit_rate') or the resolution you detected.

Progress reporting is done via the format's event emitter, not a separate listener class:

$format->on('progress', function ($video, $format, float $percent) use ($videoId) {
    Cache::put("transcode:{$videoId}", round($percent), 3600);
});

Poll that from a status endpoint. Note onProgress fires per pass and can restart — don't treat the percentage as monotonic.

Filters are the extension point for anything ffmpeg can do: ->resize(), ->crop(), ->watermark(), ->rotate(), ->synchronize() (call it before save() or filters silently don't apply). For anything the fluent API doesn't cover, PHP-FFMpeg lets you pass raw arguments through a custom FormatInterface — that's your escape hatch instead of shelling out.

Thumbnails at scale: grab several frames in a single open ($video->frame(TimeCode::fromSeconds($t))->save(...)) rather than re-opening the file per frame; open() spawns ffprobe each time.

Gotchas and Tips

  • Config key naming is inconsistent between layers. The bundle uses binary / ffprobe.binary / threads; the PHP-FFMpeg constructor uses ffmpeg.binaries / ffprobe.binaries / ffmpeg.threads. Copy-pasting a bundle YAML into a Laravel config array produces a silently defaulted configuration that then fails on a binary path — the exact trap this package sets.
  • 0.8.7 moved the service definitions from XML to PHP. If you were reading Resources/config/services.xml to understand the wiring, that file is gone — look in Resources/config/ for the PHP equivalent. The user-facing config tree is unchanged; DependencyInjection/Configuration.php is still the source of truth for YAML keys. Nothing here changes the Laravel story: still hand-wire it.
  • FFMpeg::create() does not validate anything until first use in some versions; assert binary existence yourself in a health check (is_executable(config('ffmpeg.ffmpeg_binaries'))).
  • ffmpeg and ffprobe are separate binaries. Docker images that build ffmpeg from source sometimes strip ffprobe. If probing fails but encoding works, that's why.
  • Threads vs. container CPU limits. Setting threads: 12 inside a 2-CPU container causes thrashing and slower encodes. Cap it at nproc and lower it on the queue worker so parallel jobs don't starve each other.
  • Codec availability varies by build. libx264 and aac are not guaranteed — Debian's ffmpeg package has them, minimal static builds may not. Verify with ffmpeg -codecs | grep -E 'libx264|aac' in the exact image you deploy, or X264('aac', 'libx264') throws at runtime only.
  • Timeouts multiply. timeout in config, $job->timeout, queue:work --timeout, PHP max_execution_time, and the container's own limits are five separate knobs. A hung encode usually surfaces as a killed worker with no exception — check supervisor/worker logs before debugging the code.
  • Disk and permissions. FFmpeg writes to the output path as the web/queue user. storage/app permissions bugs look like "Encoding failed" EncodingFailureException with the real error buried in $e->getPrevious(). Always log $e->getMessage() plus the command.
  • Temp files leak on failure. Wrap in try/finally and unlink; a few thousand orphaned 200MB temp files will fill the disk before you notice.
  • License is NOASSERTION. Upstream PHP-FFMpeg and the original bundle are MIT, but the fork declares nothing. Before shipping this in a commercial product, diff the fork against upstream and confirm provenance. With ~12 stars, 0 dependents, and a maintainer-of-one, budget for vendoring or patching it yourself — the surface is small enough (one DI extension, one service definition) that forking the config into your own service provider is often the lower-risk move. The 0.8.7 release is a good sign (someone is tracking Symfony releases) but it is still a single-maintainer fork; don't read two commits as a support commitment.
  • No Laravel auto-discovery. Composer will install it, Laravel will ignore it, and you'll get a confusing "class not found" for the bundle class if you try to register it. Don't.
  • Pin the version and pin the binary version. An ffmpeg minor upgrade has changed default encoder settings (-crf, -preset) before and silently altered output quality across your library. Note the version in your deploy docs.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings