李帅

1.重构一言表

...@@ -70,9 +70,10 @@ class DevFFmpeg extends Command ...@@ -70,9 +70,10 @@ class DevFFmpeg extends Command
70 // $arr = json_decode($json,true); 70 // $arr = json_decode($json,true);
71 // dd($arr); 71 // dd($arr);
72 // dd(AdminMakeVideo::query()->find(1)->poem2()); 72 // dd(AdminMakeVideo::query()->find(1)->poem2());
73 - dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray()); 73 +// dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray());
74 - return 0; 74 +// return 0;
75 - dd(AdminMakeVideo::query()->find(33)->temp->components->toArray()); 75 +// dd(AdminMakeVideo::query()->find(33)->temp->components->toArray());
76 + dd(AdminMakeVideo::query()->find(33)->temp->toArray());
76 AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components); 77 AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components);
77 78
78 79
......
1 +<?php
2 +
3 +namespace App\Jobs;
4 +
5 +use App\Models\Immerse;
6 +use App\Models\VideoTemp;
7 +use Illuminate\Bus\Queueable;
8 +use Illuminate\Contracts\Queue\ShouldQueue;
9 +use Illuminate\Foundation\Bus\Dispatchable;
10 +use Illuminate\Queue\InteractsWithQueue;
11 +use Illuminate\Queue\SerializesModels;
12 +use App\Models\AdminMakeVideo;
13 +use Illuminate\Support\Carbon;
14 +use Illuminate\Support\Facades\Log;
15 +use Illuminate\Support\Facades\Storage;
16 +
17 +class AdminMakeImmerse implements ShouldQueue
18 +{
19 + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
20 +
21 + public $adminMakeVideo;
22 +
23 + protected $ffmpeg;
24 +
25 + protected $ffprobe;
26 +
27 + protected $media_info;
28 +
29 + protected $output_width;
30 +
31 + protected $output_height;
32 +
33 + /**
34 + * Create a new job instance.
35 + * @param AdminMakeVideo $adminMakeVideo
36 + * @return void
37 + */
38 + public function __construct(AdminMakeVideo $adminMakeVideo)
39 + {
40 + $this->adminMakeVideo = $adminMakeVideo;
41 + $this->ffmpeg = env('FFMPEG_CMD');
42 + $this->ffprobe = env('FFPROBE_CMD');
43 + $this->output_width = 720;
44 + $this->output_height = 1280;
45 + }
46 +
47 + /**
48 + * Execute the job.
49 + *
50 + * @return void
51 + */
52 + public function handle()
53 + {
54 + $adminMakeVideo = $this->adminMakeVideo;
55 +
56 + // 模板
57 + $template = $adminMakeVideo->temp->where('state',1)->first();
58 +
59 + // 素材准备
60 + $watermark = $this->getAbsolutePath('images/LOGO_eng.png');
61 + $is_bgm = $template->bg_music == 1; //是否手动上传背景音
62 + $bgm = $this->getAbsolutePath($template->bgm_url);
63 +
64 + // 区分类型
65 + if ($adminMakeVideo->type == 1) { // 视频
66 + $file = $this->getAbsolutePath($adminMakeVideo->video_url);
67 + // 分析视频
68 + $media_info = $this->mediainfo($file);
69 + // 素材准备
70 + $drawtext = $this->getTextContentString();
71 +
72 + if ($media_info['format']['nb_streams'] >= 2) {
73 + /** 音频视频轨都有 */
74 + if ($is_bgm) {
75 + // 有背景音 融合
76 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
77 + $cmd = $this->ffmpeg .
78 + ' -y -i ' . escapeshellarg($file) .
79 + ' -y -i ' . escapeshellarg($bgm) .
80 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
81 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
82 + if (!$this->execmd($cmd)) return;
83 +
84 + $audio_input = ' -i ' . escapeshellarg($audio);
85 + $audio_filter = '2:a';
86 + } else {
87 + // 没有背景音
88 + $audio_input = '';
89 + $audio_filter = '0:a';
90 + }
91 + } elseif ($media_info['format']['nb_streams'] == 1) {
92 + /** 只有视频轨 */
93 + // 生成一段无声音频
94 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
95 + $cmd = $this->ffmpeg .
96 + ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) .
97 + ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
98 + if (!$this->execmd($cmd)) return;
99 +
100 + if ($is_bgm) {
101 + // 有背景音 融合
102 + $audio_empty = $audio;
103 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
104 + $cmd = $this->ffmpeg .
105 + ' -y -i ' . escapeshellarg($audio_empty) .
106 + ' -y -i ' . escapeshellarg($bgm) .
107 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
108 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
109 + if (!$this->execmd($cmd)) return;
110 + }
111 + $audio_input = ' -i ' . escapeshellarg($audio);
112 + $audio_filter = '2:a';
113 + } else {
114 + /** 音频视频轨都没有 */
115 + Log::channel('daily')->error('视频没有video track, url:' . $file);
116 + return;
117 + }
118 +
119 + $thumbnail = $this->getTempPath('.jpg','thumbnail');
120 + if ($adminMakeVideo->thumbnail == 2){
121 + // 截取中间帧作为视频封面
122 + $frame = ceil($media_info['streams'][0]['nb_frames'] / 2);
123 + $cmd = $this->ffmpeg . ' -y ' .
124 + ' -i ' . escapeshellarg($file) .
125 + ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' .
126 + ' -map [img]'.
127 + ' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
128 + escapeshellarg($this->getAbsolutePath($thumbnail));
129 + if (!$this->execmd($cmd)) return ;
130 + }else{
131 + // 手动上传封面
132 + $origin_thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url);
133 + // 将封面分辨率改为指定分辨率
134 + $cmd = $this->ffmpeg . ' -y ' .
135 + ' -i ' . escapeshellarg($origin_thumbnail) .
136 + '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
137 + escapeshellarg($this->getAbsolutePath($thumbnail));
138 + if (!$this->execmd($cmd)) return ;
139 + }
140 +
141 + $output = $this->getTempPath('.mp4','video');
142 + $cmd = $this->ffmpeg . ' -y '.
143 + ' -i ' . escapeshellarg($file).
144 + ' -i ' . escapeshellarg($watermark).
145 + $audio_input .
146 + ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
147 + ' [text];[text]'.
148 + ' [1:v]overlay=20:20[v]" ' .
149 + ' -map [v] -map '. $audio_filter .
150 + ' -c:v libx264 -bt 256k -r 25' .
151 + ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
152 + escapeshellarg($this->getAbsolutePath($output));
153 +
154 + if (!$this->execmd($cmd)) return ;
155 +
156 + $video_info = $this->mediainfo($this->getAbsolutePath($output));
157 + Immerse::query()->create([
158 + 'user_id' => 1,
159 + 'title' => '',
160 + 'weather' => $adminMakeVideo->weather,
161 + 'huangli' => $adminMakeVideo->huangli,
162 + 'content' => $adminMakeVideo->feel,
163 + 'location' => $adminMakeVideo->location,
164 + 'longitude' => $adminMakeVideo->longitude,
165 + 'latitude' => $adminMakeVideo->latitude,
166 + 'url' => $output,
167 + 'type' => $adminMakeVideo->type == 1 ? 2 : 1,
168 + 'upload_file' => '',
169 + 'duration' => $video_info['format']['duration'],
170 + 'size' => $video_info['format']['size'],
171 + 'origin_video_url' => $this->adminMakeVideo->video_url,
172 + 'origin_image_url' => '',
173 + 'poem_id' => $this->adminMakeVideo->poem_id,
174 + 'temp_id' => $this->adminMakeVideo->temp_id,
175 + 'thumbnail' => $thumbnail,
176 + 'state' => 1,
177 + 'bgm' => $is_bgm ? $bgm : '',
178 + ]);
179 +
180 + }else{ // 图文
181 + $image = $this->getAbsolutePath($adminMakeVideo->images_url);
182 + // 素材准备
183 + $drawtext = $this->getTextContentString();
184 +
185 + if ($this->adminMakeVideo->type == 2 && !$is_bgm){
186 + // 没有背景音,单图一张,输出为单图。
187 + $output = $this->getTempPath('.png','thumbnail');
188 + $cmd = $this->ffmpeg . ' -y '.
189 + ' -i ' . escapeshellarg($image).
190 + ' -i ' . escapeshellarg($watermark).
191 + ' -filter_complex "[0:0]scale=' . $this->output_width . ':' . $this->output_height . ',' .
192 + $drawtext . ' [text];[text][1:0]overlay=20:20" ' .
193 + escapeshellarg($this->getAbsolutePath($output));
194 + if (!$this->execmd($cmd)) return ;
195 + $thumbnail = $output;
196 + }else{
197 + // 有背景音 单图合成视频,时长为音频时长,音频加入背景音
198 + $output = $this->getTempPath('.mp4','video');
199 +
200 + // 分析背景音
201 + $mediainfo = $this->mediainfo($bgm);
202 + // 记录媒体信息时长
203 + $duration = $mediainfo['format']['duration'] ?: 0;
204 +
205 + // 单图、水印、bgm 合成视频
206 + $cmd = $this->ffmpeg . ' -y ' .
207 + ' -loop 1 -i ' . escapeshellarg($image) .
208 + ' -i ' . escapeshellarg($watermark) .
209 + ' -i ' . escapeshellarg($bgm) .
210 + ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',setdar=dar=9/16,' . $drawtext .
211 + ' [text];[text][2:v]overlay=20:20[v]"' .
212 + ' -map [v] -map 1:0 ' .
213 + ' -c:v libx264 -bt 256k -r 25 -t ' . $duration .
214 + ' -ar 48000 -ac 2 -qmin 30 -qmax 60 -profile:v high -pix_fmt yuv420p -preset fast '.
215 + escapeshellarg($this->getAbsolutePath($output));
216 + if (!$this->execmd($cmd)) return ;
217 +
218 + $thumbnail = $this->getTempPath('.jpg','thumbnail');
219 + if ($adminMakeVideo->thumbnail == 2){
220 + // 将封面分辨率改为指定分辨率
221 + $cmd = $this->ffmpeg . ' -y ' .
222 + ' -i ' . escapeshellarg($image) .
223 + '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
224 + escapeshellarg($this->getAbsolutePath($thumbnail));
225 + if (!$this->execmd($cmd)) return ;
226 + }else{
227 + // 手动上传封面
228 + $origin_thumbnail = $this->getAbsolutePath($adminMakeVideo->thumbnail_url);
229 + // 将封面分辨率改为指定分辨率
230 + $cmd = $this->ffmpeg . ' -y ' .
231 + ' -i ' . escapeshellarg($origin_thumbnail) .
232 + '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
233 + escapeshellarg($this->getAbsolutePath($thumbnail));
234 + if (!$this->execmd($cmd)) return ;
235 + }
236 + }
237 +
238 + // 全部合成以后创建 临境
239 + $video_info = $this->mediainfo($output);
240 +
241 + Immerse::query()->create([
242 + 'user_id' => 1,
243 + 'title' => '',
244 + 'weather' => $adminMakeVideo->weather,
245 + 'huangli' => $adminMakeVideo->huangli,
246 + 'content' => $adminMakeVideo->feel,
247 + 'location' => $adminMakeVideo->location,
248 + 'longitude' => $adminMakeVideo->longitude,
249 + 'latitude' => $adminMakeVideo->latitude,
250 + 'url' => $output,
251 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
252 + 'upload_file' => '',
253 + 'duration' => $video_info['format']['duration'] ?? 0,
254 + 'size' => $video_info['format']['size'] ?? 0,
255 + 'origin_video_url' => '',
256 + 'origin_image_url' => $this->adminMakeVideo->images_url,
257 + 'poem_id' => $this->adminMakeVideo->poem_id,
258 + 'temp_id' => $this->adminMakeVideo->temp_id,
259 + 'thumbnail' => $thumbnail,
260 + 'state' => 1,
261 + 'bgm' => $is_bgm ? $bgm : '',
262 + ]);
263 + }
264 +
265 + }
266 +
267 + public function mediainfo($file)
268 + {
269 + if ($this->media_info) return $this->media_info;
270 +
271 + $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
272 + $output = $this->execmd($cmd);
273 + $data = json_decode($output, true);
274 + if (json_last_error() === JSON_ERROR_UTF8) {
275 + $output = mb_convert_encoding($output, "UTF-8");
276 + $data = json_decode($output, true);
277 + }
278 + $this->media_info = $data;
279 + return $data;
280 + }
281 +
282 + public function execmd($cmd, $update_progress = false) {
283 + echo $cmd . "\n". "\n". "\n";
284 + $descriptorspec = array(
285 + 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
286 + );
287 + $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
288 + if (is_resource($process)) {
289 + $error0 = '';
290 + $error1 = '';
291 + $stdout = '';
292 + while (!feof($pipes[1])) {
293 + $line = fgets($pipes[1], 150);
294 + $stdout .= $line;
295 + if ($line) {
296 + //记录错误
297 + $error0 = $error1;
298 + $error1 = $line;
299 + if ($update_progress &&
300 + false !== strpos($line, 'size=') &&
301 + false !== strpos($line, 'time=') &&
302 + false !== strpos($line, 'bitrate='))
303 + {
304 + //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
305 + $line = explode(' ', $line);
306 + $time = null;
307 + foreach ($line as $item) {
308 + $item = explode('=', $item);
309 + if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
310 + $time = $item[1];
311 + break;
312 + }
313 + }
314 + }
315 + }
316 + }
317 + // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
318 + fclose($pipes[1]);
319 + $exitedcode = proc_close($process);
320 + if ($exitedcode === 0) {
321 + return $stdout;
322 + } else {
323 + $error = trim($error0,"\n") . ' '. trim($error1,"\n");
324 + // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
325 + // ErrorUtil::triggerErrorMsg($error, $exitedcode);
326 + Log::error("cmd:{$cmd}");
327 + Log::error($error);
328 + Log::error("stdout:{$stdout}");
329 + }
330 + } else {
331 + // return ErrorUtil::triggerErrorMsg('proc_open error');
332 + Log::error('proc_open error');
333 + }
334 + }
335 +
336 + /**
337 + * 获取输出临时文件名
338 + * @param string $ext
339 + * @param string $dir
340 + * @return string
341 + */
342 + public function getTempPath($ext = '.mp4',$dir = 'video')
343 + {
344 + $filename = "/output_" . time() . rand(0, 10000);
345 +
346 + $hash_hex = md5($filename);
347 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
348 + // 前16个字符用来第一级求摸,后16个用做第二级
349 + $hash_hex_l1 = substr($hash_hex, 0, 8);
350 + $hash_hex_l2 = substr($hash_hex, 8, 8);
351 + $dir_l1 = hexdec($hash_hex_l1) % 256;
352 + $dir_l2 = hexdec($hash_hex_l2) % 512;
353 + $dir = $dir . '/' . $dir_l1 . '/' . $dir_l2;
354 +
355 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
356 +
357 + return $dir . $filename . $ext;
358 + }
359 +
360 +
361 + public function getAbsolutePath($path)
362 + {
363 + if ($path == '') return '';
364 +
365 + return Storage::disk('public')->path($path);
366 + }
367 +
368 + public function getTextContentString()
369 + {
370 + $components = $this->adminMakeVideo->temp->components;
371 +
372 + $drawtext = '';
373 + foreach ($components as $component) {
374 +
375 + $text_color = $component->text_color ?? 'white';
376 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
377 + $opacity = $component->opacity ? $component->opacity / 100 : 0.5;
378 + $font_file = $this->getAbsolutePath($component->font_file);
379 + $text_bg_box = $component->text_bg_box ?? 0;
380 + $fix_bounds = $component->fix_bounds == 1;
381 +
382 + switch ($component->name){
383 + case 'every_poem':
384 + case 'one_poem':
385 + $content = $this->adminMakeVideo->poem->content;
386 + $contents = explode("\n",$content); //计算诗词行数
387 + if ($this->media_info['format']['duration'] < 2 * count($contents)) {
388 + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
389 + file_put_contents($text_file, $content);
390 + $drawtext .= 'drawtext="'.
391 + 'fontfile=' . escapeshellarg($font_file) . ':' .
392 + 'textfile=' . escapeshellarg($text_file) . ':' .
393 + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
394 + 'fontcolor=' . $text_color . '@' . $opacity . ':' .
395 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
396 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
397 + 'fix_bounds='. $fix_bounds . ':' .
398 + 'box=1:boxborderw='. $text_bg_box . ':' .
399 + 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
400 +
401 + }else{
402 + $FID = $FOD = 1.0;
403 + if ($this->media_info['format']['duration'] > 3 * count($contents)) $FID = $FOD = 1.5;
404 +
405 + $round = round($this->media_info['format']['duration'] / count($contents),1);
406 + $sub_text = '';
407 + foreach ($contents as $key => $content){
408 + $DS = $key * $round;
409 + $DE = $DS + $round;
410 + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
411 + file_put_contents($text_file, $content);
412 + $sub_text .= 'drawtext="'.
413 + 'fontfile=' . escapeshellarg($font_file) . ':' .
414 + 'textfile=' . escapeshellarg($text_file) . ':' .
415 + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
416 + '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 }') . ':' .
417 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
418 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
419 + 'fix_bounds='. $fix_bounds . '", ';
420 + }
421 +
422 + $drawtext .= $sub_text;
423 + }
424 + break;
425 + case 'weather':
426 + $content = $this->adminMakeVideo->weather;
427 + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
428 + file_put_contents($text_file, $content);
429 + $drawtext .= 'drawtext="'.
430 + 'fontfile=' . escapeshellarg($font_file) . ':' .
431 + 'textfile=' . escapeshellarg($text_file) . ':' .
432 + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
433 + 'fontcolor=' . $text_color . '@' . $opacity . ':' .
434 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
435 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
436 + 'fix_bounds='. $fix_bounds . ':' .
437 + 'box=1:boxborderw='. $text_bg_box . ':' .
438 + 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
439 +
440 + break;
441 + case 'date':
442 + $content = Carbon::now()->format('Y年m月d日H时');
443 + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
444 + file_put_contents($text_file, $content);
445 + $drawtext .= 'drawtext="'.
446 + 'fontfile=' . escapeshellarg($font_file) . ':' .
447 + 'textfile=' . escapeshellarg($text_file) . ':' .
448 + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
449 + 'fontcolor=' . $text_color . '@' . $opacity . ':' .
450 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
451 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
452 + 'fix_bounds='. $fix_bounds . ':' .
453 + 'box=1:boxborderw='. $text_bg_box . ':' .
454 + 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
455 + break;
456 + case 'feel':
457 + $content = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
458 + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
459 + file_put_contents($text_file, $content);
460 + $drawtext .= 'drawtext="'.
461 + 'fontfile=' . escapeshellarg($font_file) . ':' .
462 + 'textfile=' . escapeshellarg($text_file) . ':' .
463 + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
464 + 'fontcolor=' . $text_color . '@' . $opacity . ':' .
465 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
466 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
467 + 'fix_bounds='. $fix_bounds . ':' .
468 + 'box=1:boxborderw='. $text_bg_box . ':' .
469 + 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
470 + break;
471 + }
472 + }
473 +
474 + return rtrim($drawtext,', ');
475 + }
476 +
477 + public function calcFontSize($width)
478 + {
479 + return ceil($this->output_width / 360 * $width);
480 + }
481 +
482 + public function calcBorderSize($width)
483 + {
484 + return ceil($this->output_width / 360 * $width);
485 + }
486 +
487 + public function getTextHeight()
488 + {
489 + $height = $this->output_height;
490 +
491 + }
492 +}
...@@ -51,225 +51,143 @@ class AdminMakeImmerse implements ShouldQueue ...@@ -51,225 +51,143 @@ class AdminMakeImmerse implements ShouldQueue
51 */ 51 */
52 public function handle() 52 public function handle()
53 { 53 {
54 - $adminMakeVideo = $this->adminMakeVideo; 54 + $file = $this->getAbsolutePath($this->adminMakeVideo->video_url);
55 + // 分析视频
56 + $this->media_info = $this->mediaInfo($file);
55 57
56 - // 模板 58 + // 准备素材
57 - $template = $adminMakeVideo->temp->where('state',1)->first();
58 -
59 - // 素材准备
60 $watermark = $this->getAbsolutePath('images/LOGO_eng.png'); 59 $watermark = $this->getAbsolutePath('images/LOGO_eng.png');
61 - $is_bgm = $template->bg_music == 1; //是否手动上传背景音 60 +
62 - $bgm = $this->getAbsolutePath($template->bgm_url); 61 + // 组装文字参数
63 - 62 + $drawtext = $this->getTextContentString();
64 - // 区分类型 63 +
65 - if ($adminMakeVideo->type == 1) { // 视频 64 + // 判断双轨 没有则制作空轨
66 - $file = $this->getAbsolutePath($adminMakeVideo->video_url); 65 + $is_bgm = $this->adminMakeVideo->temp->bg_music == 1; //是否手动上传背景音
67 - // 分析视频 66 + if ($this->media_info['format']['nb_streams'] >= 2) { /** 音频视频轨都有 */
68 - $media_info = $this->mediainfo($file); 67 + if ($is_bgm) {
69 - // 素材准备 68 + // 有背景音 融合
70 - $drawtext = $this->getTextContentString();
71 -
72 - if ($media_info['format']['nb_streams'] >= 2) {
73 - /** 音频视频轨都有 */
74 - if ($is_bgm) {
75 - // 有背景音 融合
76 - $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
77 - $cmd = $this->ffmpeg .
78 - ' -y -i ' . escapeshellarg($file) .
79 - ' -y -i ' . escapeshellarg($bgm) .
80 - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
81 - '-ar 48000 -ab 64k ' . escapeshellarg($audio);
82 - if (!$this->execmd($cmd)) return;
83 -
84 - $audio_input = ' -i ' . escapeshellarg($audio);
85 - $audio_filter = '2:a';
86 - } else {
87 - // 没有背景音
88 - $audio_input = '';
89 - $audio_filter = '0:a';
90 - }
91 - } elseif ($media_info['format']['nb_streams'] == 1) {
92 - /** 只有视频轨 */
93 - // 生成一段无声音频
94 $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); 69 $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
70 + $bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
95 $cmd = $this->ffmpeg . 71 $cmd = $this->ffmpeg .
96 - ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) . 72 + ' -y -i ' . escapeshellarg($file) .
97 - ' -ar 48000 -ab 64k ' . escapeshellarg($audio); 73 + ' -y -i ' . escapeshellarg($bgm) .
98 - if (!$this->execmd($cmd)) return; 74 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
99 - 75 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
100 - if ($is_bgm) { 76 + if (!$this->execCmd($cmd)) return;
101 - // 有背景音 融合 77 +
102 - $audio_empty = $audio;
103 - $audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
104 - $cmd = $this->ffmpeg .
105 - ' -y -i ' . escapeshellarg($audio_empty) .
106 - ' -y -i ' . escapeshellarg($bgm) .
107 - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
108 - '-ar 48000 -ab 64k ' . escapeshellarg($audio);
109 - if (!$this->execmd($cmd)) return;
110 - }
111 $audio_input = ' -i ' . escapeshellarg($audio); 78 $audio_input = ' -i ' . escapeshellarg($audio);
112 $audio_filter = '2:a'; 79 $audio_filter = '2:a';
113 } else { 80 } else {
114 - /** 音频视频轨都没有 */ 81 + // 没有背景音
115 - Log::channel('daily')->error('视频没有video track, url:' . $file); 82 + $audio_input = '';
116 - return; 83 + $audio_filter = '0:a';
117 } 84 }
118 - 85 + } elseif ($this->media_info['format']['nb_streams'] == 1) { /** 只有视频轨 */
119 - $thumbnail = $this->getTempPath('.jpg','thumbnail'); 86 + // 生成一段无声音频
120 - if ($adminMakeVideo->thumbnail == 2){ 87 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
121 - // 截取中间帧作为视频封面 88 + $cmd = $this->ffmpeg .
122 - $frame = ceil($media_info['streams'][0]['nb_frames'] / 2); 89 + ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($this->media_info['format']['duration']) .
123 - $cmd = $this->ffmpeg . ' -y ' . 90 + ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
124 - ' -i ' . escapeshellarg($file) . 91 + if (!$this->execCmd($cmd)) return;
125 - ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' . 92 +
126 - ' -map [img]'. 93 + if ($is_bgm) {
127 - ' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' . 94 + // 有背景音 融合
128 - escapeshellarg($this->getAbsolutePath($thumbnail)); 95 + $audio_empty = $audio;
129 - if (!$this->execmd($cmd)) return ; 96 + $bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
130 - }else{ 97 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
131 - // 手动上传封面 98 + $cmd = $this->ffmpeg .
132 - $origin_thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url); 99 + ' -y -i ' . escapeshellarg($audio_empty) .
133 - // 将封面分辨率改为指定分辨率 100 + ' -y -i ' . escapeshellarg($bgm) .
134 - $cmd = $this->ffmpeg . ' -y ' . 101 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
135 - ' -i ' . escapeshellarg($origin_thumbnail) . 102 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
136 - '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' . 103 + if (!$this->execCmd($cmd)) return;
137 - escapeshellarg($this->getAbsolutePath($thumbnail));
138 - if (!$this->execmd($cmd)) return ;
139 - }
140 -
141 - $output = $this->getTempPath('.mp4','video');
142 - $cmd = $this->ffmpeg . ' -y '.
143 - ' -i ' . escapeshellarg($file).
144 - ' -i ' . escapeshellarg($watermark).
145 - $audio_input .
146 - ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
147 - ' [text];[text]'.
148 - ' [1:v]overlay=20:20[v]" ' .
149 - ' -map [v] -map '. $audio_filter .
150 - ' -c:v libx264 -bt 256k -r 25' .
151 - ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
152 - escapeshellarg($this->getAbsolutePath($output));
153 -
154 - if (!$this->execmd($cmd)) return ;
155 -
156 - $video_info = $this->mediainfo($this->getAbsolutePath($output));
157 - Immerse::query()->create([
158 - 'user_id' => 1,
159 - 'title' => '',
160 - 'weather' => $adminMakeVideo->weather,
161 - 'huangli' => $adminMakeVideo->huangli,
162 - 'content' => $adminMakeVideo->feel,
163 - 'location' => $adminMakeVideo->location,
164 - 'longitude' => $adminMakeVideo->longitude,
165 - 'latitude' => $adminMakeVideo->latitude,
166 - 'url' => $output,
167 - 'type' => $adminMakeVideo->type == 1 ? 2 : 1,
168 - 'upload_file' => '',
169 - 'duration' => $video_info['format']['duration'],
170 - 'size' => $video_info['format']['size'],
171 - 'origin_video_url' => $this->adminMakeVideo->video_url,
172 - 'origin_image_url' => '',
173 - 'poem_id' => $this->adminMakeVideo->poem_id,
174 - 'temp_id' => $this->adminMakeVideo->temp_id,
175 - 'thumbnail' => $thumbnail,
176 - 'state' => 1,
177 - 'bgm' => $is_bgm ? $bgm : '',
178 - ]);
179 -
180 - }else{ // 图文
181 - $image = $this->getAbsolutePath($adminMakeVideo->images_url);
182 - // 素材准备
183 - $drawtext = $this->getTextContentString();
184 -
185 - if ($this->adminMakeVideo->type == 2 && !$is_bgm){
186 - // 没有背景音,单图一张,输出为单图。
187 - $output = $this->getTempPath('.png','thumbnail');
188 - $cmd = $this->ffmpeg . ' -y '.
189 - ' -i ' . escapeshellarg($image).
190 - ' -i ' . escapeshellarg($watermark).
191 - ' -filter_complex "[0:0]scale=' . $this->output_width . ':' . $this->output_height . ',' .
192 - $drawtext . ' [text];[text][1:0]overlay=20:20" ' .
193 - escapeshellarg($this->getAbsolutePath($output));
194 - if (!$this->execmd($cmd)) return ;
195 - $thumbnail = $output;
196 - }else{
197 - // 有背景音 单图合成视频,时长为音频时长,音频加入背景音
198 - $output = $this->getTempPath('.mp4','video');
199 -
200 - // 分析背景音
201 - $mediainfo = $this->mediainfo($bgm);
202 - // 记录媒体信息时长
203 - $duration = $mediainfo['format']['duration'] ?: 0;
204 -
205 - // 单图、水印、bgm 合成视频
206 - $cmd = $this->ffmpeg . ' -y ' .
207 - ' -loop 1 -i ' . escapeshellarg($image) .
208 - ' -i ' . escapeshellarg($watermark) .
209 - ' -i ' . escapeshellarg($bgm) .
210 - ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',setdar=dar=9/16,' . $drawtext .
211 - ' [text];[text][2:v]overlay=20:20[v]"' .
212 - ' -map [v] -map 1:0 ' .
213 - ' -c:v libx264 -bt 256k -r 25 -t ' . $duration .
214 - ' -ar 48000 -ac 2 -qmin 30 -qmax 60 -profile:v high -pix_fmt yuv420p -preset fast '.
215 - escapeshellarg($this->getAbsolutePath($output));
216 - if (!$this->execmd($cmd)) return ;
217 -
218 - $thumbnail = $this->getTempPath('.jpg','thumbnail');
219 - if ($adminMakeVideo->thumbnail == 2){
220 - // 将封面分辨率改为指定分辨率
221 - $cmd = $this->ffmpeg . ' -y ' .
222 - ' -i ' . escapeshellarg($image) .
223 - '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
224 - escapeshellarg($this->getAbsolutePath($thumbnail));
225 - if (!$this->execmd($cmd)) return ;
226 - }else{
227 - // 手动上传封面
228 - $origin_thumbnail = $this->getAbsolutePath($adminMakeVideo->thumbnail_url);
229 - // 将封面分辨率改为指定分辨率
230 - $cmd = $this->ffmpeg . ' -y ' .
231 - ' -i ' . escapeshellarg($origin_thumbnail) .
232 - '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
233 - escapeshellarg($this->getAbsolutePath($thumbnail));
234 - if (!$this->execmd($cmd)) return ;
235 - }
236 } 104 }
105 + $audio_input = ' -i ' . escapeshellarg($audio);
106 + $audio_filter = '2:a';
107 + } else {
108 + /** 音频视频轨都没有 */
109 + Log::channel('daily')->error('视频没有video track, url:' . $file);
110 + return;
111 + }
237 112
238 - // 全部合成以后创建 临境 113 + // 制作封面图
239 - $video_info = $this->mediainfo($output); 114 + $thumbnail = $this->getTempPath('.jpg','thumbnail');
240 - 115 + if ($this->adminMakeVideo->thumbnail == 2){
241 - Immerse::query()->create([ 116 + // 截取中间帧作为视频封面
242 - 'user_id' => 1, 117 + $frame = ceil($this->media_info['streams'][0]['nb_frames'] / 2);
243 - 'title' => '', 118 + $cmd = $this->ffmpeg . ' -y ' .
244 - 'weather' => $adminMakeVideo->weather, 119 + ' -i ' . escapeshellarg($file) .
245 - 'huangli' => $adminMakeVideo->huangli, 120 + ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' .
246 - 'content' => $adminMakeVideo->feel, 121 + ' -map [img]'.
247 - 'location' => $adminMakeVideo->location, 122 + ' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
248 - 'longitude' => $adminMakeVideo->longitude, 123 + escapeshellarg($this->getAbsolutePath($thumbnail));
249 - 'latitude' => $adminMakeVideo->latitude, 124 + if (!$this->execCmd($cmd)) return ;
250 - 'url' => $output, 125 + }else{
251 - 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1, 126 + // 手动上传封面
252 - 'upload_file' => '', 127 + $origin_thumbnail = $this->getAbsolutePath($this->adminMakeVideo->thumbnail_url);
253 - 'duration' => $video_info['format']['duration'] ?? 0, 128 + // 将封面分辨率改为指定分辨率
254 - 'size' => $video_info['format']['size'] ?? 0, 129 + $cmd = $this->ffmpeg . ' -y ' .
255 - 'origin_video_url' => '', 130 + ' -i ' . escapeshellarg($origin_thumbnail) .
256 - 'origin_image_url' => $this->adminMakeVideo->images_url, 131 + '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
257 - 'poem_id' => $this->adminMakeVideo->poem_id, 132 + escapeshellarg($this->getAbsolutePath($thumbnail));
258 - 'temp_id' => $this->adminMakeVideo->temp_id, 133 + if (!$this->execCmd($cmd)) return ;
259 - 'thumbnail' => $thumbnail,
260 - 'state' => 1,
261 - 'bgm' => $is_bgm ? $bgm : '',
262 - ]);
263 } 134 }
264 135
136 + // 合成视频
137 + $output = $this->getTempPath('.mp4','video');
138 + $cmd = $this->ffmpeg . ' -y '.
139 + ' -i ' . escapeshellarg($file).
140 + ' -i ' . escapeshellarg($watermark).
141 + $audio_input .
142 + ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
143 + ' [text];[text]'.
144 + ' [1:v]overlay=20:20[v]" ' .
145 + ' -map [v] -map '. $audio_filter .
146 + ' -c:v libx264 -bt 256k -r 25' .
147 + ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
148 + escapeshellarg($this->getAbsolutePath($output));
149 +
150 + if (!$this->execCmd($cmd)) return ;
151 +
152 + // 分析视频 入库
153 + $video_info = $this->mediainfo($this->getAbsolutePath($output));
154 + Immerse::query()->create([
155 + 'user_id' => 1,
156 + 'title' => '',
157 + 'weather' => $this->adminMakeVideo->weather,
158 + 'huangli' => $this->adminMakeVideo->huangli,
159 + 'content' => $this->adminMakeVideo->feel,
160 + 'location' => $this->adminMakeVideo->location,
161 + 'longitude' => $this->adminMakeVideo->longitude,
162 + 'latitude' => $this->adminMakeVideo->latitude,
163 + 'url' => $output,
164 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
165 + 'upload_file' => '',
166 + 'duration' => $video_info['format']['duration'],
167 + 'size' => $video_info['format']['size'],
168 + 'origin_video_url' => $this->adminMakeVideo->video_url,
169 + 'origin_image_url' => '',
170 + 'poem_id' => $this->adminMakeVideo->poem_id,
171 + 'temp_id' => $this->adminMakeVideo->temp_id,
172 + 'thumbnail' => $thumbnail,
173 + 'state' => 1,
174 + 'bgm' => $is_bgm ? $bgm : '',
175 + ]);
265 } 176 }
266 177
267 - public function mediainfo($file) 178 + public function getAbsolutePath($path)
179 + {
180 + if ($path == '') return '';
181 +
182 + return Storage::disk('public')->path($path);
183 + }
184 +
185 + public function mediaInfo($file)
268 { 186 {
269 if ($this->media_info) return $this->media_info; 187 if ($this->media_info) return $this->media_info;
270 188
271 $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file); 189 $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
272 - $output = $this->execmd($cmd); 190 + $output = $this->execCmd($cmd);
273 $data = json_decode($output, true); 191 $data = json_decode($output, true);
274 if (json_last_error() === JSON_ERROR_UTF8) { 192 if (json_last_error() === JSON_ERROR_UTF8) {
275 $output = mb_convert_encoding($output, "UTF-8"); 193 $output = mb_convert_encoding($output, "UTF-8");
...@@ -279,90 +197,9 @@ class AdminMakeImmerse implements ShouldQueue ...@@ -279,90 +197,9 @@ class AdminMakeImmerse implements ShouldQueue
279 return $data; 197 return $data;
280 } 198 }
281 199
282 - public function execmd($cmd, $update_progress = false) { 200 + public function execCmd($cmd)
283 - echo $cmd . "\n". "\n". "\n";
284 - $descriptorspec = array(
285 - 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
286 - );
287 - $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
288 - if (is_resource($process)) {
289 - $error0 = '';
290 - $error1 = '';
291 - $stdout = '';
292 - while (!feof($pipes[1])) {
293 - $line = fgets($pipes[1], 150);
294 - $stdout .= $line;
295 - if ($line) {
296 - //记录错误
297 - $error0 = $error1;
298 - $error1 = $line;
299 - if ($update_progress &&
300 - false !== strpos($line, 'size=') &&
301 - false !== strpos($line, 'time=') &&
302 - false !== strpos($line, 'bitrate='))
303 - {
304 - //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
305 - $line = explode(' ', $line);
306 - $time = null;
307 - foreach ($line as $item) {
308 - $item = explode('=', $item);
309 - if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
310 - $time = $item[1];
311 - break;
312 - }
313 - }
314 - }
315 - }
316 - }
317 - // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
318 - fclose($pipes[1]);
319 - $exitedcode = proc_close($process);
320 - if ($exitedcode === 0) {
321 - return $stdout;
322 - } else {
323 - $error = trim($error0,"\n") . ' '. trim($error1,"\n");
324 - // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
325 - // ErrorUtil::triggerErrorMsg($error, $exitedcode);
326 - Log::error("cmd:{$cmd}");
327 - Log::error($error);
328 - Log::error("stdout:{$stdout}");
329 - }
330 - } else {
331 - // return ErrorUtil::triggerErrorMsg('proc_open error');
332 - Log::error('proc_open error');
333 - }
334 - }
335 -
336 - /**
337 - * 获取输出临时文件名
338 - * @param string $ext
339 - * @param string $dir
340 - * @return string
341 - */
342 - public function getTempPath($ext = '.mp4',$dir = 'video')
343 - {
344 - $filename = "/output_" . time() . rand(0, 10000);
345 -
346 - $hash_hex = md5($filename);
347 - // 16进制表示的字符串一共32字节,表示16个二进制字节。
348 - // 前16个字符用来第一级求摸,后16个用做第二级
349 - $hash_hex_l1 = substr($hash_hex, 0, 8);
350 - $hash_hex_l2 = substr($hash_hex, 8, 8);
351 - $dir_l1 = hexdec($hash_hex_l1) % 256;
352 - $dir_l2 = hexdec($hash_hex_l2) % 512;
353 - $dir = $dir . '/' . $dir_l1 . '/' . $dir_l2;
354 -
355 - if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
356 -
357 - return $dir . $filename . $ext;
358 - }
359 -
360 -
361 - public function getAbsolutePath($path)
362 { 201 {
363 - if ($path == '') return ''; 202 + return shell_exec("{$cmd} 2>&1");
364 -
365 - return Storage::disk('public')->path($path);
366 } 203 }
367 204
368 public function getTextContentString() 205 public function getTextContentString()
...@@ -371,103 +208,97 @@ class AdminMakeImmerse implements ShouldQueue ...@@ -371,103 +208,97 @@ class AdminMakeImmerse implements ShouldQueue
371 208
372 $drawtext = ''; 209 $drawtext = '';
373 foreach ($components as $component) { 210 foreach ($components as $component) {
374 -
375 $text_color = $component->text_color ?? 'white'; 211 $text_color = $component->text_color ?? 'white';
376 $text_bg_color = $component->text_bg_color ?? '0xd0cdcc'; 212 $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
377 $opacity = $component->opacity ? $component->opacity / 100 : 0.5; 213 $opacity = $component->opacity ? $component->opacity / 100 : 0.5;
378 $font_file = $this->getAbsolutePath($component->font_file); 214 $font_file = $this->getAbsolutePath($component->font_file);
379 $text_bg_box = $component->text_bg_box ?? 0; 215 $text_bg_box = $component->text_bg_box ?? 0;
380 - $fix_bounds = $component->fix_bounds == 1; 216 +
381 - 217 + // 文字淡入淡出模式
382 - switch ($component->name){ 218 + if ($component->draw == 'fade'){
383 - case 'every_poem': 219 + $contents = []; //
384 - case 'one_poem': 220 + switch ($component->name){
385 - $content = $this->adminMakeVideo->poem->content; 221 + case 'one_poem':
386 - $contents = explode("\n",$content); //计算诗词行数 222 + foreach ($this->adminMakeVideo->poem2->verses as $item) {
387 - if ($this->media_info['format']['duration'] < 2 * count($contents)) { 223 + if ($item->stanza != '') $contents[] = $item->stanza;
388 - $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); 224 + }
389 - file_put_contents($text_file, $content); 225 + break;
390 - $drawtext .= 'drawtext="'. 226 + case 'one_poem_with_annotate':
391 - 'fontfile=' . escapeshellarg($font_file) . ':' . 227 + foreach ($this->adminMakeVideo->poem2->verses as $item) {
392 - 'textfile=' . escapeshellarg($text_file) . ':' . 228 + if ($item->stanza != '') $contents[] = $item->stanza;
393 - 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . 229 + if ($item->annotate != '') $contents[] = $item->annotate;
394 - 'fontcolor=' . $text_color . '@' . $opacity . ':' .
395 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
396 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
397 - 'fix_bounds='. $fix_bounds . ':' .
398 - 'box=1:boxborderw='. $text_bg_box . ':' .
399 - 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
400 -
401 - }else{
402 - $FID = $FOD = 1.0;
403 - if ($this->media_info['format']['duration'] > 3 * count($contents)) $FID = $FOD = 1.5;
404 -
405 - $round = round($this->media_info['format']['duration'] / count($contents),1);
406 - $sub_text = '';
407 - foreach ($contents as $key => $content){
408 - $DS = $key * $round;
409 - $DE = $DS + $round;
410 - $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
411 - file_put_contents($text_file, $content);
412 - $sub_text .= 'drawtext="'.
413 - 'fontfile=' . escapeshellarg($font_file) . ':' .
414 - 'textfile=' . escapeshellarg($text_file) . ':' .
415 - 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
416 - '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 }') . ':' .
417 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
418 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
419 - 'fix_bounds='. $fix_bounds . '", ';
420 } 230 }
231 + break;
232 + case 'weather':
233 + $contents[] = $this->adminMakeVideo->weather;
234 + break;
235 + case 'date':
236 + $contents[] = Carbon::now()->format('Y年m月d日H时');
237 + break;
238 + case 'feel':
239 + $contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
240 + break;
241 + }
421 242
422 - $drawtext .= $sub_text; 243 + $FID = $FOD = floatval($component->fade_time / 1000);
423 - } 244 + $round = round($this->media_info['format']['duration'] / count($contents),1);
424 - break; 245 + if ($round < 1) $round = 1;
425 - case 'weather': 246 + $sub_text = '';
426 - $content = $this->adminMakeVideo->weather; 247 + foreach ($contents as $key => $content){
248 + $DS = $key * $round;
249 + $DE = $DS + $round;
427 $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); 250 $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
428 file_put_contents($text_file, $content); 251 file_put_contents($text_file, $content);
429 - $drawtext .= 'drawtext="'. 252 + $sub_text .= 'drawtext="'.
430 'fontfile=' . escapeshellarg($font_file) . ':' . 253 'fontfile=' . escapeshellarg($font_file) . ':' .
431 'textfile=' . escapeshellarg($text_file) . ':' . 254 'textfile=' . escapeshellarg($text_file) . ':' .
432 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . 255 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
433 - 'fontcolor=' . $text_color . '@' . $opacity . ':' . 256 + '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 }') . ':' .
434 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 257 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
435 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 258 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
436 - 'fix_bounds='. $fix_bounds . ':' . 259 + '", ';
437 - 'box=1:boxborderw='. $text_bg_box . ':' . 260 + }
438 - 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
439 261
440 - break; 262 + $drawtext .= $sub_text;
441 - case 'date': 263 + }
442 - $content = Carbon::now()->format('Y年m月d日H时'); 264 +
443 - $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); 265 + // 文字固定模式
444 - file_put_contents($text_file, $content); 266 + if ($component->draw == 'fix'){
445 - $drawtext .= 'drawtext="'. 267 + $contents = []; //
446 - 'fontfile=' . escapeshellarg($font_file) . ':' . 268 + switch ($component->name){
447 - 'textfile=' . escapeshellarg($text_file) . ':' . 269 + case 'one_poem_with_annotate':
448 - 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . 270 + case 'one_poem':
449 - 'fontcolor=' . $text_color . '@' . $opacity . ':' . 271 + $stanzas = '';
450 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 272 + foreach ($this->adminMakeVideo->poem2->verses as $item) {
451 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 273 + if ($item->stanza != '') $stanzas = $item->stanza . "\n";
452 - 'fix_bounds='. $fix_bounds . ':' . 274 + }
453 - 'box=1:boxborderw='. $text_bg_box . ':' . 275 + $contents[] = $stanzas;
454 - 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; 276 + break;
455 - break; 277 + case 'weather':
456 - case 'feel': 278 + $contents[] = $this->adminMakeVideo->weather;
457 - $content = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。'; 279 + break;
280 + case 'date':
281 + $contents[] = Carbon::now()->format('Y年m月d日H时');
282 + break;
283 + case 'feel':
284 + $contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
285 + break;
286 + }
287 + $sub_text = '';
288 + foreach ($contents as $key => $content){
458 $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); 289 $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
459 file_put_contents($text_file, $content); 290 file_put_contents($text_file, $content);
460 - $drawtext .= 'drawtext="'. 291 + $sub_text .= 'drawtext="'.
461 'fontfile=' . escapeshellarg($font_file) . ':' . 292 'fontfile=' . escapeshellarg($font_file) . ':' .
462 'textfile=' . escapeshellarg($text_file) . ':' . 293 'textfile=' . escapeshellarg($text_file) . ':' .
463 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . 294 'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
464 'fontcolor=' . $text_color . '@' . $opacity . ':' . 295 'fontcolor=' . $text_color . '@' . $opacity . ':' .
465 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 296 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
466 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 297 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
467 - 'fix_bounds='. $fix_bounds . ':' .
468 'box=1:boxborderw='. $text_bg_box . ':' . 298 'box=1:boxborderw='. $text_bg_box . ':' .
469 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; 299 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
470 - break; 300 + }
301 + $drawtext .= $sub_text;
471 } 302 }
472 } 303 }
473 304
...@@ -479,14 +310,27 @@ class AdminMakeImmerse implements ShouldQueue ...@@ -479,14 +310,27 @@ class AdminMakeImmerse implements ShouldQueue
479 return ceil($this->output_width / 360 * $width); 310 return ceil($this->output_width / 360 * $width);
480 } 311 }
481 312
482 - public function calcBorderSize($width) 313 + /**
314 + * 获取输出临时文件名
315 + * @param string $ext
316 + * @param string $dir
317 + * @return string
318 + */
319 + public function getTempPath($ext = '.mp4',$dir = 'video')
483 { 320 {
484 - return ceil($this->output_width / 360 * $width); 321 + $filename = "/output_" . time() . rand(0, 10000);
485 - }
486 322
487 - public function getTextHeight() 323 + $hash_hex = md5($filename);
488 - { 324 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
489 - $height = $this->output_height; 325 + // 前16个字符用来第一级求摸,后16个用做第二级
326 + $hash_hex_l1 = substr($hash_hex, 0, 8);
327 + $hash_hex_l2 = substr($hash_hex, 8, 8);
328 + $dir_l1 = hexdec($hash_hex_l1) % 256;
329 + $dir_l2 = hexdec($hash_hex_l2) % 512;
330 + $dir = $dir . '/' . $dir_l1 . '/' . $dir_l2;
490 331
332 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
333 +
334 + return $dir . $filename . $ext;
491 } 335 }
492 } 336 }
......
...@@ -44,75 +44,6 @@ class MakeVideo implements ShouldQueue ...@@ -44,75 +44,6 @@ class MakeVideo implements ShouldQueue
44 $this->ffprobe = env('FFPROBE_CMD'); 44 $this->ffprobe = env('FFPROBE_CMD');
45 $this->output_width = 720; 45 $this->output_width = 720;
46 $this->output_height = 1280; 46 $this->output_height = 1280;
47 -
48 - $file = $this->getAbsolutePath($adminMakeVideo->video_url);
49 - // 分析视频
50 - $media_info = $this->mediainfo($file);
51 - // 素材准备
52 - $drawtext = $this->getTextContentString();
53 -
54 -
55 -
56 - $thumbnail = $this->getTempPath('.jpg','thumbnail');
57 - if ($adminMakeVideo->thumbnail == 2){
58 - // 截取中间帧作为视频封面
59 - $frame = ceil($media_info['streams'][0]['nb_frames'] / 2);
60 - $cmd = $this->ffmpeg . ' -y ' .
61 - ' -i ' . escapeshellarg($file) .
62 - ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' .
63 - ' -map [img]'.
64 - ' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
65 - escapeshellarg($this->getAbsolutePath($thumbnail));
66 - if (!$this->execmd($cmd)) return ;
67 - }else{
68 - // 手动上传封面
69 - $origin_thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url);
70 - // 将封面分辨率改为指定分辨率
71 - $cmd = $this->ffmpeg . ' -y ' .
72 - ' -i ' . escapeshellarg($origin_thumbnail) .
73 - '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
74 - escapeshellarg($this->getAbsolutePath($thumbnail));
75 - if (!$this->execmd($cmd)) return ;
76 - }
77 -
78 - $output = $this->getTempPath('.mp4','video');
79 - $cmd = $this->ffmpeg . ' -y '.
80 - ' -i ' . escapeshellarg($file).
81 - ' -i ' . escapeshellarg($watermark).
82 - $audio_input .
83 - ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
84 - ' [text];[text]'.
85 - ' [1:v]overlay=20:20[v]" ' .
86 - ' -map [v] -map '. $audio_filter .
87 - ' -c:v libx264 -bt 256k -r 25' .
88 - ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
89 - escapeshellarg($this->getAbsolutePath($output));
90 -
91 - if (!$this->execmd($cmd)) return ;
92 -
93 - $video_info = $this->mediainfo($this->getAbsolutePath($output));
94 - Immerse::query()->create([
95 - 'user_id' => 1,
96 - 'title' => '',
97 - 'weather' => $adminMakeVideo->weather,
98 - 'huangli' => $adminMakeVideo->huangli,
99 - 'content' => $adminMakeVideo->feel,
100 - 'location' => $adminMakeVideo->location,
101 - 'longitude' => $adminMakeVideo->longitude,
102 - 'latitude' => $adminMakeVideo->latitude,
103 - 'url' => $output,
104 - 'type' => $adminMakeVideo->type == 1 ? 2 : 1,
105 - 'upload_file' => '',
106 - 'duration' => $video_info['format']['duration'],
107 - 'size' => $video_info['format']['size'],
108 - 'origin_video_url' => $this->adminMakeVideo->video_url,
109 - 'origin_image_url' => '',
110 - 'poem_id' => $this->adminMakeVideo->poem_id,
111 - 'temp_id' => $this->adminMakeVideo->temp_id,
112 - 'thumbnail' => $thumbnail,
113 - 'state' => 1,
114 - 'bgm' => $is_bgm ? $bgm : '',
115 - ]);
116 } 47 }
117 48
118 /** 49 /**
...@@ -127,23 +58,24 @@ class MakeVideo implements ShouldQueue ...@@ -127,23 +58,24 @@ class MakeVideo implements ShouldQueue
127 $this->media_info = $this->mediaInfo($file); 58 $this->media_info = $this->mediaInfo($file);
128 59
129 // 准备素材 60 // 准备素材
61 + $watermark = $this->getAbsolutePath('images/LOGO_eng.png');
130 62
131 // 组装文字参数 63 // 组装文字参数
132 $drawtext = $this->getTextContentString(); 64 $drawtext = $this->getTextContentString();
133 65
134 - // 合成视频 66 + // 判断双轨 没有则制作空轨
135 - 67 + $is_bgm = $this->adminMakeVideo->temp->bg_music == 1; //是否手动上传背景音
136 - if ($this->media_info['format']['nb_streams'] >= 2) { 68 + if ($this->media_info['format']['nb_streams'] >= 2) { /** 音频视频轨都有 */
137 - /** 音频视频轨都有 */
138 if ($is_bgm) { 69 if ($is_bgm) {
139 // 有背景音 融合 70 // 有背景音 融合
140 $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); 71 $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
72 + $bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
141 $cmd = $this->ffmpeg . 73 $cmd = $this->ffmpeg .
142 ' -y -i ' . escapeshellarg($file) . 74 ' -y -i ' . escapeshellarg($file) .
143 ' -y -i ' . escapeshellarg($bgm) . 75 ' -y -i ' . escapeshellarg($bgm) .
144 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . 76 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
145 '-ar 48000 -ab 64k ' . escapeshellarg($audio); 77 '-ar 48000 -ab 64k ' . escapeshellarg($audio);
146 - if (!$this->execmd($cmd)) return; 78 + if (!$this->execCmd($cmd)) return;
147 79
148 $audio_input = ' -i ' . escapeshellarg($audio); 80 $audio_input = ' -i ' . escapeshellarg($audio);
149 $audio_filter = '2:a'; 81 $audio_filter = '2:a';
...@@ -152,25 +84,25 @@ class MakeVideo implements ShouldQueue ...@@ -152,25 +84,25 @@ class MakeVideo implements ShouldQueue
152 $audio_input = ''; 84 $audio_input = '';
153 $audio_filter = '0:a'; 85 $audio_filter = '0:a';
154 } 86 }
155 - } elseif ($media_info['format']['nb_streams'] == 1) { 87 + } elseif ($this->media_info['format']['nb_streams'] == 1) { /** 只有视频轨 */
156 - /** 只有视频轨 */
157 // 生成一段无声音频 88 // 生成一段无声音频
158 $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); 89 $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
159 $cmd = $this->ffmpeg . 90 $cmd = $this->ffmpeg .
160 - ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) . 91 + ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($this->media_info['format']['duration']) .
161 ' -ar 48000 -ab 64k ' . escapeshellarg($audio); 92 ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
162 - if (!$this->execmd($cmd)) return; 93 + if (!$this->execCmd($cmd)) return;
163 94
164 if ($is_bgm) { 95 if ($is_bgm) {
165 // 有背景音 融合 96 // 有背景音 融合
166 $audio_empty = $audio; 97 $audio_empty = $audio;
167 - $audio = $this->getAbsolutePath($this->getTempPath('.mp3')); 98 + $bgm = $this->getAbsolutePath($this->adminMakeVideo->temp->bgm_url);
99 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
168 $cmd = $this->ffmpeg . 100 $cmd = $this->ffmpeg .
169 ' -y -i ' . escapeshellarg($audio_empty) . 101 ' -y -i ' . escapeshellarg($audio_empty) .
170 ' -y -i ' . escapeshellarg($bgm) . 102 ' -y -i ' . escapeshellarg($bgm) .
171 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . 103 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
172 '-ar 48000 -ab 64k ' . escapeshellarg($audio); 104 '-ar 48000 -ab 64k ' . escapeshellarg($audio);
173 - if (!$this->execmd($cmd)) return; 105 + if (!$this->execCmd($cmd)) return;
174 } 106 }
175 $audio_input = ' -i ' . escapeshellarg($audio); 107 $audio_input = ' -i ' . escapeshellarg($audio);
176 $audio_filter = '2:a'; 108 $audio_filter = '2:a';
...@@ -181,8 +113,68 @@ class MakeVideo implements ShouldQueue ...@@ -181,8 +113,68 @@ class MakeVideo implements ShouldQueue
181 } 113 }
182 114
183 // 制作封面图 115 // 制作封面图
116 + $thumbnail = $this->getTempPath('.jpg','thumbnail');
117 + if ($this->adminMakeVideo->thumbnail == 2){
118 + // 截取中间帧作为视频封面
119 + $frame = ceil($this->media_info['streams'][0]['nb_frames'] / 2);
120 + $cmd = $this->ffmpeg . ' -y ' .
121 + ' -i ' . escapeshellarg($file) .
122 + ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' .
123 + ' -map [img]'.
124 + ' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
125 + escapeshellarg($this->getAbsolutePath($thumbnail));
126 + if (!$this->execCmd($cmd)) return ;
127 + }else{
128 + // 手动上传封面
129 + $origin_thumbnail = $this->getAbsolutePath($this->adminMakeVideo->thumbnail_url);
130 + // 将封面分辨率改为指定分辨率
131 + $cmd = $this->ffmpeg . ' -y ' .
132 + ' -i ' . escapeshellarg($origin_thumbnail) .
133 + '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
134 + escapeshellarg($this->getAbsolutePath($thumbnail));
135 + if (!$this->execCmd($cmd)) return ;
136 + }
137 +
138 + // 合成视频
139 + $output = $this->getTempPath('.mp4','video');
140 + $cmd = $this->ffmpeg . ' -y '.
141 + ' -i ' . escapeshellarg($file).
142 + ' -i ' . escapeshellarg($watermark).
143 + $audio_input .
144 + ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
145 + ' [text];[text]'.
146 + ' [1:v]overlay=20:20[v]" ' .
147 + ' -map [v] -map '. $audio_filter .
148 + ' -c:v libx264 -bt 256k -r 25' .
149 + ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
150 + escapeshellarg($this->getAbsolutePath($output));
151 +
152 + if (!$this->execCmd($cmd)) return ;
184 153
185 // 分析视频 入库 154 // 分析视频 入库
155 + $video_info = $this->mediainfo($this->getAbsolutePath($output));
156 + Immerse::query()->create([
157 + 'user_id' => 1,
158 + 'title' => '',
159 + 'weather' => $this->adminMakeVideo->weather,
160 + 'huangli' => $this->adminMakeVideo->huangli,
161 + 'content' => $this->adminMakeVideo->feel,
162 + 'location' => $this->adminMakeVideo->location,
163 + 'longitude' => $this->adminMakeVideo->longitude,
164 + 'latitude' => $this->adminMakeVideo->latitude,
165 + 'url' => $output,
166 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
167 + 'upload_file' => '',
168 + 'duration' => $video_info['format']['duration'],
169 + 'size' => $video_info['format']['size'],
170 + 'origin_video_url' => $this->adminMakeVideo->video_url,
171 + 'origin_image_url' => '',
172 + 'poem_id' => $this->adminMakeVideo->poem_id,
173 + 'temp_id' => $this->adminMakeVideo->temp_id,
174 + 'thumbnail' => $thumbnail,
175 + 'state' => 1,
176 + 'bgm' => $is_bgm ? $bgm : '',
177 + ]);
186 } 178 }
187 179
188 public function getAbsolutePath($path) 180 public function getAbsolutePath($path)
......