GeneratePreviewVideo.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Libraries\CommonHelper;
  4. use Exception;
  5. use Illuminate\Console\Command;
  6. use Illuminate\Http\File;
  7. use Illuminate\Support\Carbon;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Log;
  10. use Dcat\Admin\Traits\HasUploadedFile;
  11. use FFMpeg\FFMpeg;
  12. use FFMpeg\Coordinate\TimeCode;
  13. /*
  14. * 生成预览10秒视频
  15. *
  16. * @author <EMAIL>
  17. * 运行命令:php artisan command:preview-video
  18. */
  19. class GeneratePreviewVideo extends Command
  20. {
  21. use HasUploadedFile;
  22. /**
  23. * The name and signature of the console command.
  24. *
  25. * @var string
  26. */
  27. protected $signature = 'command:preview-video';
  28. /**
  29. * The console command description.
  30. *
  31. * @var string
  32. */
  33. protected $description = '生成预览视频';
  34. public $timeSecond = 8;
  35. /**
  36. * Execute the console command.
  37. *
  38. * @return int
  39. */
  40. public function handle()
  41. {
  42. // $this->importVideo();
  43. // exit;
  44. ini_set('memory_limit', '512M');
  45. $disk = $this->disk('oss');
  46. //如果目录不存在则创建
  47. $localPreviewPath = storage_path('tmp/previews');
  48. if (!file_exists($localPreviewPath)) {
  49. mkdir($localPreviewPath, 0777, true);
  50. }
  51. $reslut = DB::table('site_preview_video')
  52. ->where('status', '==', 0) // 过滤非空 video 字段
  53. ->orderBy('id') // 按主键排序确保顺序
  54. ->limit(1) // 限制处理 1 条
  55. ->get();
  56. foreach ($reslut as $item) {
  57. try {
  58. $video_oss_url = $this->ossUrl($item->video_url);
  59. $fileName = basename($video_oss_url);
  60. $fileInfo = pathinfo($fileName);
  61. //下载$video_url到本地
  62. $localFilePath = $localPreviewPath. '/'. $fileName;
  63. $this->saveLocal($video_oss_url, $localFilePath);
  64. $previewName = $fileInfo['filename'] . '_preview.' . $fileInfo['extension'];
  65. $previewPath = storage_path('tmp/previews/'.$previewName);
  66. //截取视频前5秒
  67. $this->generatePreview($localFilePath, $previewPath);
  68. //上传到OSS
  69. $uploadPath = 'videos/uploads/previews/'. date('Ym');
  70. $file = new File($previewPath);
  71. $disk->putFileAs($uploadPath, $file, $previewName);
  72. DB::table('site_preview_video')
  73. ->where('id', $item->id)
  74. ->update([
  75. 'preview_url' => $uploadPath . '/' . $previewName,
  76. 'status' => 1,
  77. 'updated_at' => Carbon::now()
  78. ]);
  79. //删除本地文件
  80. unlink($localFilePath);
  81. unlink($previewPath);
  82. } catch (\Exception $e) {
  83. DB::table('site_preview_video')
  84. ->where('id', $item->id)
  85. ->update([
  86. 'remark' => $e->getMessage(),
  87. 'status' => -1,
  88. 'updated_at' => Carbon::now()
  89. ]);
  90. }
  91. }
  92. //$this->importVideo();
  93. return dd('success');
  94. }
  95. private function saveLocal($url, $localPath) {
  96. // 初始化 cURL 会话
  97. $ch = curl_init($url);
  98. // 设置 cURL 选项
  99. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回数据而不是直接输出
  100. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 跟踪重定向
  101. curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 设置超时时间(秒)
  102. // 执行 cURL 请求
  103. $response = curl_exec($ch);
  104. // 检查是否有错误
  105. if (curl_errno($ch)) {
  106. $error = 'cURL error: ' . curl_error($ch);
  107. curl_close($ch);
  108. throw new Exception($error);
  109. }
  110. // 获取 HTTP 状态码
  111. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  112. curl_close($ch);
  113. if ($httpCode != 200) {
  114. throw new Exception("HTTP error: {$httpCode}");
  115. }
  116. // 打开本地文件进行写入
  117. $local = fopen($localPath, 'wb');
  118. if (!$local) {
  119. throw new Exception("Failed to open local file: {$localPath}");
  120. }
  121. // 将响应数据写入本地文件
  122. fwrite($local, $response);
  123. fclose($local);
  124. return $localPath;
  125. }
  126. private function ossUrl($url)
  127. {
  128. if (strpos($url, 'http:') === 0 || strpos($url, 'https:') === 0) {
  129. return $url;
  130. }
  131. if (env('PREVIEW_OSS_URL_INTERNAL')) {
  132. return env('PREVIEW_OSS_URL_INTERNAL').'/'.$url;
  133. }
  134. return env('PREVIEW_OSS_URL').'/'.$url;
  135. }
  136. /*
  137. public function generatePreview($inputUrl, $outputPath) {
  138. $ffmpeg = FFMpeg::create([
  139. 'ffmpeg.binaries' => '/usr/bin/ffmpeg',
  140. 'ffprobe.binaries' => '/usr/bin/ffprobe',
  141. 'timeout' => 300
  142. ]);
  143. $video = $ffmpeg->open($inputUrl);
  144. $video->filters()->clip(TimeCode::fromSeconds(0), TimeCode::fromSeconds($this->timeSecond));
  145. $video->filters()->framerate(new \FFMpeg\Coordinate\FrameRate(15), 60); // 降低帧率
  146. // 输出为H.264编码的MP4
  147. $format = new \FFMpeg\Format\Video\X264();
  148. // 压缩参数:降低比特率以减小文件大小
  149. $format->setKiloBitrate(200); // 设置视频比特率(kbps),500 是一个较低的值,可根据需要调整
  150. $format->setAdditionalParameters(['-an']); // 禁用音频,或者可以用 setAdditionalParameters(['-an'])
  151. $video->save($format, $outputPath);
  152. return $outputPath;
  153. }*/
  154. public function generatePreview($inputUrl, $outputPath) {
  155. $ffmpeg = FFMpeg::create([
  156. 'ffmpeg.binaries' => '/usr/bin/ffmpeg',
  157. 'ffprobe.binaries' => '/usr/bin/ffprobe',
  158. 'timeout' => 180
  159. ]);
  160. // Open the video
  161. $video = $ffmpeg->open($inputUrl);
  162. // Use ffprobe to get original video dimensions
  163. $ffprobe = \FFMpeg\FFProbe::create(['ffprobe.binaries' => '/usr/bin/ffprobe']);
  164. $stream = $ffprobe->streams($inputUrl)->videos()->first();
  165. $originalWidth = $stream->getDimensions()->getWidth();
  166. $originalHeight = $stream->getDimensions()->getHeight();
  167. // Calculate adaptive height for a fixed width of 300
  168. $newWidth = 300;
  169. $newHeight = (int) round(($originalHeight / $originalWidth) * $newWidth);
  170. // Ensure dimensions are even (required by H.264)
  171. $newHeight = $newHeight % 2 === 0 ? $newHeight : $newHeight + 1;
  172. // Apply filters
  173. $video->filters()->clip(TimeCode::fromSeconds(0), TimeCode::fromSeconds($this->timeSecond));
  174. $video->filters()->framerate(new \FFMpeg\Coordinate\FrameRate(15), 60); // Lower frame rate
  175. $video->filters()->resize(new \FFMpeg\Coordinate\Dimension($newWidth, $newHeight)); // Adaptive resize
  176. // Output as H.264 encoded MP4
  177. $format = new \FFMpeg\Format\Video\X264();
  178. $format->setKiloBitrate(150); // Set video bitrate (kbps)
  179. $format->setAdditionalParameters(['-an']); // Disable audio
  180. // Save the video
  181. $video->save($format, $outputPath);
  182. return $outputPath;
  183. }
  184. /*ssssssss
  185. * 把旧数据导入新的预览表
  186. */
  187. function importVideo()
  188. {
  189. try {
  190. // 分批处理数据,避免内存溢出
  191. DB::table('site_album')
  192. ->where('video', '<>', '[]') // 过滤非空 video 字段
  193. ->orderBy('id') // 按主键排序确保顺序
  194. ->chunkById(10000, function ($albums) { // 每次处理 100 条
  195. $insertData = [];
  196. foreach ($albums as $album) {
  197. // 跳过无效 JSON 数据
  198. $videos = json_decode($album->video, true);
  199. if (json_last_error() !== JSON_ERROR_NONE || !is_array($videos)) {
  200. Log::warning("Invalid JSON in album ID: {$album->id}");
  201. continue;
  202. }
  203. foreach ($videos as $video) {
  204. // 校验必要字段存在性
  205. if (empty($video['video_src']) || empty($video['cover'])) {
  206. Log::warning("Missing video_src or cover in album ID: {$album->id}");
  207. continue;
  208. }
  209. // 处理 URL 长度限制
  210. $videoUrl = substr($video['video_src'], 0, 255);
  211. // 构建插入数据
  212. $insertData[] = [
  213. 'video_url' => $videoUrl,
  214. 'preview_url' => '',
  215. 'status' => 0,
  216. 'album_id' => $album->id,
  217. 'created_at' => Carbon::now(),
  218. 'updated_at' => Carbon::now(),
  219. ];
  220. }
  221. }
  222. // 插入剩余数据
  223. if (!empty($insertData)) {
  224. DB::table('site_preview_video')->insert($insertData);
  225. }
  226. });
  227. return true;
  228. } catch (\Exception $e) {
  229. Log::error("Video import failed: " . $e->getMessage());
  230. return false;
  231. }
  232. }
  233. }