* 运行命令:php artisan command:preview-video */ class GeneratePreviewVideo extends Command { use HasUploadedFile; /** * The name and signature of the console command. * * @var string */ protected $signature = 'command:preview-video'; /** * The console command description. * * @var string */ protected $description = '生成预览视频'; public $timeSecond = 5; /** * Execute the console command. * * @return int */ public function handle() { // $this->importVideo(); // exit; $disk = $this->disk('oss'); //如果目录不存在则创建 $localPreviewPath = storage_path('tmp/previews'); if (!file_exists($localPreviewPath)) { mkdir($localPreviewPath, 0777, true); } $reslut = DB::table('site_preview_video') ->where('status', '==', 0) // 过滤非空 video 字段 ->orderBy('id') // 按主键排序确保顺序 ->limit(1) // 限制处理 1 条 ->get(); foreach ($reslut as $item) { try { $video_oss_url = $this->ossUrl($item->video_url); $fileName = basename($video_oss_url); $fileInfo = pathinfo($fileName); //下载$video_url到本地 $localFilePath = $localPreviewPath. '/'. $fileName; $this->saveLocal($video_oss_url, $localFilePath); $previewName = $fileInfo['filename'] . '_preview.' . $fileInfo['extension']; $previewPath = storage_path('tmp/previews/'.$previewName); //截取视频前5秒 $this->generatePreview($localFilePath, $previewPath); //上传到OSS $uploadPath = 'videos/uploads/previews/'. date('Ym'); $file = new File($previewPath); $disk->putFileAs($uploadPath, $file, $previewName); DB::table('site_preview_video') ->where('id', $item->id) ->update([ 'preview_url' => $uploadPath . '/' . $previewName, 'status' => 1 ]); //删除本地文件 unlink($localFilePath); unlink($previewPath); } catch (\Exception $e) { DB::table('site_preview_video') ->where('id', $item->id) ->update([ 'remark' => $e->getMessage(), 'status' => -1 ]); } } //$this->importVideo(); return dd('success'); } private function saveLocal($url, $localPath) { $remote = fopen($url, 'rb'); $local = fopen($localPath, 'wb'); while (!feof($remote)) { fwrite($local, fread($remote, 8192)); // 8KB分块 } fclose($remote); fclose($local); return $localPath; } private function ossUrl($url) { if (strpos($url, 'http:') === 0 || strpos($url, 'https:') === 0) { return $url; } return "https://mietublcom.oss-cn-hongkong.aliyuncs.com".'/'.$url; } public function generatePreview($inputUrl, $outputPath) { $ffmpeg = FFMpeg::create([ 'ffmpeg.binaries' => '/usr/bin/ffmpeg', 'ffprobe.binaries' => '/usr/bin/ffprobe', 'timeout' => 300 ]); $video = $ffmpeg->open($inputUrl); $video->filters()->clip(TimeCode::fromSeconds(0), TimeCode::fromSeconds($this->timeSecond)); // 输出为H.264编码的MP4 $format = new \FFMpeg\Format\Video\X264(); $format->setAdditionalParameters([ '-an' // 禁用音频 ]); $video->save($format, $outputPath); return $outputPath; } /*ssssssss * 把旧数据导入新的预览表 */ function importVideo() { try { // 分批处理数据,避免内存溢出 DB::table('site_album') ->where('video', '<>', '[]') // 过滤非空 video 字段 ->orderBy('id') // 按主键排序确保顺序 ->chunkById(10000, function ($albums) { // 每次处理 100 条 $insertData = []; foreach ($albums as $album) { // 跳过无效 JSON 数据 $videos = json_decode($album->video, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($videos)) { Log::warning("Invalid JSON in album ID: {$album->id}"); continue; } foreach ($videos as $video) { // 校验必要字段存在性 if (empty($video['video_src']) || empty($video['cover'])) { Log::warning("Missing video_src or cover in album ID: {$album->id}"); continue; } // 处理 URL 长度限制 $videoUrl = substr($video['video_src'], 0, 255); // 构建插入数据 $insertData[] = [ 'video_url' => $videoUrl, 'preview_url' => '', 'status' => 0, 'album_id' => $album->id, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; } } // 插入剩余数据 if (!empty($insertData)) { DB::table('site_preview_video')->insert($insertData); } }); return true; } catch (\Exception $e) { Log::error("Video import failed: " . $e->getMessage()); return false; } } }