|
@@ -0,0 +1,279 @@
|
|
|
|
+<?php
|
|
|
|
+
|
|
|
|
+namespace App\Console\Commands;
|
|
|
|
+
|
|
|
|
+use App\Libraries\CommonHelper;
|
|
|
|
+use Exception;
|
|
|
|
+use Illuminate\Console\Command;
|
|
|
|
+use Illuminate\Http\File;
|
|
|
|
+use Illuminate\Support\Carbon;
|
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
|
+use Dcat\Admin\Traits\HasUploadedFile;
|
|
|
|
+use FFMpeg\FFMpeg;
|
|
|
|
+use FFMpeg\Coordinate\TimeCode;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/*
|
|
|
|
+ * 生成预览10秒视频
|
|
|
|
+ *
|
|
|
|
+ * @author <EMAIL>
|
|
|
|
+ * 运行命令: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 = 8;
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Execute the console command.
|
|
|
|
+ *
|
|
|
|
+ * @return int
|
|
|
|
+ */
|
|
|
|
+ public function handle()
|
|
|
|
+ {
|
|
|
|
+// $this->importVideo();
|
|
|
|
+// exit;
|
|
|
|
+ ini_set('memory_limit', '512M');
|
|
|
|
+ $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,
|
|
|
|
+ 'updated_at' => Carbon::now()
|
|
|
|
+ ]);
|
|
|
|
+
|
|
|
|
+ //删除本地文件
|
|
|
|
+ unlink($localFilePath);
|
|
|
|
+ unlink($previewPath);
|
|
|
|
+ } catch (\Exception $e) {
|
|
|
|
+ DB::table('site_preview_video')
|
|
|
|
+ ->where('id', $item->id)
|
|
|
|
+ ->update([
|
|
|
|
+ 'remark' => $e->getMessage(),
|
|
|
|
+ 'status' => -1,
|
|
|
|
+ 'updated_at' => Carbon::now()
|
|
|
|
+ ]);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //$this->importVideo();
|
|
|
|
+ return dd('success');
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private function saveLocal($url, $localPath) {
|
|
|
|
+ // 初始化 cURL 会话
|
|
|
|
+ $ch = curl_init($url);
|
|
|
|
+ // 设置 cURL 选项
|
|
|
|
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回数据而不是直接输出
|
|
|
|
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 跟踪重定向
|
|
|
|
+ curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 设置超时时间(秒)
|
|
|
|
+ // 执行 cURL 请求
|
|
|
|
+ $response = curl_exec($ch);
|
|
|
|
+ // 检查是否有错误
|
|
|
|
+ if (curl_errno($ch)) {
|
|
|
|
+ $error = 'cURL error: ' . curl_error($ch);
|
|
|
|
+ curl_close($ch);
|
|
|
|
+ throw new Exception($error);
|
|
|
|
+ }
|
|
|
|
+ // 获取 HTTP 状态码
|
|
|
|
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
+ curl_close($ch);
|
|
|
|
+ if ($httpCode != 200) {
|
|
|
|
+ throw new Exception("HTTP error: {$httpCode}");
|
|
|
|
+ }
|
|
|
|
+ // 打开本地文件进行写入
|
|
|
|
+ $local = fopen($localPath, 'wb');
|
|
|
|
+ if (!$local) {
|
|
|
|
+ throw new Exception("Failed to open local file: {$localPath}");
|
|
|
|
+ }
|
|
|
|
+ // 将响应数据写入本地文件
|
|
|
|
+ fwrite($local, $response);
|
|
|
|
+ fclose($local);
|
|
|
|
+ return $localPath;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private function ossUrl($url)
|
|
|
|
+ {
|
|
|
|
+ if (strpos($url, 'http:') === 0 || strpos($url, 'https:') === 0) {
|
|
|
|
+ return $url;
|
|
|
|
+ }
|
|
|
|
+ if (env('PREVIEW_OSS_URL_INTERNAL')) {
|
|
|
|
+ return env('PREVIEW_OSS_URL_INTERNAL').'/'.$url;
|
|
|
|
+ }
|
|
|
|
+ return env('PREVIEW_OSS_URL').'/'.$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));
|
|
|
|
+ $video->filters()->framerate(new \FFMpeg\Coordinate\FrameRate(15), 60); // 降低帧率
|
|
|
|
+
|
|
|
|
+ // 输出为H.264编码的MP4
|
|
|
|
+ $format = new \FFMpeg\Format\Video\X264();
|
|
|
|
+ // 压缩参数:降低比特率以减小文件大小
|
|
|
|
+ $format->setKiloBitrate(200); // 设置视频比特率(kbps),500 是一个较低的值,可根据需要调整
|
|
|
|
+ $format->setAdditionalParameters(['-an']); // 禁用音频,或者可以用 setAdditionalParameters(['-an'])
|
|
|
|
+ $video->save($format, $outputPath);
|
|
|
|
+ return $outputPath;
|
|
|
|
+ }*/
|
|
|
|
+
|
|
|
|
+ public function generatePreview($inputUrl, $outputPath) {
|
|
|
|
+ $ffmpeg = FFMpeg::create([
|
|
|
|
+ 'ffmpeg.binaries' => '/usr/bin/ffmpeg',
|
|
|
|
+ 'ffprobe.binaries' => '/usr/bin/ffprobe',
|
|
|
|
+ 'timeout' => 180
|
|
|
|
+ ]);
|
|
|
|
+
|
|
|
|
+ // Open the video
|
|
|
|
+ $video = $ffmpeg->open($inputUrl);
|
|
|
|
+
|
|
|
|
+ // Use ffprobe to get original video dimensions
|
|
|
|
+ $ffprobe = \FFMpeg\FFProbe::create(['ffprobe.binaries' => '/usr/bin/ffprobe']);
|
|
|
|
+ $stream = $ffprobe->streams($inputUrl)->videos()->first();
|
|
|
|
+ $originalWidth = $stream->getDimensions()->getWidth();
|
|
|
|
+ $originalHeight = $stream->getDimensions()->getHeight();
|
|
|
|
+
|
|
|
|
+ // Calculate adaptive height for a fixed width of 300
|
|
|
|
+ $newWidth = 300;
|
|
|
|
+ $newHeight = (int) round(($originalHeight / $originalWidth) * $newWidth);
|
|
|
|
+
|
|
|
|
+ // Ensure dimensions are even (required by H.264)
|
|
|
|
+ $newHeight = $newHeight % 2 === 0 ? $newHeight : $newHeight + 1;
|
|
|
|
+
|
|
|
|
+ // Apply filters
|
|
|
|
+ $video->filters()->clip(TimeCode::fromSeconds(0), TimeCode::fromSeconds($this->timeSecond));
|
|
|
|
+ $video->filters()->framerate(new \FFMpeg\Coordinate\FrameRate(15), 60); // Lower frame rate
|
|
|
|
+ $video->filters()->resize(new \FFMpeg\Coordinate\Dimension($newWidth, $newHeight)); // Adaptive resize
|
|
|
|
+
|
|
|
|
+ // Output as H.264 encoded MP4
|
|
|
|
+ $format = new \FFMpeg\Format\Video\X264();
|
|
|
|
+ $format->setKiloBitrate(150); // Set video bitrate (kbps)
|
|
|
|
+ $format->setAdditionalParameters(['-an']); // Disable audio
|
|
|
|
+
|
|
|
|
+ // Save the video
|
|
|
|
+ $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;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+}
|