李帅

1.后台可上传图文内容。

...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 namespace App\Console\Commands; 3 namespace App\Console\Commands;
4 4
5 +use App\Jobs\MakeImages;
5 use App\Models\AdminMakeVideo; 6 use App\Models\AdminMakeVideo;
6 use App\Models\Immerse; 7 use App\Models\Immerse;
7 use App\Models\VideoTemp; 8 use App\Models\VideoTemp;
...@@ -50,6 +51,71 @@ class DevFFmpeg extends Command ...@@ -50,6 +51,71 @@ class DevFFmpeg extends Command
50 */ 51 */
51 public function handle() 52 public function handle()
52 { 53 {
54 + MakeImages::dispatch(AdminMakeVideo::query()->find(24));
55 + dd(1);
56 + $image = Storage::disk('public')->path('images/73f18d443820334c51c36f443c9683b3.png');
57 + $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
58 + $end_wallpaper = Storage::disk('public')->path('ffmpeg/output_new_end_wallpaper.png');
59 +
60 +
61 + // 制作最后一帧
62 + $size = '1242x2208';
63 + $time_length = 0.7;
64 + $r = 24;
65 + $last_frame_video = $this->getTempPath('.mp4');
66 + $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
67 +
68 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($image) .
69 + ' -i ' . escapeshellarg($watermark) .
70 + " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
71 + ' -filter_complex "'.
72 + ' [0:0] ' . $this->getTextContentString() .
73 + '[text];[text][1:0]overlay=20:20[water];' .
74 + ' [water]select=\'eq(n,0)\',setpts=PTS-STARTPTS[lastframe];[2:v][lastframe]overlay[v] " ' .
75 + ' -map [v] -map 3:a ' . escapeshellarg($last_frame_video);
76 + $output = $this->execmd($cmd);
77 +
78 +
79 + // 利用最后一帧制作动画
80 + $signature_x = 0;
81 + $signature_y = -20;
82 + $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
83 +
84 +
85 +
86 + dd($animate);
87 +
88 +
89 +
90 +
91 +
92 + // 这样实现不了
93 + $cmd = $this->ffmpeg . ' -y -i ' .
94 + escapeshellarg($image) .
95 + " -f lavfi -i nullsrc=s=1242x2208:d=0.7:r=24 ".
96 + ' -i ' . escapeshellarg($watermark) .
97 + ' -i ' . escapeshellarg($end_wallpaper) .
98 +
99 + ' -filter_complex "' .
100 + ' [0:v] ' . $this->getTextContentString() .
101 + ' [text];[text][2:0]overlay=20:20[water];' .
102 +
103 +
104 + ' [water]select=\'eq(n,1)\',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[last];' .
105 + ' [last]boxblur=8[blur];' .
106 + ' [blur][3:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];' .
107 + ' [lay]geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];' .
108 + ' [lay][grad]alphamerge[alpha];' .
109 +// ' [last][alpha]overlay[concat2];'.
110 +
111 + ' [water][alpha] concat=n=2:v=1[v]" ' .
112 +
113 + escapeshellarg($this->getTempPath('.mp4'));
114 +
115 + $output = $this->execmd($cmd);
116 +
117 + dd($output);
118 +
53 dd(Str::contains("/Users/lishuai/Documents/source/OnePoem-Server/storage/app/public/ffmpeg/output_16479198841364.mp4",'/storage/app/public/')); 119 dd(Str::contains("/Users/lishuai/Documents/source/OnePoem-Server/storage/app/public/ffmpeg/output_16479198841364.mp4",'/storage/app/public/'));
54 120
55 $path = '/Users/lishuai/Desktop/test/'; 121 $path = '/Users/lishuai/Desktop/test/';
......
...@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; ...@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
6 use App\Models\Immerse; 6 use App\Models\Immerse;
7 use App\Models\UserMakeVideo; 7 use App\Models\UserMakeVideo;
8 use App\Jobs\UserMakeVideo as MakeVideo; 8 use App\Jobs\UserMakeVideo as MakeVideo;
9 +use App\Jobs\UserMakeImages as MakeImages;
9 use Illuminate\Http\Request; 10 use Illuminate\Http\Request;
10 use Illuminate\Support\Facades\Storage; 11 use Illuminate\Support\Facades\Storage;
11 use Illuminate\Support\Facades\Validator; 12 use Illuminate\Support\Facades\Validator;
...@@ -34,8 +35,9 @@ class ImmerseController extends Controller ...@@ -34,8 +35,9 @@ class ImmerseController extends Controller
34 public function store(Request $request) 35 public function store(Request $request)
35 { 36 {
36 $validator = Validator::make($request->all(),[ 37 $validator = Validator::make($request->all(),[
37 - 'video_url' => 'required|string', 38 + 'item_url' => 'required|string',
38 - 'video_id' => 'required', 39 + 'item_id' => 'required',
40 + 'type' => 'required',
39 'content' => 'sometimes', 41 'content' => 'sometimes',
40 'weather' => 'sometimes', 42 'weather' => 'sometimes',
41 'thumbnail_url' => 'sometimes', 43 'thumbnail_url' => 'sometimes',
...@@ -47,20 +49,39 @@ class ImmerseController extends Controller ...@@ -47,20 +49,39 @@ class ImmerseController extends Controller
47 49
48 $validated = $validator->validated(); 50 $validated = $validator->validated();
49 51
50 - if (Str::contains($validated['video_url'],'//')){ 52 + if (Str::contains($validated['item_url'],'//')){
51 - $video_url = '' ; 53 + $item_url = '' ;
52 - }elseif (Str::contains($validated['video_url'],'/storage/app/public/')){ 54 + }elseif (Str::contains($validated['item_url'],'/storage/app/public/')){
53 - $video_url = $validated['video_url']; 55 + $item_url = $validated['item_url'];
54 }else{ 56 }else{
55 - $video_url = Storage::disk('public')->path($validated['video_url']); 57 + $item_url = Storage::disk('public')->path($validated['item_url']);
56 } 58 }
57 59
58 - $immerse = Immerse::query()->find($request->video_id); 60 + $immerse = Immerse::query()->find($request->item_id);
59 61
60 - $video = UserMakeVideo::query()->create([ 62 + if ($validated['type'] == 1){
63 + // 图文音频
64 + $create = UserMakeVideo::query()->create([
61 'poem_id' => $immerse->poem_id, 65 'poem_id' => $immerse->poem_id,
62 'type' => $immerse->type, 66 'type' => $immerse->type,
63 - 'video_url' => $video_url, 67 + 'video_url' => $item_url,
68 + 'image_url' => $immerse->image_url,
69 + 'bg_music' => $immerse->bg_music,
70 + 'bgm_url' => $immerse->bgm_url,
71 + 'feel' => $validated['content'],
72 + 'weather' => $validated['weather'],
73 + 'temp_id' => $immerse->temp_id,
74 + 'thumbnail' => $validated['thumbnail_url'] ? 1 : 0,
75 + 'thumbnail_url' => $validated['thumbnail_url'],
76 + ]);
77 + // 添加至队列
78 + MakeImages::dispatch($create);
79 + }else{
80 + // 视频
81 + $create = UserMakeVideo::query()->create([
82 + 'poem_id' => $immerse->poem_id,
83 + 'type' => $immerse->type,
84 + 'video_url' => $item_url,
64 'image_url' => $immerse->image_url, 85 'image_url' => $immerse->image_url,
65 'bg_music' => $immerse->bg_music, 86 'bg_music' => $immerse->bg_music,
66 'bgm_url' => $immerse->bgm_url, 87 'bgm_url' => $immerse->bgm_url,
...@@ -72,7 +93,10 @@ class ImmerseController extends Controller ...@@ -72,7 +93,10 @@ class ImmerseController extends Controller
72 ]); 93 ]);
73 94
74 // 添加至队列 95 // 添加至队列
75 - MakeVideo::dispatch($video); 96 + MakeVideo::dispatch($create);
97 + }
98 +
99 +
76 100
77 return Response::created(); 101 return Response::created();
78 } 102 }
......
...@@ -3,12 +3,16 @@ ...@@ -3,12 +3,16 @@
3 namespace App\Jobs; 3 namespace App\Jobs;
4 4
5 use App\Models\AdminMakeVideo; 5 use App\Models\AdminMakeVideo;
6 +use App\Models\Immerse;
7 +use App\Models\VideoTemp;
8 +use Carbon\Carbon;
6 use Illuminate\Bus\Queueable; 9 use Illuminate\Bus\Queueable;
7 use Illuminate\Contracts\Queue\ShouldBeUnique; 10 use Illuminate\Contracts\Queue\ShouldBeUnique;
8 use Illuminate\Contracts\Queue\ShouldQueue; 11 use Illuminate\Contracts\Queue\ShouldQueue;
9 use Illuminate\Foundation\Bus\Dispatchable; 12 use Illuminate\Foundation\Bus\Dispatchable;
10 use Illuminate\Queue\InteractsWithQueue; 13 use Illuminate\Queue\InteractsWithQueue;
11 use Illuminate\Queue\SerializesModels; 14 use Illuminate\Queue\SerializesModels;
15 +use Illuminate\Support\Facades\Storage;
12 16
13 class MakeImages implements ShouldQueue 17 class MakeImages implements ShouldQueue
14 { 18 {
...@@ -22,6 +26,10 @@ class MakeImages implements ShouldQueue ...@@ -22,6 +26,10 @@ class MakeImages implements ShouldQueue
22 26
23 protected $ffplay; 27 protected $ffplay;
24 28
29 + protected $width;
30 +
31 + protected $height;
32 +
25 /** 33 /**
26 * Create a new job instance. 34 * Create a new job instance.
27 * @param AdminMakeVideo $adminMakeVideo 35 * @param AdminMakeVideo $adminMakeVideo
...@@ -43,9 +51,513 @@ class MakeImages implements ShouldQueue ...@@ -43,9 +51,513 @@ class MakeImages implements ShouldQueue
43 */ 51 */
44 public function handle() 52 public function handle()
45 { 53 {
46 - //思路: 54 + $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
47 - // if 有背景音 多张图合成视频,时长为音频时长,音频加入背景音 55 + $image = Storage::disk('public')->path($this->adminMakeVideo->images_url);
48 - // else 没有背景音,单图一张,输出为单图。 56 + $media_info = $this->mediainfo($image);
57 + $this->width = $width = $media_info['streams'][0]['width'];
58 + $this->height = $height = $media_info['streams'][0]['height'];
59 +
60 + if ($this->adminMakeVideo->type == 2 && $this->adminMakeVideo->bg_music == 0){
61 + // 没有背景音,单图一张,输出为单图。
62 +
63 + $output = $this->getTempPath('.png',false);
64 +
65 + $cmd = $this->ffmpeg . ' -y '.
66 + ' -i ' . escapeshellarg($image).
67 + ' -i ' . escapeshellarg($watermark).
68 + ' -filter_complex "[0:0] ' .
69 + $this->getTextContentString().
70 + ' [text];[text]'.
71 + ' [1:0]overlay=20:20" ' .
72 + escapeshellarg($output);
73 +
74 + if (!$this->execmd($cmd)) return;
75 +
76 + // 全部合成以后创建 临境
77 + $video_info = $this->mediainfo($output);
78 +
79 + $create = [
80 + 'user_id' => 1,
81 + 'title' => '',
82 + 'content' => $this->adminMakeVideo->feel,
83 + 'url' => $output,
84 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
85 + 'duration' => 0,
86 + 'size' => $video_info['format']['size'],
87 + 'poem_id' => $this->adminMakeVideo->poem_id,
88 + 'temp_id' => $this->adminMakeVideo->temp_id,
89 + 'thumbnail' => '',
90 + 'bgm' => $this->adminMakeVideo->bgm_url,
91 + ];
92 +
93 + }else{
94 +
95 + $end_wallpaper = Storage::disk('public')->path('ffmpeg/end_wallpaper.png');
96 + $thumbnail = Storage::disk('public')->path('ffmpeg/thumbnail.png');
97 + $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
98 + $signature = "一言 · 官方出品";
99 +
100 + // 生成贴纸和签名
101 + $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
102 +
103 + // 有背景音 单图合成视频,时长为音频时长,音频加入背景音
104 + $bgm = Storage::disk('public')->path($this->adminMakeVideo->bgm_url);
105 +
106 +
107 + // 制作最后一帧
108 + $size = $this->width . 'x' . $this->height;
109 + $time_length = 0.7;
110 + $r = 24;
111 + $last_frame_video = $this->getTempPath('.mp4');
112 + $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
113 +
114 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($image) .
115 + ' -i ' . escapeshellarg($watermark) .
116 + " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
117 + ' -filter_complex "'.
118 + ' [0:0] ' . $this->getTextContentString() .
119 + '[text];[text][1:0]overlay=20:20[water];' .
120 + ' [water]select=\'eq(n,0)\',setpts=PTS-STARTPTS[lastframe];[2:v][lastframe]overlay[v] " ' .
121 + ' -map [v] -map 3:a ' . escapeshellarg($last_frame_video);
122 +
123 + if (!$this->execmd($cmd)) return;
124 +
125 + // 利用最后一帧制作动画
126 + $signature_x = 0;
127 + $signature_y = -20;
128 + $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
129 +
130 +
131 + $output = $this->getTempPath('.mp4',false);
132 +
133 + $cmd = $this->ffmpeg . ' -y ' .
134 + ' -i ' . escapeshellarg($image).
135 + ' -i ' . escapeshellarg($watermark).
136 + ' -i ' . escapeshellarg($bgm) .
137 + ' -i ' . escapeshellarg($animate) .
138 + ' -filter_complex "[0:0] ' . $this->getTextContentString().
139 + '[text];[text][1:0]overlay=20:20[water];' .
140 + '[water][2:a][3:v][3:a]concat=n=2:v=1:a=1[v][a]" '.
141 + ' -map [v] -map [a] '.
142 + ' -c:v libx264 -bt 256k -r 25' .
143 + ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
144 + . escapeshellarg($output);
145 +
146 + if (!$this->execmd($cmd)) return;
147 +
148 +
149 +
150 + // 全部合成以后创建 临境
151 + $video_info = $this->mediainfo($output);
152 +
153 + $create = [
154 + 'user_id' => 1,
155 + 'title' => '',
156 + 'content' => $this->adminMakeVideo->feel,
157 + 'url' => $output,
158 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
159 + 'duration' => $video_info['format']['duration'],
160 + 'size' => $video_info['format']['size'],
161 + 'poem_id' => $this->adminMakeVideo->poem_id,
162 + 'temp_id' => $this->adminMakeVideo->temp_id,
163 + 'thumbnail' => '',
164 + 'bgm' => $this->adminMakeVideo->bgm_url,
165 + ];
166 + }
167 +
168 + Immerse::query()->create($create);
169 + }
170 +
171 + /***
172 + * 获取视频信息(配合ffprobe)
173 + * @param $file
174 + * @param bool $cache
175 + * @return mixed
176 + */
177 + public function mediainfo($file, $cache = true) {
178 + global $_mediainfo;
179 + $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
180 + if ($cache && isset($_mediainfo[$file])) {
181 + return $_mediainfo[$file];
182 + }
183 + $output = $this->execmd($cmd);
184 + $data = json_decode($output, true);
185 + if (json_last_error() === JSON_ERROR_UTF8) {
186 + $output = mb_convert_encoding($output, "UTF-8");
187 + $data = json_decode($output, true);
188 + }
189 + if ($cache) {
190 + $mediainfo[$file] = $data;
191 + }
192 + return $data;
193 + }
194 +
195 + /**
196 + * 获取输出临时文件名
197 + * @param string $ext
198 + * @param bool $is_temp
199 + * @return string
200 + */
201 + public function getTempPath($ext = '.mp4',$is_temp = true)
202 + {
203 + $filename = "/output_" . time() . rand(0, 10000);
204 +
205 + $prefix = $is_temp ? 'temp/' : 'video/';
206 + $hash_hex = md5($filename);
207 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
208 + // 前16个字符用来第一级求摸,后16个用做第二级
209 + $hash_hex_l1 = substr($hash_hex, 0, 8);
210 + $hash_hex_l2 = substr($hash_hex, 8, 8);
211 + $dir_l1 = hexdec($hash_hex_l1) % 256;
212 + $dir_l2 = hexdec($hash_hex_l2) % 512;
213 + $dir = $prefix . $dir_l1 . '/' . $dir_l2;
214 +
215 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
216 +
217 + return Storage::disk('public')->path($dir . $filename . $ext);
218 + }
219 +
220 + /**
221 + * 执行命令
222 + * @param $cmd
223 + * @param bool $update_progress
224 + * @return string
225 + */
226 + public function execmd($cmd, $update_progress = false) {
227 + echo $cmd . "\n". "\n". "\n";
228 + $descriptorspec = array(
229 + 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
230 + );
231 + $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
232 + if (is_resource($process)) {
233 + $error0 = '';
234 + $error1 = '';
235 + $stdout = '';
236 + while (!feof($pipes[1])) {
237 + $line = fgets($pipes[1], 150);
238 + $stdout .= $line;
239 + if ($line) {
240 + //记录错误
241 + $error0 = $error1;
242 + $error1 = $line;
243 + if ($update_progress &&
244 + false !== strpos($line, 'size=') &&
245 + false !== strpos($line, 'time=') &&
246 + false !== strpos($line, 'bitrate='))
247 + {
248 + //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
249 + $line = explode(' ', $line);
250 + $time = null;
251 + foreach ($line as $item) {
252 + $item = explode('=', $item);
253 + if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
254 + $time = $item[1];
255 + break;
256 + }
257 + }
258 + }
259 + }
260 + }
261 + // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
262 + fclose($pipes[1]);
263 + $exitedcode = proc_close($process);
264 + if ($exitedcode === 0) {
265 + return $stdout;
266 + } else {
267 + $error = trim($error0,"\n") . ' '. trim($error1,"\n");
268 + // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
269 + // ErrorUtil::triggerErrorMsg($error, $exitedcode);
270 + }
271 + } else {
272 + // return ErrorUtil::triggerErrorMsg('proc_open error');
273 + }
274 + }
275 +
276 + public function getTextContentString()
277 + {
278 + $components = $this->adminMakeVideo->temp()->first()->components()->get();
279 +
280 + $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
281 +
282 + $drawtext = '';
49 283
284 + foreach ($components as $component) {
285 + switch ($component->name){
286 + case 'one_poem':
287 + $content = $this->adminMakeVideo->poem->content;
288 + $text_file = $this->getTempPath('.txt');
289 + file_put_contents($text_file, $content);
290 +
291 + $text_color = $component->text_color ?? 'white';
292 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
293 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
294 +
295 + $drawtext .= 'drawtext="'.
296 + 'fontfile=' . escapeshellarg($font) . ':' .
297 + 'textfile=' . escapeshellarg($text_file) . ':' .
298 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
299 + 'fontcolor=' . $text_color . '@1.0:' .
300 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
301 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
302 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
303 +
304 + break;
305 + case 'every_poem':
306 + break;
307 + case 'weather':
308 + $content = '多云';
309 + $text_color = $component->text_color ?? 'white';
310 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
311 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
312 +
313 + $drawtext .= 'drawtext="'.
314 + 'fontfile=' . escapeshellarg($font) . ':' .
315 + 'text=' . escapeshellarg($content) . ':' .
316 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
317 + 'fontcolor=' . $text_color . '@1.0:' .
318 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
319 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
320 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
321 +
322 + break;
323 + case 'date':
324 + $content = Carbon::now()->format('Y年m月d日H时');
325 + $text_color = $component->text_color ?? 'white';
326 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
327 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
328 +
329 + $drawtext .= 'drawtext="'.
330 + 'fontfile=' . escapeshellarg($font) . ':' .
331 + 'text=' . escapeshellarg($content) . ':' .
332 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
333 + 'fontcolor=' . $text_color . '@1.0:' .
334 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
335 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
336 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
337 + break;
338 + case 'feel':
339 + $content = $this->adminMakeVideo->feel;
340 + $text_color = $component->text_color ?? 'white';
341 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
342 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
343 +
344 + $drawtext .= 'drawtext="'.
345 + 'fontfile=' . escapeshellarg($font) . ':' .
346 + 'text=' . escapeshellarg($content) . ':' .
347 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
348 + 'fontcolor=' . $text_color . '@1.0:' .
349 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
350 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
351 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
352 + break;
353 + }
354 + }
355 +
356 + return rtrim($drawtext,', ');
357 + }
358 +
359 + /**
360 + * @param $width
361 + * @param $content
362 + * @return float
363 + */
364 + public function calcFontSize($width, $content)
365 + {
366 + $max_len = 1;
367 + foreach (explode("\n",$content) as $item){
368 + if (mb_strlen($item) > $max_len){
369 + $max_len = mb_strlen($item);
370 + }
371 + }
372 +
373 + return ceil($this->width * $width / 100 / $max_len);
374 + }
375 +
376 + /**
377 + * 贴纸和签名
378 + * @param $end_wallpaper
379 + * @param $thumbnail
380 + * @param $signature
381 + * @param $font
382 + * @return string
383 + */
384 + public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
385 + $_imagetype = $this->getImageType($thumbnail);
386 + $_img = null;
387 + switch ($_imagetype) {
388 + case 'gif':
389 + if (function_exists('imagecreatefromgif')) {
390 + $_img = imagecreatefromgif($thumbnail);
391 + }
392 + break;
393 + case 'jpg':
394 + case 'jpeg':
395 + $_img = imagecreatefromjpeg($thumbnail);
396 + break;
397 + case 'png':
398 + $_img = imagecreatefrompng($thumbnail);
399 + break;
400 + default:
401 + $_img = imagecreatefromstring($thumbnail);
402 + break;
403 + }
404 + $width = 130;
405 + $height = 130;
406 + $_width = 130;
407 + $_height = 130;
408 + if(is_resource($_img)){
409 + $_width = imagesx($_img);
410 + $_height = imagesy($_img);
411 + }
412 +
413 + $bite = $_width / $_height;
414 +
415 + if($_width > $_height){
416 + if($_width > $width){
417 + $height = round($width / $bite);
418 + }
419 + }else{
420 + if($_height > $height){
421 + $width = round($height * $bite);
422 + }
423 + }
424 +
425 + $tmpimg = imagecreatetruecolor($width,$height);
426 + if(function_exists('imagecopyresampled')) {
427 + imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
428 + } else {
429 + imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
430 + }
431 + if(is_resource($_img)) imagedestroy($_img);
432 + $_img = $this->getCircleAvatar($tmpimg);
433 + if(is_resource($tmpimg)) imagedestroy($tmpimg);
434 +
435 + $wp = $this->imagesMerge($end_wallpaper, $_img);
436 +// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
437 + $white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
438 + imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
439 +
440 +// $dst = "./output_new_end_wallpaper.png";
441 + $dst = $this->getTempPath('.png');
442 + imagepng($wp, $dst);
443 + if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
444 + if(is_resource($_img)) imagedestroy($_img);
445 +
446 + return $dst;
447 + }
448 +
449 + /**
450 + * 获取图像文件类型
451 + * @param $img_name
452 + * @return string
453 + */
454 + public function getImageType($img_name)
455 + {
456 + if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
457 + $type = strtolower($matches[1]);
458 + }else{
459 + $type = "string";
460 + }
461 + return $type;
462 + }
463 +
464 + /**
465 + * 多图融合
466 + * @param $end_wallpaper
467 + * @param $thumbnail
468 + * @return resource
469 + */
470 + public function imagesMerge($end_wallpaper, $thumbnail) {
471 + $end_wallpaper = imagecreatefrompng($end_wallpaper);
472 + $background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
473 + imagesavealpha($background,true);
474 + $temp_wallpaper = imagecreatetruecolor(350, 204);
475 + $color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
476 +// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
477 + imagefill($temp_wallpaper, 0, 0, $color);
478 + imageColorTransparent($temp_wallpaper, $color);
479 + imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
480 + imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
481 + imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
482 + return $background;
483 + }
484 +
485 + /**
486 + * 获取圆形头像
487 + * @param $img
488 + * @param int $dst_w
489 + * @param int $dst_h
490 + * @return resource
491 + */
492 + public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96)
493 + {
494 + $w = 130;
495 + $h = 130;
496 + $src = imagecreatetruecolor($dst_w, $dst_h);
497 + imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
498 +
499 + $newpic = imagecreatetruecolor($dst_w, $dst_h);
500 + imagealphablending($newpic, false);
501 + imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
502 + $mask = imagecreatetruecolor($dst_w, $dst_h);
503 + $transparent = imagecolorallocate($mask, 255, 0, 0);
504 + imagecolortransparent($mask,$transparent);
505 + imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
506 + $red = imagecolorallocate($mask, 0, 0, 0);
507 + imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
508 + imagecolortransparent($newpic,$red);
509 + imagesavealpha($newpic,true);
510 + imagefill($newpic, 0, 0, $red);
511 + imagedestroy($mask);
512 + return $newpic;
513 + }
514 +
515 + /**
516 + * 用最后一帧和贴纸制作动画
517 + * @param $last_frame_video
518 + * @param $end_wallpaper
519 + * @param $signature
520 + * @param $signature_x
521 + * @param $signature_y
522 + * @param $font
523 + * @return bool|string
524 + */
525 + public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
526 + $signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
527 + $signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
528 + $video = $this->getTempPath();
529 + if ($signature !== '') {
530 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
531 + ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
532 + ' -filter_complex "'.
533 + 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
534 + '[0:v]boxblur=8[blur];'.
535 + '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
536 + 'drawtext='.
537 + 'fontfile=' . escapeshellarg($font) . ':'.
538 + 'text=' . escapeshellarg($signature) . ':'.
539 + 'fontsize=23:'.
540 + 'fontcolor=white@1.0:'.
541 + 'x=main_w/2' . $signature_x . ':'.
542 + 'y=main_h/2' . $signature_y . '[text];[text]'.
543 + '[grad]alphamerge[alpha];'.
544 + '[0:v][alpha]overlay'.
545 + '" ' . escapeshellarg($video);
546 + } else {
547 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
548 + ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
549 + ' -filter_complex "'.
550 + 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
551 + '[0:v]boxblur=8[blur];'.
552 + '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
553 + '[lay][grad]alphamerge[alpha];'.
554 + '[0:v][alpha]overlay'.
555 + '" ' . escapeshellarg($video);
556 + }
557 + if ($this->execmd($cmd)) {
558 + return $video;
559 + } else {
560 + return false;
561 + }
50 } 562 }
51 } 563 }
......
1 +<?php
2 +
3 +namespace App\Jobs;
4 +
5 +use App\Models\AdminMakeVideo;
6 +use App\Models\Immerse;
7 +use App\Models\VideoTemp;
8 +use Carbon\Carbon;
9 +use Illuminate\Bus\Queueable;
10 +use Illuminate\Contracts\Queue\ShouldBeUnique;
11 +use Illuminate\Contracts\Queue\ShouldQueue;
12 +use Illuminate\Foundation\Bus\Dispatchable;
13 +use Illuminate\Queue\InteractsWithQueue;
14 +use Illuminate\Queue\SerializesModels;
15 +use Illuminate\Support\Facades\Storage;
16 +
17 +class UserMakeImages implements ShouldQueue
18 +{
19 + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
20 +
21 + public $adminMakeVideo;
22 +
23 + protected $ffmpeg;
24 +
25 + protected $ffprobe;
26 +
27 + protected $ffplay;
28 +
29 + protected $width;
30 +
31 + protected $height;
32 +
33 + /**
34 + * Create a new job instance.
35 + * @param AdminMakeVideo $adminMakeVideo
36 + * @return void
37 + */
38 + public function __construct(AdminMakeVideo $adminMakeVideo)
39 + {
40 + $this->adminMakeVideo = $adminMakeVideo;
41 +
42 + $this->ffmpeg = env('FFMPEG_CMD');
43 + $this->ffprobe = env('FFPROBE_CMD');
44 + $this->ffplay = env('FFPLAY_CMD');
45 + }
46 +
47 + /**
48 + * Execute the job.
49 + *
50 + * @return void
51 + */
52 + public function handle()
53 + {
54 + $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
55 + $image = Storage::disk('public')->path($this->adminMakeVideo->images_url);
56 + $media_info = $this->mediainfo($image);
57 + $this->width = $width = $media_info['streams'][0]['width'];
58 + $this->height = $height = $media_info['streams'][0]['height'];
59 +
60 + if ($this->adminMakeVideo->type == 2 && $this->adminMakeVideo->bg_music == 0){
61 + // 没有背景音,单图一张,输出为单图。
62 +
63 + $output = $this->getTempPath('.png',false);
64 +
65 + $cmd = $this->ffmpeg . ' -y '.
66 + ' -i ' . escapeshellarg($image).
67 + ' -i ' . escapeshellarg($watermark).
68 + ' -filter_complex "[0:0] ' .
69 + $this->getTextContentString().
70 + ' [text];[text]'.
71 + ' [1:0]overlay=20:20" ' .
72 + escapeshellarg($output);
73 +
74 + if (!$this->execmd($cmd)) return;
75 +
76 + // 全部合成以后创建 临境
77 + $video_info = $this->mediainfo($output);
78 +
79 + $create = [
80 + 'user_id' => 1,
81 + 'title' => '',
82 + 'content' => $this->adminMakeVideo->feel,
83 + 'url' => $output,
84 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
85 + 'duration' => 0,
86 + 'size' => $video_info['format']['size'],
87 + 'poem_id' => $this->adminMakeVideo->poem_id,
88 + 'temp_id' => $this->adminMakeVideo->temp_id,
89 + 'thumbnail' => '',
90 + 'bgm' => $this->adminMakeVideo->bgm_url,
91 + ];
92 +
93 + }else{
94 +
95 + $end_wallpaper = Storage::disk('public')->path('ffmpeg/end_wallpaper.png');
96 + $thumbnail = Storage::disk('public')->path('ffmpeg/thumbnail.png');
97 + $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
98 + $signature = "一言 · 官方出品";
99 +
100 + // 生成贴纸和签名
101 + $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
102 +
103 + // 有背景音 单图合成视频,时长为音频时长,音频加入背景音
104 + $bgm = Storage::disk('public')->path($this->adminMakeVideo->bgm_url);
105 +
106 +
107 + // 制作最后一帧
108 + $size = $this->width . 'x' . $this->height;
109 + $time_length = 0.7;
110 + $r = 24;
111 + $last_frame_video = $this->getTempPath('.mp4');
112 + $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
113 +
114 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($image) .
115 + ' -i ' . escapeshellarg($watermark) .
116 + " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
117 + ' -filter_complex "'.
118 + ' [0:0] ' . $this->getTextContentString() .
119 + '[text];[text][1:0]overlay=20:20[water];' .
120 + ' [water]select=\'eq(n,0)\',setpts=PTS-STARTPTS[lastframe];[2:v][lastframe]overlay[v] " ' .
121 + ' -map [v] -map 3:a ' . escapeshellarg($last_frame_video);
122 +
123 + if (!$this->execmd($cmd)) return;
124 +
125 + // 利用最后一帧制作动画
126 + $signature_x = 0;
127 + $signature_y = -20;
128 + $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
129 +
130 +
131 + $output = $this->getTempPath('.mp4',false);
132 +
133 + $cmd = $this->ffmpeg . ' -y ' .
134 + ' -i ' . escapeshellarg($image).
135 + ' -i ' . escapeshellarg($watermark).
136 + ' -i ' . escapeshellarg($bgm) .
137 + ' -i ' . escapeshellarg($animate) .
138 + ' -filter_complex "[0:0] ' . $this->getTextContentString().
139 + '[text];[text][1:0]overlay=20:20[water];' .
140 + '[water][2:a][3:v][3:a]concat=n=2:v=1:a=1[v][a]" '.
141 + ' -map [v] -map [a] '.
142 + ' -c:v libx264 -bt 256k -r 25' .
143 + ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
144 + . escapeshellarg($output);
145 +
146 + if (!$this->execmd($cmd)) return;
147 +
148 +
149 +
150 + // 全部合成以后创建 临境
151 + $video_info = $this->mediainfo($output);
152 +
153 + $create = [
154 + 'user_id' => 1,
155 + 'title' => '',
156 + 'content' => $this->adminMakeVideo->feel,
157 + 'url' => $output,
158 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
159 + 'duration' => $video_info['format']['duration'],
160 + 'size' => $video_info['format']['size'],
161 + 'poem_id' => $this->adminMakeVideo->poem_id,
162 + 'temp_id' => $this->adminMakeVideo->temp_id,
163 + 'thumbnail' => '',
164 + 'bgm' => $this->adminMakeVideo->bgm_url,
165 + ];
166 + }
167 +
168 + Immerse::query()->create($create);
169 + }
170 +
171 + /***
172 + * 获取视频信息(配合ffprobe)
173 + * @param $file
174 + * @param bool $cache
175 + * @return mixed
176 + */
177 + public function mediainfo($file, $cache = true) {
178 + global $_mediainfo;
179 + $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
180 + if ($cache && isset($_mediainfo[$file])) {
181 + return $_mediainfo[$file];
182 + }
183 + $output = $this->execmd($cmd);
184 + $data = json_decode($output, true);
185 + if (json_last_error() === JSON_ERROR_UTF8) {
186 + $output = mb_convert_encoding($output, "UTF-8");
187 + $data = json_decode($output, true);
188 + }
189 + if ($cache) {
190 + $mediainfo[$file] = $data;
191 + }
192 + return $data;
193 + }
194 +
195 + /**
196 + * 获取输出临时文件名
197 + * @param string $ext
198 + * @param bool $is_temp
199 + * @return string
200 + */
201 + public function getTempPath($ext = '.mp4',$is_temp = true)
202 + {
203 + $filename = "/output_" . time() . rand(0, 10000);
204 +
205 + $prefix = $is_temp ? 'temp/' : 'video/';
206 + $hash_hex = md5($filename);
207 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
208 + // 前16个字符用来第一级求摸,后16个用做第二级
209 + $hash_hex_l1 = substr($hash_hex, 0, 8);
210 + $hash_hex_l2 = substr($hash_hex, 8, 8);
211 + $dir_l1 = hexdec($hash_hex_l1) % 256;
212 + $dir_l2 = hexdec($hash_hex_l2) % 512;
213 + $dir = $prefix . $dir_l1 . '/' . $dir_l2;
214 +
215 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
216 +
217 + return Storage::disk('public')->path($dir . $filename . $ext);
218 + }
219 +
220 + /**
221 + * 执行命令
222 + * @param $cmd
223 + * @param bool $update_progress
224 + * @return string
225 + */
226 + public function execmd($cmd, $update_progress = false) {
227 + echo $cmd . "\n". "\n". "\n";
228 + $descriptorspec = array(
229 + 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
230 + );
231 + $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
232 + if (is_resource($process)) {
233 + $error0 = '';
234 + $error1 = '';
235 + $stdout = '';
236 + while (!feof($pipes[1])) {
237 + $line = fgets($pipes[1], 150);
238 + $stdout .= $line;
239 + if ($line) {
240 + //记录错误
241 + $error0 = $error1;
242 + $error1 = $line;
243 + if ($update_progress &&
244 + false !== strpos($line, 'size=') &&
245 + false !== strpos($line, 'time=') &&
246 + false !== strpos($line, 'bitrate='))
247 + {
248 + //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
249 + $line = explode(' ', $line);
250 + $time = null;
251 + foreach ($line as $item) {
252 + $item = explode('=', $item);
253 + if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
254 + $time = $item[1];
255 + break;
256 + }
257 + }
258 + }
259 + }
260 + }
261 + // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
262 + fclose($pipes[1]);
263 + $exitedcode = proc_close($process);
264 + if ($exitedcode === 0) {
265 + return $stdout;
266 + } else {
267 + $error = trim($error0,"\n") . ' '. trim($error1,"\n");
268 + // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
269 + // ErrorUtil::triggerErrorMsg($error, $exitedcode);
270 + }
271 + } else {
272 + // return ErrorUtil::triggerErrorMsg('proc_open error');
273 + }
274 + }
275 +
276 + public function getTextContentString()
277 + {
278 + $components = $this->adminMakeVideo->temp()->first()->components()->get();
279 +
280 + $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
281 +
282 + $drawtext = '';
283 +
284 + foreach ($components as $component) {
285 + switch ($component->name){
286 + case 'one_poem':
287 + $content = $this->adminMakeVideo->poem->content;
288 + $text_file = $this->getTempPath('.txt');
289 + file_put_contents($text_file, $content);
290 +
291 + $text_color = $component->text_color ?? 'white';
292 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
293 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
294 +
295 + $drawtext .= 'drawtext="'.
296 + 'fontfile=' . escapeshellarg($font) . ':' .
297 + 'textfile=' . escapeshellarg($text_file) . ':' .
298 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
299 + 'fontcolor=' . $text_color . '@1.0:' .
300 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
301 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
302 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
303 +
304 + break;
305 + case 'every_poem':
306 + break;
307 + case 'weather':
308 + $content = '多云';
309 + $text_color = $component->text_color ?? 'white';
310 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
311 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
312 +
313 + $drawtext .= 'drawtext="'.
314 + 'fontfile=' . escapeshellarg($font) . ':' .
315 + 'text=' . escapeshellarg($content) . ':' .
316 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
317 + 'fontcolor=' . $text_color . '@1.0:' .
318 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
319 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
320 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
321 +
322 + break;
323 + case 'date':
324 + $content = Carbon::now()->format('Y年m月d日H时');
325 + $text_color = $component->text_color ?? 'white';
326 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
327 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
328 +
329 + $drawtext .= 'drawtext="'.
330 + 'fontfile=' . escapeshellarg($font) . ':' .
331 + 'text=' . escapeshellarg($content) . ':' .
332 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
333 + 'fontcolor=' . $text_color . '@1.0:' .
334 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
335 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
336 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
337 + break;
338 + case 'feel':
339 + $content = $this->adminMakeVideo->feel;
340 + $text_color = $component->text_color ?? 'white';
341 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
342 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
343 +
344 + $drawtext .= 'drawtext="'.
345 + 'fontfile=' . escapeshellarg($font) . ':' .
346 + 'text=' . escapeshellarg($content) . ':' .
347 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
348 + 'fontcolor=' . $text_color . '@1.0:' .
349 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
350 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
351 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
352 + break;
353 + }
354 + }
355 +
356 + return rtrim($drawtext,', ');
357 + }
358 +
359 + /**
360 + * @param $width
361 + * @param $content
362 + * @return float
363 + */
364 + public function calcFontSize($width, $content)
365 + {
366 + $max_len = 1;
367 + foreach (explode("\n",$content) as $item){
368 + if (mb_strlen($item) > $max_len){
369 + $max_len = mb_strlen($item);
370 + }
371 + }
372 +
373 + return ceil($this->width * $width / 100 / $max_len);
374 + }
375 +
376 + /**
377 + * 贴纸和签名
378 + * @param $end_wallpaper
379 + * @param $thumbnail
380 + * @param $signature
381 + * @param $font
382 + * @return string
383 + */
384 + public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
385 + $_imagetype = $this->getImageType($thumbnail);
386 + $_img = null;
387 + switch ($_imagetype) {
388 + case 'gif':
389 + if (function_exists('imagecreatefromgif')) {
390 + $_img = imagecreatefromgif($thumbnail);
391 + }
392 + break;
393 + case 'jpg':
394 + case 'jpeg':
395 + $_img = imagecreatefromjpeg($thumbnail);
396 + break;
397 + case 'png':
398 + $_img = imagecreatefrompng($thumbnail);
399 + break;
400 + default:
401 + $_img = imagecreatefromstring($thumbnail);
402 + break;
403 + }
404 + $width = 130;
405 + $height = 130;
406 + $_width = 130;
407 + $_height = 130;
408 + if(is_resource($_img)){
409 + $_width = imagesx($_img);
410 + $_height = imagesy($_img);
411 + }
412 +
413 + $bite = $_width / $_height;
414 +
415 + if($_width > $_height){
416 + if($_width > $width){
417 + $height = round($width / $bite);
418 + }
419 + }else{
420 + if($_height > $height){
421 + $width = round($height * $bite);
422 + }
423 + }
424 +
425 + $tmpimg = imagecreatetruecolor($width,$height);
426 + if(function_exists('imagecopyresampled')) {
427 + imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
428 + } else {
429 + imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
430 + }
431 + if(is_resource($_img)) imagedestroy($_img);
432 + $_img = $this->getCircleAvatar($tmpimg);
433 + if(is_resource($tmpimg)) imagedestroy($tmpimg);
434 +
435 + $wp = $this->imagesMerge($end_wallpaper, $_img);
436 +// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
437 + $white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
438 + imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
439 +
440 +// $dst = "./output_new_end_wallpaper.png";
441 + $dst = $this->getTempPath('.png');
442 + imagepng($wp, $dst);
443 + if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
444 + if(is_resource($_img)) imagedestroy($_img);
445 +
446 + return $dst;
447 + }
448 +
449 + /**
450 + * 获取图像文件类型
451 + * @param $img_name
452 + * @return string
453 + */
454 + public function getImageType($img_name)
455 + {
456 + if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
457 + $type = strtolower($matches[1]);
458 + }else{
459 + $type = "string";
460 + }
461 + return $type;
462 + }
463 +
464 + /**
465 + * 多图融合
466 + * @param $end_wallpaper
467 + * @param $thumbnail
468 + * @return resource
469 + */
470 + public function imagesMerge($end_wallpaper, $thumbnail) {
471 + $end_wallpaper = imagecreatefrompng($end_wallpaper);
472 + $background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
473 + imagesavealpha($background,true);
474 + $temp_wallpaper = imagecreatetruecolor(350, 204);
475 + $color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
476 +// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
477 + imagefill($temp_wallpaper, 0, 0, $color);
478 + imageColorTransparent($temp_wallpaper, $color);
479 + imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
480 + imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
481 + imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
482 + return $background;
483 + }
484 +
485 + /**
486 + * 获取圆形头像
487 + * @param $img
488 + * @param int $dst_w
489 + * @param int $dst_h
490 + * @return resource
491 + */
492 + public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96)
493 + {
494 + $w = 130;
495 + $h = 130;
496 + $src = imagecreatetruecolor($dst_w, $dst_h);
497 + imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
498 +
499 + $newpic = imagecreatetruecolor($dst_w, $dst_h);
500 + imagealphablending($newpic, false);
501 + imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
502 + $mask = imagecreatetruecolor($dst_w, $dst_h);
503 + $transparent = imagecolorallocate($mask, 255, 0, 0);
504 + imagecolortransparent($mask,$transparent);
505 + imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
506 + $red = imagecolorallocate($mask, 0, 0, 0);
507 + imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
508 + imagecolortransparent($newpic,$red);
509 + imagesavealpha($newpic,true);
510 + imagefill($newpic, 0, 0, $red);
511 + imagedestroy($mask);
512 + return $newpic;
513 + }
514 +
515 + /**
516 + * 用最后一帧和贴纸制作动画
517 + * @param $last_frame_video
518 + * @param $end_wallpaper
519 + * @param $signature
520 + * @param $signature_x
521 + * @param $signature_y
522 + * @param $font
523 + * @return bool|string
524 + */
525 + public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
526 + $signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
527 + $signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
528 + $video = $this->getTempPath();
529 + if ($signature !== '') {
530 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
531 + ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
532 + ' -filter_complex "'.
533 + 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
534 + '[0:v]boxblur=8[blur];'.
535 + '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
536 + 'drawtext='.
537 + 'fontfile=' . escapeshellarg($font) . ':'.
538 + 'text=' . escapeshellarg($signature) . ':'.
539 + 'fontsize=23:'.
540 + 'fontcolor=white@1.0:'.
541 + 'x=main_w/2' . $signature_x . ':'.
542 + 'y=main_h/2' . $signature_y . '[text];[text]'.
543 + '[grad]alphamerge[alpha];'.
544 + '[0:v][alpha]overlay'.
545 + '" ' . escapeshellarg($video);
546 + } else {
547 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
548 + ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
549 + ' -filter_complex "'.
550 + 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
551 + '[0:v]boxblur=8[blur];'.
552 + '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
553 + '[lay][grad]alphamerge[alpha];'.
554 + '[0:v][alpha]overlay'.
555 + '" ' . escapeshellarg($video);
556 + }
557 + if ($this->execmd($cmd)) {
558 + return $video;
559 + } else {
560 + return false;
561 + }
562 + }
563 +}
...@@ -33,15 +33,6 @@ class AdminMakeVideo extends Model ...@@ -33,15 +33,6 @@ class AdminMakeVideo extends Model
33 return Storage::disk('public')->url($this->thumbnail_url); 33 return Storage::disk('public')->url($this->thumbnail_url);
34 } 34 }
35 35
36 - public function getImagesUrl()
37 - {
38 - if (Str::contains($this->images_url, '//')) {
39 - return $this->images_url;
40 - }
41 -
42 - return Storage::disk('public')->url($this->images_url);
43 - }
44 -
45 public function poem() 36 public function poem()
46 { 37 {
47 return $this->hasOne(OnePoem::class,'id','poem_id'); 38 return $this->hasOne(OnePoem::class,'id','poem_id');
......