FFmpeg转码X264格式耗时过长求助(Laravel后端)
问题描述
前端用Vue.js、后端用PHP Laravel实现视频录制后上传至AWS S3的功能,此前直接存储未转码导致Android录制的视频无法在Apple设备播放(编解码器兼容问题)。改用FFmpeg将视频转码为X264格式后,1分钟视频转码耗时长达6-7分钟,排除S3存储耗时问题(转码后存本地依然慢),需优化代码并缩短转码时间。
代码优化点及转码加速方案
1. 启用FFmpeg硬件加速
默认FFmpeg用CPU软编码,速度极慢,优先利用服务器硬件加速:
- 若服务器有NVIDIA显卡,启用NVENC编码;
- 若为Intel CPU,启用QSV或VA-API;
- 实例化FFmpeg时指定硬件加速参数。
2. 优化X264转码参数
使用X264的快速预设(如veryfast、fast),平衡编码速度与视频质量;同时调整CRF值(推荐18-28,值越大文件越小、编码越快)。
3. 优化文件上传逻辑
避免用file_get_contents读取整个文件到内存,改用流式上传,减少内存占用并提升上传速度。
4. 异步转码(核心解决用户等待阻塞)
将转码+上传逻辑放入Laravel队列,前端上传原始视频后立即返回,后台异步处理转码与S3存储,完成后可通过WebSocket或推送通知用户。
修改后的代码片段
同步优化版(缩短转码时间,但仍需用户等待)
public function video_upload(Request $request) { $response = [ 'status' => false, 'data' => [] ]; $response_code = 200; // 验证优化:指定视频格式 $validator = Validator::make($request->all(), [ 'file' => 'required|mimes:mp4,webm,ogg,avi,mov' ]); if ($validator->fails()) { $response['data']['validator'] = $validator->errors(); return response()->json($response); } try { $file = $request->file('file'); // 启用硬件加速(以NVENC为例,根据服务器硬件调整) $ffmpeg = FFMpeg\FFMpeg::create([ 'ffmpeg.binaries' => '/usr/bin/ffmpeg', // 确保路径正确 'ffprobe.binaries' => '/usr/bin/ffprobe', 'timeout' => 3600, // 延长超时时间 'ffmpeg.threads' => 8, // 利用多线程 ], [ 'ffmpeg.options' => [ '-hwaccel' => 'cuda', // NVIDIA显卡用cuda '-hwaccel_device' => '0' ] ]); $video = $ffmpeg->open($file->getRealPath()); // 优化X264格式参数 $format = new X264(); $format->setAdditionalParameters([ '-preset' => 'veryfast', // 快速预设 '-crf' => '23', // 平衡质量与速度的CRF值 '-c:a' => 'aac', // 指定音频编码为AAC,兼容全平台 '-b:a' => '128k' ]); // 简化文件名生成 $file_name = Str::random(20) . '.mp4'; $temp_path = storage_path('app/temp/' . $file_name); // 确保临时目录存在 if (!file_exists(storage_path('app/temp'))) { mkdir(storage_path('app/temp'), 0755, true); } $video->save($format, $temp_path); $file_folder = 'uploads/video/'; // 流式上传到S3,避免读取整个文件到内存 $store = Storage::disk('s3')->putFileAs( $file_folder, $temp_path, $file_name ); if ($store) { // 删除临时文件 unlink($temp_path); // 删除旧文件 if (isset($request->old_file)) { $old_file_path = $file_folder . basename($request->old_file); if (Storage::disk('s3')->exists($old_file_path)) { Storage::disk('s3')->delete($old_file_path); } } $response['status'] = true; $response['data'] = '/s3/' . $file_folder . $file_name; } } catch (\Exception $e) { $response['data']['message'] = $e->getMessage() . " line " . $e->getLine(); $response_code = 400; } return response()->json($response, $response_code); }
异步队列版(彻底解决用户等待阻塞)
1. 创建转码任务
// 执行命令生成任务:php artisan make:job TranscodeVideoJob namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use FFMpeg\FFMpeg; use FFMpeg\Format\Video\X264; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; class TranscodeVideoJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $filePath; protected $oldFile; protected $userId; // 可选,用于通知用户 public function __construct($filePath, $oldFile = null, $userId = null) { $this->filePath = $filePath; $this->oldFile = $oldFile; $this->userId = $userId; } public function handle() { try { $ffmpeg = FFMpeg::create([ 'ffmpeg.binaries' => '/usr/bin/ffmpeg', 'ffprobe.binaries' => '/usr/bin/ffprobe', 'timeout' => 3600, 'ffmpeg.threads' => 8, ], [ 'ffmpeg.options' => [ '-hwaccel' => 'cuda', '-hwaccel_device' => '0' ] ]); $video = $ffmpeg->open($this->filePath); $format = new X264(); $format->setAdditionalParameters([ '-preset' => 'veryfast', '-crf' => '23', '-c:a' => 'aac', '-b:a' => '128k' ]); $file_name = Str::random(20) . '.mp4'; $temp_path = storage_path('app/temp/' . $file_name); if (!file_exists(storage_path('app/temp'))) { mkdir(storage_path('app/temp'), 0755, true); } $video->save($format, $temp_path); $file_folder = 'uploads/video/'; Storage::disk('s3')->putFileAs($file_folder, $temp_path, $file_name); // 删除临时文件和原始上传文件 unlink($temp_path); unlink($this->filePath); // 删除旧文件 if ($this->oldFile) { $old_file_path = $file_folder . basename($this->oldFile); if (Storage::disk('s3')->exists($old_file_path)) { Storage::disk('s3')->delete($old_file_path); } } // 可选:通知用户转码完成(如通过WebSocket、邮件) // Example: broadcast(new VideoTranscodedEvent($this->userId, $file_name)); } catch (\Exception $e) { // 记录错误日志 \Log::error('视频转码失败:' . $e->getMessage()); } } }
2. 修改控制器方法
public function video_upload(Request $request) { $response = [ 'status' => false, 'data' => [] ]; $response_code = 200; $validator = Validator::make($request->all(), [ 'file' => 'required|mimes:mp4,webm,ogg,avi,mov' ]); if ($validator->fails()) { $response['data']['validator'] = $validator->errors(); return response()->json($response); } try { $file = $request->file('file'); // 先把原始文件存到本地临时目录 $temp_original_path = storage_path('app/temp/original_' . Str::random(20) . '.' . $file->getClientOriginalExtension()); $file->move(storage_path('app/temp'), basename($temp_original_path)); // 分发转码任务到队列 TranscodeVideoJob::dispatch($temp_original_path, $request->old_file, auth()->id()); $response['status'] = true; $response['data'] = '视频已提交,转码完成后将通知您'; } catch (\Exception $e) { $response['data']['message'] = $e->getMessage() . " line " . $e->getLine(); $response_code = 400; } return response()->json($response, $response_code); }
额外注意事项
- 确保服务器已安装带硬件加速支持的FFmpeg版本,可通过
ffmpeg -hwaccels命令查看支持的硬件加速选项; - 调整FFmpeg的线程数(
ffmpeg.threads)为服务器CPU核心数的1-2倍,最大化利用CPU; - 若业务允许,可限制前端上传视频的分辨率、码率,减少转码压力;
- 配置Laravel队列的驱动(如Redis、数据库),确保队列能高效处理任务。
内容的提问来源于stack exchange,提问作者Sara
相关产品推荐
相关产品推荐

