李帅

1.首页接口实现。

2.支持用户端上传。
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 namespace App\Http\Controllers\V1; 3 namespace App\Http\Controllers\V1;
4 4
5 use App\Http\Controllers\Controller; 5 use App\Http\Controllers\Controller;
6 +use App\Models\Immerse;
6 use App\Models\PackPoem; 7 use App\Models\PackPoem;
7 use Illuminate\Http\Request; 8 use Illuminate\Http\Request;
8 use Illuminate\Support\Facades\Validator; 9 use Illuminate\Support\Facades\Validator;
...@@ -17,8 +18,11 @@ class HomeController extends Controller ...@@ -17,8 +18,11 @@ class HomeController extends Controller
17 */ 18 */
18 public function index() 19 public function index()
19 { 20 {
20 - // 21 + // admin video
21 - return Response::success(); 22 +
23 +
24 + // user video
25 + return Response::success(Immerse::query()->get());
22 } 26 }
23 27
24 28
......
...@@ -3,7 +3,12 @@ ...@@ -3,7 +3,12 @@
3 namespace App\Http\Controllers\V1; 3 namespace App\Http\Controllers\V1;
4 4
5 use App\Http\Controllers\Controller; 5 use App\Http\Controllers\Controller;
6 +use App\Models\Immerse;
7 +use App\Models\UserMakeVideo;
8 +use App\Jobs\UserMakeVideo as MakeVideo;
6 use Illuminate\Http\Request; 9 use Illuminate\Http\Request;
10 +use Illuminate\Support\Facades\Validator;
11 +use Jiannei\Response\Laravel\Support\Facades\Response;
7 12
8 class ImmerseController extends Controller 13 class ImmerseController extends Controller
9 { 14 {
...@@ -21,16 +26,46 @@ class ImmerseController extends Controller ...@@ -21,16 +26,46 @@ class ImmerseController extends Controller
21 * Store a newly created resource in storage. 26 * Store a newly created resource in storage.
22 * 27 *
23 * @param \Illuminate\Http\Request $request 28 * @param \Illuminate\Http\Request $request
24 - * @return \Illuminate\Http\Response 29 + * @throws \Illuminate\Validation\ValidationException
30 + * @return \Illuminate\Http\JsonResponse
25 */ 31 */
26 public function store(Request $request) 32 public function store(Request $request)
27 { 33 {
28 - //todo 发布流程 34 + $validator = Validator::make($request->all(),[
29 - // if type == 1 35 + 'video_url' => 'required|string',
30 - // 写入audio-show表,写入immerse表,发送异步转码任务, 36 + 'video_id' => 'required',
31 - // if type == 2 37 + 'content' => 'sometimes',
32 - // 写入video-show表,写入immerse表,发送异步转码合成视频水印任务 38 + 'weather' => 'sometimes',
39 + 'thumbnail_url' => 'sometimes',
40 + ]);
41 +
42 + if ($validator->fails()){
43 + return Response::fail('',500,$validator->errors());
44 + }
45 +
46 + $validated = $validator->validated();
47 +
48 +
49 + $immerse = Immerse::query()->find($request->video_id);
50 +
51 + $video = UserMakeVideo::query()->create([
52 + 'poem_id' => $immerse->poem_id,
53 + 'type' => $immerse->type,
54 + 'video_url' => $validated['video_url'],
55 + 'image_url' => $immerse->image_url,
56 + 'bg_music' => $immerse->bg_music,
57 + 'bgm_url' => $immerse->bgm_url,
58 + 'feel' => $validated['content'],
59 + 'weather' => $validated['weather'],
60 + 'temp_id' => $immerse->temp_id,
61 + 'thumbnail' => $validated['thumbnail_url'] ? 1 : 0,
62 + 'thumbnail_url' => $validated['thumbnail_url'],
63 + ]);
64 +
65 + // 添加至队列
66 + MakeVideo::dispatch($video);
33 67
68 + return Response::created();
34 } 69 }
35 70
36 /** 71 /**
......
...@@ -37,7 +37,7 @@ class SettingController extends Controller ...@@ -37,7 +37,7 @@ class SettingController extends Controller
37 return Response::success($array); 37 return Response::success($array);
38 } 38 }
39 39
40 - public function upload(Request $request) 40 + public function uploadImage(Request $request)
41 { 41 {
42 $validator = Validator::make($request->all(),[ 42 $validator = Validator::make($request->all(),[
43 'image' => 'required|mimes:jpeg,png,bmp,gif' 43 'image' => 'required|mimes:jpeg,png,bmp,gif'
...@@ -59,12 +59,41 @@ class SettingController extends Controller ...@@ -59,12 +59,41 @@ class SettingController extends Controller
59 $dir_l2 = hexdec($hash_hex_l2) % 512; 59 $dir_l2 = hexdec($hash_hex_l2) % 512;
60 $dir = 'uploads/'. $dir_l1. '/' . $dir_l2; 60 $dir = 'uploads/'. $dir_l1. '/' . $dir_l2;
61 61
62 - if( !Storage::disk('public')->exists($dir)) { 62 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
63 63
64 - Storage::disk('public')->makeDirectory($dir); 64 + $file = $request->file('image')->store($dir,'public');
65 +
66 + return Response::success([
67 + 'relative_path' => $file,
68 + 'absolute_path' => Storage::disk('public')->url($file),
69 + ]);
70 + }
71 +
72 + public function uploadVideo(Request $request)
73 + {
74 + $validator = Validator::make($request->all(),[
75 + 'video' => 'required|mimes:mp4,flv,mov,avi'
76 + ]);
77 +
78 + if ($validator->fails()){
79 + return Response::fail('',500,$validator->errors());
65 } 80 }
66 81
67 - $file = $request->file('image')->store($dir,'public'); 82 + $hashName = $request->file('video')->hashName();
83 +
84 + $hash_hex = md5($hashName);
85 +
86 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
87 + // 前16个字符用来第一级求摸,后16个用做第二级
88 + $hash_hex_l1 = substr($hash_hex, 0, 8);
89 + $hash_hex_l2 = substr($hash_hex, 8, 8);
90 + $dir_l1 = hexdec($hash_hex_l1) % 256;
91 + $dir_l2 = hexdec($hash_hex_l2) % 512;
92 + $dir = 'uploads/'. $dir_l1. '/' . $dir_l2;
93 +
94 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
95 +
96 + $file = $request->file('video')->store($dir,'public');
68 97
69 return Response::success([ 98 return Response::success([
70 'relative_path' => $file, 99 'relative_path' => $file,
......
...@@ -604,7 +604,7 @@ class MakeVideo implements ShouldQueue ...@@ -604,7 +604,7 @@ class MakeVideo implements ShouldQueue
604 foreach ($components as $component) { 604 foreach ($components as $component) {
605 switch ($component->name){ 605 switch ($component->name){
606 case 'one_poem': 606 case 'one_poem':
607 - $content = $this->adminMakeVideo->poem->content . PHP_EOL; 607 + $content = $this->adminMakeVideo->poem->content;
608 $text_file = $this->getTempPath('txt'); 608 $text_file = $this->getTempPath('txt');
609 file_put_contents($text_file, $content); 609 file_put_contents($text_file, $content);
610 610
...@@ -619,7 +619,7 @@ class MakeVideo implements ShouldQueue ...@@ -619,7 +619,7 @@ class MakeVideo implements ShouldQueue
619 'fontcolor=' . $text_color . '@1.0:' . 619 'fontcolor=' . $text_color . '@1.0:' .
620 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . 620 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
621 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . 621 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
622 - 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", '; 622 + 'box=1:boxborderw=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
623 623
624 break; 624 break;
625 case 'every_poem': 625 case 'every_poem':
......
1 +<?php
2 +
3 +namespace App\Jobs;
4 +
5 +use App\Models\AdminMakeVideo;
6 +use App\Models\Immerse;
7 +use App\Models\User;
8 +use App\Models\VideoTemp;
9 +use Carbon\Carbon;
10 +use Illuminate\Bus\Queueable;
11 +use Illuminate\Contracts\Queue\ShouldBeUnique;
12 +use Illuminate\Contracts\Queue\ShouldQueue;
13 +use Illuminate\Foundation\Bus\Dispatchable;
14 +use Illuminate\Queue\InteractsWithQueue;
15 +use Illuminate\Queue\SerializesModels;
16 +use Illuminate\Support\Facades\Log;
17 +use Illuminate\Support\Facades\Storage;
18 +
19 +class UserMakeVideo implements ShouldQueue
20 +{
21 + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
22 +
23 + public $adminMakeVideo;
24 +
25 + protected $ffmpeg;
26 +
27 + protected $ffprobe;
28 +
29 + protected $ffplay;
30 +
31 + protected $width;
32 +
33 + /**
34 + * Create a new job instance.
35 + * @param AdminMakeVideo $adminMakeVideo
36 + * @return void
37 + */
38 + public function __construct(\App\Models\UserMakeVideo $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 + $adminMakeVideo = $this->adminMakeVideo;
55 + $file = Storage::disk('public')->path($adminMakeVideo->video_url);
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 + // 有背景音 融合
79 + $audio = $this->getTempPath('.mp3');
80 + $cmd = $this->ffmpeg.
81 + ' -y -i ' . escapeshellarg($file).
82 + ' -y -i ' . escapeshellarg($bgm).
83 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
84 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
85 + if (!$this->execmd($cmd)) return;
86 +
87 + $audio_input = ' -i ' . escapeshellarg($audio);
88 + $audio_filter = '[3:a]';
89 + }else{
90 + // 没有背景音
91 + $audio_input = '';
92 + $audio_filter = '[0:1]';
93 + }
94 + }elseif ( $media_info['format']['nb_streams'] == 1 ){
95 + $audio = $this->getTempPath('.mp3');
96 + $cmd = $this->ffmpeg .
97 + ' -y -f lavfi -i aevalsrc=0:duration='. escapeshellarg($media_file_time_length) .
98 + ' -ar 48000 -ab 64k ' . escapeshellarg($audio);
99 + if (!$this->execmd($cmd)) return;
100 +
101 + if ($is_bgm){
102 + $audio_empty = $audio;
103 + $audio = $this->getTempPath('.mp3');
104 + $cmd = $this->ffmpeg.
105 + ' -y -i ' . escapeshellarg($audio_empty).
106 + ' -y -i ' . escapeshellarg($bgm).
107 + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
108 + '-ar 48000 -ab 64k ' . escapeshellarg($audio);
109 + if (!$this->execmd($cmd)) return;
110 + }
111 + $audio_input = ' -i ' . escapeshellarg($audio);
112 + $audio_filter = '[3:a]';
113 +
114 + }else{ /** 音频视频轨都没有 */
115 + Log::channel('daily')->error('视频没有video track');
116 + return;
117 + }
118 +
119 + $end_wallpaper = Storage::disk('public')->path('ffmpeg') . "/end_wallpaper.png";
120 + $thumbnail = Storage::disk('public')->path('ffmpeg') . "/thumbnail.png";
121 + $font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
122 +
123 + $user = User::query()->find($this->adminMakeVideo->user_id);
124 + $signature = $user->nickname;
125 +
126 + // 生成贴纸和签名
127 + $end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
128 +
129 + // 截取最后一帧
130 + $last_frame_video = $this->getTempPath();
131 + $this->width = $width = $media_info['streams'][0]['width'];
132 + $height = $media_info['streams'][0]['height'];
133 + $size = $width . 'x' . $height;
134 + $time_length = 0.7;
135 + $r = 24;
136 + $frame_n = $media_info['streams'][0]['nb_frames'] - 2;
137 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
138 + " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
139 + " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
140 + ' -map [v] -map 2:a ' . escapeshellarg($last_frame_video);
141 + if (!$this->execmd($cmd)) return;
142 +
143 +
144 + $signature_x = 0;
145 + $signature_y = -20;
146 + $animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
147 +
148 + $watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
149 +
150 + $video = $this->getTempPath('.mp4',false);
151 + $cmd = $this->ffmpeg . ' -y '.
152 + ' -i ' . escapeshellarg($file).
153 + ' -i ' . escapeshellarg($animate).
154 + ' -i ' . escapeshellarg($watermark).
155 + $audio_input .
156 + ' -filter_complex "[0:0] ' .
157 + $this->getTextContentString().
158 + ' [text];[text]'.
159 + ' [2:v]overlay=20:20[water];[water]' . $audio_filter . '[1:0][1:1] concat=n=2:v=1:a=1[v][a]" ' .
160 + ' -map [v] -map [a]'.
161 + ' -c:v libx264 -bt 256k -r 25' .
162 + ' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
163 + escapeshellarg($video);
164 +
165 + if (!$this->execmd($cmd)) return;
166 +
167 +// $video = $this->getTempPath();
168 +// if ( $adminMakeVideo->thumbnail == 1 && $adminMakeVideo->thumbnail_url){
169 +// $thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url);
170 +// }else{
171 +// $thumbnail = $last_frame_video;
172 +// }
173 +// $cmd = $this->ffmpeg. ' -y'.
174 +// ' -i ' . escapeshellarg($video_temp).
175 +// ' -i ' . escapeshellarg($thumbnail).
176 +// ' -map 1 -map 0 -c copy -disposition:0 attached_pic '.
177 +// escapeshellarg($video);
178 +// $this->execmd($cmd);
179 +
180 + // 全部合成以后创建 临境
181 + $video_info = $this->mediainfo($video);
182 +
183 + Immerse::query()->create([
184 + 'user_id' => $this->adminMakeVideo->user_id,
185 + 'title' => '',
186 + 'content' => $this->adminMakeVideo->feel,
187 + 'url' => $video,
188 + 'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
189 + 'duration' => $video_info['format']['duration'],
190 + 'size' => $video_info['format']['size'],
191 + 'poem_id' => $this->adminMakeVideo->poem_id,
192 + 'temp_id' => $this->adminMakeVideo->temp_id,
193 + 'thumbnail' => $thumbnail,
194 + 'bgm' => $this->adminMakeVideo->bgm_url,
195 + ]);
196 +
197 + }
198 +
199 + /**
200 + * 获取圆形头像
201 + * @param $img
202 + * @param int $dst_w
203 + * @param int $dst_h
204 + * @return resource
205 + */
206 + public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96)
207 + {
208 + $w = 130;
209 + $h = 130;
210 + $src = imagecreatetruecolor($dst_w, $dst_h);
211 + imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
212 +
213 + $newpic = imagecreatetruecolor($dst_w, $dst_h);
214 + imagealphablending($newpic, false);
215 + imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
216 + $mask = imagecreatetruecolor($dst_w, $dst_h);
217 + $transparent = imagecolorallocate($mask, 255, 0, 0);
218 + imagecolortransparent($mask,$transparent);
219 + imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
220 + $red = imagecolorallocate($mask, 0, 0, 0);
221 + imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
222 + imagecolortransparent($newpic,$red);
223 + imagesavealpha($newpic,true);
224 + imagefill($newpic, 0, 0, $red);
225 + imagedestroy($mask);
226 + return $newpic;
227 + }
228 +
229 + /**
230 + * 制作最后一帧
231 + * @param $file
232 + * @return bool|string
233 + */
234 + public function makeLastFrameVideo($file) {
235 + $video = $this->getTempPath();
236 + $width = $this->getVideoWith($file);
237 + $height = $this->getVideoHeight($file);
238 + $size = $width . 'x' . $height;
239 + $time_length = 0.7;
240 + $r = 24;
241 + $frame_n = $this->getVideoFrameNum($file) - 2;
242 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($file) .
243 + " -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
244 + " -filter_complex \"[0:v]select='eq(n,{$frame_n})',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[v]\"" .
245 + ' -map [v] -map 2:a ' . escapeshellarg($video);
246 + if ($this->execmd($cmd)) {
247 + return $video;
248 + } else {
249 + return false;
250 + }
251 + }
252 +
253 + /**
254 + * 用最后一帧和贴纸制作动画
255 + * @param $last_frame_video
256 + * @param $end_wallpaper
257 + * @param $signature
258 + * @param $signature_x
259 + * @param $signature_y
260 + * @param $font
261 + * @return bool|string
262 + */
263 + public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
264 + $signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
265 + $signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
266 + $video = $this->getTempPath();
267 + if ($signature !== '') {
268 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
269 + ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
270 + ' -filter_complex "'.
271 + 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
272 + '[0:v]boxblur=8[blur];'.
273 + '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
274 + 'drawtext='.
275 + 'fontfile=' . escapeshellarg($font) . ':'.
276 + 'text=' . escapeshellarg($signature) . ':'.
277 + 'fontsize=23:'.
278 + 'fontcolor=white@1.0:'.
279 + 'x=main_w/2' . $signature_x . ':'.
280 + 'y=main_h/2' . $signature_y . '[text];[text]'.
281 + '[grad]alphamerge[alpha];'.
282 + '[0:v][alpha]overlay'.
283 + '" ' . escapeshellarg($video);
284 + } else {
285 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
286 + ' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
287 + ' -filter_complex "'.
288 + 'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
289 + '[0:v]boxblur=8[blur];'.
290 + '[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
291 + '[lay][grad]alphamerge[alpha];'.
292 + '[0:v][alpha]overlay'.
293 + '" ' . escapeshellarg($video);
294 + }
295 + if ($this->execmd($cmd)) {
296 + return $video;
297 + } else {
298 + return false;
299 + }
300 + }
301 +
302 + /**
303 + * 获取视频宽度
304 + * @param $file
305 + * @param bool $cache
306 + * @return int|null
307 + */
308 + public function getVideoWith($file, $cache = true) {
309 + $result = $this->getFirstVideoTrackOption($file, $option = 'width', $cache);
310 + if ($result) {
311 + return (int)$result;
312 + } else {
313 + return $result;
314 + }
315 + }
316 +
317 + /**
318 + * 获取视频高度
319 + * @param $file
320 + * @param bool $cache
321 + * @return int|null
322 + */
323 + public function getVideoHeight($file, $cache = true) {
324 + $result = $this->getFirstVideoTrackOption($file, $option = 'height', $cache);
325 + if ($result) {
326 + return (int)$result;
327 + } else {
328 + return $result;
329 + }
330 + }
331 +
332 + /**
333 + * 获取视频帧数
334 + * @param $file
335 + * @param bool $cache
336 + * @return null
337 + */
338 + public function getVideoFrameNum($file, $cache = true) {
339 + return $this->getFirstVideoTrackOption($file, $option = 'nb_frames', $cache);
340 + }
341 +
342 +
343 + public function getFirstVideoTrackOption($file, $option, $cache = true) {
344 + return $this->getFirstTrackOption($file, $option, $codec_type = 'video', $cache = true);
345 + }
346 +
347 + public function getFirstTrackOption($file, $option, $codec_type = '', $cache = true) {
348 + $result = $this->mediainfo($file, $cache);
349 + if (!isset($result['streams'])) {
350 + return null;
351 + }
352 + $_track = null;
353 + foreach($result['streams'] as $track) {
354 + if (empty($codec_type)) {
355 + $_track = $track;
356 + break;
357 + } elseif ($track['codec_type'] == $codec_type) {
358 + $_track = $track;
359 + break;
360 + }
361 + }
362 + if (isset($_track[$option])) {
363 + return $_track[$option];
364 + }
365 + return null;
366 + }
367 +
368 + /***
369 + * 获取视频信息(配合ffprobe)
370 + * @param $file
371 + * @param bool $cache
372 + * @return mixed
373 + */
374 + public function mediainfo($file, $cache = true) {
375 + global $_mediainfo;
376 + $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
377 + if ($cache && isset($_mediainfo[$file])) {
378 + return $_mediainfo[$file];
379 + }
380 + $output = $this->execmd($cmd);
381 + $data = json_decode($output, true);
382 + if (json_last_error() === JSON_ERROR_UTF8) {
383 + $output = mb_convert_encoding($output, "UTF-8");
384 + $data = json_decode($output, true);
385 + }
386 + if ($cache) {
387 + $mediainfo[$file] = $data;
388 + }
389 + return $data;
390 + }
391 +
392 + /**
393 + * 获取输出临时文件名
394 + * @param string $ext
395 + * @param bool $is_temp
396 + * @return string
397 + */
398 + public function getTempPath($ext = '.mp4',$is_temp = true)
399 + {
400 + $filename = "/output_" . time() . rand(0, 10000);
401 +
402 + $prefix = $is_temp ? 'temp/' : 'video/';
403 + $hash_hex = md5($filename);
404 + // 16进制表示的字符串一共32字节,表示16个二进制字节。
405 + // 前16个字符用来第一级求摸,后16个用做第二级
406 + $hash_hex_l1 = substr($hash_hex, 0, 8);
407 + $hash_hex_l2 = substr($hash_hex, 8, 8);
408 + $dir_l1 = hexdec($hash_hex_l1) % 256;
409 + $dir_l2 = hexdec($hash_hex_l2) % 512;
410 + $dir = $prefix . $dir_l1 . '/' . $dir_l2;
411 +
412 + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
413 +
414 + return Storage::disk('public')->path($dir . $filename . $ext);
415 + }
416 +
417 + /**
418 + * 执行命令
419 + * @param $cmd
420 + * @param bool $update_progress
421 + * @return string
422 + */
423 + public function execmd($cmd, $update_progress = false) {
424 + echo $cmd . "\n". "\n". "\n";
425 + $descriptorspec = array(
426 + 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
427 + );
428 + $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
429 + if (is_resource($process)) {
430 + $error0 = '';
431 + $error1 = '';
432 + $stdout = '';
433 + while (!feof($pipes[1])) {
434 + $line = fgets($pipes[1], 150);
435 + $stdout .= $line;
436 + if ($line) {
437 + //记录错误
438 + $error0 = $error1;
439 + $error1 = $line;
440 + if ($update_progress &&
441 + false !== strpos($line, 'size=') &&
442 + false !== strpos($line, 'time=') &&
443 + false !== strpos($line, 'bitrate='))
444 + {
445 + //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
446 + $line = explode(' ', $line);
447 + $time = null;
448 + foreach ($line as $item) {
449 + $item = explode('=', $item);
450 + if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
451 + $time = $item[1];
452 + break;
453 + }
454 + }
455 + }
456 + }
457 + }
458 + // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
459 + fclose($pipes[1]);
460 + $exitedcode = proc_close($process);
461 + if ($exitedcode === 0) {
462 + return $stdout;
463 + } else {
464 + $error = trim($error0,"\n") . ' '. trim($error1,"\n");
465 + // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
466 + // ErrorUtil::triggerErrorMsg($error, $exitedcode);
467 + }
468 + } else {
469 + // return ErrorUtil::triggerErrorMsg('proc_open error');
470 + }
471 + }
472 +
473 + /**
474 + * 贴纸和签名
475 + * @param $end_wallpaper
476 + * @param $thumbnail
477 + * @param $signature
478 + * @param $font
479 + * @return string
480 + */
481 + public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
482 + $_imagetype = $this->getImageType($thumbnail);
483 + $_img = null;
484 + switch ($_imagetype) {
485 + case 'gif':
486 + if (function_exists('imagecreatefromgif')) {
487 + $_img = imagecreatefromgif($thumbnail);
488 + }
489 + break;
490 + case 'jpg':
491 + case 'jpeg':
492 + $_img = imagecreatefromjpeg($thumbnail);
493 + break;
494 + case 'png':
495 + $_img = imagecreatefrompng($thumbnail);
496 + break;
497 + default:
498 + $_img = imagecreatefromstring($thumbnail);
499 + break;
500 + }
501 + $width = 130;
502 + $height = 130;
503 + $_width = 130;
504 + $_height = 130;
505 + if(is_resource($_img)){
506 + $_width = imagesx($_img);
507 + $_height = imagesy($_img);
508 + }
509 +
510 + $bite = $_width / $_height;
511 +
512 + if($_width > $_height){
513 + if($_width > $width){
514 + $height = round($width / $bite);
515 + }
516 + }else{
517 + if($_height > $height){
518 + $width = round($height * $bite);
519 + }
520 + }
521 +
522 + $tmpimg = imagecreatetruecolor($width,$height);
523 + if(function_exists('imagecopyresampled')) {
524 + imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
525 + } else {
526 + imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
527 + }
528 + if(is_resource($_img)) imagedestroy($_img);
529 + $_img = $this->getCircleAvatar($tmpimg);
530 + if(is_resource($tmpimg)) imagedestroy($tmpimg);
531 +
532 + $wp = $this->imagesMerge($end_wallpaper, $_img);
533 +// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
534 + $white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
535 + imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
536 +
537 +// $dst = "./output_new_end_wallpaper.png";
538 + $dst = Storage::disk('public')->path('ffmpeg') . "/output_new_end_wallpaper.png";
539 + imagepng($wp, $dst);
540 + if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
541 + if(is_resource($_img)) imagedestroy($_img);
542 +
543 + return $dst;
544 + }
545 +
546 + /**
547 + * 获取图像文件类型
548 + * @param $img_name
549 + * @return string
550 + */
551 + public function getImageType($img_name)
552 + {
553 + if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
554 + $type = strtolower($matches[1]);
555 + }else{
556 + $type = "string";
557 + }
558 + return $type;
559 + }
560 +
561 + /**
562 + * 多图融合
563 + * @param $end_wallpaper
564 + * @param $thumbnail
565 + * @return resource
566 + */
567 + public function imagesMerge($end_wallpaper, $thumbnail) {
568 + $end_wallpaper = imagecreatefrompng($end_wallpaper);
569 + $background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
570 + imagesavealpha($background,true);
571 + $temp_wallpaper = imagecreatetruecolor(350, 204);
572 + $color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
573 +// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
574 + imagefill($temp_wallpaper, 0, 0, $color);
575 + imageColorTransparent($temp_wallpaper, $color);
576 + imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
577 + imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
578 + imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
579 + return $background;
580 + }
581 +
582 + /**
583 + * logo 大小转换
584 + * @param $logo
585 + * @return bool
586 + */
587 + public function translateLogo($logo)
588 + {
589 + $image = Storage::disk('public')->path('ffmpeg/output_150x150.jpg');
590 + $cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($logo) .
591 + ' -vf scale=150:150 ' . escapeshellarg($image);
592 + if ($this->execmd($cmd)) {
593 + return $image;
594 + } else {
595 + return false;
596 + }
597 + }
598 +
599 + public function getTextContentString()
600 + {
601 + $components = $this->adminMakeVideo->temp()->first()->components()->get();
602 +
603 + $font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
604 +
605 + $drawtext = '';
606 +
607 + foreach ($components as $component) {
608 + switch ($component->name){
609 + case 'one_poem':
610 + $content = $this->adminMakeVideo->poem->content;
611 + $text_file = $this->getTempPath('txt');
612 + file_put_contents($text_file, $content);
613 +
614 + $text_color = $component->text_color ?? 'white';
615 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
616 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
617 +
618 + $drawtext .= 'drawtext="'.
619 + 'fontfile=' . escapeshellarg($font) . ':' .
620 + 'textfile=' . escapeshellarg($text_file) . ':' .
621 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
622 + 'fontcolor=' . $text_color . '@1.0:' .
623 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
624 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
625 + 'box=1:boxborderw=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
626 +
627 + break;
628 + case 'every_poem':
629 + break;
630 + case 'weather':
631 + $content = '多云';
632 + $text_color = $component->text_color ?? 'white';
633 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
634 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
635 +
636 + $drawtext .= 'drawtext="'.
637 + 'fontfile=' . escapeshellarg($font) . ':' .
638 + 'text=' . escapeshellarg($content) . ':' .
639 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
640 + 'fontcolor=' . $text_color . '@1.0:' .
641 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
642 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
643 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
644 +
645 + break;
646 + case 'date':
647 + $content = Carbon::now()->format('Y年m月d日H时');
648 + $text_color = $component->text_color ?? 'white';
649 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
650 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
651 +
652 + $drawtext .= 'drawtext="'.
653 + 'fontfile=' . escapeshellarg($font) . ':' .
654 + 'text=' . escapeshellarg($content) . ':' .
655 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
656 + 'fontcolor=' . $text_color . '@1.0:' .
657 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
658 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
659 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
660 + break;
661 + case 'feel':
662 + $content = $this->adminMakeVideo->feel;
663 + $text_color = $component->text_color ?? 'white';
664 + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
665 + $opacity = $component->opacity ? $component->opacity / 100 : '0.5';
666 +
667 + $drawtext .= 'drawtext="'.
668 + 'fontfile=' . escapeshellarg($font) . ':' .
669 + 'text=' . escapeshellarg($content) . ':' .
670 + 'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
671 + 'fontcolor=' . $text_color . '@1.0:' .
672 + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
673 + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
674 + 'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
675 + break;
676 + }
677 + }
678 +
679 + return rtrim($drawtext,', ');
680 + }
681 +
682 + /**
683 + * @param $width
684 + * @param $content
685 + * @return float
686 + */
687 + public function calcFontSize($width, $content)
688 + {
689 + $max_len = 1;
690 + foreach (explode("\n",$content) as $item){
691 + if (mb_strlen($item) > $max_len){
692 + $max_len = mb_strlen($item);
693 + }
694 + }
695 +
696 + return ceil($this->width * $width / 10 / $max_len);
697 + }
698 +}
1 +<?php
2 +
3 +namespace App\Models;
4 +
5 +use Illuminate\Database\Eloquent\Factories\HasFactory;
6 +use Illuminate\Database\Eloquent\Model;
7 +
8 +class UserMakeVideo extends Model
9 +{
10 + use HasFactory;
11 +
12 + protected $guarded = [''];
13 +}
1 +<?php
2 +
3 +use Illuminate\Database\Migrations\Migration;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Support\Facades\Schema;
6 +
7 +class CreateUserMakeVideosTable extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::create('user_make_videos', function (Blueprint $table) {
17 + $table->id();
18 + $table->unsignedInteger('user_id')->comment('用户id');
19 + $table->string('poem_id')->default('')->comment('一言id');
20 + $table->unsignedTinyInteger('type')->comment('类型');
21 + $table->string('video_url')->nullable()->comment('视频地址');
22 + $table->string('images_url')->nullable()->comment('图片地址');
23 + $table->unsignedTinyInteger('bg_music')->comment('是否背景音');
24 + $table->string('bgm_url')->nullable()->comment('背景音地址');
25 + $table->text('feel')->nullable()->comment('有感');
26 + $table->text('weather')->nullable()->comment('天气');
27 + $table->string('temp_id')->default('')->comment('模板id');
28 + $table->unsignedTinyInteger('thumbnail')->comment('封面图');
29 + $table->string('thumbnail_url')->nullable()->comment('封面图地址');
30 + $table->timestamps();
31 + });
32 + }
33 +
34 + /**
35 + * Reverse the migrations.
36 + *
37 + * @return void
38 + */
39 + public function down()
40 + {
41 + Schema::dropIfExists('user_make_videos');
42 + }
43 +}
...@@ -59,6 +59,9 @@ Route::prefix('v1')->namespace('App\Http\Controllers\V1')->group(function (Route ...@@ -59,6 +59,9 @@ Route::prefix('v1')->namespace('App\Http\Controllers\V1')->group(function (Route
59 /** 会员页 */ 59 /** 会员页 */
60 $api->apiResource('/membership', 'MembershipController'); 60 $api->apiResource('/membership', 'MembershipController');
61 61
62 - /** 文件上传 */ 62 + /** 图片上传 */
63 - $api->post('/upload/image', 'SettingController@upload'); 63 + $api->post('/upload/image', 'SettingController@uploadImage');
64 +
65 + /** 视频上传 */
66 + $api->post('/upload/video', 'SettingController@uploadVideo');
64 }); 67 });
...\ No newline at end of file ...\ No newline at end of file
......