MakeVideo.php 9.63 KB
<?php

namespace App\Jobs;

use App\Models\AdminMakeVideo;
use App\Models\Immerse;
use App\Models\VideoTemp;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

class MakeVideo implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $adminMakeVideo;

    protected $ffmpeg;

    protected $ffprobe;

    protected $media_info;

    protected $output_width;

    protected $output_height;

    /**
     * Create a new job instance.
     * @param AdminMakeVideo $adminMakeVideo
     * @return void
     */
    public function __construct(AdminMakeVideo $adminMakeVideo)
    {
        $this->adminMakeVideo = $adminMakeVideo;
        $this->ffmpeg = env('FFMPEG_CMD');
        $this->ffprobe = env('FFPROBE_CMD');
        $this->output_width = 720;
        $this->output_height = 1280;

        $file = $this->getAbsolutePath($adminMakeVideo->video_url);
        // 分析视频
        $media_info = $this->mediainfo($file);
        // 素材准备
        $drawtext = $this->getTextContentString();

        if ($media_info['format']['nb_streams'] >= 2) {
            /** 音频视频轨都有 */
            if ($is_bgm) {
                // 有背景音  融合
                $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
                $cmd = $this->ffmpeg .
                    ' -y -i ' . escapeshellarg($file) .
                    ' -y -i ' . escapeshellarg($bgm) .
                    ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
                    '-ar 48000 -ab 64k  ' . escapeshellarg($audio);
                if (!$this->execmd($cmd)) return;

                $audio_input = ' -i ' . escapeshellarg($audio);
                $audio_filter = '2:a';
            } else {
                // 没有背景音
                $audio_input = '';
                $audio_filter = '0:a';
            }
        } elseif ($media_info['format']['nb_streams'] == 1) {
            /** 只有视频轨 */
            // 生成一段无声音频
            $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
            $cmd = $this->ffmpeg .
                ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) .
                ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
            if (!$this->execmd($cmd)) return;

            if ($is_bgm) {
                // 有背景音  融合
                $audio_empty = $audio;
                $audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
                $cmd = $this->ffmpeg .
                    ' -y -i ' . escapeshellarg($audio_empty) .
                    ' -y -i ' . escapeshellarg($bgm) .
                    ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
                    '-ar 48000 -ab 64k  ' . escapeshellarg($audio);
                if (!$this->execmd($cmd)) return;
            }
            $audio_input = ' -i ' . escapeshellarg($audio);
            $audio_filter = '2:a';
        } else {
            /** 音频视频轨都没有 */
            Log::channel('daily')->error('视频没有video track, url:' . $file);
            return;
        }

        $thumbnail = $this->getTempPath('.jpg','thumbnail');
        if ($adminMakeVideo->thumbnail == 2){
            // 截取中间帧作为视频封面
            $frame = ceil($media_info['streams'][0]['nb_frames'] / 2);
            $cmd = $this->ffmpeg . ' -y ' .
                ' -i ' . escapeshellarg($file) .
                ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' .
                ' -map [img]'.
                ' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
                escapeshellarg($this->getAbsolutePath($thumbnail));
            if (!$this->execmd($cmd)) return ;
        }else{
            // 手动上传封面
            $origin_thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url);
            // 将封面分辨率改为指定分辨率
            $cmd = $this->ffmpeg . ' -y ' .
                ' -i ' . escapeshellarg($origin_thumbnail) .
                '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
                escapeshellarg($this->getAbsolutePath($thumbnail));
            if (!$this->execmd($cmd)) return ;
        }

        $output = $this->getTempPath('.mp4','video');
        $cmd = $this->ffmpeg . ' -y '.
            ' -i ' . escapeshellarg($file).
            ' -i ' . escapeshellarg($watermark).
            $audio_input .
            ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
            ' [text];[text]'.
            ' [1:v]overlay=20:20[v]" ' .
            ' -map [v] -map '. $audio_filter .
            ' -c:v libx264 -bt 256k -r 25' .
            ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
            escapeshellarg($this->getAbsolutePath($output));

        if (!$this->execmd($cmd)) return ;

        $video_info = $this->mediainfo($this->getAbsolutePath($output));
        Immerse::query()->create([
            'user_id' => 1,
            'title' => '',
            'weather' => $adminMakeVideo->weather,
            'huangli' => $adminMakeVideo->huangli,
            'content' => $adminMakeVideo->feel,
            'location' => $adminMakeVideo->location,
            'longitude' => $adminMakeVideo->longitude,
            'latitude' => $adminMakeVideo->latitude,
            'url' => $output,
            'type' => $adminMakeVideo->type == 1 ? 2 : 1,
            'upload_file' => '',
            'duration' => $video_info['format']['duration'],
            'size' => $video_info['format']['size'],
            'origin_video_url' => $this->adminMakeVideo->video_url,
            'origin_image_url' => '',
            'poem_id' => $this->adminMakeVideo->poem_id,
            'temp_id' => $this->adminMakeVideo->temp_id,
            'thumbnail' => $thumbnail,
            'state' => 1,
            'bgm' => $is_bgm ? $bgm : '',
        ]);
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $file = $this->getAbsolutePath($this->adminMakeVideo->video_url);
        // 分析视频
        $this->media_info = $this->mediaInfo($file);

        // 准备素材

        // 组装文字参数

        // 合成视频

        // 制作封面图

        // 分析视频 入库
    }

    public function getAbsolutePath($path)
    {
        if ($path == '') return '';

        return Storage::disk('public')->path($path);
    }

    public function mediaInfo($file)
    {
        if ($this->media_info) return $this->media_info;

        $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
        $output = $this->execmd($cmd);
        $data = json_decode($output, true);
        if (json_last_error() === JSON_ERROR_UTF8) {
            $output = mb_convert_encoding($output, "UTF-8");
            $data = json_decode($output, true);
        }
        $this->media_info = $data;
        return $data;
    }

    public function execmd($cmd, $update_progress = false) {
        echo $cmd . "\n". "\n". "\n";
        $descriptorspec = array(
            1 => array("pipe", "w"),  // 标准输出,子进程向此管道中写入数据
        );
        $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
        if (is_resource($process)) {
            $error0 = '';
            $error1 = '';
            $stdout = '';
            while (!feof($pipes[1])) {
                $line = fgets($pipes[1], 150);
                $stdout .= $line;
                if ($line) {
                    //记录错误
                    $error0 = $error1;
                    $error1 = $line;
                    if ($update_progress &&
                        false !== strpos($line, 'size=') &&
                        false !== strpos($line, 'time=') &&
                        false !== strpos($line, 'bitrate='))
                    {
                        //记录进度 size=    3142kB time=00:00:47.22 bitrate= 545.1kbits/s
                        $line = explode(' ', $line);
                        $time = null;
                        foreach ($line as $item) {
                            $item = explode('=', $item);
                            if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
                                $time = $item[1];
                                break;
                            }
                        }
                    }
                }
            }
            // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
            fclose($pipes[1]);
            $exitedcode = proc_close($process);
            if ($exitedcode === 0) {
                return $stdout;
            } else {
                $error = trim($error0,"\n") . ' '. trim($error1,"\n");
                // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
                // ErrorUtil::triggerErrorMsg($error, $exitedcode);
                Log::error("cmd:{$cmd}");
                Log::error($error);
                Log::error("stdout:{$stdout}");
            }
        } else {
            // return ErrorUtil::triggerErrorMsg('proc_open error');
            Log::error('proc_open error');
        }
    }
}