Skip to content

Instantly share code, notes, and snippets.

@Nks
Last active February 15, 2024 02:18
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save Nks/b3b1cd7398a560eda8ddb7e37901869e to your computer and use it in GitHub Desktop.
Save Nks/b3b1cd7398a560eda8ddb7e37901869e to your computer and use it in GitHub Desktop.
Laravel Media Library converting video to .mp4 after saving

How it works?

This listener will convert your .mov, .avi and a lot of others video files to .mp4 after adding media with https://github.com/spatie/laravel-medialibrary Feel free ask anything what you want.

Requirements

Usage

In EventServiceProvider add your event listener for spatie/laravel-medialibrary

protected $listen = [
        'Spatie\MediaLibrary\Events\MediaHasBeenAdded' => [
            'App\Listeners\MediaVideoConverterListener'
        ],

Want use other audio codec? Rewrite it by yourself in the code or just add this in the medialibrary.php config file with your codec.

'audio_codec'                 => 'libvo_aacenc',

Configure supervisord with following instructions:

[program:laravel_queue]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /home/vagrant/Code/laravel/artisan queue:work --timeout=0 --memory=512 --tries=3
autostart=true
autorestart=true
user=vagrant
numprocs=3
redirect_stderr=true
stdout_logfile=/home/vagrant/Code/laravel/storage/logs/default_queue.log

numprocs <- How many queues will be work on your server. Depends on your memory, CPU, etc. This is how many files will processing in one time. By default 3 video files will be in processing. --memory=512 <- by default it's 128 Mb

<?php
namespace App\Listeners;
use App\Media;
use FFMpeg\FFMpeg;
use FFMpeg\Format\Video\X264;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;
use Spatie\MediaLibrary\Events\MediaHasBeenAdded;
use Spatie\MediaLibrary\Helpers\File as MediaLibraryFileHelper;
class MediaVideoConverterListener implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
protected $media;
/**
* Create the event listener.
*
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @return void
*/
public function handle(MediaHasBeenAdded $event)
{
$this->media = $event->media;
//prevent any events from media model
$this->media->flushEventListeners();
if ((!$this->isVideo())
|| $this->media->getCustomProperty('status') !== Media::MEDIA_STATUS_TO_CONVERT
|| strtolower($this->media->extension) == 'mp4' || strtolower($this->media->mime_type) == 'video/mp4'
) {
$this->media->setCustomProperty('status', Media::MEDIA_STATUS_READY);
$this->media->setCustomProperty('progress', 100);
$this->media->save();
return;
}
$this->media->setCustomProperty('status', Media::MEDIA_STATUS_PROCESSING);
$this->media->save();
try {
$fullPath = $this->media->getPath();
$newFileFullPath = pathinfo($fullPath, PATHINFO_DIRNAME)
. DIRECTORY_SEPARATOR . pathinfo($fullPath, PATHINFO_FILENAME)
. Media::MEDIA_VIDEO_EXT;
if (file_exists($newFileFullPath)) {
unlink($newFileFullPath);
}
$ffmpeg = FFMpeg::create([
'ffmpeg.binaries' => config('medialibrary.ffmpeg_binaries'),
'ffprobe.binaries' => config('medialibrary.ffprobe_binaries'),
'timeout' => 3600,
'ffmpeg.threads' => 12,
]);
$video = $ffmpeg->open($fullPath);
$format = new X264();
$format->on('progress', function ($video, $format, $percentage) use ($fullPath, $newFileFullPath) {
if ($percentage >= 100) {
$this->mediaConvertingCompleted($fullPath, $newFileFullPath);
} elseif (!($percentage % 10)) {
$this->media->setCustomProperty('progress', $percentage);
$this->media->save();
}
});
$format->setAudioCodec(config('medialibrary.audio_codec', 'libvo_aacenc'))
->setKiloBitrate(1000)
->setAudioChannels(2)
->setAudioKiloBitrate(256);
$video->save($format, $newFileFullPath);
} catch (\Exception $e) {
$this->media->setCustomProperty('status', Media::MEDIA_STATUS_FAILED);
$this->media->setCustomProperty('error', $e->getMessage());
$this->media->save();
}
}
/**
* @param $originalFilePath
* @param $convertedFilePath
*/
protected function mediaConvertingCompleted($originalFilePath, $convertedFilePath)
{
if (file_exists($originalFilePath)) {
unlink($originalFilePath);
}
$this->media->file_name = pathinfo($convertedFilePath, PATHINFO_BASENAME);
$this->media->mime_type = MediaLibraryFileHelper::getMimetype($convertedFilePath);
$this->media->size = filesize($convertedFilePath);
$this->media->setCustomProperty('status', Media::MEDIA_STATUS_READY);
$this->media->setCustomProperty('progress', 100);
$this->media->save();
}
/**
* Is media a video?
*
* @return bool
*/
protected function isVideo()
{
return (strpos($this->media->mime_type, 'video') !== false);
}
}
@PimButton
Copy link

Hi @Nks,
Thanks for this gist, it looks very promising. It appears that you also use a custom media class, could you also share that? In the current form the gist fails because the statics like Media::MEDIA_STATUS_TO_CONVERT are not available in the default media class.
Thanks!

@pedrofurtado
Copy link

@Nks, really thanks man! You saved the day here 🎉 🤝

@wecodelaravel
Copy link

you should probably use the spatie model for media.

use Spatie\MediaLibrary\Models\Media;

@wecodelaravel
Copy link

wecodelaravel commented Dec 6, 2018

Is it possible to have the image generator also generate a screenshot every 5 seconds of the converted video?

How would i implement ExtractMultipleFramesFilter in your listener. your help would be awesome. I love you listener its very helpful.

Another question, is it possible to format the size of the mp4 also?

Thanks in advance for your help.

@lukecurtis93
Copy link

Hi @Nks,
Thanks for this gist, it looks very promising. It appears that you also use a custom media class, could you also share that? In the current form the gist fails because the statics like Media::MEDIA_STATUS_TO_CONVERT are not available in the default media class.
Thanks!

Did you ever get these?

@victor-priceputu
Copy link

Hi, thank you for this listener, it is amazing, but I do have one issue: is the progress 100% working? My code stops receiving progress at 99% and I cannot find any list of events anywhere to check if there is one for complete or something like that.

@Grigory-Rechkin
Copy link

Hi, thank you for this listener, it is amazing, but I do have one issue: is the progress 100% working? My code stops receiving progress at 99% and I cannot find any list of events anywhere to check if there is one for complete or something like that.

Seems related to that issue
Instead of checking 100% progress you can try to call $this->mediaConvertingCompleted($fullPath, $newFileFullPath); after $video->save($format, $newFileFullPath);

@victor-priceputu
Copy link

Hi, thank you for this listener, it is amazing, but I do have one issue: is the progress 100% working? My code stops receiving progress at 99% and I cannot find any list of events anywhere to check if there is one for complete or something like that.

Seems related to that issue
Instead of checking 100% progress you can try to call $this->mediaConvertingCompleted($fullPath, $newFileFullPath); after $video->save($format, $newFileFullPath);

I am doing that now, and strangely it actually stopped giving progress up until 100%

@james-lukensow
Copy link

@Nks Question... Is this approach currently the only way to process videos that are uploaded via Laravel Media library? If my research is right, then I believe so. My use case is similar to what you're doing, anytime a video is uploaded, I want to transcode it to mp4 and do a few other FFmpeg optimizations.

@Nks
Copy link
Author

Nks commented Oct 26, 2020

@Nks Question... Is this approach currently the only way to process videos that are uploaded via Laravel Media library? If my research is right, then I believe so. My use case is similar to what you're doing, anytime a video is uploaded, I want to transcode it to mp4 and do a few other FFmpeg optimizations.

This is just an example of how to proceed with the media conversion on upload. For better performance and handling more options, I suggest using a separate service layer in your application and have parameters set in the listener to your service. If you are using your solution to store the media data you can create your own events and listen to it. eg. in my current solutions I have separated servers with the same Redis queue. When a new video added I triggering the job "ConvertMedia" and listen to it on transcoding microservices and then converting it. I can pass parameters to the job if it is necessary.

@nmriahi
Copy link

nmriahi commented Jan 31, 2021

Hi, thanks for this great listener.
I have a question: Does it work also with Amazon S3 upload feature of the Meadia library package?

@Nks
Copy link
Author

Nks commented Jan 31, 2021

Hi, thanks for this great listener.
I have a question: Does it work also with Amazon S3 upload feature of the Meadia library package?

As far as I know, FFMPEG does not support uploading files to s3 or google cloud storage. But you can simply implement functionality where after store the file locally you running the method which will upload converted files to AWS or GCS or anywhere where you want.

@nmriahi
Copy link

nmriahi commented Jan 31, 2021

Hi, thanks for this great listener.
I have a question: Does it work also with Amazon S3 upload feature of the Meadia library package?

As far as I know, FFMPEG does not support uploading files to s3 or google cloud storage. But you can simply implement functionality where after store the file locally you running the method which will upload converted files to AWS or GCS or anywhere where you want.

Thank you for your quick reply @Nks. Actually I'm using Amazon S3 as the file driver of spatie media library package. I think as you said, I should use your event listener, before triggering the upload to S3 process.

@Nks
Copy link
Author

Nks commented Jan 31, 2021

Hi, thanks for this great listener.
I have a question: Does it work also with Amazon S3 upload feature of the Meadia library package?

As far as I know, FFMPEG does not support uploading files to s3 or google cloud storage. But you can simply implement functionality where after store the file locally you running the method which will upload converted files to AWS or GCS or anywhere where you want.

Thank you for your quick reply @Nks. Actually I'm using Amazon S3 as the file driver of spatie media library package. I think as you said, I should use your event listener, before triggering the upload to S3 process.

Technically this is easy to implement with a separated service:

  • Listen for the upload and call the event listener.
  • Separate from your listener with the separated service. eg. you can wrong a new Job and dispatch it from the current event listener. You still can use event listener in any case. Make sure that you are using a long queue. Do not use a default laravel timeout for the queue and keep your eyes on the retry_after option for your queue. It should be more than you expect that your job will work. I suggest using a separated queue and launch your job on a separated long polling queue (I'm using a 43200 timeouts for my jobs).
  • After converting the video file upload file to the s3 or to the google cloud storage (I'm using Google Cloud Storage). If you converting big files to the HLS better to use parallel uploading. I suggest using gsutils with the Symfony process package. You can launch it like this:
    gsutil -q -m -o Credentials:gs_service_key_file=/var/www/storage/gcs/your-file.json -o GSUtil:parallel_process_count=8 cp /path/to/hls/files/ gs://storage/path/. For AWS you can use your command tools like described here. If s3 supports upload files with parallel processing it should give you the fastest solution instead of upload a lot of files one by one.
  • Update your media model that you uploaded file to s3/gcs. You can store the information in meta.

@nmriahi
Copy link

nmriahi commented Jan 31, 2021

@Nks Thank you very much for your great helo. I woudld appreciate if I can have your code blocks for interacting with GCS.

@nmriahi
Copy link

nmriahi commented Feb 15, 2021

@Nks One other question:
As I understand, you have a Media model in your code with App\Media namespace which has these constants in it: MEDIA_STATUS_TO_CONVERT, MEDIA_STATUS_READY, MEDIA_STATUS_PROCESSING, MEDIA_VIDEO_EXT.
Because I don't have access to that model, replaced it with 'Spatie\MediaLibrary\Models\Media' in my listener, which doesn't have those constants.
Can you please guide me how to resolve the issue?

@dnnp2011
Copy link

dnnp2011 commented Jun 16, 2021

To whom it may concern:

use Spatie\MediaLibrary\Helpers\File as MediaLibraryFileHelper;

should now be (^9.0)

use Spatie\MediaLibrary\Support\File as MediaLibraryFileHelper;

@me-devms
Copy link

MEDIA_STATUS_TO_CONVERT, MEDIA_STATUS_READY, MEDIA_STATUS_PROCESSING, MEDIA_VIDEO_EXT.

Any Solution for these constants ?

@Nks
Copy link
Author

Nks commented Feb 16, 2022

@ilikewebsite you can use your own constants that you can define in your app.

@horizonvert1027
Copy link

@Nks I have to play .mov file in Laravel. Uploading is running well. but I can't play .mov file.
How to play .mov file with this library?

@Nks
Copy link
Author

Nks commented Jan 25, 2023

@horizonvert1027, you need to convert the file to any playable file in the browser. I suggest using mp4 format for this and convert it to mp4 from mov using ffmpeg: ffmpeg -i input.mov -qscale 0 output.mp4

The provided solution should already be able to convert any supported files to mp4 though.

@horizonvert1027
Copy link

@Nks, Thank you for your reply.
I have made a Laravel framework. But I don't know what to do. Can you send me your contact information?
My e-mail is horizonvert.pu@gmail.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment