You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 20:25:17