技术分享

Laravel 中使用 FFmpeg 进行视频处理

作者头像 人称外号大脸猫
49 阅读
Laravel 中使用 FFmpeg 进行视频处理

在当今视频内容为主的时代,为网站添加视频处理功能已成为刚需。今天,我们将深入探讨如何在 Laravel 项目中集成 FFmpeg,实现强大的视频处理能力。

为什么选择 FFmpeg?

FFmpeg 是一个完整的、跨平台的音视频解决方案,可以用于录制、转换、流式传输和编辑音视频内容。它被许多知名公司使用,包括 YouTube、Facebook 和 VLC 播放器。

主要优势:

  • 开源免费
  • 功能强大,支持几乎所有音视频格式
  • 跨平台支持
  • 丰富的滤镜和效果

环境准备

1. 安装 FFmpeg

Ubuntu/Debian:

sudo apt update
sudo apt install ffmpeg

macOS:

brew install ffmpeg

Windows:FFmpeg 官网 下载并配置环境变量

2. 安装 Laravel 扩展包

我们推荐使用 pbmedia/laravel-ffmpeg,它对 Laravel 有更好的集成:

composer require pbmedia/laravel-ffmpeg

发布配置文件:

php artisan vendor:publish --provider="ProtoneMedia\LaravelFFMpeg\Support\ServiceProvider"

基础配置

配置文件

config/ffmpeg.php 中配置 FFmpeg 路径:

<?php

return [
    'ffmpeg' => [
        'binaries' => env('FFMPEG_BINARIES', 'ffmpeg'),
        'threads' => 12,
    ],

    'ffprobe' => [
        'binaries' => env('FFPROBE_BINARIES', 'ffprobe'),
    ],

    'timeout' => 3600,
];

.env 文件中设置路径:

FFMPEG_BINARIES=/usr/bin/ffmpeg
FFPROBE_BINARIES=/usr/bin/ffprobe

核心功能实现

创建视频处理服务

<?php

namespace App\Services;

use ProtoneMedia\LaravelFFMpeg\Support\FFMpeg;

class VideoProcessorService
{
    /**
     * 剪辑视频片段
     */
    public function clipVideo($inputPath, $outputPath, $startTime, $duration)
    {
        return FFMpeg::fromDisk('local')
            ->open($inputPath)
            ->export()
            ->toDisk('local')
            ->inFormat(new \FFMpeg\Format\Video\X264('aac'))
            ->addFilter(['-ss', $startTime, '-t', $duration])
            ->save($outputPath);
    }

    /**
     * 生成视频缩略图
     */
    public function generateThumbnail($videoPath, $thumbnailPath, $time = 10)
    {
        return FFMpeg::fromDisk('local')
            ->open($videoPath)
            ->getFrameFromSeconds($time)
            ->export()
            ->toDisk('local')
            ->save($thumbnailPath);
    }

    /**
     * 调整视频尺寸
     */
    public function resizeVideo($inputPath, $outputPath, $width, $height)
    {
        return FFMpeg::fromDisk('local')
            ->open($inputPath)
            ->export()
            ->toDisk('local')
            ->inFormat(new \FFMpeg\Format\Video\X264('aac'))
            ->resize($width, $height)
            ->save($outputPath);
    }

    /**
     * 获取视频信息
     */
    public function getVideoInfo($videoPath)
    {
        $media = FFMpeg::fromDisk('local')->open($videoPath);
        
        return [
            'duration' => $media->getDurationInSeconds(),
            'width'    => $media->getStreams()->first()->get('width'),
            'height'   => $media->getStreams()->first()->get('height'),
            'format'   => $media->getFormat(),
        ];
    }
}

控制器实现

<?php

namespace App\Http\Controllers;

use App\Services\VideoProcessorService;
use Illuminate\Http\Request;

class VideoController extends Controller
{
    protected $videoProcessor;

    public function __construct(VideoProcessorService $videoProcessor)
    {
        $this->videoProcessor = $videoProcessor;
    }

    public function processVideo(Request $request)
    {
        $request->validate([
            'video' => 'required|file|mimes:mp4,avi,mov|max:102400',
            'start_time' => 'required|numeric',
            'duration' => 'required|numeric',
        ]);

        try {
            // 保存上传的视频
            $videoPath = $request->file('video')->store('videos/temp');
            
            // 处理视频
            $outputPath = 'videos/processed/' . uniqid() . '.mp4';
            $this->videoProcessor->clipVideo(
                $videoPath,
                $outputPath,
                $request->start_time,
                $request->duration
            );

            // 生成缩略图
            $thumbnailPath = 'thumbnails/' . uniqid() . '.jpg';
            $this->videoProcessor->generateThumbnail($videoPath, $thumbnailPath);

            return response()->json([
                'success' => true,
                'video_url' => Storage::url($outputPath),
                'thumbnail_url' => Storage::url($thumbnailPath),
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '视频处理失败: ' . $e->getMessage(),
            ], 500);
        }
    }
}

高级功能

视频转码

public function transcodeVideo($inputPath, $outputPath, $format = 'mp4')
{
    $formatMap = [
        'mp4' => new \FFMpeg\Format\Video\X264('aac'),
        'webm' => new \FFMpeg\Format\Video\WebM(),
        'ogg' => new \FFMpeg\Format\Video\Ogg(),
    ];

    $format = $formatMap[$format] ?? $formatMap['mp4'];

    return FFMpeg::fromDisk('local')
        ->open($inputPath)
        ->export()
        ->toDisk('local')
        ->inFormat($format)
        ->save($outputPath);
}

添加水印

public function addWatermark($inputPath, $outputPath, $watermarkPath)
{
    return FFMpeg::fromDisk('local')
        ->open($inputPath)
        ->addFilter(function ($filters) use ($watermarkPath) {
            $filters->watermark($watermarkPath, [
                'position' => 'relative',
                'bottom' => 10,
                'right' => 10,
            ]);
        })
        ->export()
        ->toDisk('local')
        ->save($outputPath);
}

提取音频

public function extractAudio($videoPath, $audioPath)
{
    return FFMpeg::fromDisk('local')
        ->open($videoPath)
        ->export()
        ->toDisk('local')
        ->inFormat(new \FFMpeg\Format\Audio\Mp3())
        ->save($audioPath);
}

处理大文件 - 使用队列

对于大文件处理,建议使用队列避免请求超时:

<?php

namespace App\Jobs;

use App\Services\VideoProcessorService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessVideoJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $videoPath;
    protected $startTime;
    protected $duration;

    public function __construct($videoPath, $startTime, $duration)
    {
        $this->videoPath = $videoPath;
        $this->startTime = $startTime;
        $this->duration = $duration;
    }

    public function handle(VideoProcessorService $videoProcessor)
    {
        $outputPath = 'videos/processed/' . uniqid() . '.mp4';
        
        $videoProcessor->clipVideo(
            $this->videoPath,
            $outputPath,
            $this->startTime,
            $this->duration
        );

        // 发送通知或更新处理状态
    }
}

在控制器中使用队列:

ProcessVideoJob::dispatch($videoPath, $startTime, $duration);

常见问题及解决方案

1. open_basedir 限制错误

错误信息:

file_exists(): open_basedir restriction in effect.

解决方案:

方法一:修改 PHP 配置

open_basedir = "/your/web/root/:/tmp/:/path/to/ffmpeg/"

方法二:复制 FFmpeg 到项目目录

cp /usr/bin/ffmpeg /www/wwwroot/your-project/bin/
cp /usr/bin/ffprobe /www/wwwroot/your-project/bin/

2. 内存不足错误

增加 PHP 内存限制:

memory_limit = 512M

3. 执行超时

设置合适的超时时间:

'timeout' => 3600, // 1小时

性能优化建议

  1. 使用合适的视频格式:MP4 通常是最佳选择
  2. 合理设置视频参数:根据需求调整分辨率、码率等
  3. 启用硬件加速(如果可用)
  4. 使用队列处理大文件
  5. 合理设置并发线程数

安全注意事项

  1. 验证上传文件类型:防止上传恶意文件
  2. 限制文件大小:避免服务器资源耗尽
  3. 设置合理的超时时间:防止长时间占用进程
  4. 处理失败情况:完善的错误处理和日志记录

总结

在 Laravel 中集成 FFmpeg 可以为你的应用添加强大的视频处理能力。通过使用 pbmedia/laravel-ffmpeg 包,我们可以避免许多底层配置的复杂性,专注于业务逻辑的实现。

主要优势:

  • 🎯 简单易用的 API
  • 🔧 与 Laravel 完美集成
  • 📦 丰富的功能支持
  • 🚀 良好的性能表现

无论你是要构建视频分享平台、在线教育系统,还是内容管理系统,FFmpeg 都能为你的 Laravel 应用提供强大的视频处理能力。