I’ve been working on a project where we were using AWS elastic transcoder for media conversion. Elastic transcoder is a highly scalable solution for media transcoding. However, it charges your per minute for media conversion depending on your region. To reduce operational costs, we decided to shift away from AWS transcoder and use FFmpeg with Laravel for media conversion on our own servers. In this tutorial, I’ll show you how we can use FFmpeg for media conversion and defer processing using Laravel Queues.
Let’s get started by setting up a new project. Create a new Video model, its migration, and controller. We will store uploaded videos information on videos table.
1 |
php artisan make:model Video --migration --controller |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class CreateVideosTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('videos', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->string('original_name'); $table->string('disk'); $table->string('path'); $table->string('stream_path')->nullable(); $table->boolean('processed')->default(false); $table->datetime('converted_for_streaming_at')->nullable(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('videos'); } } |
On file upload, we will store video title, original file name, and path of the stored file in the database. After upload, we will dispatch a Job for transcoding it to a web streamable format and update stream_path with the output file path, update converted_for_streaming_at timestamp and set processed to true after FFmpeg is done processing uploaded media file.
1 2 3 4 5 6 7 8 |
class Video extends Model { protected $dates = [ 'converted_for_streaming_at', ]; protected $guarded = []; } |
In Video model class, add the converted_for_streaming_at column to $dates array so that it should be mutated to dates like created_at or updated_at columns.
Add these routes to web.php file.
1 2 3 4 5 6 7 8 |
Route::group(['middleware' => ['auth']], function(){ Route::get('/', 'VideoController@index'); Route::get('/uploader', 'VideoController@uploader')->name('uploader'); Route::post('/upload', 'VideoController@store')->name('upload'); }); |
GET /uploader route will render a form for uploading videos and POST /upload route will handle the form submission, upload video, create a database record and dispatch an FFmpeg transcoding job. GET / index route will render videos view where all uploaded videos will be displayed in native HTML video player.
In VideoController add these methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
class VideoController extends Controller { /** * Return video blade view and pass videos to it. * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function index() { $videos = Video::orderBy('created_at', 'DESC')->get(); return view('videos')->with('videos', $videos); } /** * Return uploader form view for uploading videos * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function uploader(){ return view('uploader'); } /** * Handles form submission after uploader form submits * @param StoreVideoRequest $request * @return \Illuminate\Http\RedirectResponse */ public function store(StoreVideoRequest $request) { $path = str_random(16) . '.' . $request->video->getClientOriginalExtension(); $request->video->storeAs('public', $path); $video = Video::create([ 'disk' => 'public', 'original_name' => $request->video->getClientOriginalName(), 'path' => $path, 'title' => $request->title, ]); ConvertVideoForStreaming::dispatch($video); return redirect('/uploader') ->with( 'message', 'Your video will be available shortly after we process it' ); } } |
Create uploader.blade.php under views directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
@extends('layouts.app') @section('content') <div class="col-xs-12 col-sm-12 col-md-8 col-lg-6 mr-auto ml-auto mt-5"> <h3 class="text-center"> Upload Video </h3> <form method="post" action="{{ route('upload') }}" enctype="multipart/form-data"> <div class="form-group"> <label for="video-title">Title</label> <input type="text" class="form-control" name="title" placeholder="Enter video title"> @if($errors->has('title')) <span class="text-danger"> {{$errors->first('title')}} </span> @endif </div> <div class="form-group"> <label for="exampleFormControlFile1">Video File</label> <input type="file" class="form-control-file" name="video"> @if($errors->has('video')) <span class="text-danger"> {{$errors->first('video')}} </span> @endif </div> <div class="form-group"> <input type="submit" class="btn btn-default"> </div> {{csrf_field()}} </form> </div> @endSection |
Also, create a StoreVideoRequest form request for validating uploader form input.
1 |
php artisan make:request StoreVideoRequest |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class StoreVideoRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' => 'required', 'video' => 'required|file|mimetypes:video/*', ]; } } |
We have a mimetypes validation rule with video/* wildcard to only allow video uploads.
Now create a ConvertVideoForStreaming job which will be dispatched after video is done uploading and a database record is created in VideoController@store method.
1 |
php artisan make:job ConvertVideoForStreaming |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
class ConvertVideoForStreaming implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $video; /** * Create a new job instance. * * @param Video $video */ public function __construct(Video $video) { $this->video = $video; } /** * Execute the job. * * @return void */ public function handle() { // create a video format... $lowBitrateFormat = (new X264('libmp3lame', 'libx264'))->setKiloBitrate(500); $converted_name = $this->getCleanFileName($this->video->path); // open the uploaded video from the right disk... FFMpeg::fromDisk($this->video->disk) ->open($this->video->path) // add the 'resize' filter... ->addFilter(function ($filters) { $filters->resize(new Dimension(960, 540)); }) // call the 'export' method... ->export() // tell the MediaExporter to which disk and in which format we want to export... ->toDisk('public') ->inFormat($lowBitrateFormat) // call the 'save' method with a filename... ->save($converted_name); // update the database so we know the convertion is done! $this->video->update([ 'converted_for_streaming_at' => Carbon::now(), 'processed' => true, 'stream_path' => $converted_name ]); } private function getCleanFileName($filename){ return preg_replace('/\\.[^.\\s]{3,4}$/', '', $filename) . '.mp4'; } } |
In handle() method of the dispatched job, we will create a low bitrate X264 format. We will open uploaded file from public disk and add a resize filter to it. Then we will tell FFmpeg to start transcoding by calling export() method and output file to public disk in a low bitrate mp4 container format.
Before you go an test it, make sure you have installed Laravel FFmpeg package that we are using in our transcoding job.
1 |
composer require pbmedia/laravel-ffmpeg |
Also, make sure you have ffmpeg binaries installed of your machine. If you’re running Linux, you can easily install it by running following apt install command.
1 |
sudo apt-get install ffmpeg |
You must also add FFmpeg Service Provider and Facade to app.php.
1 2 3 4 5 6 7 8 9 10 11 |
'providers' => [ ... Pbmedia\LaravelFFMpeg\FFMpegServiceProvider::class, ... ]; 'aliases' => [ ... 'FFMpeg' => Pbmedia\LaravelFFMpeg\FFMpegFacade::class ... ]; |
and run following command to publish package configuration files.
1 |
php artisan vendor:publish --provider="Pbmedia\LaravelFFMpeg\FFMpegServiceProvider" |
If you’re running windows, you must add ffmpeg binaries to the system PATH. If you don’t have access to that, you can define these environment variables in your .env file.
1 2 |
FFMPEG_BINARIES='PATH_TO_FFMPEG_BINARUES' FFPROBE_BINARIES='PATH_TO_FFPROBE_BINARIES' |
Laravel Queues Configuration
You also need to configure queue connection in your env file. For this tutorial, I’m using database queue connection. Edit .env file and update QUEUE_CONNECTION variable to database.
Also run php artisan queue:table to create database queue table migration and php artisan migrate to create table. To deal with failed jobs, run php artisan queue:failed-table to create failed queue jobs migration table and php artisan migrate to create table.
Running Queue Worker
Before we go and test, run Laravel’s queue worker
1 |
php artisan queue:work --tries=3 --timeout=8600 |
we have added a --timeout flag to queue worker. This indicates that don’t want our queue jobs to run longer than 8600 seconds.
Now if you head over to /uploader route in your application and upload a video file, a database record will be created a transcoding job will be dispatched. You’ll be able to view your dispatched job in the terminal.
Displaying Videos
Create videos.blade.php file under view directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@extends('layouts.app') @section('content') <div class="col-xs-12 col-sm-12 col-md-8 col-lg-8 mr-auto ml-auto mt-5"> <h3 class="text-center"> Videos </h3> @foreach($videos as $video) <div class="row mt-5"> <div class="video" > <div class="title"> <h4> {{$video->title}} </h4> </div> @if($video->processed) <video src="/storage/{{$video->stream_path}}" class="w-100" controls></video> @else <div class="alert alert-info w-100"> Video is currently being processed and will be available shortly </div> @endif </div> </div> @endforeach </div> @endSection |
We will display an alert for videos that are currently being processed. For processed videos, we will render a video element with transcoded stream_path.
Here’s a demo of what we have done so far.
I have set up a Github repository with example application code. If you run into any issue or have any questions, leave a comment and I will try to help you in any way possible.
sorry I have error, pls help me
Hello Waleed,
I wanted to repicate your transcoder webservice, unfortunatelly I keep running into the same issue like the guy who posted on your youtuve video.
[2019-04-28 10:18:38][1] Processing: App\Jobs\ConvertVideoForStreaming
[2019-04-28 10:18:38][2] Processing: App\Jobs\ConvertVideoForStreaming
[2019-04-28 10:18:38][3] Processing: App\Jobs\ConvertVideoForStreaming
[2019-04-28 10:18:38][3] Failed: App\Jobs\ConvertVideoForStreaming
I copied the binaries into my web root folder in htdocs/xampp (local webserver)
FFmpeg seems to be installed correctly since it is responding on comands globally with no problems.
The video I am trying to upload and convert is a test.mp4 about 50 MB big
I adjusted the .ENV variable to
FFMPEG_BINARIES=’/FFmpeg’
FFPROBE_BINARIES=’/FFmpeg’
where I put the binaries and I also upgraded them under ENVIRONMENT VARIABLES System Variables in windows.
Let me know if you have any ideas 🙂
Cheers
Rainer
What was the exception logged in the failed queues table? You should add the absolute path to
ffmpeg
andffprobe
in your.env
file.I am also getting failed jobs error.Please mention the correct way of declaring “FFMPEG_BINARIES” and “FFPROBE_BINARIES”.
Hello! I have some strange problem. I don’t have anything in queue:work.
Its just staying without printing any single line. But the movies are been converted.
Unable to load FFProbe
I have this type of error
I have the same error as other students. I have set up the absolute path but still not working
FFMPEG_BINARIES=’C:/ffmpeg/bin/ffmpeg.exe’ even if l remove the .exe
FFPROBE_BINARIES=’C:/ffmpeg/bin/ffprobe.exe’
Also, the package doesn’t recognize use FFMpeg;
‘local’,
‘ffmpeg’ => [
‘binaries’ => env(‘FFMPEG_BINARIES’, ‘ffmpeg’),
‘threads’ => 12,
],
‘ffprobe’ => [
‘binaries’ => env(‘FFPROBE_BINARIES’, ‘ffprobe’),
],
‘timeout’ => 3600,
];
in the log file
[2019-12-01 20:58:29] local.INFO: ffprobe running command “C:/ffmpeg/bin/ffprobe.exe” -help -loglevel quiet
[2019-12-01 20:58:30] local.INFO: ffprobe executed command successfully
[2019-12-01 20:58:30] local.INFO: ffprobe running command “C:/ffmpeg/bin/ffprobe.exe” “C:\laragon\www\clone-youtube-application\storage\app\channels/3bf10f3c-59f6-4e9d-94ea-110459ece645/4nQxPhT8ejB724eilbwxrFxHkgwfMV8pKycTWkpE.mp4” -show_streams -print_format json
[2019-12-01 20:58:30] local.ERROR: ffprobe failed to execute command “C:/ffmpeg/bin/ffprobe.exe” “C:\laragon\www\clone-youtube-application\storage\app\channels/3bf10f3c-59f6-4e9d-94ea-110459ece645/4nQxPhT8ejB724eilbwxrFxHkgwfMV8pKycTWkpE.mp4” -show_streams -print_format json
[2019-12-01 20:58:30] local.ERROR: Unable to probe C:\laragon\www\clone-youtube-application\storage\app\channels/3bf10f3c-59f6-4e9d-94ea-110459ece645/4nQxPhT8ejB724eilbwxrFxHkgwfMV8pKycTWkpE.mp4 {“exception”:”[object] (FFMpeg\\Exception\\RuntimeException(code: 0): Unable to probe C:\\laragon\\www\\clone-youtube-application\\storage\\app\\channels/3bf10f3c-59f6-4e9d-94ea-110459ece645/4nQxPhT8ejB724eilbwxrFxHkgwfMV8pKycTWkpE.mp4 at C:\\laragon\\www\\clone-youtube-application\\vendor\\pbmedia\\php-ffmpeg\\src\\FFMpeg\\FFProbe.php:263)
[stacktrace]
i
Which PHP version are you running?
I am running on php 7.37
[2019-12-01 20:58:29] local.INFO: ffprobe running command “C:/ffmpeg/bin/ffprobe.exe” -help -loglevel quiet
[2019-12-01 20:58:30] local.INFO: ffprobe executed command successfully
[2019-12-01 20:58:30] local.INFO: ffprobe running command “C:/ffmpeg/bin/ffprobe.exe” “C:\laragon\www\clone-youtube-application\storage\app\channels/3bf10f3c-59f6-4e9d-94ea-110459ece645/4nQxPhT8ejB724eilbwxrFxHkgwfMV8pKycTWkpE.mp4” -show_streams -print_format json
[2019-12-01 20:58:30] local.ERROR: ffprobe failed to execute command “C:/ffmpeg/bin/ffprobe.exe” “C:\laragon\www\clone-youtube-application\storage\app\channels/3bf10f3c-59f6-4e9d-94ea-110459ece645/4nQxPhT8ejB724eilbwxrFxHkgwfMV8pKycTWkpE.mp4” -show_streams -print_format json
[2019-12-01 20:58:30] local.ERROR: Unable to probe C:\laragon\www\clone-youtube-application\storage\app\channels/3bf10f3c-59f6-4e9d-94ea-110459ece645/4nQxPhT8ejB724eilbwxrFxHkgwfMV8pKycTWkpE.mp4 {“exception”:”[object] (FFMpeg\\Exception\\RuntimeException(code: 0):
I got error how can i fix it? thank you.
message: “Encoding failed”
exception: “FFMpeg\Exception\RuntimeException”
file: “C:\Users\visal\Desktop\project test\ffmpeg_blog\vendor\pbmedia\php-ffmpeg\src\FFMpeg\Media\AbstractVideo.php”
line: 106
hi buddy,
I am using ajax in controller store method , after :
ProcessVideoForStreming::dispatch($video);
return response()->json([message => ‘video under processing’]);
here, return response is not working. It is only working after the job finishes , but I want to
send message first and process the video after it
Hi Waleed,
Thanks for your sharing, but i’m getting an error when run php artisan vendor:publish –provider=”Pbmedia\LaravelFFMpeg\FFMpegServiceProvider”
Message: Class Pbmedia\LaravelFFMpeg\FFMpegServiceProvider not found.
I install ffmpeg at my applycation root/app so my path is :
FFMPEG_BINARIES=/app/ffmpeg/bin
FFPROBE_BINARIES=/app/ffmpeg/bin
Is there some thing wrong?
Hi Cheng,
I have the same issue with FFMpegServiceProvider
In ProviderRepository.php line 208:
Class ‘Pbmedia\LaravelFFMpeg\FFMpegServiceProvider’ not found
I really couldn’t find correct answers or solutions for this problem.
Yes i did one,
comment out that line from app/config.php and after run this two lines:
php artisan cache:clear
php artisan config:clear
and run again your command
I solved this by removing the two lines from the config/app.php and then run the following:
COMPOSER_MEMORY_LIMIT=-1 composer require pbmedia/laravel-ffmpeg
Worked like a charm 🤓
Hi, I’m pretty new in laravel. And when I want to run the server it says the following:
Warning: require(C:\wamp64\www\laravel-stream/vendor/autoload.php): failed to open stream: No such file or directory in C:\wamp64\www\laravel-stream\artisan on line 18
Fatal error: require(): Failed opening required ‘C:\wamp64\www\laravel-stream/vendor/autoload.php’ (include_path=’.;C:\php\pear’) in C:\wamp64\www\laravel-stream\artisan
on line 18
What should I do in this situation?
Run
composer install
Hello Waleed,
My jobs get executed correctly, but my videos are just 1 seconds long and gray or very pixely quality and one second long.
Class “Pbmedia\LaravelFFMpeg\FFMpegServiceProvider” not found
My PHP version: 8.0
Laravel Version: 8.67