Skip to content

Commit b8b5cd2

Browse files
committed
feat: add Laravel Event System integration for media processing
- Add MediaProcessingStarted, MediaProcessingProgress, MediaProcessingCompleted, and MediaProcessingFailed events - Events fire automatically during all media processing operations including HLS exports - Add enable_events configuration option (defaults to true) - Events include input media collection, output path, and processing metadata - Progress events include real-time percentage, remaining time, and processing rate - Update README with comprehensive event system documentation - Add FFmpeg 7.x compatibility while maintaining support for 4.4 and 5.0 - Fix HLS test patterns to handle AVERAGE-BANDWIDTH field in newer FFmpeg versions
1 parent 53c7a0a commit b8b5cd2

File tree

11 files changed

+309
-10
lines changed

11 files changed

+309
-10
lines changed

README.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ This package provides an integration with FFmpeg for Laravel 10. [Laravel's File
2525
* Built-in support for watermarks (positioning and manipulation).
2626
* Built-in support for creating a mosaic/sprite/tile from a video.
2727
* Built-in support for generating *VTT Preview Thumbnail* files.
28+
* **Laravel Event System integration** for real-time progress monitoring and workflow automation.
2829
* Requires PHP 8.1 or higher.
29-
* Tested with FFmpeg 4.4 and 5.0.
30+
* Tested with FFmpeg 4.4, 5.0, and 7.x.
3031

3132
## Installation
3233

@@ -122,6 +123,94 @@ FFMpeg::open('steve_howe.mp4')
122123
});
123124
```
124125

126+
### Laravel Events
127+
128+
The package fires Laravel events during media processing, enabling you to build reactive workflows and real-time progress monitoring. You can listen for these events to trigger notifications, update databases, or integrate with WebSocket broadcasting.
129+
130+
#### Available Events
131+
132+
- **`MediaProcessingStarted`** - Fired when encoding begins
133+
- **`MediaProcessingProgress`** - Fired during encoding with real-time progress updates
134+
- **`MediaProcessingCompleted`** - Fired when encoding completes successfully
135+
- **`MediaProcessingFailed`** - Fired when encoding encounters an error
136+
137+
#### Event Properties
138+
139+
Each event contains:
140+
- `inputMedia` - Collection of input media files
141+
- `outputPath` - The target output file path (when available)
142+
- `metadata` - Additional processing context
143+
144+
Progress events additionally include:
145+
- `percentage` - Completion percentage (0-100)
146+
- `remainingSeconds` - Estimated time remaining
147+
- `rate` - Processing rate
148+
149+
#### Listening for Events
150+
151+
```php
152+
use ProtoneMedia\LaravelFFMpeg\Events\MediaProcessingCompleted;
153+
use ProtoneMedia\LaravelFFMpeg\Events\MediaProcessingProgress;
154+
155+
// Listen for completion to send notifications
156+
Event::listen(MediaProcessingCompleted::class, function ($event) {
157+
// Notify user that their video is ready
158+
$user->notify(new VideoReadyNotification($event->outputPath));
159+
160+
// Update database status
161+
Video::where('path', $event->inputMedia->first()->getPath())
162+
->update(['status' => 'completed']);
163+
});
164+
165+
// Real-time progress for WebSocket updates
166+
Event::listen(MediaProcessingProgress::class, function ($event) {
167+
broadcast(new EncodingProgressUpdate([
168+
'percentage' => $event->percentage,
169+
'remaining' => $event->remainingSeconds,
170+
'rate' => $event->rate
171+
]));
172+
});
173+
```
174+
175+
#### Event Listeners
176+
177+
You can create dedicated event listeners:
178+
179+
```php
180+
php artisan make:listener ProcessVideoCompleted --event=MediaProcessingCompleted
181+
```
182+
183+
```php
184+
class ProcessVideoCompleted
185+
{
186+
public function handle(MediaProcessingCompleted $event): void
187+
{
188+
// Send email notification
189+
Mail::to($event->user)->send(new VideoProcessedMail($event->outputPath));
190+
191+
// Generate thumbnail
192+
FFMpeg::open($event->outputPath)
193+
->getFrameFromSeconds(1)
194+
->export()->save('thumbnail.jpg');
195+
}
196+
}
197+
```
198+
199+
#### Configuration
200+
201+
Events are enabled by default. You can disable them in your configuration:
202+
203+
```php
204+
// config/laravel-ffmpeg.php
205+
'enable_events' => env('FFMPEG_ENABLE_EVENTS', false),
206+
```
207+
208+
Or via environment variable:
209+
210+
```bash
211+
FFMPEG_ENABLE_EVENTS=false
212+
```
213+
125214
### Opening uploaded files
126215

127216
You can open uploaded files directly from the `Request` instance. It's probably better to first save the uploaded file in case the request aborts, but if you want to, you can open a `UploadedFile` instance:

config/config.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@
1818
'temporary_files_root' => env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir()),
1919

2020
'temporary_files_encrypted_hls' => env('FFMPEG_TEMPORARY_ENCRYPTED_HLS', env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir())),
21+
22+
'enable_events' => env('FFMPEG_ENABLE_EVENTS', true),
2123
];
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace ProtoneMedia\LaravelFFMpeg\Events;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Foundation\Events\Dispatchable;
7+
use Illuminate\Queue\SerializesModels;
8+
use ProtoneMedia\LaravelFFMpeg\Filesystem\MediaCollection;
9+
10+
class MediaProcessingCompleted
11+
{
12+
use Dispatchable, InteractsWithSockets, SerializesModels;
13+
14+
public function __construct(
15+
public MediaCollection $inputMedia,
16+
public ?string $outputPath = null,
17+
public array $metadata = []
18+
) {
19+
}
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace ProtoneMedia\LaravelFFMpeg\Events;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Foundation\Events\Dispatchable;
7+
use Illuminate\Queue\SerializesModels;
8+
use ProtoneMedia\LaravelFFMpeg\Filesystem\MediaCollection;
9+
use Throwable;
10+
11+
class MediaProcessingFailed
12+
{
13+
use Dispatchable, InteractsWithSockets, SerializesModels;
14+
15+
public function __construct(
16+
public MediaCollection $inputMedia,
17+
public Throwable $exception,
18+
public ?string $outputPath = null,
19+
public array $metadata = []
20+
) {
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace ProtoneMedia\LaravelFFMpeg\Events;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Foundation\Events\Dispatchable;
7+
use Illuminate\Queue\SerializesModels;
8+
use ProtoneMedia\LaravelFFMpeg\Filesystem\MediaCollection;
9+
10+
class MediaProcessingProgress
11+
{
12+
use Dispatchable, InteractsWithSockets, SerializesModels;
13+
14+
public function __construct(
15+
public MediaCollection $inputMedia,
16+
public float $percentage,
17+
public ?int $remainingSeconds = null,
18+
public ?float $rate = null,
19+
public ?string $outputPath = null
20+
) {
21+
}
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace ProtoneMedia\LaravelFFMpeg\Events;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Foundation\Events\Dispatchable;
7+
use Illuminate\Queue\SerializesModels;
8+
use ProtoneMedia\LaravelFFMpeg\Filesystem\MediaCollection;
9+
10+
class MediaProcessingStarted
11+
{
12+
use Dispatchable, InteractsWithSockets, SerializesModels;
13+
14+
public function __construct(
15+
public MediaCollection $inputMedia,
16+
public ?string $outputPath = null,
17+
public array $metadata = []
18+
) {
19+
}
20+
}

src/Exporters/HLSExporter.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ public function getCommand(?string $path = null)
285285
public function save(?string $mainPlaylistPath = null): MediaOpener
286286
{
287287
return $this->prepareSaving($mainPlaylistPath)->pipe(function ($segmentPlaylists) use ($mainPlaylistPath) {
288+
$this->outputPath = $mainPlaylistPath;
288289
$result = parent::save();
289290

290291
$playlist = $this->getPlaylistGenerator()->get(

src/Exporters/HasProgressListener.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Closure;
66
use Evenement\EventEmitterInterface;
7+
use ProtoneMedia\LaravelFFMpeg\Events\MediaProcessingProgress;
78

89
trait HasProgressListener
910
{
@@ -47,7 +48,19 @@ private function applyProgressListenerToFormat(EventEmitterInterface $format)
4748
$this->lastPercentage = $percentage;
4849
$this->lastRemaining = $remaining ?: $this->lastRemaining;
4950

50-
call_user_func($this->onProgressCallback, $this->lastPercentage, $this->lastRemaining, $rate);
51+
if ($this->onProgressCallback) {
52+
call_user_func($this->onProgressCallback, $this->lastPercentage, $this->lastRemaining, $rate);
53+
}
54+
55+
if (config('laravel-ffmpeg.enable_events', true)) {
56+
MediaProcessingProgress::dispatch(
57+
$this->driver->getMediaCollection(),
58+
$this->lastPercentage,
59+
$this->lastRemaining,
60+
$rate,
61+
$this->getOutputPath()
62+
);
63+
}
5164
}
5265
});
5366
}

src/Exporters/MediaExporter.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
use ProtoneMedia\LaravelFFMpeg\FFMpeg\StdListener;
1212
use ProtoneMedia\LaravelFFMpeg\Filesystem\Disk;
1313
use ProtoneMedia\LaravelFFMpeg\Filesystem\Media;
14+
use ProtoneMedia\LaravelFFMpeg\Events\MediaProcessingCompleted;
15+
use ProtoneMedia\LaravelFFMpeg\Events\MediaProcessingFailed;
16+
use ProtoneMedia\LaravelFFMpeg\Events\MediaProcessingStarted;
1417
use ProtoneMedia\LaravelFFMpeg\Filters\TileFactory;
1518
use ProtoneMedia\LaravelFFMpeg\MediaOpener;
1619
use ProtoneMedia\LaravelFFMpeg\Support\ProcessOutput;
@@ -47,6 +50,11 @@ class MediaExporter
4750
*/
4851
private $toDisk;
4952

53+
/**
54+
* @var string|null
55+
*/
56+
private $outputPath;
57+
5058
/**
5159
* Callbacks that should be called directly after the
5260
* underlying library completed the save method.
@@ -159,6 +167,11 @@ public function afterSaving(callable $callback): self
159167
return $this;
160168
}
161169

170+
protected function getOutputPath(): ?string
171+
{
172+
return $this->outputPath;
173+
}
174+
162175
private function prepareSaving(?string $path = null): ?Media
163176
{
164177
$outputMedia = $path ? $this->getDisk()->makeMedia($path) : null;
@@ -197,8 +210,16 @@ protected function runAfterSavingCallbacks(?Media $outputMedia = null)
197210

198211
public function save(?string $path = null)
199212
{
213+
$this->outputPath = $path;
200214
$outputMedia = $this->prepareSaving($path);
201215

216+
if (config('laravel-ffmpeg.enable_events', true)) {
217+
MediaProcessingStarted::dispatch(
218+
$this->driver->getMediaCollection(),
219+
$this->outputPath
220+
);
221+
}
222+
202223
$this->driver->applyBeforeSavingCallbacks();
203224

204225
if ($this->maps->isNotEmpty()) {
@@ -218,6 +239,13 @@ public function save(?string $path = null)
218239
if ($this->returnFrameContents) {
219240
$this->runAfterSavingCallbacks($outputMedia);
220241

242+
if (config('laravel-ffmpeg.enable_events', true)) {
243+
MediaProcessingCompleted::dispatch(
244+
$this->driver->getMediaCollection(),
245+
$this->outputPath
246+
);
247+
}
248+
221249
return $data;
222250
}
223251
} else {
@@ -227,6 +255,14 @@ public function save(?string $path = null)
227255
);
228256
}
229257
} catch (RuntimeException $exception) {
258+
if (config('laravel-ffmpeg.enable_events', true)) {
259+
MediaProcessingFailed::dispatch(
260+
$this->driver->getMediaCollection(),
261+
$exception,
262+
$this->outputPath
263+
);
264+
}
265+
230266
throw EncodingException::decorate($exception);
231267
}
232268

@@ -241,6 +277,13 @@ public function save(?string $path = null)
241277

242278
$this->runAfterSavingCallbacks($outputMedia);
243279

280+
if (config('laravel-ffmpeg.enable_events', true)) {
281+
MediaProcessingCompleted::dispatch(
282+
$this->driver->getMediaCollection(),
283+
$this->outputPath
284+
);
285+
}
286+
244287
return $this->getMediaOpener();
245288
}
246289

@@ -262,6 +305,14 @@ private function saveWithMappings(): MediaOpener
262305
try {
263306
$this->driver->save();
264307
} catch (RuntimeException $exception) {
308+
if (config('laravel-ffmpeg.enable_events', true)) {
309+
MediaProcessingFailed::dispatch(
310+
$this->driver->getMediaCollection(),
311+
$exception,
312+
$this->outputPath
313+
);
314+
}
315+
265316
throw EncodingException::decorate($exception);
266317
}
267318

@@ -271,6 +322,13 @@ private function saveWithMappings(): MediaOpener
271322

272323
$this->maps->map->getOutputMedia()->each->copyAllFromTemporaryDirectory($this->visibility);
273324

325+
if (config('laravel-ffmpeg.enable_events', true)) {
326+
MediaProcessingCompleted::dispatch(
327+
$this->driver->getMediaCollection(),
328+
$this->outputPath
329+
);
330+
}
331+
274332
return $this->getMediaOpener();
275333
}
276334

0 commit comments

Comments
 (0)