李帅

1.重构一言表

...@@ -115,7 +115,6 @@ class VideoTempController extends AdminController ...@@ -115,7 +115,6 @@ class VideoTempController extends AdminController
115 $form->hasMany('components','组件', function (Form\NestedForm $form) { 115 $form->hasMany('components','组件', function (Form\NestedForm $form) {
116 $form->select('name','组件名称')->options([ 116 $form->select('name','组件名称')->options([
117 'one_poem_with_annotate' => '一言带注解组件', 117 'one_poem_with_annotate' => '一言带注解组件',
118 - 'every_poem' => '每日一言组件',
119 'one_poem' => '一言组件', 118 'one_poem' => '一言组件',
120 'weather' => '天气组件', 119 'weather' => '天气组件',
121 'date' => '日期组件', 120 'date' => '日期组件',
...@@ -123,22 +122,30 @@ class VideoTempController extends AdminController ...@@ -123,22 +122,30 @@ class VideoTempController extends AdminController
123 ]); 122 ]);
124 $form->select('position','组件位置')->options(VideoTemp::POSITION_OPTIONS); 123 $form->select('position','组件位置')->options(VideoTemp::POSITION_OPTIONS);
125 124
126 - $form->switch('fade', '淡入淡出')->help("开启淡入淡出会使背景色失效"); 125 + $form->radio('draw', '文字效果')
127 - 126 + ->options(['fade'=>'淡入淡出', 'fix'=>'固定显示'])->default('fade')
128 - $form->number('text_bg_box', '背景厚度')->default(0) 127 + ->when('fade',function (Form\NestedForm $form){
129 - ->addElementClass('text_bg_box')->help('设置背景块边缘厚度(用于在背景块边缘用背景色填充一圈),默认为0'); 128 + $form->selectTable('font_file','字体')
130 - $form->color('text_bg_color', '背景色')->default('#5c6bc6')->addElementClass('text_bg_color'); 129 + ->title('字体选择')
131 - $form->selectTable('font_file','字体') 130 + ->from(FontTable::make())
132 - ->title('字体选择') 131 + ->model(Font::class,'file','name');
133 - ->from(FontTable::make()) 132 + $form->number('font_size', '字号')->default(12)->min(12);
134 - ->model(Font::class,'file','name'); 133 + $form->color('text_color', '字体颜色')->default('#f5f5f5')->addElementClass('text_color');
135 - $form->number('font_size', '字号')->default(12)->min(12); 134 + })
136 - $form->color('text_color', '字体颜色')->default('#f5f5f5')->addElementClass('text_color'); 135 + ->when('fix',function (Form\NestedForm $form){
137 - $form->number('opacity', '透明度')->min(0)->max(100) 136 + $form->number('text_bg_box', '背景厚度')->default(0)
138 - ->addElementClass('opacity')->default(100) 137 + ->addElementClass('text_bg_box')->help('设置背景块边缘厚度(用于在背景块边缘用背景色填充一圈),默认为0');
139 - ->help('范围为0-100,100表示不透明,0表示完全透明'); 138 + $form->color('text_bg_color', '背景色')->default('#5c6bc6')->addElementClass('text_bg_color');
140 - $form->switch('fix_bounds', '避免剪切'); 139 + $form->selectTable('font_file','字体')
141 - 140 + ->title('字体选择')
141 + ->from(FontTable::make())
142 + ->model(Font::class,'file','name');
143 + $form->number('font_size', '字号')->default(12)->min(12);
144 + $form->color('text_color', '字体颜色')->default('#f5f5f5')->addElementClass('text_color');
145 + $form->number('opacity', '透明度')->min(0)->max(100)
146 + ->addElementClass('opacity')->default(100)
147 + ->help('范围为0-100,100表示不透明,0表示完全透明');
148 + });
142 }); 149 });
143 150
144 $form->hidden('state')->default(1) 151 $form->hidden('state')->default(1)
......
...@@ -26,9 +26,11 @@ class MakeVideo implements ShouldQueue ...@@ -26,9 +26,11 @@ class MakeVideo implements ShouldQueue
26 26
27 protected $ffprobe; 27 protected $ffprobe;
28 28
29 - protected $ffplay; 29 + protected $media_info;
30 30
31 - protected $width; 31 + protected $output_width;
32 +
33 + protected $output_height;
32 34
33 /** 35 /**
34 * Create a new job instance. 36 * Create a new job instance.
...@@ -38,403 +40,172 @@ class MakeVideo implements ShouldQueue ...@@ -38,403 +40,172 @@ class MakeVideo implements ShouldQueue
38 public function __construct(AdminMakeVideo $adminMakeVideo) 40 public function __construct(AdminMakeVideo $adminMakeVideo)
39 { 41 {
40 $this->adminMakeVideo = $adminMakeVideo; 42 $this->adminMakeVideo = $adminMakeVideo;
41 -
42 $this->ffmpeg = env('FFMPEG_CMD'); 43 $this->ffmpeg = env('FFMPEG_CMD');
43 $this->ffprobe = env('FFPROBE_CMD'); 44 $this->ffprobe = env('FFPROBE_CMD');
44 - $this->ffplay = env('FFPLAY_CMD'); 45 + $this->output_width = 720;
45 - } 46 + $this->output_height = 1280;
46 - 47 +
47 - /** 48 + $file = $this->getAbsolutePath($adminMakeVideo->video_url);
48 - * Execute the job. 49 + // 分析视频
49 - * 50 + $media_info = $this->mediainfo($file);
50 - * @return void 51 + // 素材准备
51 - */ 52 + $drawtext = $this->getTextContentString();
52 - public function handle() 53 +
53 - { 54 + if ($media_info['format']['nb_streams'] >= 2) {
54 - $adminMakeVideo = $this->adminMakeVideo; 55 + /** 音频视频轨都有 */
55 - $file = Storage::disk('public')->path($adminMakeVideo->video_url); 56 + if ($is_bgm) {
56 - $is_bgm = $adminMakeVideo->bg_music;
57 - $bgm = Storage::disk('public')->path($adminMakeVideo->bgm_url);
58 -
59 - // 1.getmediainfo 记录时长,音频视频取最长。
60 - $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
61 - $output = $this->execmd($cmd);
62 - $media_info = json_decode($output, true);
63 - if (json_last_error() === JSON_ERROR_UTF8) {
64 - $output = mb_convert_encoding($output, "UTF-8");
65 - $media_info = json_decode($output, true);
66 - }
67 -
68 - /** 记录媒体信息时长*/
69 - $media_file_time_length = isset($media_info['format']['duration']) ? $media_info['format']['duration'] : 0;
70 - if ($media_info['streams'][0]['codec_type'] !== 'video') {
71 - Log::channel('daily')->error('视频没有video track');
72 - return;
73 - }
74 -
75 - // 2. 判断是否有视频原音,没有原音用背景音,没有背景音则混入anullsrc
76 - if ( $media_info['format']['nb_streams'] >= 2 ){ /** 音频视频轨都有 */
77 - if ($is_bgm){
78 // 有背景音 融合 57 // 有背景音 融合
79 - $audio = $this->getTempPath('.mp3'); 58 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
80 - $cmd = $this->ffmpeg. 59 + $cmd = $this->ffmpeg .
81 - ' -y -i ' . escapeshellarg($file). 60 + ' -y -i ' . escapeshellarg($file) .
82 - ' -y -i ' . escapeshellarg($bgm). 61 + ' -y -i ' . escapeshellarg($bgm) .
83 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . 62 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
84 '-ar 48000 -ab 64k ' . escapeshellarg($audio); 63 '-ar 48000 -ab 64k ' . escapeshellarg($audio);
85 if (!$this->execmd($cmd)) return; 64 if (!$this->execmd($cmd)) return;
86 65
87 $audio_input = ' -i ' . escapeshellarg($audio); 66 $audio_input = ' -i ' . escapeshellarg($audio);
88 - $audio_filter = '[3:a]'; 67 + $audio_filter = '2:a';
89 - }else{ 68 + } else {
90 // 没有背景音 69 // 没有背景音
91 $audio_input = ''; 70 $audio_input = '';
92 - $audio_filter = '[0:1]'; 71 + $audio_filter = '0:a';
93 } 72 }
94 - }elseif ( $media_info['format']['nb_streams'] == 1 ){ 73 + } elseif ($media_info['format']['nb_streams'] == 1) {
95 - $audio = $this->getTempPath('.mp3'); 74 + /** 只有视频轨 */
75 + // 生成一段无声音频
76 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
96 $cmd = $this->ffmpeg . 77 $cmd = $this->ffmpeg .
97 - ' -y -f lavfi -i aevalsrc=0:duration='. escapeshellarg($media_file_time_length) . 78 + ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) .
98 ' -ar 48000 -ab 64k ' . escapeshellarg($audio); 79 ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
99 if (!$this->execmd($cmd)) return; 80 if (!$this->execmd($cmd)) return;
100 81
101 - if ($is_bgm){ 82 + if ($is_bgm) {
83 + // 有背景音 融合
102 $audio_empty = $audio; 84 $audio_empty = $audio;
103 - $audio = $this->getTempPath('.mp3'); 85 + $audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
104 - $cmd = $this->ffmpeg. 86 + $cmd = $this->ffmpeg .
105 - ' -y -i ' . escapeshellarg($audio_empty). 87 + ' -y -i ' . escapeshellarg($audio_empty) .
106 - ' -y -i ' . escapeshellarg($bgm). 88 + ' -y -i ' . escapeshellarg($bgm) .
107 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . 89 ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
108 '-ar 48000 -ab 64k ' . escapeshellarg($audio); 90 '-ar 48000 -ab 64k ' . escapeshellarg($audio);
109 if (!$this->execmd($cmd)) return; 91 if (!$this->execmd($cmd)) return;
110 } 92 }
111 $audio_input = ' -i ' . escapeshellarg($audio); 93 $audio_input = ' -i ' . escapeshellarg($audio);
112 - $audio_filter = '[3:a]'; 94 + $audio_filter = '2:a';
113 - 95 + } else {
114 - }else{ /** 音频视频轨都没有 */ 96 + /** 音频视频轨都没有 */
115 - Log::channel('daily')->error('视频没有video track'); 97 + Log::channel('daily')->error('视频没有video track, url:' . $file);
116 return; 98 return;
117 } 99 }
118 100
119 - if ($this->adminMakeVideo->thumbnail == 2){ 101 + $thumbnail = $this->getTempPath('.jpg','thumbnail');
102 + if ($adminMakeVideo->thumbnail == 2){
120 // 截取中间帧作为视频封面 103 // 截取中间帧作为视频封面
121 $frame = ceil($media_info['streams'][0]['nb_frames'] / 2); 104 $frame = ceil($media_info['streams'][0]['nb_frames'] / 2);
122 - $thumbnail = $this->getTempPath('.jpg',false);
123 $cmd = $this->ffmpeg . ' -y ' . 105 $cmd = $this->ffmpeg . ' -y ' .
124 ' -i ' . escapeshellarg($file) . 106 ' -i ' . escapeshellarg($file) .
125 ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' . 107 ' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' .
126 ' -map [img]'. 108 ' -map [img]'.
127 - ' -frames:v 1 -s 720x1280 -preset superfast '. 109 + ' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
128 - escapeshellarg($thumbnail); 110 + escapeshellarg($this->getAbsolutePath($thumbnail));
129 if (!$this->execmd($cmd)) return ; 111 if (!$this->execmd($cmd)) return ;
130 }else{ 112 }else{
131 - $thumbnail = $adminMakeVideo->thumbnail_url; 113 + // 手动上传封面
114 + $origin_thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url);
115 + // 将封面分辨率改为指定分辨率
116 + $cmd = $this->ffmpeg . ' -y ' .
117 + ' -i ' . escapeshellarg($origin_thumbnail) .
118 + '-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
119 + escapeshellarg($this->getAbsolutePath($thumbnail));
120 + if (!$this->execmd($cmd)) return ;
132 } 121 }
133 122
134 - $end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png"; 123 + $output = $this->getTempPath('.mp4','video');
135 - $avatar = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png";
136 - $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
137 - $signature = "一言 · 官方出品";
138 -
139 - // 生成贴纸和签名
140 - $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $avatar, $signature, $font);
141 -
142 - // 截取最后一帧
143 - $last_frame_video = $this->getTempPath();
144 - $this->width = $width = $media_info['streams'][0]['width'];
145 - $height = $media_info['streams'][0]['height'];
146 - $size = $width . 'x' . $height;
147 - $time_length = 0.7;
148 - $r = 24;
149 - $frame_n = $media_info['streams'][0]['nb_frames'] - 2;
150 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
151 - " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
152 - " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
153 - ' -map [v] -map 2:a ' . escapeshellarg($last_frame_video);
154 - if (!$this->execmd($cmd)) return;
155 -
156 -
157 - $signature_x = 0;
158 - $signature_y = -20;
159 - $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
160 -
161 - $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
162 -
163 - $video = $this->getTempPath('.mp4',false);
164 $cmd = $this->ffmpeg . ' -y '. 124 $cmd = $this->ffmpeg . ' -y '.
165 ' -i ' . escapeshellarg($file). 125 ' -i ' . escapeshellarg($file).
166 - ' -i ' . escapeshellarg($animate).
167 ' -i ' . escapeshellarg($watermark). 126 ' -i ' . escapeshellarg($watermark).
168 $audio_input . 127 $audio_input .
169 - ' -filter_complex "[0:0] ' . 128 + ' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
170 - $this->getTextContentString().
171 ' [text];[text]'. 129 ' [text];[text]'.
172 - ' [2:v]overlay=20:20[water];[water]' . $audio_filter . '[1:0][1:1] concat=n=2:v=1:a=1[v][a]" ' . 130 + ' [1:v]overlay=20:20[v]" ' .
173 - ' -map [v] -map [a]'. 131 + ' -map [v] -map '. $audio_filter .
174 ' -c:v libx264 -bt 256k -r 25' . 132 ' -c:v libx264 -bt 256k -r 25' .
175 ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' . 133 ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
176 - escapeshellarg($video); 134 + escapeshellarg($this->getAbsolutePath($output));
177 - 135 +
178 - $exec = $this->execmd($cmd); 136 + if (!$this->execmd($cmd)) return ;
179 - if (is_array($exec)){ 137 +
180 - print_r($exec,1); 138 + $video_info = $this->mediainfo($this->getAbsolutePath($output));
181 - return; 139 + Immerse::query()->create([
182 - } 140 + 'user_id' => 1,
183 - 141 + 'title' => '',
184 - try{ 142 + 'weather' => $adminMakeVideo->weather,
185 - // 全部合成以后创建 临境 143 + 'huangli' => $adminMakeVideo->huangli,
186 - $video_info = $this->mediainfo($video); 144 + 'content' => $adminMakeVideo->feel,
187 - 145 + 'location' => $adminMakeVideo->location,
188 - Immerse::query()->create([ 146 + 'longitude' => $adminMakeVideo->longitude,
189 - 'user_id' => 1, 147 + 'latitude' => $adminMakeVideo->latitude,
190 - 'title' => '', 148 + 'url' => $output,
191 - 'content' => $this->adminMakeVideo->feel, 149 + 'type' => $adminMakeVideo->type == 1 ? 2 : 1,
192 - 'url' => str_replace(Storage::disk('public')->path(''),'',$video), 150 + 'upload_file' => '',
193 - 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1, 151 + 'duration' => $video_info['format']['duration'],
194 - 'upload_file' => '', 152 + 'size' => $video_info['format']['size'],
195 - 'duration' => $video_info['format']['duration'], 153 + 'origin_video_url' => $this->adminMakeVideo->video_url,
196 - 'size' => $video_info['format']['size'], 154 + 'origin_image_url' => '',
197 - 'origin_video_url' => $this->adminMakeVideo->video_url, 155 + 'poem_id' => $this->adminMakeVideo->poem_id,
198 - 'origin_image_url' => '', 156 + 'temp_id' => $this->adminMakeVideo->temp_id,
199 - 'poem_id' => $this->adminMakeVideo->poem_id, 157 + 'thumbnail' => $thumbnail,
200 - 'temp_id' => $this->adminMakeVideo->temp_id, 158 + 'state' => 1,
201 - 'thumbnail' => str_replace(Storage::disk('public')->path(''), '', $thumbnail), 159 + 'bgm' => $is_bgm ? $bgm : '',
202 - 'state' => 1, 160 + ]);
203 - 'bgm' => $this->adminMakeVideo->bgm_url ?? '',
204 - ]);
205 -
206 - }catch (\Exception $exception){
207 -// echo $exception->getMessage();
208 - }
209 -
210 } 161 }
211 162
212 /** 163 /**
213 - * 获取圆形头像 164 + * Execute the job.
214 - * @param $img 165 + *
215 - * @param int $dst_w 166 + * @return void
216 - * @param int $dst_h
217 - * @return resource
218 */ 167 */
219 - public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96) 168 + public function handle()
220 { 169 {
221 - $w = 130; 170 + $file = $this->getAbsolutePath($this->adminMakeVideo->video_url);
222 - $h = 130; 171 + // 分析视频
223 - $src = imagecreatetruecolor($dst_w, $dst_h); 172 + $this->media_info = $this->mediaInfo($file);
224 - imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
225 173
226 - $newpic = imagecreatetruecolor($dst_w, $dst_h); 174 + // 准备素材
227 - imagealphablending($newpic, false);
228 - imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
229 - $mask = imagecreatetruecolor($dst_w, $dst_h);
230 - $transparent = imagecolorallocate($mask, 255, 0, 0);
231 - imagecolortransparent($mask,$transparent);
232 - imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
233 - $red = imagecolorallocate($mask, 0, 0, 0);
234 - imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
235 - imagecolortransparent($newpic,$red);
236 - imagesavealpha($newpic,true);
237 - imagefill($newpic, 0, 0, $red);
238 - imagedestroy($mask);
239 - return $newpic;
240 - }
241 175
242 - /** 176 + // 组装文字参数
243 - * 制作最后一帧
244 - * @param $file
245 - * @return bool|string
246 - */
247 - public function makeLastFrameVideo($file) {
248 - $video = $this->getTempPath();
249 - $width = $this->getVideoWith($file);
250 - $height = $this->getVideoHeight($file);
251 - $size = $width . 'x' . $height;
252 - $time_length = 0.7;
253 - $r = 24;
254 - $frame_n = $this->getVideoFrameNum($file) - 2;
255 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
256 - " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
257 - " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
258 - ' -map [v] -map 2:a ' . escapeshellarg($video);
259 - if ($this->execmd($cmd)) {
260 - return $video;
261 - } else {
262 - return false;
263 - }
264 - }
265 -
266 - /**
267 - * 用最后一帧和贴纸制作动画
268 - * @param $last_frame_video
269 - * @param $end_wallpaper
270 - * @param $signature
271 - * @param $signature_x
272 - * @param $signature_y
273 - * @param $font
274 - * @return bool|string
275 - */
276 - public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
277 - $signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
278 - $signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
279 - $video = $this->getTempPath();
280 - if ($signature !== '') {
281 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
282 - ' -t 0.7 -loop 1 -i ' . escapeshellarg($end_wallpaper) .
283 - ' -filter_complex "'.
284 - 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
285 - '[0:v]boxblur=8[blur];'.
286 - '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
287 - 'drawtext='.
288 - 'fontfile=' . escapeshellarg($font) . ':'.
289 - 'text=' . escapeshellarg($signature) . ':'.
290 - 'fontsize=23:'.
291 - 'fontcolor=white@1.0:'.
292 - 'x=main_w/2' . $signature_x . ':'.
293 - 'y=main_h/2' . $signature_y . '[text];[text]'.
294 - '[grad]alphamerge[alpha];'.
295 - '[0:v][alpha]overlay'.
296 - '" ' . escapeshellarg($video);
297 - } else {
298 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
299 - ' -t 0.7 -loop 1 -i ' . escapeshellarg($end_wallpaper) .
300 - ' -filter_complex "'.
301 - 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
302 - '[0:v]boxblur=8[blur];'.
303 - '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
304 - '[lay][grad]alphamerge[alpha];'.
305 - '[0:v][alpha]overlay'.
306 - '" ' . escapeshellarg($video);
307 - }
308 - if ($this->execmd($cmd)) {
309 - return $video;
310 - } else {
311 - return false;
312 - }
313 - }
314 177
315 - /** 178 + // 合成视频
316 - * 获取视频宽度
317 - * @param $file
318 - * @param bool $cache
319 - * @return int|null
320 - */
321 - public function getVideoWith($file, $cache = true) {
322 - $result = $this->getFirstVideoTrackOption($file, $option = 'width', $cache);
323 - if ($result) {
324 - return (int)$result;
325 - } else {
326 - return $result;
327 - }
328 - }
329 179
330 - /** 180 + // 制作封面图
331 - * 获取视频高度
332 - * @param $file
333 - * @param bool $cache
334 - * @return int|null
335 - */
336 - public function getVideoHeight($file, $cache = true) {
337 - $result = $this->getFirstVideoTrackOption($file, $option = 'height', $cache);
338 - if ($result) {
339 - return (int)$result;
340 - } else {
341 - return $result;
342 - }
343 - }
344 181
345 - /** 182 + // 分析视频 入库
346 - * 获取视频帧数
347 - * @param $file
348 - * @param bool $cache
349 - * @return null
350 - */
351 - public function getVideoFrameNum($file, $cache = true) {
352 - return $this->getFirstVideoTrackOption($file, $option = 'nb_frames', $cache);
353 } 183 }
354 184
185 + public function getAbsolutePath($path)
186 + {
187 + if ($path == '') return '';
355 188
356 - public function getFirstVideoTrackOption($file, $option, $cache = true) { 189 + return Storage::disk('public')->path($path);
357 - return $this->getFirstTrackOption($file, $option, $codec_type = 'video', $cache = true);
358 } 190 }
359 191
360 - public function getFirstTrackOption($file, $option, $codec_type = '', $cache = true) { 192 + public function mediaInfo($file)
361 - $result = $this->mediainfo($file, $cache); 193 + {
362 - if (!isset($result['streams'])) { 194 + if ($this->media_info) return $this->media_info;
363 - return null;
364 - }
365 - $_track = null;
366 - foreach($result['streams'] as $track) {
367 - if (empty($codec_type)) {
368 - $_track = $track;
369 - break;
370 - } elseif ($track['codec_type'] == $codec_type) {
371 - $_track = $track;
372 - break;
373 - }
374 - }
375 - if (isset($_track[$option])) {
376 - return $_track[$option];
377 - }
378 - return null;
379 - }
380 195
381 - /***
382 - * 获取视频信息(配合ffprobe)
383 - * @param $file
384 - * @param bool $cache
385 - * @return mixed
386 - */
387 - public function mediainfo($file, $cache = true) {
388 - global $_mediainfo;
389 $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file); 196 $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
390 - if ($cache && isset($_mediainfo[$file])) {
391 - return $_mediainfo[$file];
392 - }
393 $output = $this->execmd($cmd); 197 $output = $this->execmd($cmd);
394 $data = json_decode($output, true); 198 $data = json_decode($output, true);
395 if (json_last_error() === JSON_ERROR_UTF8) { 199 if (json_last_error() === JSON_ERROR_UTF8) {
396 $output = mb_convert_encoding($output, "UTF-8"); 200 $output = mb_convert_encoding($output, "UTF-8");
397 $data = json_decode($output, true); 201 $data = json_decode($output, true);
398 } 202 }
399 - if ($cache) { 203 + $this->media_info = $data;
400 - $mediainfo[$file] = $data;
401 - }
402 return $data; 204 return $data;
403 } 205 }
404 206
405 - /**
406 - * 获取输出临时文件名
407 - * @param string $ext
408 - * @param bool $is_temp
409 - * @return string
410 - */
411 - public function getTempPath($ext = '.mp4',$is_temp = true)
412 - {
413 - $filename = "/output_" . time() . rand(0, 10000);
414 -
415 - $prefix = $is_temp ? 'temp/' : 'video/';
416 - $hash_hex = md5($filename);
417 - // 16进制表示的字符串一共32字节,表示16个二进制字节。
418 - // 前16个字符用来第一级求摸,后16个用做第二级
419 - $hash_hex_l1 = substr($hash_hex, 0, 8);
420 - $hash_hex_l2 = substr($hash_hex, 8, 8);
421 - $dir_l1 = hexdec($hash_hex_l1) % 256;
422 - $dir_l2 = hexdec($hash_hex_l2) % 512;
423 - $dir = $prefix . $dir_l1 . '/' . $dir_l2;
424 -
425 - if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
426 -
427 - return Storage::disk('public')->path($dir . $filename . $ext);
428 - }
429 -
430 - /**
431 - * 执行命令
432 - * @param $cmd
433 - * @param bool $update_progress
434 - * @return mixed
435 - */
436 public function execmd($cmd, $update_progress = false) { 207 public function execmd($cmd, $update_progress = false) {
437 -// echo $cmd . "\n". "\n". "\n"; 208 + echo $cmd . "\n". "\n". "\n";
438 $descriptorspec = array( 209 $descriptorspec = array(
439 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据 210 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
440 ); 211 );
...@@ -475,237 +246,15 @@ class MakeVideo implements ShouldQueue ...@@ -475,237 +246,15 @@ class MakeVideo implements ShouldQueue
475 return $stdout; 246 return $stdout;
476 } else { 247 } else {
477 $error = trim($error0,"\n") . ' '. trim($error1,"\n"); 248 $error = trim($error0,"\n") . ' '. trim($error1,"\n");
478 - Log::channel('daily')->error(print_r(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"),1)); 249 + // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
479 - return [$error,$exitedcode]; 250 + // ErrorUtil::triggerErrorMsg($error, $exitedcode);
480 - } 251 + Log::error("cmd:{$cmd}");
481 - } else { 252 + Log::error($error);
482 - Log::channel('daily')->error('proc_open error'); 253 + Log::error("stdout:{$stdout}");
483 - }
484 - }
485 -
486 - /**
487 - * 贴纸和签名
488 - * @param $end_wallpaper
489 - * @param $thumbnail
490 - * @param $signature
491 - * @param $font
492 - * @return string
493 - */
494 - public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
495 - $_imagetype = $this->getImageType($thumbnail);
496 - $_img = null;
497 - switch ($_imagetype) {
498 - case 'gif':
499 - if (function_exists('imagecreatefromgif')) {
500 - $_img = imagecreatefromgif($thumbnail);
501 - }
502 - break;
503 - case 'jpg':
504 - case 'jpeg':
505 - $_img = imagecreatefromjpeg($thumbnail);
506 - break;
507 - case 'png':
508 - $_img = imagecreatefrompng($thumbnail);
509 - break;
510 - default:
511 - $_img = imagecreatefromstring($thumbnail);
512 - break;
513 - }
514 - $width = 130;
515 - $height = 130;
516 - $_width = 130;
517 - $_height = 130;
518 - if(is_resource($_img)){
519 - $_width = imagesx($_img);
520 - $_height = imagesy($_img);
521 - }
522 -
523 - $bite = $_width / $_height;
524 -
525 - if($_width > $_height){
526 - if($_width > $width){
527 - $height = round($width / $bite);
528 - }
529 - }else{
530 - if($_height > $height){
531 - $width = round($height * $bite);
532 } 254 }
533 - }
534 -
535 - $tmpimg = imagecreatetruecolor($width,$height);
536 - if(function_exists('imagecopyresampled')) {
537 - imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
538 - } else {
539 - imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
540 - }
541 - if(is_resource($_img)) imagedestroy($_img);
542 - $_img = $this->getCircleAvatar($tmpimg);
543 - if(is_resource($tmpimg)) imagedestroy($tmpimg);
544 -
545 - $wp = $this->imagesMerge($end_wallpaper, $_img);
546 -// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
547 - $white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
548 - imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
549 -
550 -// $dst = "./output_new_end_wallpaper.png";
551 - $dst = Storage::disk('public')->path('ffmpeg') . "/output_new_end_wallpaper.png";
552 - imagepng($wp, $dst);
553 - if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
554 - if(is_resource($_img)) imagedestroy($_img);
555 -
556 - return $dst;
557 - }
558 -
559 - /**
560 - * 获取图像文件类型
561 - * @param $img_name
562 - * @return string
563 - */
564 - public function getImageType($img_name)
565 - {
566 - if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
567 - $type = strtolower($matches[1]);
568 - }else{
569 - $type = "string";
570 - }
571 - return $type;
572 - }
573 -
574 - /**
575 - * 多图融合
576 - * @param $end_wallpaper
577 - * @param $thumbnail
578 - * @return resource
579 - */
580 - public function imagesMerge($end_wallpaper, $thumbnail) {
581 - $end_wallpaper = imagecreatefrompng($end_wallpaper);
582 - $background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
583 - imagesavealpha($background,true);
584 - $temp_wallpaper = imagecreatetruecolor(350, 204);
585 - $color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
586 -// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
587 - imagefill($temp_wallpaper, 0, 0, $color);
588 - imageColorTransparent($temp_wallpaper, $color);
589 - imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
590 - imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
591 - imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
592 - return $background;
593 - }
594 -
595 - /**
596 - * logo 大小转换
597 - * @param $logo
598 - * @return bool
599 - */
600 - public function translateLogo($logo)
601 - {
602 - $image = Storage::disk('public')->path('ffmpeg/output_150x150.jpg');
603 - $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($logo) .
604 - ' -vf scale=150:150 ' . escapeshellarg($image);
605 - if ($this->execmd($cmd)) {
606 - return $image;
607 } else { 255 } else {
608 - return false; 256 + // return ErrorUtil::triggerErrorMsg('proc_open error');
609 - } 257 + Log::error('proc_open error');
610 - }
611 -
612 - public function getTextContentString()
613 - {
614 - $components = $this->adminMakeVideo->temp()->first()->components()->get();
615 -
616 - $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
617 -
618 - $drawtext = '';
619 -
620 - foreach ($components as $component) {
621 - switch ($component->name){
622 - case 'one_poem':
623 - $content = $this->adminMakeVideo->poem->content;
624 - $text_file = $this->getTempPath('.txt');
625 - file_put_contents($text_file, $content);
626 -
627 - $text_color = $component->text_color ?? 'white';
628 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
629 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
630 -
631 - $drawtext .= 'drawtext="'.
632 - 'fontfile=' . escapeshellarg($font) . ':' .
633 - 'textfile=' . escapeshellarg($text_file) . ':' .
634 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
635 - 'fontcolor=' . $text_color . '@1.0:' .
636 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
637 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
638 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
639 -
640 - break;
641 - case 'every_poem':
642 - break;
643 - case 'weather':
644 - $content = '多云';
645 - $text_color = $component->text_color ?? 'white';
646 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
647 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
648 -
649 - $drawtext .= 'drawtext="'.
650 - 'fontfile=' . escapeshellarg($font) . ':' .
651 - 'text=' . escapeshellarg($content) . ':' .
652 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
653 - 'fontcolor=' . $text_color . '@1.0:' .
654 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
655 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
656 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
657 -
658 - break;
659 - case 'date':
660 - $content = Carbon::now()->format('Y年m月d日H时');
661 - $text_color = $component->text_color ?? 'white';
662 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
663 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
664 -
665 - $drawtext .= 'drawtext="'.
666 - 'fontfile=' . escapeshellarg($font) . ':' .
667 - 'text=' . escapeshellarg($content) . ':' .
668 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
669 - 'fontcolor=' . $text_color . '@1.0:' .
670 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
671 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
672 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
673 - break;
674 - case 'feel':
675 - $content = $this->adminMakeVideo->feel;
676 - $text_color = $component->text_color ?? 'white';
677 - $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
678 - $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
679 -
680 - $drawtext .= 'drawtext="'.
681 - 'fontfile=' . escapeshellarg($font) . ':' .
682 - 'text=' . escapeshellarg($content) . ':' .
683 - 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
684 - 'fontcolor=' . $text_color . '@1.0:' .
685 - 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
686 - 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
687 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
688 - break;
689 - }
690 } 258 }
691 -
692 - return rtrim($drawtext,', ');
693 - }
694 -
695 - /**
696 - * @param $width
697 - * @param $content
698 - * @return float
699 - */
700 - public function calcFontSize($width, $content)
701 - {
702 - $max_len = 1;
703 - foreach (explode("\n",$content) as $item){
704 - if (mb_strlen($item) > $max_len){
705 - $max_len = mb_strlen($item);
706 - }
707 - }
708 -
709 - return ceil($this->width * $width / 10 / $max_len);
710 } 259 }
711 } 260 }
......
1 +<?php
2 +
3 +use Illuminate\Database\Migrations\Migration;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Support\Facades\Schema;
6 +
7 +class UpdateComponentsTable extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::dropColumns('components', ['fix_bounds']);
17 +
18 + Schema::table('components', function (Blueprint $table) {
19 + $table->string('draw')->after('position')->comment('文字效果');
20 + });
21 + }
22 +
23 + /**
24 + * Reverse the migrations.
25 + *
26 + * @return void
27 + */
28 + public function down()
29 + {
30 + Schema::table('components', function (Blueprint $table) {
31 + $table->string('fix_bounds')->after('opacity')->comment('超出避免剪切');
32 + });
33 + }
34 +}