李帅

1.重新整合ffmpeg命令

......@@ -46,45 +46,143 @@ class DevFFmpeg extends Command
*/
public function handle()
{
$path = '/Users/lishuai/Desktop/test/';
$file = $path . 'qinghuaci.mp4';
// $file = $path . 'lingdang.mov';
$is_bgm = false;
$bgm = $path . 'bgm.aac';
// 1.getmediainfo 记录时长,音频视频取最长。
$cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
$output = $this->execmd($cmd);
$media_info = json_decode($output, true);
if (json_last_error() === JSON_ERROR_UTF8) {
$output = mb_convert_encoding($output, "UTF-8");
$media_info = json_decode($output, true);
}
/** 记录媒体信息时长*/
$media_file_time_length = isset($media_info['format']['duration']) ? $media_info['format']['duration'] : 0;
$media_file_size = $media_info['format']['size'];
if ($media_info['streams'][0]['codec_type'] !== 'video') {
return 0;
}
// 2. 判断是否有视频原音,没有原音用背景音,没有背景音则混入anullsrc
if ( $media_info['format']['nb_streams'] >= 2 ){ /** 音频视频轨都有 */
if ($is_bgm){
// 有背景音 融合
$audio = $this->getTempPath('.mp3');
$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 0;
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '[3:a]';
}else{
// 没有背景音
$audio_input = '';
$audio_filter = '[0:1]';
}
}elseif ( $media_info['format']['nb_streams'] == 1 ){
$audio = $this->getTempPath('.mp3');
$cmd = $this->ffmpeg .
' -y -f lavfi -i aevalsrc=0:duration='. escapeshellarg($media_file_time_length) .
' -ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return 0;
if ($is_bgm){
$audio_empty = $audio;
$audio = $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 0;
}
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '[3:a]';
}else{ /** 音频视频轨都没有 */
return 0;
}
$end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png";
$thumbnail = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png";
$font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
$signature = "一言 username";
// 生成贴纸和签名
$end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
// 截取最后一帧
$last_frame_video = $this->getTempPath();
$width = $media_info['streams'][0]['width'];
$height = $media_info['streams'][0]['height'];
$size = $width . 'x' . $height;
$time_length = 0.7;
$r = 24;
$frame_n = $media_info['streams'][0]['nb_frames'] - 2;
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
" -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
" -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
' -map [v] -map 2:a ' . escapeshellarg($last_frame_video);
if (!$this->execmd($cmd)) return 0;
$signature_x = 0;
$signature_y = -20;
$animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
$font_size = ceil($width / 14);
$content = '题破山寺后禅院' . "\t" . ' -- 常建' . PHP_EOL .
'清晨入古寺,初日照高林。' . PHP_EOL .
'曲径通幽处,禅房花木深。' . PHP_EOL .
'山光悦鸟性,潭影空人心。' . PHP_EOL .
'万籁此都寂,但余钟磬音。' . PHP_EOL ;
$path = '/Users/lishuai/Desktop/test/';
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($path . 'qinghuaci.mp4').
' -i ' . escapeshellarg($path . 'suffiix.mp4').
' -i ' . escapeshellarg($file).
' -i ' . escapeshellarg($animate).
' -i ' . escapeshellarg($path . 'logo.png').
$audio_input.
' -filter_complex "[0:0] ' .
'drawtext="'.
'fontfile=' . escapeshellarg($path . 'arialuni.ttf') . ':' .
'text=' . escapeshellarg($content) . ':'.
'fontsize=43:'.
'fontsize='.$font_size.':'.
'fontcolor=white@1.0:'.
'x=main_w/2' . '-260' . ':'.
'y=main_h/2' . '-20' . ':'.
'x=' . escapeshellarg('(w-text_w)/2') . ':' .
'y=' . escapeshellarg('(h-text_h)/2') . ':' .
'box=1:boxcolor=0xd0cdcc@0.5'.
'" [text];'.
'[text] [2:v]overlay=20:20[water];[water][0:1][1:0][1:1] concat=n=2:v=1:a=1[v][a]" '.
'[text] [2:v]overlay=20:20[water];[water]'.$audio_filter.'[1:0][1:1] concat=n=2:v=1:a=1[v][a]" '.
' -map [v] -map [a]'.
' -c:v libx264 -bt 256k -r 25' .
// ' -c:v:thumb png -disposition:v:thumb attached_pic' .
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
escapeshellarg($path . 'out.mp4');
$this->execmd($cmd);
$cmd = $this->ffmpeg. ' -y'.
' -i ' . escapeshellarg($path . 'out.mp4').
' -i ' . escapeshellarg($path . 'thumbnail.png').
' -map 1 -map 0 -c copy -disposition:0 attached_pic '.
escapeshellarg($path . 'out2.mp4');
$this->execmd($cmd);
// $cmd = $this->ffmpeg. ' -y'.
// ' -i ' . escapeshellarg($path . 'out.mp4').
// ' -i ' . escapeshellarg($path . 'thumbnail.png').
// ' -map 1 -map 0 -c copy -disposition:0 attached_pic '.
// escapeshellarg($path . 'out2.mp4');
//
// $this->execmd($cmd);
return 0;
......@@ -385,8 +483,22 @@ class DevFFmpeg extends Command
* 获取输出临时文件名
* @return string
*/
public function getTempPath() {
return Storage::disk('public')->path('ffmpeg') . "/output_" . time() . rand(0, 10000) . ".mp4";
public function getTempPath($ext = '.mp4')
{
$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 = 'temp/'. $dir_l1. '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return Storage::disk('public')->path($dir . $filename . $ext);
}
/**
......
......@@ -47,60 +47,127 @@ class MakeVideo implements ShouldQueue
public function handle()
{
$adminMakeVideo = $this->adminMakeVideo;
$adminMakeVideo->video_url = Storage::disk('public')->path($adminMakeVideo->video_url);
$adminMakeVideo->poem;
$adminMakeVideo->temp->components;
$file = Storage::disk('public')->path($adminMakeVideo->video_url);
$is_bgm = $adminMakeVideo->bg_music;
$bgm = Storage::disk('public')->path($adminMakeVideo->bgm_url);
// 1.getmediainfo 记录时长,音频视频取最长。
$cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
$output = $this->execmd($cmd);
$media_info = json_decode($output, true);
if (json_last_error() === JSON_ERROR_UTF8) {
$output = mb_convert_encoding($output, "UTF-8");
$media_info = json_decode($output, true);
}
/** 记录媒体信息时长*/
$media_file_time_length = isset($media_info['format']['duration']) ? $media_info['format']['duration'] : 0;
if ($media_info['streams'][0]['codec_type'] !== 'video') {
Log::channel('daily')->error('视频没有video track');
return;
}
// 2. 判断是否有视频原音,没有原音用背景音,没有背景音则混入anullsrc
if ( $media_info['format']['nb_streams'] >= 2 ){ /** 音频视频轨都有 */
if ($is_bgm){
// 有背景音 融合
$audio = $this->getTempPath('.mp3');
$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 = '[3:a]';
}else{
// 没有背景音
$audio_input = '';
$audio_filter = '[0:1]';
}
}elseif ( $media_info['format']['nb_streams'] == 1 ){
$audio = $this->getTempPath('.mp3');
$cmd = $this->ffmpeg .
' -y -f lavfi -i aevalsrc=0:duration='. escapeshellarg($media_file_time_length) .
' -ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
if ($is_bgm){
$audio_empty = $audio;
$audio = $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 = '[3:a]';
}else{ /** 音频视频轨都没有 */
Log::channel('daily')->error('视频没有video track');
return;
}
$file = $adminMakeVideo->video_url;
$watermark_x = 20;
$watermark_y = 20;
$end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png";
$thumbnail = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png";
$font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
$signature = "一言";
$signature_x = 0;
$signature_y = -20;
$content = $adminMakeVideo->poem->content . PHP_EOL;
$content_position = $adminMakeVideo->getContentPosition();
// 转换logo大小
// $watermark = $this->translateLogo(Storage::disk('public')->path('image/logo.jpg'));
$watermark = Storage::disk('public')->path('ffmpeg') . "/LOGO_eng.png";
$signature = "一言官方出品";
// 生成贴纸和签名
$end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
// 截取最后一帧
$last_frame_video = $this->makeLastFrameVideo($file);
$last_frame_video = $this->getTempPath();
$width = $media_info['streams'][0]['width'];
$height = $media_info['streams'][0]['height'];
$size = $width . 'x' . $height;
$time_length = 0.7;
$r = 24;
$frame_n = $media_info['streams'][0]['nb_frames'] - 2;
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
" -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
" -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
' -map [v] -map 2:a ' . escapeshellarg($last_frame_video);
if (!$this->execmd($cmd)) return;
$animate = '';
if ($last_frame_video) {
$signature_x = 0;
$signature_y = -20;
$animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
}
$video = $this->getTempPath();
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($file) .
' -i ' . escapeshellarg($animate) .
' -i ' . escapeshellarg($watermark) .
$font_size = ceil($width / 14);
$content = $adminMakeVideo->poem->content . PHP_EOL;
$content_position = $adminMakeVideo->getContentPosition();
$watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
$font_family = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
$video = $this->getTempPath('.mp4',false);
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($file).
' -i ' . escapeshellarg($animate).
' -i ' . escapeshellarg($watermark).
$audio_input .
' -filter_complex "[0:0] ' .
'drawtext="' .
'fontfile=' . escapeshellarg($font) . ':' .
'text=' . escapeshellcmd($content) . ':' .
'fontsize=43:' .
'fontcolor=white@1.0:' .
'x=' . $content_position[0] . ':' .
'y=' . $content_position[1] . ':' .
'box=1:boxcolor=0xd0cdcc@0.5' .
'" [text];' .
'[text] [2:v]overlay=' . $watermark_x . ':' . $watermark_y . '[water];[water][0:1][1:0][1:1] concat=n=2:v=1:a=1[v][a]" ' .
' -map [v] -map [a]' .
'drawtext="'.
'fontfile=' . escapeshellarg($font_family) . ':' .
'text=' . escapeshellarg($content) . ':'.
'fontsize='.$font_size.':'.
'fontcolor=white@1.0:'.
'x=' . escapeshellarg($content_position[0]) . ':' .
'y=' . escapeshellarg($content_position[1]) . ':' .
'box=1:boxcolor=0xd0cdcc@0.5'.
'" [text];'.
'[text] [2:v]overlay=20:20[water];[water]' . $audio_filter . '[1:0][1:1] concat=n=2:v=1:a=1[v][a]" ' .
' -map [v] -map [a]'.
' -c:v libx264 -bt 256k -r 25' .
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
escapeshellarg($video);
$this->execmd($cmd);
if (!$this->execmd($cmd)) return;
// $video = $this->getTempPath();
// if ( $adminMakeVideo->thumbnail == 1 && $adminMakeVideo->thumbnail_url){
......@@ -108,7 +175,6 @@ class MakeVideo implements ShouldQueue
// }else{
// $thumbnail = $last_frame_video;
// }
// $cmd = $this->ffmpeg. ' -y'.
// ' -i ' . escapeshellarg($video_temp).
// ' -i ' . escapeshellarg($thumbnail).
......@@ -330,10 +396,27 @@ class MakeVideo implements ShouldQueue
/**
* 获取输出临时文件名
* @param string $ext
* @param bool $is_temp
* @return string
*/
public function getTempPath() {
return Storage::disk('public')->path('ffmpeg') . "/output_" . time() . rand(0, 10000) . ".mp4";
public function getTempPath($ext = '.mp4',$is_temp = true)
{
$filename = "/output_" . time() . rand(0, 10000);
$prefix = $is_temp ? 'temp/' : 'video';
$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 = $prefix . $dir_l1 . '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return Storage::disk('public')->path($dir . $filename . $ext);
}
/**
......