李帅

1.重新整合ffmpeg命令

...@@ -46,45 +46,143 @@ class DevFFmpeg extends Command ...@@ -46,45 +46,143 @@ class DevFFmpeg extends Command
46 */ 46 */
47 public function handle() 47 public function handle()
48 { 48 {
49 + $path = '/Users/lishuai/Desktop/test/';
50 + $file = $path . 'qinghuaci.mp4';
51 +// $file = $path . 'lingdang.mov';
52 + $is_bgm = false;
53 + $bgm = $path . 'bgm.aac';
54 +
55 + // 1.getmediainfo 记录时长,音频视频取最长。
56 + $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
57 + $output = $this->execmd($cmd);
58 + $media_info = json_decode($output, true);
59 + if (json_last_error() === JSON_ERROR_UTF8) {
60 + $output = mb_convert_encoding($output, "UTF-8");
61 + $media_info = json_decode($output, true);
62 + }
63 +
64 + /** 记录媒体信息时长*/
65 + $media_file_time_length = isset($media_info['format']['duration']) ? $media_info['format']['duration'] : 0;
66 + $media_file_size = $media_info['format']['size'];
67 + if ($media_info['streams'][0]['codec_type'] !== 'video') {
68 + return 0;
69 + }
70 +
71 +
72 + // 2. 判断是否有视频原音,没有原音用背景音,没有背景音则混入anullsrc
73 +
74 + if ( $media_info['format']['nb_streams'] >= 2 ){ /** 音频视频轨都有 */
75 + if ($is_bgm){
76 + // 有背景音 融合
77 + $audio = $this->getTempPath('.mp3');
78 +
79 + $cmd = $this->ffmpeg.
80 + ' -y -i ' . escapeshellarg($file).
81 + ' -y -i ' . escapeshellarg($bgm).
82 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
83 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
84 + if (!$this->execmd($cmd)) return 0;
85 +
86 + $audio_input = ' -i ' . escapeshellarg($audio);
87 + $audio_filter = '[3:a]';
88 + }else{
89 + // 没有背景音
90 + $audio_input = '';
91 + $audio_filter = '[0:1]';
92 +
93 + }
94 +
95 + }elseif ( $media_info['format']['nb_streams'] == 1 ){
96 +
97 + $audio = $this->getTempPath('.mp3');
98 + $cmd = $this->ffmpeg .
99 + ' -y -f lavfi -i aevalsrc=0:duration='. escapeshellarg($media_file_time_length) .
100 + ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
101 + if (!$this->execmd($cmd)) return 0;
102 +
103 + if ($is_bgm){
104 + $audio_empty = $audio;
105 + $audio = $this->getTempPath('.mp3');
106 + $cmd = $this->ffmpeg.
107 + ' -y -i ' . escapeshellarg($audio_empty).
108 + ' -y -i ' . escapeshellarg($bgm).
109 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
110 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
111 + if (!$this->execmd($cmd)) return 0;
112 + }
113 +
114 + $audio_input = ' -i ' . escapeshellarg($audio);
115 + $audio_filter = '[3:a]';
116 +
117 + }else{ /** 音频视频轨都没有 */
118 + return 0;
119 + }
120 +
121 + $end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png";
122 + $thumbnail = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png";
123 + $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
124 + $signature = "一言 username";
125 +
126 + // 生成贴纸和签名
127 + $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
128 +
129 + // 截取最后一帧
130 + $last_frame_video = $this->getTempPath();
131 + $width = $media_info['streams'][0]['width'];
132 + $height = $media_info['streams'][0]['height'];
133 + $size = $width . 'x' . $height;
134 + $time_length = 0.7;
135 + $r = 24;
136 + $frame_n = $media_info['streams'][0]['nb_frames'] - 2;
137 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
138 + " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
139 + " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
140 + ' -map [v] -map 2:a ' . escapeshellarg($last_frame_video);
141 + if (!$this->execmd($cmd)) return 0;
142 +
143 +
144 + $signature_x = 0;
145 + $signature_y = -20;
146 + $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
147 +
148 + $font_size = ceil($width / 14);
49 $content = '题破山寺后禅院' . "\t" . ' -- 常建' . PHP_EOL . 149 $content = '题破山寺后禅院' . "\t" . ' -- 常建' . PHP_EOL .
50 '清晨入古寺,初日照高林。' . PHP_EOL . 150 '清晨入古寺,初日照高林。' . PHP_EOL .
51 '曲径通幽处,禅房花木深。' . PHP_EOL . 151 '曲径通幽处,禅房花木深。' . PHP_EOL .
52 '山光悦鸟性,潭影空人心。' . PHP_EOL . 152 '山光悦鸟性,潭影空人心。' . PHP_EOL .
53 '万籁此都寂,但余钟磬音。' . PHP_EOL ; 153 '万籁此都寂,但余钟磬音。' . PHP_EOL ;
54 - $path = '/Users/lishuai/Desktop/test/';
55 154
56 $cmd = $this->ffmpeg . ' -y '. 155 $cmd = $this->ffmpeg . ' -y '.
57 - ' -i ' . escapeshellarg($path . 'qinghuaci.mp4'). 156 + ' -i ' . escapeshellarg($file).
58 - ' -i ' . escapeshellarg($path . 'suffiix.mp4'). 157 + ' -i ' . escapeshellarg($animate).
59 ' -i ' . escapeshellarg($path . 'logo.png'). 158 ' -i ' . escapeshellarg($path . 'logo.png').
60 - 159 + $audio_input.
61 ' -filter_complex "[0:0] ' . 160 ' -filter_complex "[0:0] ' .
62 'drawtext="'. 161 'drawtext="'.
63 'fontfile=' . escapeshellarg($path . 'arialuni.ttf') . ':' . 162 'fontfile=' . escapeshellarg($path . 'arialuni.ttf') . ':' .
64 'text=' . escapeshellarg($content) . ':'. 163 'text=' . escapeshellarg($content) . ':'.
65 - 'fontsize=43:'. 164 + 'fontsize='.$font_size.':'.
66 'fontcolor=white@1.0:'. 165 'fontcolor=white@1.0:'.
67 - 'x=main_w/2' . '-260' . ':'. 166 + 'x=' . escapeshellarg('(w-text_w)/2') . ':' .
68 - 'y=main_h/2' . '-20' . ':'. 167 + 'y=' . escapeshellarg('(h-text_h)/2') . ':' .
69 'box=1:boxcolor=0xd0cdcc@0.5'. 168 'box=1:boxcolor=0xd0cdcc@0.5'.
70 '" [text];'. 169 '" [text];'.
71 - '[text] [2:v]overlay=20:20[water];[water][0:1][1:0][1:1] concat=n=2:v=1:a=1[v][a]" '. 170 + '[text] [2:v]overlay=20:20[water];[water]'.$audio_filter.'[1:0][1:1] concat=n=2:v=1:a=1[v][a]" '.
72 ' -map [v] -map [a]'. 171 ' -map [v] -map [a]'.
73 ' -c:v libx264 -bt 256k -r 25' . 172 ' -c:v libx264 -bt 256k -r 25' .
74 -// ' -c:v:thumb png -disposition:v:thumb attached_pic' .
75 ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' . 173 ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
76 escapeshellarg($path . 'out.mp4'); 174 escapeshellarg($path . 'out.mp4');
77 175
78 $this->execmd($cmd); 176 $this->execmd($cmd);
79 177
80 178
81 - $cmd = $this->ffmpeg. ' -y'. 179 +// $cmd = $this->ffmpeg. ' -y'.
82 - ' -i ' . escapeshellarg($path . 'out.mp4'). 180 +// ' -i ' . escapeshellarg($path . 'out.mp4').
83 - ' -i ' . escapeshellarg($path . 'thumbnail.png'). 181 +// ' -i ' . escapeshellarg($path . 'thumbnail.png').
84 - ' -map 1 -map 0 -c copy -disposition:0 attached_pic '. 182 +// ' -map 1 -map 0 -c copy -disposition:0 attached_pic '.
85 - escapeshellarg($path . 'out2.mp4'); 183 +// escapeshellarg($path . 'out2.mp4');
86 - 184 +//
87 - $this->execmd($cmd); 185 +// $this->execmd($cmd);
88 186
89 return 0; 187 return 0;
90 188
...@@ -385,8 +483,22 @@ class DevFFmpeg extends Command ...@@ -385,8 +483,22 @@ class DevFFmpeg extends Command
385 * 获取输出临时文件名 483 * 获取输出临时文件名
386 * @return string 484 * @return string
387 */ 485 */
388 - public function getTempPath() { 486 + public function getTempPath($ext = '.mp4')
389 - return Storage::disk('public')->path('ffmpeg') . "/output_" . time() . rand(0, 10000) . ".mp4"; 487 + {
488 + $filename = "/output_" . time() . rand(0, 10000);
489 +
490 + $hash_hex = md5($filename);
491 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
492 + // 前16个字符用来第一级求摸,后16个用做第二级
493 + $hash_hex_l1 = substr($hash_hex, 0, 8);
494 + $hash_hex_l2 = substr($hash_hex, 8, 8);
495 + $dir_l1 = hexdec($hash_hex_l1) % 256;
496 + $dir_l2 = hexdec($hash_hex_l2) % 512;
497 + $dir = 'temp/'. $dir_l1. '/' . $dir_l2;
498 +
499 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
500 +
501 + return Storage::disk('public')->path($dir . $filename . $ext);
390 } 502 }
391 503
392 /** 504 /**
......
...@@ -47,60 +47,127 @@ class MakeVideo implements ShouldQueue ...@@ -47,60 +47,127 @@ class MakeVideo implements ShouldQueue
47 public function handle() 47 public function handle()
48 { 48 {
49 $adminMakeVideo = $this->adminMakeVideo; 49 $adminMakeVideo = $this->adminMakeVideo;
50 - $adminMakeVideo->video_url = Storage::disk('public')->path($adminMakeVideo->video_url); 50 + $file = Storage::disk('public')->path($adminMakeVideo->video_url);
51 - $adminMakeVideo->poem; 51 + $is_bgm = $adminMakeVideo->bg_music;
52 - $adminMakeVideo->temp->components; 52 + $bgm = Storage::disk('public')->path($adminMakeVideo->bgm_url);
53 +
54 + // 1.getmediainfo 记录时长,音频视频取最长。
55 + $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
56 + $output = $this->execmd($cmd);
57 + $media_info = json_decode($output, true);
58 + if (json_last_error() === JSON_ERROR_UTF8) {
59 + $output = mb_convert_encoding($output, "UTF-8");
60 + $media_info = json_decode($output, true);
61 + }
62 +
63 + /** 记录媒体信息时长*/
64 + $media_file_time_length = isset($media_info['format']['duration']) ? $media_info['format']['duration'] : 0;
65 + if ($media_info['streams'][0]['codec_type'] !== 'video') {
66 + Log::channel('daily')->error('视频没有video track');
67 + return;
68 + }
69 +
70 + // 2. 判断是否有视频原音,没有原音用背景音,没有背景音则混入anullsrc
71 + if ( $media_info['format']['nb_streams'] >= 2 ){ /** 音频视频轨都有 */
72 + if ($is_bgm){
73 + // 有背景音 融合
74 + $audio = $this->getTempPath('.mp3');
75 + $cmd = $this->ffmpeg.
76 + ' -y -i ' . escapeshellarg($file).
77 + ' -y -i ' . escapeshellarg($bgm).
78 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
79 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
80 + if (!$this->execmd($cmd)) return;
81 +
82 + $audio_input = ' -i ' . escapeshellarg($audio);
83 + $audio_filter = '[3:a]';
84 + }else{
85 + // 没有背景音
86 + $audio_input = '';
87 + $audio_filter = '[0:1]';
88 + }
89 + }elseif ( $media_info['format']['nb_streams'] == 1 ){
90 + $audio = $this->getTempPath('.mp3');
91 + $cmd = $this->ffmpeg .
92 + ' -y -f lavfi -i aevalsrc=0:duration='. escapeshellarg($media_file_time_length) .
93 + ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
94 + if (!$this->execmd($cmd)) return;
95 +
96 + if ($is_bgm){
97 + $audio_empty = $audio;
98 + $audio = $this->getTempPath('.mp3');
99 + $cmd = $this->ffmpeg.
100 + ' -y -i ' . escapeshellarg($audio_empty).
101 + ' -y -i ' . escapeshellarg($bgm).
102 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
103 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
104 + if (!$this->execmd($cmd)) return;
105 + }
106 + $audio_input = ' -i ' . escapeshellarg($audio);
107 + $audio_filter = '[3:a]';
108 +
109 + }else{ /** 音频视频轨都没有 */
110 + Log::channel('daily')->error('视频没有video track');
111 + return;
112 + }
53 113
54 - $file = $adminMakeVideo->video_url;
55 - $watermark_x = 20;
56 - $watermark_y = 20;
57 $end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png"; 114 $end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png";
58 $thumbnail = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png"; 115 $thumbnail = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png";
59 $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf"; 116 $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
60 - $signature = "一言"; 117 + $signature = "一言官方出品";
61 - $signature_x = 0;
62 - $signature_y = -20;
63 - $content = $adminMakeVideo->poem->content . PHP_EOL;
64 - $content_position = $adminMakeVideo->getContentPosition();
65 -
66 - // 转换logo大小
67 -// $watermark = $this->translateLogo(Storage::disk('public')->path('image/logo.jpg'));
68 - $watermark = Storage::disk('public')->path('ffmpeg') . "/LOGO_eng.png";
69 118
70 // 生成贴纸和签名 119 // 生成贴纸和签名
71 $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font); 120 $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
72 121
73 // 截取最后一帧 122 // 截取最后一帧
74 - $last_frame_video = $this->makeLastFrameVideo($file); 123 + $last_frame_video = $this->getTempPath();
124 + $width = $media_info['streams'][0]['width'];
125 + $height = $media_info['streams'][0]['height'];
126 + $size = $width . 'x' . $height;
127 + $time_length = 0.7;
128 + $r = 24;
129 + $frame_n = $media_info['streams'][0]['nb_frames'] - 2;
130 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
131 + " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
132 + " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
133 + ' -map [v] -map 2:a ' . escapeshellarg($last_frame_video);
134 + if (!$this->execmd($cmd)) return;
75 135
76 - $animate = '';
77 - if ($last_frame_video) {
78 - $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
79 - }
80 136
81 - $video = $this->getTempPath(); 137 + $signature_x = 0;
82 - $cmd = $this->ffmpeg . ' -y ' . 138 + $signature_y = -20;
83 - ' -i ' . escapeshellarg($file) . 139 + $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
84 - ' -i ' . escapeshellarg($animate) .
85 - ' -i ' . escapeshellarg($watermark) .
86 140
141 + $font_size = ceil($width / 14);
142 + $content = $adminMakeVideo->poem->content . PHP_EOL;
143 + $content_position = $adminMakeVideo->getContentPosition();
144 + $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
145 + $font_family = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
146 +
147 +
148 + $video = $this->getTempPath('.mp4',false);
149 + $cmd = $this->ffmpeg . ' -y '.
150 + ' -i ' . escapeshellarg($file).
151 + ' -i ' . escapeshellarg($animate).
152 + ' -i ' . escapeshellarg($watermark).
153 + $audio_input .
87 ' -filter_complex "[0:0] ' . 154 ' -filter_complex "[0:0] ' .
88 - 'drawtext="' . 155 + 'drawtext="'.
89 - 'fontfile=' . escapeshellarg($font) . ':' . 156 + 'fontfile=' . escapeshellarg($font_family) . ':' .
90 - 'text=' . escapeshellcmd($content) . ':' . 157 + 'text=' . escapeshellarg($content) . ':'.
91 - 'fontsize=43:' . 158 + 'fontsize='.$font_size.':'.
92 - 'fontcolor=white@1.0:' . 159 + 'fontcolor=white@1.0:'.
93 - 'x=' . $content_position[0] . ':' . 160 + 'x=' . escapeshellarg($content_position[0]) . ':' .
94 - 'y=' . $content_position[1] . ':' . 161 + 'y=' . escapeshellarg($content_position[1]) . ':' .
95 - 'box=1:boxcolor=0xd0cdcc@0.5' . 162 + 'box=1:boxcolor=0xd0cdcc@0.5'.
96 - '" [text];' . 163 + '" [text];'.
97 - '[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]" ' . 164 + '[text] [2:v]overlay=20:20[water];[water]' . $audio_filter . '[1:0][1:1] concat=n=2:v=1:a=1[v][a]" ' .
98 - ' -map [v] -map [a]' . 165 + ' -map [v] -map [a]'.
99 ' -c:v libx264 -bt 256k -r 25' . 166 ' -c:v libx264 -bt 256k -r 25' .
100 ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' . 167 ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
101 escapeshellarg($video); 168 escapeshellarg($video);
102 169
103 - $this->execmd($cmd); 170 + if (!$this->execmd($cmd)) return;
104 171
105 // $video = $this->getTempPath(); 172 // $video = $this->getTempPath();
106 // if ( $adminMakeVideo->thumbnail == 1 && $adminMakeVideo->thumbnail_url){ 173 // if ( $adminMakeVideo->thumbnail == 1 && $adminMakeVideo->thumbnail_url){
...@@ -108,7 +175,6 @@ class MakeVideo implements ShouldQueue ...@@ -108,7 +175,6 @@ class MakeVideo implements ShouldQueue
108 // }else{ 175 // }else{
109 // $thumbnail = $last_frame_video; 176 // $thumbnail = $last_frame_video;
110 // } 177 // }
111 -
112 // $cmd = $this->ffmpeg. ' -y'. 178 // $cmd = $this->ffmpeg. ' -y'.
113 // ' -i ' . escapeshellarg($video_temp). 179 // ' -i ' . escapeshellarg($video_temp).
114 // ' -i ' . escapeshellarg($thumbnail). 180 // ' -i ' . escapeshellarg($thumbnail).
...@@ -330,10 +396,27 @@ class MakeVideo implements ShouldQueue ...@@ -330,10 +396,27 @@ class MakeVideo implements ShouldQueue
330 396
331 /** 397 /**
332 * 获取输出临时文件名 398 * 获取输出临时文件名
399 + * @param string $ext
400 + * @param bool $is_temp
333 * @return string 401 * @return string
334 */ 402 */
335 - public function getTempPath() { 403 + public function getTempPath($ext = '.mp4',$is_temp = true)
336 - return Storage::disk('public')->path('ffmpeg') . "/output_" . time() . rand(0, 10000) . ".mp4"; 404 + {
405 + $filename = "/output_" . time() . rand(0, 10000);
406 +
407 + $prefix = $is_temp ? 'temp/' : 'video';
408 + $hash_hex = md5($filename);
409 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
410 + // 前16个字符用来第一级求摸,后16个用做第二级
411 + $hash_hex_l1 = substr($hash_hex, 0, 8);
412 + $hash_hex_l2 = substr($hash_hex, 8, 8);
413 + $dir_l1 = hexdec($hash_hex_l1) % 256;
414 + $dir_l2 = hexdec($hash_hex_l2) % 512;
415 + $dir = $prefix . $dir_l1 . '/' . $dir_l2;
416 +
417 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
418 +
419 + return Storage::disk('public')->path($dir . $filename . $ext);
337 } 420 }
338 421
339 /** 422 /**
......