Skip to content

Instantly share code, notes, and snippets.

@4unkur
Created January 29, 2020 06:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save 4unkur/e066fd3058b62c1c38808448aa682da7 to your computer and use it in GitHub Desktop.
Save 4unkur/e066fd3058b62c1c38808448aa682da7 to your computer and use it in GitHub Desktop.
Laravel Job class which converts video to MP4 format + appends logo image on the left top corner
<?php
namespace App\Jobs;
use App\Exceptions\ProcessVideoException;
use App\Interview;
use App\Plan;
use App\ProcessVideoPitchLog;
use App\RawVideoPitch;
use App\Resume;
use App\Video;
use App\Events\ResumeUpdated;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\File;
use mikehaertl\shellcommand\Command;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ProcessVideo implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 1800; // 30 min
/**
* @var RawVideoPitch
*/
private $rawVideoPitch;
private $videoFileName;
private $inputPath;
private $outputPath;
private $model;
/**
* @var ProcessVideoPitchLog
*/
private $processVideoPitchLog;
private $duration;
/**
* @param ProcessVideoPitchLog $processVideoPitchLog
*/
public function __construct(ProcessVideoPitchLog $processVideoPitchLog)
{
$this->processVideoPitchLog = $processVideoPitchLog;
$this->rawVideoPitch = $this->processVideoPitchLog->rawVideoPitch;
$this->model = $this->rawVideoPitch->videoAttachable;
}
/**
* @throws ProcessVideoException
*/
public function handle()
{
$this->processVideoPitchLog->updateStatus(ProcessVideoPitchLog::STATUS_STARTED);
$this->createOutputFolderIfNotExists($this->model->getMorphClass());
$this->videoFileName = Video::FOLDER[$this->model->getMorphClass()] . '/' . pathinfo($this->rawVideoPitch->path)['filename'];
$this->inputPath = storage_path('app/' . $this->rawVideoPitch->path);
$this->outputPath = storage_path('app/' . $this->videoFileName);
$this->convertToMp4();
if ($this->isVideoDurationExceeded()) {
$this->removeOutputFile();
$this->processVideoPitchLog->updateStatus(ProcessVideoPitchLog::STATUS_DURATION_EXCEEDED);
return false;
}
$video = Video::create([
'mp4_file' => $this->videoFileName . '.mp4',
'original_file_name' => $this->rawVideoPitch->filename,
'user_id' => $this->rawVideoPitch->user_id,
'raw_video_pitch_id' => $this->rawVideoPitch->id,
'process_video_pitch_log_id' => $this->processVideoPitchLog->id,
'duration' => $this->duration
]);
switch ($this->model->getMorphClass()) {
case Resume::class :
$video->resumes()->save($this->model);
event(new ResumeUpdated($this->model));
break;
case Interview::class :
$video->user()->associate($this->model->employer);
$video->interview()->associate($this->model);
$video->save();
$this->model->video_id = $video->id;
$this->model->save();
break;
}
}
private function getConvertToMp4Command()
{
$logo = public_path('images/logo.png');
return <<<COMMAND
ffmpeg -y -i {$this->inputPath} -i $logo -filter_complex "overlay=x=(main_w-overlay_w)/(main_w-overlay_w):y=(main_h-overlay_h)/(main_h-overlay_h)" -c:v libx264 -pix_fmt yuv420p -profile:v baseline -level 3 -c:a aac -strict -2 {$this->outputPath}.mp4
COMMAND;
}
private function getVideoDurationCommand()
{
return <<<COMMAND
ffprobe -i {$this->outputPath}.mp4 -show_entries format=duration -v quiet -of csv="p=0"
COMMAND;
}
private function isVideoDurationExceeded()
{
$this->duration = $this->getVideoDuration();
if ($this->model->getMorphClass() == Resume::class) {
$allowedDuration = $this->model->subscription
? $this->model->subscription->plan->video_pitch_limit_seconds
: Plan::byName(Plan::SILVER)->first()->video_pitch_limit_seconds;
if ($this->duration > $allowedDuration) {
return true;
}
return false;
}
}
private function removeOutputFile()
{
File::delete($this->outputPath . '.mp4');
}
private function createOutputFolderIfNotExists($model)
{
$outputFolder = storage_path('app/' . Video::FOLDER[$model]);
if (!is_dir($outputFolder)) {
mkdir($outputFolder);
}
}
/**
* @return int
* @throws ProcessVideoException
*/
private function getVideoDuration()
{
$getVideoDurationCommand = new Command($this->getVideoDurationCommand());
$getVideoDurationCommand->execute();
$duration = $getVideoDurationCommand->getOutput();
if (empty($duration)) {
$this->removeOutputFile();
throw new ProcessVideoException('Video duration could not be fetched');
}
return intval($duration);
}
/**
* @throws ProcessVideoException
*/
private function convertToMp4()
{
$rawCommand = $this->getConvertToMp4Command();
$command = new Command($rawCommand);
$result = $command->execute();
if (empty($result)) {
$this->processVideoPitchLog
->updateStatus(ProcessVideoPitchLog::STATUS_FAILED)
->log([
'error' => $command->getError(),
'code' => $command->getExitCode(),
'command' => $rawCommand,
]);
throw new ProcessVideoException(
$command->getError(),
$command->getExitCode()
);
}
$this->processVideoPitchLog
->updateStatus(ProcessVideoPitchLog::STATUS_COMPLETED);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment