李帅

1.重构合成脚本。

1 -<?php
2 -
3 -namespace App\Jobs;
4 -
5 -use App\Models\Immerse;
6 -use App\Models\VideoTemp;
7 -use Carbon\Carbon;
8 -use Illuminate\Bus\Queueable;
9 -use Illuminate\Contracts\Queue\ShouldBeUnique;
10 -use Illuminate\Contracts\Queue\ShouldQueue;
11 -use Illuminate\Foundation\Bus\Dispatchable;
12 -use Illuminate\Queue\InteractsWithQueue;
13 -use Illuminate\Queue\SerializesModels;
14 -use Illuminate\Support\Facades\File;
15 -use Illuminate\Support\Facades\Log;
16 -use Illuminate\Support\Facades\Storage;
17 -use Illuminate\Support\Str;
18 -
19 -class UserMakeImages implements ShouldQueue
20 -{
21 - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
22 -
23 - public $debug = true;
24 -
25 - public $immerse;
26 -
27 - protected $ffmpeg;
28 -
29 - protected $ffprobe;
30 -
31 - protected $ffplay;
32 -
33 - protected $width;
34 -
35 - protected $height;
36 -
37 - /**
38 - * Create a new job instance.
39 - * @param Immerse $immerse
40 - * @return void
41 - */
42 - public function __construct(Immerse $immerse)
43 - {
44 - $this->immerse = $immerse;
45 -
46 - $this->ffmpeg = env('FFMPEG_CMD');
47 - $this->ffprobe = env('FFPROBE_CMD');
48 - $this->ffplay = env('FFPLAY_CMD');
49 - }
50 -
51 - /**
52 - * Execute the job.
53 - *
54 - * @return void
55 - */
56 - public function handle()
57 - {
58 - $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
59 - $image = Storage::disk('public')->path($this->immerse->thumbnail);
60 - if (!File::exists($image)) return;
61 - if ($this->debug) Log::debug('image url :' . $image);
62 - $media_info = $this->mediainfo($image);
63 - $this->width = $width = $media_info['streams'][0]['width'];
64 - $this->height = $height = $media_info['streams'][0]['height'];
65 -
66 - try{
67 - if ($this->immerse->bgm == '' || $this->immerse->bgm == null) {
68 - // 没有背景音,单图一张,输出为单图。
69 - $output = $this->getTempPath('.png',false);
70 - $cmd = $this->ffmpeg . ' -y '.
71 - ' -i ' . escapeshellarg($image).
72 - ' -i ' . escapeshellarg($watermark).
73 - ' -filter_complex "[0:0] ' .
74 - $this->getTextContentString().
75 - ' [text];[text]'.
76 - ' [1:0]overlay=20:20" ' .
77 - escapeshellarg($output);
78 -
79 - if (!$this->execmd($cmd)) return;
80 -
81 - // 全部合成以后创建 临境
82 - $video_info = $this->mediainfo($output);
83 - }else{
84 - // 有背景音 单图合成视频,时长为音频时长,音频加入背景音
85 - $bgm = Storage::disk('public')->path($this->immerse->bgm);
86 -
87 - $end_wallpaper = Storage::disk('public')->path('ffmpeg/end_wallpaper.png');
88 - $thumbnail = Storage::disk('public')->path('ffmpeg/thumbnail.png');
89 - $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
90 - $signature = "一言 · 官方出品";
91 -
92 - // 生成贴纸和签名
93 - $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
94 -
95 - // 制作最后一帧
96 - $size = $this->width . 'x' . $this->height;
97 - $time_length = 0.7;
98 - $r = 24;
99 - $last_frame_video = $this->getTempPath('.mp4');
100 - $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
101 -
102 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($image) .
103 - ' -i ' . escapeshellarg($watermark) .
104 - " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
105 - ' -filter_complex "'.
106 - ' [0:0] ' . $this->getTextContentString() .
107 - '[text];[text][1:0]overlay=20:20[water];' .
108 - ' [water]select=\'eq(n,0)\',setpts=PTS-STARTPTS[lastframe];[2:v][lastframe]overlay[v] " ' .
109 - ' -map [v] -map 3:a ' . escapeshellarg($last_frame_video);
110 -
111 - if (!$this->execmd($cmd)) return;
112 -
113 - // 利用最后一帧制作动画
114 - $signature_x = 0;
115 - $signature_y = -20;
116 - $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
117 -
118 - $output = $this->getTempPath('.mp4',false);
119 -
120 - $cmd = $this->ffmpeg . ' -y ' .
121 - ' -i ' . escapeshellarg($image).
122 - ' -i ' . escapeshellarg($watermark).
123 - ' -i ' . escapeshellarg($bgm) .
124 - ' -i ' . escapeshellarg($animate) .
125 - ' -filter_complex "[0:0] ' . $this->getTextContentString().
126 - '[text];[text][1:0]overlay=20:20[water];' .
127 - '[water][2:a][3:v][3:a]concat=n=2:v=1:a=1[v][a]" '.
128 - ' -map [v] -map [a] '.
129 - ' -c:v libx264 -bt 256k -r 25' .
130 - ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
131 - . escapeshellarg($output);
132 -
133 - if (!$this->execmd($cmd)) return;
134 -
135 - $video_info = $this->mediainfo($output);
136 - }
137 -
138 - $this->immerse->url = Str::of($output)->replace(Storage::disk('public')->path(''), '');
139 - $this->immerse->state = 1;
140 - $this->immerse->duration = $video_info['format']['duration'] ?? 0.00;
141 - $this->immerse->size = $video_info['format']['size'];
142 - $this->immerse->save();
143 - }catch (\Exception $exception){
144 - $this->immerse->state = 2;
145 - $this->immerse->save();
146 - Log::channel('daily')->error($exception->getMessage());
147 - }
148 - }
149 -
150 - /***
151 - * 获取视频信息(配合ffprobe)
152 - * @param $file
153 - * @param bool $cache
154 - * @return mixed
155 - */
156 - public function mediainfo($file, $cache = true) {
157 - global $_mediainfo;
158 - $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
159 - if ($cache && isset($_mediainfo[$file])) {
160 - return $_mediainfo[$file];
161 - }
162 - $output = $this->execmd($cmd);
163 - $data = json_decode($output, true);
164 - if (json_last_error() === JSON_ERROR_UTF8) {
165 - $output = mb_convert_encoding($output, "UTF-8");
166 - $data = json_decode($output, true);
167 - }
168 - if ($cache) {
169 - $mediainfo[$file] = $data;
170 - }
171 - return $data;
172 - }
173 -
174 - /**
175 - * 获取输出临时文件名
176 - * @param string $ext
177 - * @param bool $is_temp
178 - * @return string
179 - */
180 - public function getTempPath($ext = '.mp4',$is_temp = true)
181 - {
182 - $filename = "/output_" . time() . rand(0, 10000);
183 -
184 - $prefix = $is_temp ? 'temp/' : 'video/';
185 - $hash_hex = md5($filename);
186 - // 16进制表示的字符串一共32字节,表示16个二进制字节。
187 - // 前16个字符用来第一级求摸,后16个用做第二级
188 - $hash_hex_l1 = substr($hash_hex, 0, 8);
189 - $hash_hex_l2 = substr($hash_hex, 8, 8);
190 - $dir_l1 = hexdec($hash_hex_l1) % 256;
191 - $dir_l2 = hexdec($hash_hex_l2) % 512;
192 - $dir = $prefix . $dir_l1 . '/' . $dir_l2;
193 -
194 - if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
195 -
196 - return Storage::disk('public')->path($dir . $filename . $ext);
197 - }
198 -
199 - /**
200 - * 执行命令
201 - * @param $cmd
202 - * @param bool $update_progress
203 - * @return string
204 - */
205 - public function execmd($cmd, $update_progress = false) {
206 - echo $cmd . "\n". "\n". "\n";
207 - $descriptorspec = array(
208 - 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
209 - );
210 - $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
211 - if (is_resource($process)) {
212 - $error0 = '';
213 - $error1 = '';
214 - $stdout = '';
215 - while (!feof($pipes[1])) {
216 - $line = fgets($pipes[1], 150);
217 - $stdout .= $line;
218 - if ($line) {
219 - //记录错误
220 - $error0 = $error1;
221 - $error1 = $line;
222 - if ($update_progress &&
223 - false !== strpos($line, 'size=') &&
224 - false !== strpos($line, 'time=') &&
225 - false !== strpos($line, 'bitrate='))
226 - {
227 - //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
228 - $line = explode(' ', $line);
229 - $time = null;
230 - foreach ($line as $item) {
231 - $item = explode('=', $item);
232 - if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
233 - $time = $item[1];
234 - break;
235 - }
236 - }
237 - }
238 - }
239 - }
240 - // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
241 - fclose($pipes[1]);
242 - $exitedcode = proc_close($process);
243 - if ($exitedcode === 0) {
244 - return $stdout;
245 - } else {
246 - $error = trim($error0,"\n") . ' '. trim($error1,"\n");
247 - // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
248 - // ErrorUtil::triggerErrorMsg($error, $exitedcode);
249 - }
250 - } else {
251 - // return ErrorUtil::triggerErrorMsg('proc_open error');
252 - }
253 - }
254 -
255 - public function getTextContentString()
256 - {
257 - $components = $this->immerse->temp()->first()->components()->get();
258 -
259 - $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
260 -
261 - $drawtext = '';
262 -
263 - foreach ($components as $component) {
264 - switch ($component->name){
265 - case 'one_poem':
266 - $content = $this->immerse->poem->content;
267 - $text_file = $this->getTempPath('.txt');
268 - file_put_contents($text_file, $content);
269 -
270 - $text_color = $component->text_color ?? 'white';
271 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
272 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
273 -
274 - $drawtext .= 'drawtext="'.
275 - 'fontfile=' . escapeshellarg($font) . ':' .
276 - 'textfile=' . escapeshellarg($text_file) . ':' .
277 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
278 - 'fontcolor=' . $text_color . '@1.0:' .
279 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
280 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
281 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
282 -
283 - break;
284 - case 'every_poem':
285 - break;
286 - case 'weather':
287 - $content = '多云';
288 - $text_color = $component->text_color ?? 'white';
289 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
290 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
291 -
292 - $drawtext .= 'drawtext="'.
293 - 'fontfile=' . escapeshellarg($font) . ':' .
294 - 'text=' . escapeshellarg($content) . ':' .
295 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
296 - 'fontcolor=' . $text_color . '@1.0:' .
297 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
298 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
299 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
300 -
301 - break;
302 - case 'date':
303 - $content = Carbon::now()->format('Y年m月d日H时');
304 - $text_color = $component->text_color ?? 'white';
305 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
306 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
307 -
308 - $drawtext .= 'drawtext="'.
309 - 'fontfile=' . escapeshellarg($font) . ':' .
310 - 'text=' . escapeshellarg($content) . ':' .
311 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
312 - 'fontcolor=' . $text_color . '@1.0:' .
313 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
314 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
315 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
316 - break;
317 - case 'feel':
318 - $content = $this->immerse->feel;
319 - $text_color = $component->text_color ?? 'white';
320 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
321 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
322 -
323 - $drawtext .= 'drawtext="'.
324 - 'fontfile=' . escapeshellarg($font) . ':' .
325 - 'text=' . escapeshellarg($content) . ':' .
326 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
327 - 'fontcolor=' . $text_color . '@1.0:' .
328 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
329 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
330 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
331 - break;
332 - }
333 - }
334 -
335 - return rtrim($drawtext,', ');
336 - }
337 -
338 - /**
339 - * @param $width
340 - * @param $content
341 - * @return float
342 - */
343 - public function calcFontSize($width, $content)
344 - {
345 - $max_len = 1;
346 - foreach (explode("\n",$content) as $item){
347 - if (mb_strlen($item) > $max_len){
348 - $max_len = mb_strlen($item);
349 - }
350 - }
351 -
352 - return ceil($this->width * $width / 100 / $max_len);
353 - }
354 -
355 - /**
356 - * 贴纸和签名
357 - * @param $end_wallpaper
358 - * @param $thumbnail
359 - * @param $signature
360 - * @param $font
361 - * @return string
362 - */
363 - public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
364 - $_imagetype = $this->getImageType($thumbnail);
365 - $_img = null;
366 - switch ($_imagetype) {
367 - case 'gif':
368 - if (function_exists('imagecreatefromgif')) {
369 - $_img = imagecreatefromgif($thumbnail);
370 - }
371 - break;
372 - case 'jpg':
373 - case 'jpeg':
374 - $_img = imagecreatefromjpeg($thumbnail);
375 - break;
376 - case 'png':
377 - $_img = imagecreatefrompng($thumbnail);
378 - break;
379 - default:
380 - $_img = imagecreatefromstring($thumbnail);
381 - break;
382 - }
383 - $width = 130;
384 - $height = 130;
385 - $_width = 130;
386 - $_height = 130;
387 - if(is_resource($_img)){
388 - $_width = imagesx($_img);
389 - $_height = imagesy($_img);
390 - }
391 -
392 - $bite = $_width / $_height;
393 -
394 - if($_width > $_height){
395 - if($_width > $width){
396 - $height = round($width / $bite);
397 - }
398 - }else{
399 - if($_height > $height){
400 - $width = round($height * $bite);
401 - }
402 - }
403 -
404 - $tmpimg = imagecreatetruecolor($width,$height);
405 - if(function_exists('imagecopyresampled')) {
406 - imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
407 - } else {
408 - imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
409 - }
410 - if(is_resource($_img)) imagedestroy($_img);
411 - $_img = $this->getCircleAvatar($tmpimg);
412 - if(is_resource($tmpimg)) imagedestroy($tmpimg);
413 -
414 - $wp = $this->imagesMerge($end_wallpaper, $_img);
415 -// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
416 - $white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
417 - imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
418 -
419 -// $dst = "./output_new_end_wallpaper.png";
420 - $dst = $this->getTempPath('.png');
421 - imagepng($wp, $dst);
422 - if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
423 - if(is_resource($_img)) imagedestroy($_img);
424 -
425 - return $dst;
426 - }
427 -
428 - /**
429 - * 获取图像文件类型
430 - * @param $img_name
431 - * @return string
432 - */
433 - public function getImageType($img_name)
434 - {
435 - if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
436 - $type = strtolower($matches[1]);
437 - }else{
438 - $type = "string";
439 - }
440 - return $type;
441 - }
442 -
443 - /**
444 - * 多图融合
445 - * @param $end_wallpaper
446 - * @param $thumbnail
447 - * @return resource
448 - */
449 - public function imagesMerge($end_wallpaper, $thumbnail) {
450 - $end_wallpaper = imagecreatefrompng($end_wallpaper);
451 - $background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
452 - imagesavealpha($background,true);
453 - $temp_wallpaper = imagecreatetruecolor(350, 204);
454 - $color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
455 -// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
456 - imagefill($temp_wallpaper, 0, 0, $color);
457 - imageColorTransparent($temp_wallpaper, $color);
458 - imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
459 - imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
460 - imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
461 - return $background;
462 - }
463 -
464 - /**
465 - * 获取圆形头像
466 - * @param $img
467 - * @param int $dst_w
468 - * @param int $dst_h
469 - * @return resource
470 - */
471 - public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96)
472 - {
473 - $w = 130;
474 - $h = 130;
475 - $src = imagecreatetruecolor($dst_w, $dst_h);
476 - imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
477 -
478 - $newpic = imagecreatetruecolor($dst_w, $dst_h);
479 - imagealphablending($newpic, false);
480 - imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
481 - $mask = imagecreatetruecolor($dst_w, $dst_h);
482 - $transparent = imagecolorallocate($mask, 255, 0, 0);
483 - imagecolortransparent($mask,$transparent);
484 - imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
485 - $red = imagecolorallocate($mask, 0, 0, 0);
486 - imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
487 - imagecolortransparent($newpic,$red);
488 - imagesavealpha($newpic,true);
489 - imagefill($newpic, 0, 0, $red);
490 - imagedestroy($mask);
491 - return $newpic;
492 - }
493 -
494 - /**
495 - * 用最后一帧和贴纸制作动画
496 - * @param $last_frame_video
497 - * @param $end_wallpaper
498 - * @param $signature
499 - * @param $signature_x
500 - * @param $signature_y
501 - * @param $font
502 - * @return bool|string
503 - */
504 - public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
505 - $signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
506 - $signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
507 - $video = $this->getTempPath();
508 - if ($signature !== '') {
509 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
510 - ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
511 - ' -filter_complex "'.
512 - 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
513 - '[0:v]boxblur=8[blur];'.
514 - '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
515 - 'drawtext='.
516 - 'fontfile=' . escapeshellarg($font) . ':'.
517 - 'text=' . escapeshellarg($signature) . ':'.
518 - 'fontsize=23:'.
519 - 'fontcolor=white@1.0:'.
520 - 'x=main_w/2' . $signature_x . ':'.
521 - 'y=main_h/2' . $signature_y . '[text];[text]'.
522 - '[grad]alphamerge[alpha];'.
523 - '[0:v][alpha]overlay'.
524 - '" ' . escapeshellarg($video);
525 - } else {
526 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
527 - ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
528 - ' -filter_complex "'.
529 - 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
530 - '[0:v]boxblur=8[blur];'.
531 - '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
532 - '[lay][grad]alphamerge[alpha];'.
533 - '[0:v][alpha]overlay'.
534 - '" ' . escapeshellarg($video);
535 - }
536 - if ($this->execmd($cmd)) {
537 - return $video;
538 - } else {
539 - return false;
540 - }
541 - }
542 -}
...@@ -251,7 +251,7 @@ class UserMakeImmerse implements ShouldQueue ...@@ -251,7 +251,7 @@ class UserMakeImmerse implements ShouldQueue
251 $video_info = $this->mediainfo($video); 251 $video_info = $this->mediainfo($video);
252 252
253 $this->immerse->url = Str::of($video)->replace(Storage::disk('public')->path(''), ''); 253 $this->immerse->url = Str::of($video)->replace(Storage::disk('public')->path(''), '');
254 - $this->immerse->thumbnail = $thumbnail; 254 + $this->immerse->thumbnail = Str::of($thumbnail)->replace(Storage::disk('public')->path(''), '');
255 $this->immerse->state = 1; 255 $this->immerse->state = 1;
256 $this->immerse->duration = $video_info['format']['duration'] ?? 0; 256 $this->immerse->duration = $video_info['format']['duration'] ?? 0;
257 $this->immerse->size = $video_info['format']['size']; 257 $this->immerse->size = $video_info['format']['size'];
...@@ -510,8 +510,7 @@ class UserMakeImmerse implements ShouldQueue ...@@ -510,8 +510,7 @@ class UserMakeImmerse implements ShouldQueue
510 'fontcolor=' . $text_color . '@1.0:' . 510 'fontcolor=' . $text_color . '@1.0:' .
511 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 511 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
512 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 512 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
513 -// 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' . //todo 10号机不支持此选项 513 + 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' .
514 - 'box=1:' .
515 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; 514 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
516 515
517 break; 516 break;
...@@ -530,8 +529,7 @@ class UserMakeImmerse implements ShouldQueue ...@@ -530,8 +529,7 @@ class UserMakeImmerse implements ShouldQueue
530 'fontcolor=' . $text_color . '@1.0:' . 529 'fontcolor=' . $text_color . '@1.0:' .
531 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 530 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
532 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 531 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
533 -// 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' . //todo 10号机不支持此选项 532 + 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' .
534 - 'box=1:' .
535 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; 533 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
536 534
537 break; 535 break;
...@@ -548,8 +546,7 @@ class UserMakeImmerse implements ShouldQueue ...@@ -548,8 +546,7 @@ class UserMakeImmerse implements ShouldQueue
548 'fontcolor=' . $text_color . '@1.0:' . 546 'fontcolor=' . $text_color . '@1.0:' .
549 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 547 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
550 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 548 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
551 -// 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' . //todo 10号机不支持此选项 549 + 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' .
552 - 'box=1:' .
553 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; 550 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
554 break; 551 break;
555 case 'feel': 552 case 'feel':
...@@ -565,8 +562,7 @@ class UserMakeImmerse implements ShouldQueue ...@@ -565,8 +562,7 @@ class UserMakeImmerse implements ShouldQueue
565 'fontcolor=' . $text_color . '@1.0:' . 562 'fontcolor=' . $text_color . '@1.0:' .
566 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 563 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
567 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 564 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
568 -// 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' . //todo 10号机不支持此选项 565 + 'box=1:boxborderw='. $this->calcBorderSize($component->font_size) . ':' .
569 - 'box=1:' .
570 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; 566 'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
571 break; 567 break;
572 } 568 }
......
1 -<?php
2 -
3 -namespace App\Jobs;
4 -
5 -use App\Models\AdminMakeVideo;
6 -use App\Models\Immerse;
7 -use App\Models\User;
8 -use App\Models\VideoTemp;
9 -use Carbon\Carbon;
10 -use Illuminate\Bus\Queueable;
11 -use Illuminate\Contracts\Queue\ShouldBeUnique;
12 -use Illuminate\Contracts\Queue\ShouldQueue;
13 -use Illuminate\Foundation\Bus\Dispatchable;
14 -use Illuminate\Queue\InteractsWithQueue;
15 -use Illuminate\Queue\SerializesModels;
16 -use Illuminate\Support\Facades\Log;
17 -use Illuminate\Support\Facades\Storage;
18 -use Illuminate\Support\Str;
19 -
20 -class UserMakeVideo implements ShouldQueue
21 -{
22 - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
23 -
24 - public $immerse;
25 -
26 - public $resource_url;
27 -
28 - protected $ffmpeg;
29 -
30 - protected $ffprobe;
31 -
32 - protected $ffplay;
33 -
34 - protected $width;
35 -
36 - /**
37 - * Create a new job instance.
38 - * @param Immerse $immerse
39 - * @return void
40 - */
41 - public function __construct(Immerse $immerse, $resource_url)
42 - {
43 - $this->immerse = $immerse;
44 -
45 - $this->resource_url = $resource_url;
46 -
47 - $this->ffmpeg = env('FFMPEG_CMD');
48 - $this->ffprobe = env('FFPROBE_CMD');
49 - $this->ffplay = env('FFPLAY_CMD');
50 - }
51 -
52 - /**
53 - * Execute the job.
54 - *
55 - * @return void
56 - */
57 - public function handle()
58 - {
59 - $file = Storage::disk('public')->path($this->resource_url);
60 - $is_bgm = isset($this->immerse->bgm) && $this->immerse->bgm != '';
61 - $bgm = Storage::disk('public')->path($this->immerse->bgm);
62 -
63 - // 1.getmediainfo 记录时长,音频视频取最长。
64 - $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
65 - $output = $this->execmd($cmd);
66 - $media_info = json_decode($output, true);
67 - if (json_last_error() === JSON_ERROR_UTF8) {
68 - $output = mb_convert_encoding($output, "UTF-8");
69 - $media_info = json_decode($output, true);
70 - }
71 -
72 - /** 记录媒体信息时长*/
73 - $media_file_time_length = isset($media_info['format']['duration']) ?: 0;
74 - if ($media_info['streams'][0]['codec_type'] !== 'video') {
75 - Log::channel('daily')->error('视频没有video track');
76 - return;
77 - }
78 -
79 - // 2. 判断是否有视频原音,没有原音用背景音,没有背景音则混入anullsrc
80 - if ( $media_info['format']['nb_streams'] >= 2 ){ /** 音频视频轨都有 */
81 - if ($is_bgm){
82 - // 有背景音 融合
83 - $audio = $this->getTempPath('.mp3');
84 - $cmd = $this->ffmpeg.
85 - ' -y -i ' . escapeshellarg($file).
86 - ' -y -i ' . escapeshellarg($bgm).
87 - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
88 - '-ar 48000 -ab 64k ' . escapeshellarg($audio);
89 - if (!$this->execmd($cmd)) return;
90 -
91 - $audio_input = ' -i ' . escapeshellarg($audio);
92 - $audio_filter = '[3:a]';
93 - }else{
94 - // 没有背景音
95 - $audio_input = '';
96 - $audio_filter = '[0:1]';
97 - }
98 - }elseif ( $media_info['format']['nb_streams'] == 1 ){
99 - $audio = $this->getTempPath('.mp3');
100 - $cmd = $this->ffmpeg .
101 - ' -y -f lavfi -i aevalsrc=0:duration='. escapeshellarg($media_file_time_length) .
102 - ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
103 - if (!$this->execmd($cmd)) return;
104 -
105 - if ($is_bgm){
106 - $audio_empty = $audio;
107 - $audio = $this->getTempPath('.mp3');
108 - $cmd = $this->ffmpeg.
109 - ' -y -i ' . escapeshellarg($audio_empty).
110 - ' -y -i ' . escapeshellarg($bgm).
111 - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
112 - '-ar 48000 -ab 64k ' . escapeshellarg($audio);
113 - if (!$this->execmd($cmd)) return;
114 - }
115 - $audio_input = ' -i ' . escapeshellarg($audio);
116 - $audio_filter = '[3:a]';
117 -
118 - }else{ /** 音频视频轨都没有 */
119 - Log::channel('daily')->error('视频没有video track');
120 - return;
121 - }
122 -
123 - try{
124 - $end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png";
125 - $thumbnail = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png";
126 - $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
127 -
128 - $user = User::query()->find($this->immerse->user_id);
129 - $signature = $user->nickname ?? $user->email;
130 -
131 - // 生成贴纸和签名
132 - $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
133 -
134 - // 截取最后一帧
135 - $last_frame_video = $this->getTempPath();
136 - $this->width = $width = $media_info['streams'][0]['width'];
137 - $height = $media_info['streams'][0]['height'];
138 - $size = $width . 'x' . $height;
139 - $time_length = 0.7;
140 - $r = 24;
141 - $frame_n = $media_info['streams'][0]['nb_frames'] - 2;
142 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
143 - " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
144 - " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
145 - ' -map [v] -map 2:a ' . escapeshellarg($last_frame_video);
146 - if (!$this->execmd($cmd)) return;
147 -
148 -
149 - $signature_x = 0;
150 - $signature_y = -20;
151 - $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
152 -
153 - $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
154 -
155 - $video = $this->getTempPath('.mp4',false);
156 - $cmd = $this->ffmpeg . ' -y '.
157 - ' -i ' . escapeshellarg($file).
158 - ' -i ' . escapeshellarg($animate).
159 - ' -i ' . escapeshellarg($watermark).
160 - $audio_input .
161 - ' -filter_complex "[0:0] ' .
162 - $this->getTextContentString().
163 - ' [text];[text]'.
164 - ' [2:v]overlay=20:20[water];[water]' . $audio_filter . '[1:0][1:1] concat=n=2:v=1:a=1[v][a]" ' .
165 - ' -map [v] -map [a]'.
166 - ' -c:v libx264 -bt 256k -r 25' .
167 - ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
168 - escapeshellarg($video);
169 -
170 - if (!$this->execmd($cmd)) return;
171 -
172 - // 全部合成以后创建 临境
173 - $video_info = $this->mediainfo($video);
174 -
175 - $this->immerse->url = Str::of($video)->replace(Storage::disk('public')->path(''), '');
176 - $this->immerse->state = 1;
177 - $this->immerse->duration = $video_info['format']['duration'] ?? 0.00;
178 - $this->immerse->size = $video_info['format']['size'];
179 - $this->immerse->save();
180 - }catch (\Exception $exception){
181 - $this->immerse->state = 2;
182 - $this->immerse->save();
183 - Log::channel('daily')->error($exception->getMessage());
184 - }
185 -
186 - return;
187 - }
188 -
189 - /**
190 - * 获取圆形头像
191 - * @param $img
192 - * @param int $dst_w
193 - * @param int $dst_h
194 - * @return resource
195 - */
196 - public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96)
197 - {
198 - $w = 130;
199 - $h = 130;
200 - $src = imagecreatetruecolor($dst_w, $dst_h);
201 - imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
202 -
203 - $newpic = imagecreatetruecolor($dst_w, $dst_h);
204 - imagealphablending($newpic, false);
205 - imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
206 - $mask = imagecreatetruecolor($dst_w, $dst_h);
207 - $transparent = imagecolorallocate($mask, 255, 0, 0);
208 - imagecolortransparent($mask,$transparent);
209 - imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
210 - $red = imagecolorallocate($mask, 0, 0, 0);
211 - imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
212 - imagecolortransparent($newpic,$red);
213 - imagesavealpha($newpic,true);
214 - imagefill($newpic, 0, 0, $red);
215 - imagedestroy($mask);
216 - return $newpic;
217 - }
218 -
219 - /**
220 - * 制作最后一帧
221 - * @param $file
222 - * @return bool|string
223 - */
224 - public function makeLastFrameVideo($file) {
225 - $video = $this->getTempPath();
226 - $width = $this->getVideoWith($file);
227 - $height = $this->getVideoHeight($file);
228 - $size = $width . 'x' . $height;
229 - $time_length = 0.7;
230 - $r = 24;
231 - $frame_n = $this->getVideoFrameNum($file) - 2;
232 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
233 - " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
234 - " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
235 - ' -map [v] -map 2:a ' . escapeshellarg($video);
236 - if ($this->execmd($cmd)) {
237 - return $video;
238 - } else {
239 - return false;
240 - }
241 - }
242 -
243 - /**
244 - * 用最后一帧和贴纸制作动画
245 - * @param $last_frame_video
246 - * @param $end_wallpaper
247 - * @param $signature
248 - * @param $signature_x
249 - * @param $signature_y
250 - * @param $font
251 - * @return bool|string
252 - */
253 - public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
254 - $signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
255 - $signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
256 - $video = $this->getTempPath();
257 - if ($signature !== '') {
258 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
259 - ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
260 - ' -filter_complex "'.
261 - 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
262 - '[0:v]boxblur=8[blur];'.
263 - '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
264 - 'drawtext='.
265 - 'fontfile=' . escapeshellarg($font) . ':'.
266 - 'text=' . escapeshellarg($signature) . ':'.
267 - 'fontsize=23:'.
268 - 'fontcolor=white@1.0:'.
269 - 'x=main_w/2' . $signature_x . ':'.
270 - 'y=main_h/2' . $signature_y . '[text];[text]'.
271 - '[grad]alphamerge[alpha];'.
272 - '[0:v][alpha]overlay'.
273 - '" ' . escapeshellarg($video);
274 - } else {
275 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
276 - ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
277 - ' -filter_complex "'.
278 - 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
279 - '[0:v]boxblur=8[blur];'.
280 - '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
281 - '[lay][grad]alphamerge[alpha];'.
282 - '[0:v][alpha]overlay'.
283 - '" ' . escapeshellarg($video);
284 - }
285 - if ($this->execmd($cmd)) {
286 - return $video;
287 - } else {
288 - return false;
289 - }
290 - }
291 -
292 - /**
293 - * 获取视频宽度
294 - * @param $file
295 - * @param bool $cache
296 - * @return int|null
297 - */
298 - public function getVideoWith($file, $cache = true) {
299 - $result = $this->getFirstVideoTrackOption($file, $option = 'width', $cache);
300 - if ($result) {
301 - return (int)$result;
302 - } else {
303 - return $result;
304 - }
305 - }
306 -
307 - /**
308 - * 获取视频高度
309 - * @param $file
310 - * @param bool $cache
311 - * @return int|null
312 - */
313 - public function getVideoHeight($file, $cache = true) {
314 - $result = $this->getFirstVideoTrackOption($file, $option = 'height', $cache);
315 - if ($result) {
316 - return (int)$result;
317 - } else {
318 - return $result;
319 - }
320 - }
321 -
322 - /**
323 - * 获取视频帧数
324 - * @param $file
325 - * @param bool $cache
326 - * @return null
327 - */
328 - public function getVideoFrameNum($file, $cache = true) {
329 - return $this->getFirstVideoTrackOption($file, $option = 'nb_frames', $cache);
330 - }
331 -
332 -
333 - public function getFirstVideoTrackOption($file, $option, $cache = true) {
334 - return $this->getFirstTrackOption($file, $option, $codec_type = 'video', $cache = true);
335 - }
336 -
337 - public function getFirstTrackOption($file, $option, $codec_type = '', $cache = true) {
338 - $result = $this->mediainfo($file, $cache);
339 - if (!isset($result['streams'])) {
340 - return null;
341 - }
342 - $_track = null;
343 - foreach($result['streams'] as $track) {
344 - if (empty($codec_type)) {
345 - $_track = $track;
346 - break;
347 - } elseif ($track['codec_type'] == $codec_type) {
348 - $_track = $track;
349 - break;
350 - }
351 - }
352 - if (isset($_track[$option])) {
353 - return $_track[$option];
354 - }
355 - return null;
356 - }
357 -
358 - /***
359 - * 获取视频信息(配合ffprobe)
360 - * @param $file
361 - * @param bool $cache
362 - * @return mixed
363 - */
364 - public function mediainfo($file, $cache = true) {
365 - global $_mediainfo;
366 - $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
367 - if ($cache && isset($_mediainfo[$file])) {
368 - return $_mediainfo[$file];
369 - }
370 - $output = $this->execmd($cmd);
371 - $data = json_decode($output, true);
372 - if (json_last_error() === JSON_ERROR_UTF8) {
373 - $output = mb_convert_encoding($output, "UTF-8");
374 - $data = json_decode($output, true);
375 - }
376 - if ($cache) {
377 - $mediainfo[$file] = $data;
378 - }
379 - return $data;
380 - }
381 -
382 - /**
383 - * 获取输出临时文件名
384 - * @param string $ext
385 - * @param bool $is_temp
386 - * @return string
387 - */
388 - public function getTempPath($ext = '.mp4',$is_temp = true)
389 - {
390 - $filename = "/output_" . time() . rand(0, 10000);
391 -
392 - $prefix = $is_temp ? 'temp/' : 'video/';
393 - $hash_hex = md5($filename);
394 - // 16进制表示的字符串一共32字节,表示16个二进制字节。
395 - // 前16个字符用来第一级求摸,后16个用做第二级
396 - $hash_hex_l1 = substr($hash_hex, 0, 8);
397 - $hash_hex_l2 = substr($hash_hex, 8, 8);
398 - $dir_l1 = hexdec($hash_hex_l1) % 256;
399 - $dir_l2 = hexdec($hash_hex_l2) % 512;
400 - $dir = $prefix . $dir_l1 . '/' . $dir_l2;
401 -
402 - if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
403 -
404 - return Storage::disk('public')->path($dir . $filename . $ext);
405 - }
406 -
407 - /**
408 - * 执行命令
409 - * @param $cmd
410 - * @param bool $update_progress
411 - * @return string
412 - */
413 - public function execmd($cmd, $update_progress = false) {
414 - echo $cmd . "\n". "\n". "\n";
415 - $descriptorspec = array(
416 - 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
417 - );
418 - $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
419 - if (is_resource($process)) {
420 - $error0 = '';
421 - $error1 = '';
422 - $stdout = '';
423 - while (!feof($pipes[1])) {
424 - $line = fgets($pipes[1], 150);
425 - $stdout .= $line;
426 - if ($line) {
427 - //记录错误
428 - $error0 = $error1;
429 - $error1 = $line;
430 - if ($update_progress &&
431 - false !== strpos($line, 'size=') &&
432 - false !== strpos($line, 'time=') &&
433 - false !== strpos($line, 'bitrate='))
434 - {
435 - //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
436 - $line = explode(' ', $line);
437 - $time = null;
438 - foreach ($line as $item) {
439 - $item = explode('=', $item);
440 - if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
441 - $time = $item[1];
442 - break;
443 - }
444 - }
445 - }
446 - }
447 - }
448 - // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
449 - fclose($pipes[1]);
450 - $exitedcode = proc_close($process);
451 - if ($exitedcode === 0) {
452 - return $stdout;
453 - } else {
454 - $error = trim($error0,"\n") . ' '. trim($error1,"\n");
455 - // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
456 - // ErrorUtil::triggerErrorMsg($error, $exitedcode);
457 - }
458 - } else {
459 - // return ErrorUtil::triggerErrorMsg('proc_open error');
460 - }
461 - }
462 -
463 - /**
464 - * 贴纸和签名
465 - * @param $end_wallpaper
466 - * @param $thumbnail
467 - * @param $signature
468 - * @param $font
469 - * @return string
470 - */
471 - public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
472 - $_imagetype = $this->getImageType($thumbnail);
473 - $_img = null;
474 - switch ($_imagetype) {
475 - case 'gif':
476 - if (function_exists('imagecreatefromgif')) {
477 - $_img = imagecreatefromgif($thumbnail);
478 - }
479 - break;
480 - case 'jpg':
481 - case 'jpeg':
482 - $_img = imagecreatefromjpeg($thumbnail);
483 - break;
484 - case 'png':
485 - $_img = imagecreatefrompng($thumbnail);
486 - break;
487 - default:
488 - $_img = imagecreatefromstring($thumbnail);
489 - break;
490 - }
491 - $width = 130;
492 - $height = 130;
493 - $_width = 130;
494 - $_height = 130;
495 - if(is_resource($_img)){
496 - $_width = imagesx($_img);
497 - $_height = imagesy($_img);
498 - }
499 -
500 - $bite = $_width / $_height;
501 -
502 - if($_width > $_height){
503 - if($_width > $width){
504 - $height = round($width / $bite);
505 - }
506 - }else{
507 - if($_height > $height){
508 - $width = round($height * $bite);
509 - }
510 - }
511 -
512 - $tmpimg = imagecreatetruecolor($width,$height);
513 - if(function_exists('imagecopyresampled')) {
514 - imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
515 - } else {
516 - imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
517 - }
518 - if(is_resource($_img)) imagedestroy($_img);
519 - $_img = $this->getCircleAvatar($tmpimg);
520 - if(is_resource($tmpimg)) imagedestroy($tmpimg);
521 -
522 - $wp = $this->imagesMerge($end_wallpaper, $_img);
523 -// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
524 - $white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
525 - imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
526 -
527 -// $dst = "./output_new_end_wallpaper.png";
528 - $dst = Storage::disk('public')->path('ffmpeg') . "/output_new_end_wallpaper.png";
529 - imagepng($wp, $dst);
530 - if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
531 - if(is_resource($_img)) imagedestroy($_img);
532 -
533 - return $dst;
534 - }
535 -
536 - /**
537 - * 获取图像文件类型
538 - * @param $img_name
539 - * @return string
540 - */
541 - public function getImageType($img_name)
542 - {
543 - if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
544 - $type = strtolower($matches[1]);
545 - }else{
546 - $type = "string";
547 - }
548 - return $type;
549 - }
550 -
551 - /**
552 - * 多图融合
553 - * @param $end_wallpaper
554 - * @param $thumbnail
555 - * @return resource
556 - */
557 - public function imagesMerge($end_wallpaper, $thumbnail) {
558 - $end_wallpaper = imagecreatefrompng($end_wallpaper);
559 - $background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
560 - imagesavealpha($background,true);
561 - $temp_wallpaper = imagecreatetruecolor(350, 204);
562 - $color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
563 -// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
564 - imagefill($temp_wallpaper, 0, 0, $color);
565 - imageColorTransparent($temp_wallpaper, $color);
566 - imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
567 - imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
568 - imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
569 - return $background;
570 - }
571 -
572 - /**
573 - * logo 大小转换
574 - * @param $logo
575 - * @return bool
576 - */
577 - public function translateLogo($logo)
578 - {
579 - $image = Storage::disk('public')->path('ffmpeg/output_150x150.jpg');
580 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($logo) .
581 - ' -vf scale=150:150 ' . escapeshellarg($image);
582 - if ($this->execmd($cmd)) {
583 - return $image;
584 - } else {
585 - return false;
586 - }
587 - }
588 -
589 - public function getTextContentString()
590 - {
591 - $components = $this->immerse->temp()->first()->components()->get();
592 -
593 - $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
594 -
595 - $drawtext = '';
596 -
597 - foreach ($components as $component) {
598 - switch ($component->name){
599 - case 'one_poem':
600 - $content = $this->immerse->poem->content;
601 - $text_file = $this->getTempPath('txt');
602 - file_put_contents($text_file, $content);
603 -
604 - $text_color = $component->text_color ?? 'white';
605 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
606 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
607 -
608 - $drawtext .= 'drawtext="'.
609 - 'fontfile=' . escapeshellarg($font) . ':' .
610 - 'textfile=' . escapeshellarg($text_file) . ':' .
611 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
612 - 'fontcolor=' . $text_color . '@1.0:' .
613 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
614 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
615 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
616 -
617 - break;
618 - case 'every_poem':
619 - break;
620 - case 'weather':
621 - $content = '多云';
622 - $text_color = $component->text_color ?? 'white';
623 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
624 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
625 -
626 - $drawtext .= 'drawtext="'.
627 - 'fontfile=' . escapeshellarg($font) . ':' .
628 - 'text=' . escapeshellarg($content) . ':' .
629 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
630 - 'fontcolor=' . $text_color . '@1.0:' .
631 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
632 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
633 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
634 -
635 - break;
636 - case 'date':
637 - $content = Carbon::now()->format('Y年m月d日H时');
638 - $text_color = $component->text_color ?? 'white';
639 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
640 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
641 -
642 - $drawtext .= 'drawtext="'.
643 - 'fontfile=' . escapeshellarg($font) . ':' .
644 - 'text=' . escapeshellarg($content) . ':' .
645 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
646 - 'fontcolor=' . $text_color . '@1.0:' .
647 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
648 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
649 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
650 - break;
651 - case 'feel':
652 - $content = $this->immerse->content;
653 - $text_color = $component->text_color ?? 'white';
654 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
655 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
656 -
657 - $drawtext .= 'drawtext="'.
658 - 'fontfile=' . escapeshellarg($font) . ':' .
659 - 'text=' . escapeshellarg($content) . ':' .
660 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
661 - 'fontcolor=' . $text_color . '@1.0:' .
662 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
663 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
664 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
665 - break;
666 - }
667 - }
668 -
669 - return rtrim($drawtext,', ');
670 - }
671 -
672 - /**
673 - * @param $width
674 - * @param $content
675 - * @return float
676 - */
677 - public function calcFontSize($width, $content)
678 - {
679 - $max_len = 1;
680 - foreach (explode("\n",$content) as $item){
681 - if (mb_strlen($item) > $max_len){
682 - $max_len = mb_strlen($item);
683 - }
684 - }
685 -
686 - return ceil($this->width * $width / 100 / $max_len);
687 - }
688 -}