Showing
3 changed files
with
5 additions
and
1239 deletions
app/Jobs/UserMakeImages.php
deleted
100644 → 0
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 | } | ... | ... |
app/Jobs/UserMakeVideo.php
deleted
100644 → 0
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 | -} |
-
Please register or login to post a comment