李帅

1.重构一言表

......@@ -70,9 +70,10 @@ class DevFFmpeg extends Command
// $arr = json_decode($json,true);
// dd($arr);
// dd(AdminMakeVideo::query()->find(1)->poem2());
dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray());
return 0;
dd(AdminMakeVideo::query()->find(33)->temp->components->toArray());
// dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray());
// return 0;
// dd(AdminMakeVideo::query()->find(33)->temp->components->toArray());
dd(AdminMakeVideo::query()->find(33)->temp->toArray());
AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components);
......
<?php
namespace App\Jobs;
use App\Models\Immerse;
use App\Models\VideoTemp;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\AdminMakeVideo;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class AdminMakeImmerse 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;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$adminMakeVideo = $this->adminMakeVideo;
// 模板
$template = $adminMakeVideo->temp->where('state',1)->first();
// 素材准备
$watermark = $this->getAbsolutePath('images/LOGO_eng.png');
$is_bgm = $template->bg_music == 1; //是否手动上传背景音
$bgm = $this->getAbsolutePath($template->bgm_url);
// 区分类型
if ($adminMakeVideo->type == 1) { // 视频
$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 : '',
]);
}else{ // 图文
$image = $this->getAbsolutePath($adminMakeVideo->images_url);
// 素材准备
$drawtext = $this->getTextContentString();
if ($this->adminMakeVideo->type == 2 && !$is_bgm){
// 没有背景音,单图一张,输出为单图。
$output = $this->getTempPath('.png','thumbnail');
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($image).
' -i ' . escapeshellarg($watermark).
' -filter_complex "[0:0]scale=' . $this->output_width . ':' . $this->output_height . ',' .
$drawtext . ' [text];[text][1:0]overlay=20:20" ' .
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
$thumbnail = $output;
}else{
// 有背景音 单图合成视频,时长为音频时长,音频加入背景音
$output = $this->getTempPath('.mp4','video');
// 分析背景音
$mediainfo = $this->mediainfo($bgm);
// 记录媒体信息时长
$duration = $mediainfo['format']['duration'] ?: 0;
// 单图、水印、bgm 合成视频
$cmd = $this->ffmpeg . ' -y ' .
' -loop 1 -i ' . escapeshellarg($image) .
' -i ' . escapeshellarg($watermark) .
' -i ' . escapeshellarg($bgm) .
' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',setdar=dar=9/16,' . $drawtext .
' [text];[text][2:v]overlay=20:20[v]"' .
' -map [v] -map 1:0 ' .
' -c:v libx264 -bt 256k -r 25 -t ' . $duration .
' -ar 48000 -ac 2 -qmin 30 -qmax 60 -profile:v high -pix_fmt yuv420p -preset fast '.
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
$thumbnail = $this->getTempPath('.jpg','thumbnail');
if ($adminMakeVideo->thumbnail == 2){
// 将封面分辨率改为指定分辨率
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($image) .
'-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
escapeshellarg($this->getAbsolutePath($thumbnail));
if (!$this->execmd($cmd)) return ;
}else{
// 手动上传封面
$origin_thumbnail = $this->getAbsolutePath($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 ;
}
}
// 全部合成以后创建 临境
$video_info = $this->mediainfo($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' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'upload_file' => '',
'duration' => $video_info['format']['duration'] ?? 0,
'size' => $video_info['format']['size'] ?? 0,
'origin_video_url' => '',
'origin_image_url' => $this->adminMakeVideo->images_url,
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => $thumbnail,
'state' => 1,
'bgm' => $is_bgm ? $bgm : '',
]);
}
}
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');
}
}
/**
* 获取输出临时文件名
* @param string $ext
* @param string $dir
* @return string
*/
public function getTempPath($ext = '.mp4',$dir = 'video')
{
$filename = "/output_" . time() . rand(0, 10000);
$hash_hex = md5($filename);
// 16进制表示的字符串一共32字节,表示16个二进制字节。
// 前16个字符用来第一级求摸,后16个用做第二级
$hash_hex_l1 = substr($hash_hex, 0, 8);
$hash_hex_l2 = substr($hash_hex, 8, 8);
$dir_l1 = hexdec($hash_hex_l1) % 256;
$dir_l2 = hexdec($hash_hex_l2) % 512;
$dir = $dir . '/' . $dir_l1 . '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return $dir . $filename . $ext;
}
public function getAbsolutePath($path)
{
if ($path == '') return '';
return Storage::disk('public')->path($path);
}
public function getTextContentString()
{
$components = $this->adminMakeVideo->temp->components;
$drawtext = '';
foreach ($components as $component) {
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : 0.5;
$font_file = $this->getAbsolutePath($component->font_file);
$text_bg_box = $component->text_bg_box ?? 0;
$fix_bounds = $component->fix_bounds == 1;
switch ($component->name){
case 'every_poem':
case 'one_poem':
$content = $this->adminMakeVideo->poem->content;
$contents = explode("\n",$content); //计算诗词行数
if ($this->media_info['format']['duration'] < 2 * count($contents)) {
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
}else{
$FID = $FOD = 1.0;
if ($this->media_info['format']['duration'] > 3 * count($contents)) $FID = $FOD = 1.5;
$round = round($this->media_info['format']['duration'] / count($contents),1);
$sub_text = '';
foreach ($contents as $key => $content){
$DS = $key * $round;
$DE = $DS + $round;
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$sub_text .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor_expr=' . escapeshellarg($text_color . '%{eif\\\\: clip(255*(1*between(t\\, ' . $DS . ' + ' . $FID . '\\, ' . $DE . ' - ' . $FOD . ') + ((t - ' . $DS . ')/' . $FID . ')*between(t\\, ' . $DS . '\\, ' . $DS . ' + ' . $FID . ') + (-(t - ' . $DE . ')/' . $FOD . ')*between(t\\, ' . $DE . ' - ' . $FOD . '\\, ' . $DE . '))\\, 0\\, 255) \\\\: x\\\\: 2 }') . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . '", ';
}
$drawtext .= $sub_text;
}
break;
case 'weather':
$content = $this->adminMakeVideo->weather;
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'date':
$content = Carbon::now()->format('Y年m月d日H时');
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'feel':
$content = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
}
}
return rtrim($drawtext,', ');
}
public function calcFontSize($width)
{
return ceil($this->output_width / 360 * $width);
}
public function calcBorderSize($width)
{
return ceil($this->output_width / 360 * $width);
}
public function getTextHeight()
{
$height = $this->output_height;
}
}
......@@ -51,35 +51,29 @@ class AdminMakeImmerse implements ShouldQueue
*/
public function handle()
{
$adminMakeVideo = $this->adminMakeVideo;
// 模板
$template = $adminMakeVideo->temp->where('state',1)->first();
$file = $this->getAbsolutePath($this->adminMakeVideo->video_url);
// 分析视频
$this->media_info = $this->mediaInfo($file);
// 素材准备
// 准备素材
$watermark = $this->getAbsolutePath('images/LOGO_eng.png');
$is_bgm = $template->bg_music == 1; //是否手动上传背景音
$bgm = $this->getAbsolutePath($template->bgm_url);
// 区分类型
if ($adminMakeVideo->type == 1) { // 视频
$file = $this->getAbsolutePath($adminMakeVideo->video_url);
// 分析视频
$media_info = $this->mediainfo($file);
// 素材准备
// 组装文字参数
$drawtext = $this->getTextContentString();
if ($media_info['format']['nb_streams'] >= 2) {
/** 音频视频轨都有 */
// 判断双轨 没有则制作空轨
$is_bgm = $this->adminMakeVideo->temp->bg_music == 1; //是否手动上传背景音
if ($this->media_info['format']['nb_streams'] >= 2) { /** 音频视频轨都有 */
if ($is_bgm) {
// 有背景音 融合
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
$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;
if (!$this->execCmd($cmd)) return;
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
......@@ -88,25 +82,25 @@ class AdminMakeImmerse implements ShouldQueue
$audio_input = '';
$audio_filter = '0:a';
}
} elseif ($media_info['format']['nb_streams'] == 1) {
/** 只有视频轨 */
} elseif ($this->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']) .
' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($this->media_info['format']['duration']) .
' -ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
if (!$this->execCmd($cmd)) return;
if ($is_bgm) {
// 有背景音 融合
$audio_empty = $audio;
$audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
$bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$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;
if (!$this->execCmd($cmd)) return;
}
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
......@@ -116,28 +110,30 @@ class AdminMakeImmerse implements ShouldQueue
return;
}
// 制作封面图
$thumbnail = $this->getTempPath('.jpg','thumbnail');
if ($adminMakeVideo->thumbnail == 2){
if ($this->adminMakeVideo->thumbnail == 2){
// 截取中间帧作为视频封面
$frame = ceil($media_info['streams'][0]['nb_frames'] / 2);
$frame = ceil($this->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 ;
if (!$this->execCmd($cmd)) return ;
}else{
// 手动上传封面
$origin_thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url);
$origin_thumbnail = $this->getAbsolutePath($this->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 ;
if (!$this->execCmd($cmd)) return ;
}
// 合成视频
$output = $this->getTempPath('.mp4','video');
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($file).
......@@ -151,20 +147,21 @@ class AdminMakeImmerse implements ShouldQueue
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
if (!$this->execCmd($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,
'weather' => $this->adminMakeVideo->weather,
'huangli' => $this->adminMakeVideo->huangli,
'content' => $this->adminMakeVideo->feel,
'location' => $this->adminMakeVideo->location,
'longitude' => $this->adminMakeVideo->longitude,
'latitude' => $this->adminMakeVideo->latitude,
'url' => $output,
'type' => $adminMakeVideo->type == 1 ? 2 : 1,
'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'upload_file' => '',
'duration' => $video_info['format']['duration'],
'size' => $video_info['format']['size'],
......@@ -176,100 +173,21 @@ class AdminMakeImmerse implements ShouldQueue
'state' => 1,
'bgm' => $is_bgm ? $bgm : '',
]);
}else{ // 图文
$image = $this->getAbsolutePath($adminMakeVideo->images_url);
// 素材准备
$drawtext = $this->getTextContentString();
if ($this->adminMakeVideo->type == 2 && !$is_bgm){
// 没有背景音,单图一张,输出为单图。
$output = $this->getTempPath('.png','thumbnail');
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($image).
' -i ' . escapeshellarg($watermark).
' -filter_complex "[0:0]scale=' . $this->output_width . ':' . $this->output_height . ',' .
$drawtext . ' [text];[text][1:0]overlay=20:20" ' .
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
$thumbnail = $output;
}else{
// 有背景音 单图合成视频,时长为音频时长,音频加入背景音
$output = $this->getTempPath('.mp4','video');
// 分析背景音
$mediainfo = $this->mediainfo($bgm);
// 记录媒体信息时长
$duration = $mediainfo['format']['duration'] ?: 0;
// 单图、水印、bgm 合成视频
$cmd = $this->ffmpeg . ' -y ' .
' -loop 1 -i ' . escapeshellarg($image) .
' -i ' . escapeshellarg($watermark) .
' -i ' . escapeshellarg($bgm) .
' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',setdar=dar=9/16,' . $drawtext .
' [text];[text][2:v]overlay=20:20[v]"' .
' -map [v] -map 1:0 ' .
' -c:v libx264 -bt 256k -r 25 -t ' . $duration .
' -ar 48000 -ac 2 -qmin 30 -qmax 60 -profile:v high -pix_fmt yuv420p -preset fast '.
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
$thumbnail = $this->getTempPath('.jpg','thumbnail');
if ($adminMakeVideo->thumbnail == 2){
// 将封面分辨率改为指定分辨率
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($image) .
'-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
escapeshellarg($this->getAbsolutePath($thumbnail));
if (!$this->execmd($cmd)) return ;
}else{
// 手动上传封面
$origin_thumbnail = $this->getAbsolutePath($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 ;
}
}
// 全部合成以后创建 临境
$video_info = $this->mediainfo($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' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'upload_file' => '',
'duration' => $video_info['format']['duration'] ?? 0,
'size' => $video_info['format']['size'] ?? 0,
'origin_video_url' => '',
'origin_image_url' => $this->adminMakeVideo->images_url,
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => $thumbnail,
'state' => 1,
'bgm' => $is_bgm ? $bgm : '',
]);
}
public function getAbsolutePath($path)
{
if ($path == '') return '';
return Storage::disk('public')->path($path);
}
public function mediainfo($file)
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);
$output = $this->execCmd($cmd);
$data = json_decode($output, true);
if (json_last_error() === JSON_ERROR_UTF8) {
$output = mb_convert_encoding($output, "UTF-8");
......@@ -279,90 +197,9 @@ class AdminMakeImmerse implements ShouldQueue
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');
}
}
/**
* 获取输出临时文件名
* @param string $ext
* @param string $dir
* @return string
*/
public function getTempPath($ext = '.mp4',$dir = 'video')
{
$filename = "/output_" . time() . rand(0, 10000);
$hash_hex = md5($filename);
// 16进制表示的字符串一共32字节,表示16个二进制字节。
// 前16个字符用来第一级求摸,后16个用做第二级
$hash_hex_l1 = substr($hash_hex, 0, 8);
$hash_hex_l2 = substr($hash_hex, 8, 8);
$dir_l1 = hexdec($hash_hex_l1) % 256;
$dir_l2 = hexdec($hash_hex_l2) % 512;
$dir = $dir . '/' . $dir_l1 . '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return $dir . $filename . $ext;
}
public function getAbsolutePath($path)
public function execCmd($cmd)
{
if ($path == '') return '';
return Storage::disk('public')->path($path);
return shell_exec("{$cmd} 2>&1");
}
public function getTextContentString()
......@@ -371,38 +208,41 @@ class AdminMakeImmerse implements ShouldQueue
$drawtext = '';
foreach ($components as $component) {
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : 0.5;
$font_file = $this->getAbsolutePath($component->font_file);
$text_bg_box = $component->text_bg_box ?? 0;
$fix_bounds = $component->fix_bounds == 1;
// 文字淡入淡出模式
if ($component->draw == 'fade'){
$contents = []; //
switch ($component->name){
case 'every_poem':
case 'one_poem':
$content = $this->adminMakeVideo->poem->content;
$contents = explode("\n",$content); //计算诗词行数
if ($this->media_info['format']['duration'] < 2 * count($contents)) {
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
}else{
$FID = $FOD = 1.0;
if ($this->media_info['format']['duration'] > 3 * count($contents)) $FID = $FOD = 1.5;
foreach ($this->adminMakeVideo->poem2->verses as $item) {
if ($item->stanza != '') $contents[] = $item->stanza;
}
break;
case 'one_poem_with_annotate':
foreach ($this->adminMakeVideo->poem2->verses as $item) {
if ($item->stanza != '') $contents[] = $item->stanza;
if ($item->annotate != '') $contents[] = $item->annotate;
}
break;
case 'weather':
$contents[] = $this->adminMakeVideo->weather;
break;
case 'date':
$contents[] = Carbon::now()->format('Y年m月d日H时');
break;
case 'feel':
$contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
break;
}
$FID = $FOD = floatval($component->fade_time / 1000);
$round = round($this->media_info['format']['duration'] / count($contents),1);
if ($round < 1) $round = 1;
$sub_text = '';
foreach ($contents as $key => $content){
$DS = $key * $round;
......@@ -416,58 +256,49 @@ class AdminMakeImmerse implements ShouldQueue
'fontcolor_expr=' . escapeshellarg($text_color . '%{eif\\\\: clip(255*(1*between(t\\, ' . $DS . ' + ' . $FID . '\\, ' . $DE . ' - ' . $FOD . ') + ((t - ' . $DS . ')/' . $FID . ')*between(t\\, ' . $DS . '\\, ' . $DS . ' + ' . $FID . ') + (-(t - ' . $DE . ')/' . $FOD . ')*between(t\\, ' . $DE . ' - ' . $FOD . '\\, ' . $DE . '))\\, 0\\, 255) \\\\: x\\\\: 2 }') . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . '", ';
'", ';
}
$drawtext .= $sub_text;
}
// 文字固定模式
if ($component->draw == 'fix'){
$contents = []; //
switch ($component->name){
case 'one_poem_with_annotate':
case 'one_poem':
$stanzas = '';
foreach ($this->adminMakeVideo->poem2->verses as $item) {
if ($item->stanza != '') $stanzas = $item->stanza . "\n";
}
$contents[] = $stanzas;
break;
case 'weather':
$content = $this->adminMakeVideo->weather;
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
$contents[] = $this->adminMakeVideo->weather;
break;
case 'date':
$content = Carbon::now()->format('Y年m月d日H时');
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
$contents[] = Carbon::now()->format('Y年m月d日H时');
break;
case 'feel':
$content = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
$contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
break;
}
$sub_text = '';
foreach ($contents as $key => $content){
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= 'drawtext="'.
$sub_text .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
}
$drawtext .= $sub_text;
}
}
......@@ -479,14 +310,27 @@ class AdminMakeImmerse implements ShouldQueue
return ceil($this->output_width / 360 * $width);
}
public function calcBorderSize($width)
/**
* 获取输出临时文件名
* @param string $ext
* @param string $dir
* @return string
*/
public function getTempPath($ext = '.mp4',$dir = 'video')
{
return ceil($this->output_width / 360 * $width);
}
$filename = "/output_" . time() . rand(0, 10000);
public function getTextHeight()
{
$height = $this->output_height;
$hash_hex = md5($filename);
// 16进制表示的字符串一共32字节,表示16个二进制字节。
// 前16个字符用来第一级求摸,后16个用做第二级
$hash_hex_l1 = substr($hash_hex, 0, 8);
$hash_hex_l2 = substr($hash_hex, 8, 8);
$dir_l1 = hexdec($hash_hex_l1) % 256;
$dir_l2 = hexdec($hash_hex_l2) % 512;
$dir = $dir . '/' . $dir_l1 . '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return $dir . $filename . $ext;
}
}
......
......@@ -44,75 +44,6 @@ class MakeVideo implements ShouldQueue
$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();
$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 : '',
]);
}
/**
......@@ -127,23 +58,24 @@ class MakeVideo implements ShouldQueue
$this->media_info = $this->mediaInfo($file);
// 准备素材
$watermark = $this->getAbsolutePath('images/LOGO_eng.png');
// 组装文字参数
$drawtext = $this->getTextContentString();
// 合成视频
if ($this->media_info['format']['nb_streams'] >= 2) {
/** 音频视频轨都有 */
// 判断双轨 没有则制作空轨
$is_bgm = $this->adminMakeVideo->temp->bg_music == 1; //是否手动上传背景音
if ($this->media_info['format']['nb_streams'] >= 2) { /** 音频视频轨都有 */
if ($is_bgm) {
// 有背景音 融合
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
$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;
if (!$this->execCmd($cmd)) return;
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
......@@ -152,25 +84,25 @@ class MakeVideo implements ShouldQueue
$audio_input = '';
$audio_filter = '0:a';
}
} elseif ($media_info['format']['nb_streams'] == 1) {
/** 只有视频轨 */
} elseif ($this->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']) .
' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($this->media_info['format']['duration']) .
' -ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
if (!$this->execCmd($cmd)) return;
if ($is_bgm) {
// 有背景音 融合
$audio_empty = $audio;
$audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
$bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$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;
if (!$this->execCmd($cmd)) return;
}
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
......@@ -181,8 +113,68 @@ class MakeVideo implements ShouldQueue
}
// 制作封面图
$thumbnail = $this->getTempPath('.jpg','thumbnail');
if ($this->adminMakeVideo->thumbnail == 2){
// 截取中间帧作为视频封面
$frame = ceil($this->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->execCmd($cmd)) return ;
}else{
// 手动上传封面
$origin_thumbnail = $this->getAbsolutePath($this->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->execCmd($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->execCmd($cmd)) return ;
// 分析视频 入库
$video_info = $this->mediainfo($this->getAbsolutePath($output));
Immerse::query()->create([
'user_id' => 1,
'title' => '',
'weather' => $this->adminMakeVideo->weather,
'huangli' => $this->adminMakeVideo->huangli,
'content' => $this->adminMakeVideo->feel,
'location' => $this->adminMakeVideo->location,
'longitude' => $this->adminMakeVideo->longitude,
'latitude' => $this->adminMakeVideo->latitude,
'url' => $output,
'type' => $this->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 : '',
]);
}
public function getAbsolutePath($path)
......