Showing
5 changed files
with
1179 additions
and
23 deletions
... | @@ -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 | } | ... | ... |
app/Jobs/UserMakeImages.php
0 → 100644
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'); | ... | ... |
-
Please register or login to post a comment