李帅

1.优化一言添加

...@@ -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 });
......