Showing
8 changed files
with
841 additions
and
16 deletions
| ... | @@ -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': | ... | ... |
app/Jobs/UserMakeVideo.php
0 → 100644
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +namespace App\Jobs; | ||
| 4 | + | ||
| 5 | +use App\Models\AdminMakeVideo; | ||
| 6 | +use App\Models\Immerse; | ||
| 7 | +use App\Models\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 | +} |
app/Models/UserMakeVideo.php
0 → 100644
| 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 | ... | ... |
-
Please register or login to post a comment