Showing
6 changed files
with
191 additions
and
90 deletions
... | @@ -125,6 +125,7 @@ class VideoTempController extends AdminController | ... | @@ -125,6 +125,7 @@ class VideoTempController extends AdminController |
125 | $form->radio('draw', '文字效果') | 125 | $form->radio('draw', '文字效果') |
126 | ->options(['fade'=>'淡入淡出', 'fix'=>'固定显示'])->default('fade') | 126 | ->options(['fade'=>'淡入淡出', 'fix'=>'固定显示'])->default('fade') |
127 | ->when('fade',function (Form\NestedForm $form){ | 127 | ->when('fade',function (Form\NestedForm $form){ |
128 | + $form->number('fade_time', 'fade时间')->default(1500); | ||
128 | $form->selectTable('font_file','字体') | 129 | $form->selectTable('font_file','字体') |
129 | ->title('字体选择') | 130 | ->title('字体选择') |
130 | ->from(FontTable::make()) | 131 | ->from(FontTable::make()) | ... | ... |
... | @@ -66,6 +66,12 @@ class DevFFmpeg extends Command | ... | @@ -66,6 +66,12 @@ class DevFFmpeg extends Command |
66 | */ | 66 | */ |
67 | public function handle() | 67 | public function handle() |
68 | { | 68 | { |
69 | +// $json = shell_exec(env('FFPROBE_CMD') . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg(storage_path('app/public/uploads/131/309/K4LTgHuDhcmDr3MZBDkd0vyMUmRbfBxrFbU0CoNs.png')). ' 2>&1'); | ||
70 | +// $arr = json_decode($json,true); | ||
71 | +// dd($arr); | ||
72 | +// dd(AdminMakeVideo::query()->find(1)->poem2()); | ||
73 | + dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray()); | ||
74 | + return 0; | ||
69 | dd(AdminMakeVideo::query()->find(33)->temp->components->toArray()); | 75 | dd(AdminMakeVideo::query()->find(33)->temp->components->toArray()); |
70 | AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components); | 76 | AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components); |
71 | 77 | ... | ... |
... | @@ -51,52 +51,7 @@ class MakeVideo implements ShouldQueue | ... | @@ -51,52 +51,7 @@ class MakeVideo implements ShouldQueue |
51 | // 素材准备 | 51 | // 素材准备 |
52 | $drawtext = $this->getTextContentString(); | 52 | $drawtext = $this->getTextContentString(); |
53 | 53 | ||
54 | - if ($media_info['format']['nb_streams'] >= 2) { | ||
55 | - /** 音频视频轨都有 */ | ||
56 | - if ($is_bgm) { | ||
57 | - // 有背景音 融合 | ||
58 | - $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
59 | - $cmd = $this->ffmpeg . | ||
60 | - ' -y -i ' . escapeshellarg($file) . | ||
61 | - ' -y -i ' . escapeshellarg($bgm) . | ||
62 | - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
63 | - '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
64 | - if (!$this->execmd($cmd)) return; | ||
65 | 54 | ||
66 | - $audio_input = ' -i ' . escapeshellarg($audio); | ||
67 | - $audio_filter = '2:a'; | ||
68 | - } else { | ||
69 | - // 没有背景音 | ||
70 | - $audio_input = ''; | ||
71 | - $audio_filter = '0:a'; | ||
72 | - } | ||
73 | - } elseif ($media_info['format']['nb_streams'] == 1) { | ||
74 | - /** 只有视频轨 */ | ||
75 | - // 生成一段无声音频 | ||
76 | - $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
77 | - $cmd = $this->ffmpeg . | ||
78 | - ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) . | ||
79 | - ' -ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
80 | - if (!$this->execmd($cmd)) return; | ||
81 | - | ||
82 | - if ($is_bgm) { | ||
83 | - // 有背景音 融合 | ||
84 | - $audio_empty = $audio; | ||
85 | - $audio = $this->getAbsolutePath($this->getTempPath('.mp3')); | ||
86 | - $cmd = $this->ffmpeg . | ||
87 | - ' -y -i ' . escapeshellarg($audio_empty) . | ||
88 | - ' -y -i ' . escapeshellarg($bgm) . | ||
89 | - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
90 | - '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
91 | - if (!$this->execmd($cmd)) return; | ||
92 | - } | ||
93 | - $audio_input = ' -i ' . escapeshellarg($audio); | ||
94 | - $audio_filter = '2:a'; | ||
95 | - } else { | ||
96 | - /** 音频视频轨都没有 */ | ||
97 | - Log::channel('daily')->error('视频没有video track, url:' . $file); | ||
98 | - return; | ||
99 | - } | ||
100 | 55 | ||
101 | $thumbnail = $this->getTempPath('.jpg','thumbnail'); | 56 | $thumbnail = $this->getTempPath('.jpg','thumbnail'); |
102 | if ($adminMakeVideo->thumbnail == 2){ | 57 | if ($adminMakeVideo->thumbnail == 2){ |
... | @@ -174,9 +129,57 @@ class MakeVideo implements ShouldQueue | ... | @@ -174,9 +129,57 @@ class MakeVideo implements ShouldQueue |
174 | // 准备素材 | 129 | // 准备素材 |
175 | 130 | ||
176 | // 组装文字参数 | 131 | // 组装文字参数 |
132 | + $drawtext = $this->getTextContentString(); | ||
177 | 133 | ||
178 | // 合成视频 | 134 | // 合成视频 |
179 | 135 | ||
136 | + if ($this->media_info['format']['nb_streams'] >= 2) { | ||
137 | + /** 音频视频轨都有 */ | ||
138 | + if ($is_bgm) { | ||
139 | + // 有背景音 融合 | ||
140 | + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
141 | + $cmd = $this->ffmpeg . | ||
142 | + ' -y -i ' . escapeshellarg($file) . | ||
143 | + ' -y -i ' . escapeshellarg($bgm) . | ||
144 | + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
145 | + '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
146 | + if (!$this->execmd($cmd)) return; | ||
147 | + | ||
148 | + $audio_input = ' -i ' . escapeshellarg($audio); | ||
149 | + $audio_filter = '2:a'; | ||
150 | + } else { | ||
151 | + // 没有背景音 | ||
152 | + $audio_input = ''; | ||
153 | + $audio_filter = '0:a'; | ||
154 | + } | ||
155 | + } elseif ($media_info['format']['nb_streams'] == 1) { | ||
156 | + /** 只有视频轨 */ | ||
157 | + // 生成一段无声音频 | ||
158 | + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
159 | + $cmd = $this->ffmpeg . | ||
160 | + ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) . | ||
161 | + ' -ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
162 | + if (!$this->execmd($cmd)) return; | ||
163 | + | ||
164 | + if ($is_bgm) { | ||
165 | + // 有背景音 融合 | ||
166 | + $audio_empty = $audio; | ||
167 | + $audio = $this->getAbsolutePath($this->getTempPath('.mp3')); | ||
168 | + $cmd = $this->ffmpeg . | ||
169 | + ' -y -i ' . escapeshellarg($audio_empty) . | ||
170 | + ' -y -i ' . escapeshellarg($bgm) . | ||
171 | + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
172 | + '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
173 | + if (!$this->execmd($cmd)) return; | ||
174 | + } | ||
175 | + $audio_input = ' -i ' . escapeshellarg($audio); | ||
176 | + $audio_filter = '2:a'; | ||
177 | + } else { | ||
178 | + /** 音频视频轨都没有 */ | ||
179 | + Log::channel('daily')->error('视频没有video track, url:' . $file); | ||
180 | + return; | ||
181 | + } | ||
182 | + | ||
180 | // 制作封面图 | 183 | // 制作封面图 |
181 | 184 | ||
182 | // 分析视频 入库 | 185 | // 分析视频 入库 |
... | @@ -194,7 +197,7 @@ class MakeVideo implements ShouldQueue | ... | @@ -194,7 +197,7 @@ class MakeVideo implements ShouldQueue |
194 | if ($this->media_info) return $this->media_info; | 197 | if ($this->media_info) return $this->media_info; |
195 | 198 | ||
196 | $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file); | 199 | $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file); |
197 | - $output = $this->execmd($cmd); | 200 | + $output = $this->execCmd($cmd); |
198 | $data = json_decode($output, true); | 201 | $data = json_decode($output, true); |
199 | if (json_last_error() === JSON_ERROR_UTF8) { | 202 | if (json_last_error() === JSON_ERROR_UTF8) { |
200 | $output = mb_convert_encoding($output, "UTF-8"); | 203 | $output = mb_convert_encoding($output, "UTF-8"); |
... | @@ -204,57 +207,140 @@ class MakeVideo implements ShouldQueue | ... | @@ -204,57 +207,140 @@ class MakeVideo implements ShouldQueue |
204 | return $data; | 207 | return $data; |
205 | } | 208 | } |
206 | 209 | ||
207 | - public function execmd($cmd, $update_progress = false) { | 210 | + public function execCmd($cmd) |
208 | - echo $cmd . "\n". "\n". "\n"; | 211 | + { |
209 | - $descriptorspec = array( | 212 | + return shell_exec("{$cmd} 2>&1"); |
210 | - 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据 | 213 | + } |
211 | - ); | 214 | + |
212 | - $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes); | 215 | + public function getTextContentString() |
213 | - if (is_resource($process)) { | ||
214 | - $error0 = ''; | ||
215 | - $error1 = ''; | ||
216 | - $stdout = ''; | ||
217 | - while (!feof($pipes[1])) { | ||
218 | - $line = fgets($pipes[1], 150); | ||
219 | - $stdout .= $line; | ||
220 | - if ($line) { | ||
221 | - //记录错误 | ||
222 | - $error0 = $error1; | ||
223 | - $error1 = $line; | ||
224 | - if ($update_progress && | ||
225 | - false !== strpos($line, 'size=') && | ||
226 | - false !== strpos($line, 'time=') && | ||
227 | - false !== strpos($line, 'bitrate=')) | ||
228 | { | 216 | { |
229 | - //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s | 217 | + $components = $this->adminMakeVideo->temp->components; |
230 | - $line = explode(' ', $line); | 218 | + |
231 | - $time = null; | 219 | + $drawtext = ''; |
232 | - foreach ($line as $item) { | 220 | + foreach ($components as $component) { |
233 | - $item = explode('=', $item); | 221 | + $text_color = $component->text_color ?? 'white'; |
234 | - if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') { | 222 | + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc'; |
235 | - $time = $item[1]; | 223 | + $opacity = $component->opacity ? $component->opacity / 100 : 0.5; |
224 | + $font_file = $this->getAbsolutePath($component->font_file); | ||
225 | + $text_bg_box = $component->text_bg_box ?? 0; | ||
226 | + | ||
227 | + // 文字淡入淡出模式 | ||
228 | + if ($component->draw == 'fade'){ | ||
229 | + $contents = []; // | ||
230 | + switch ($component->name){ | ||
231 | + case 'one_poem': | ||
232 | + foreach ($this->adminMakeVideo->poem2->verses as $item) { | ||
233 | + if ($item->stanza != '') $contents[] = $item->stanza; | ||
234 | + } | ||
236 | break; | 235 | break; |
236 | + case 'one_poem_with_annotate': | ||
237 | + foreach ($this->adminMakeVideo->poem2->verses as $item) { | ||
238 | + if ($item->stanza != '') $contents[] = $item->stanza; | ||
239 | + if ($item->annotate != '') $contents[] = $item->annotate; | ||
237 | } | 240 | } |
241 | + break; | ||
242 | + case 'weather': | ||
243 | + $contents[] = $this->adminMakeVideo->weather; | ||
244 | + break; | ||
245 | + case 'date': | ||
246 | + $contents[] = Carbon::now()->format('Y年m月d日H时'); | ||
247 | + break; | ||
248 | + case 'feel': | ||
249 | + $contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。'; | ||
250 | + break; | ||
238 | } | 251 | } |
252 | + | ||
253 | + $FID = $FOD = floatval($component->fade_time / 1000); | ||
254 | + $round = round($this->media_info['format']['duration'] / count($contents),1); | ||
255 | + if ($round < 1) $round = 1; | ||
256 | + $sub_text = ''; | ||
257 | + foreach ($contents as $key => $content){ | ||
258 | + $DS = $key * $round; | ||
259 | + $DE = $DS + $round; | ||
260 | + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); | ||
261 | + file_put_contents($text_file, $content); | ||
262 | + $sub_text .= 'drawtext="'. | ||
263 | + 'fontfile=' . escapeshellarg($font_file) . ':' . | ||
264 | + 'textfile=' . escapeshellarg($text_file) . ':' . | ||
265 | + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . | ||
266 | + 'fontcolor_expr=' . escapeshellarg($text_color . '%{eif\\\\: clip(255*(1*between(t\\, ' . $DS . ' + ' . $FID . '\\, ' . $DE . ' - ' . $FOD . ') + ((t - ' . $DS . ')/' . $FID . ')*between(t\\, ' . $DS . '\\, ' . $DS . ' + ' . $FID . ') + (-(t - ' . $DE . ')/' . $FOD . ')*between(t\\, ' . $DE . ' - ' . $FOD . '\\, ' . $DE . '))\\, 0\\, 255) \\\\: x\\\\: 2 }') . ':' . | ||
267 | + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . | ||
268 | + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . | ||
269 | + '", '; | ||
239 | } | 270 | } |
271 | + | ||
272 | + $drawtext .= $sub_text; | ||
240 | } | 273 | } |
274 | + | ||
275 | + // 文字固定模式 | ||
276 | + if ($component->draw == 'fix'){ | ||
277 | + $contents = []; // | ||
278 | + switch ($component->name){ | ||
279 | + case 'one_poem_with_annotate': | ||
280 | + case 'one_poem': | ||
281 | + $stanzas = ''; | ||
282 | + foreach ($this->adminMakeVideo->poem2->verses as $item) { | ||
283 | + if ($item->stanza != '') $stanzas = $item->stanza . "\n"; | ||
241 | } | 284 | } |
242 | - // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。 | 285 | + $contents[] = $stanzas; |
243 | - fclose($pipes[1]); | 286 | + break; |
244 | - $exitedcode = proc_close($process); | 287 | + case 'weather': |
245 | - if ($exitedcode === 0) { | 288 | + $contents[] = $this->adminMakeVideo->weather; |
246 | - return $stdout; | 289 | + break; |
247 | - } else { | 290 | + case 'date': |
248 | - $error = trim($error0,"\n") . ' '. trim($error1,"\n"); | 291 | + $contents[] = Carbon::now()->format('Y年m月d日H时'); |
249 | - // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__); | 292 | + break; |
250 | - // ErrorUtil::triggerErrorMsg($error, $exitedcode); | 293 | + case 'feel': |
251 | - Log::error("cmd:{$cmd}"); | 294 | + $contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。'; |
252 | - Log::error($error); | 295 | + break; |
253 | - Log::error("stdout:{$stdout}"); | ||
254 | } | 296 | } |
255 | - } else { | 297 | + $sub_text = ''; |
256 | - // return ErrorUtil::triggerErrorMsg('proc_open error'); | 298 | + foreach ($contents as $key => $content){ |
257 | - Log::error('proc_open error'); | 299 | + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); |
300 | + file_put_contents($text_file, $content); | ||
301 | + $sub_text .= 'drawtext="'. | ||
302 | + 'fontfile=' . escapeshellarg($font_file) . ':' . | ||
303 | + 'textfile=' . escapeshellarg($text_file) . ':' . | ||
304 | + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . | ||
305 | + 'fontcolor=' . $text_color . '@' . $opacity . ':' . | ||
306 | + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . | ||
307 | + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . | ||
308 | + 'box=1:boxborderw='. $text_bg_box . ':' . | ||
309 | + 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; | ||
310 | + } | ||
311 | + $drawtext .= $sub_text; | ||
312 | + } | ||
313 | + } | ||
314 | + | ||
315 | + return rtrim($drawtext,', '); | ||
316 | + } | ||
317 | + | ||
318 | + public function calcFontSize($width) | ||
319 | + { | ||
320 | + return ceil($this->output_width / 360 * $width); | ||
258 | } | 321 | } |
322 | + | ||
323 | + /** | ||
324 | + * 获取输出临时文件名 | ||
325 | + * @param string $ext | ||
326 | + * @param string $dir | ||
327 | + * @return string | ||
328 | + */ | ||
329 | + public function getTempPath($ext = '.mp4',$dir = 'video') | ||
330 | + { | ||
331 | + $filename = "/output_" . time() . rand(0, 10000); | ||
332 | + | ||
333 | + $hash_hex = md5($filename); | ||
334 | + // 16进制表示的字符串一共32字节,表示16个二进制字节。 | ||
335 | + // 前16个字符用来第一级求摸,后16个用做第二级 | ||
336 | + $hash_hex_l1 = substr($hash_hex, 0, 8); | ||
337 | + $hash_hex_l2 = substr($hash_hex, 8, 8); | ||
338 | + $dir_l1 = hexdec($hash_hex_l1) % 256; | ||
339 | + $dir_l2 = hexdec($hash_hex_l2) % 512; | ||
340 | + $dir = $dir . '/' . $dir_l1 . '/' . $dir_l2; | ||
341 | + | ||
342 | + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir); | ||
343 | + | ||
344 | + return $dir . $filename . $ext; | ||
259 | } | 345 | } |
260 | } | 346 | } | ... | ... |
... | @@ -38,6 +38,11 @@ class AdminMakeVideo extends Model | ... | @@ -38,6 +38,11 @@ class AdminMakeVideo extends Model |
38 | return $this->hasOne(OnePoem::class,'id','poem_id'); | 38 | return $this->hasOne(OnePoem::class,'id','poem_id'); |
39 | } | 39 | } |
40 | 40 | ||
41 | + public function poem2() | ||
42 | + { | ||
43 | + return $this->hasOne(OnePoem2::class,'id','poem_id'); | ||
44 | + } | ||
45 | + | ||
41 | public function temp() | 46 | public function temp() |
42 | { | 47 | { |
43 | return $this->hasOne(VideoTemp::class,'id','temp_id'); | 48 | return $this->hasOne(VideoTemp::class,'id','temp_id'); | ... | ... |
... | @@ -34,7 +34,7 @@ class VideoTemp extends Model | ... | @@ -34,7 +34,7 @@ class VideoTemp extends Model |
34 | public function componentsTable() | 34 | public function componentsTable() |
35 | { | 35 | { |
36 | return $this->hasMany('App\Models\Component', 'temp_id') | 36 | return $this->hasMany('App\Models\Component', 'temp_id') |
37 | - ->select(['id', 'temp_id', 'name', 'position', 'font_size', 'text_color', 'text_bg_color', 'text_bg_box','opacity','fix_bounds']); | 37 | + ->select(['id', 'temp_id', 'name', 'position', 'font_size', 'text_color', 'text_bg_color', 'text_bg_box','opacity']); |
38 | } | 38 | } |
39 | 39 | ||
40 | public function admin_make_video() | 40 | public function admin_make_video() | ... | ... |
... | @@ -17,6 +17,7 @@ class UpdateComponentsTable extends Migration | ... | @@ -17,6 +17,7 @@ class UpdateComponentsTable extends Migration |
17 | 17 | ||
18 | Schema::table('components', function (Blueprint $table) { | 18 | Schema::table('components', function (Blueprint $table) { |
19 | $table->string('draw')->after('position')->comment('文字效果'); | 19 | $table->string('draw')->after('position')->comment('文字效果'); |
20 | + $table->integer('fade_time')->after('draw')->default(1500)->comment('fade切换时间(毫秒)'); | ||
20 | }); | 21 | }); |
21 | } | 22 | } |
22 | 23 | ||
... | @@ -27,6 +28,8 @@ class UpdateComponentsTable extends Migration | ... | @@ -27,6 +28,8 @@ class UpdateComponentsTable extends Migration |
27 | */ | 28 | */ |
28 | public function down() | 29 | public function down() |
29 | { | 30 | { |
31 | + Schema::dropColumns('components', ['draw', 'fade_time']); | ||
32 | + | ||
30 | Schema::table('components', function (Blueprint $table) { | 33 | Schema::table('components', function (Blueprint $table) { |
31 | $table->string('fix_bounds')->after('opacity')->comment('超出避免剪切'); | 34 | $table->string('fix_bounds')->after('opacity')->comment('超出避免剪切'); |
32 | }); | 35 | }); | ... | ... |
-
Please register or login to post a comment