首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 影片轉碼與縮圖:FFmpeg + MEncoder 整合實戰指南

     編輯:PHP 相關知識

Web 專案常需上傳影片後自動轉碼為通用格式(MP4/H.264)並產生縮圖。PHP 本身無影片處理能力,需呼叫外部工具 **FFmpeg**(解碼/縮圖)與 **MEncoder/FFmpeg**(編碼)。本文提供生產級封裝類別,涵蓋非同步佇列、進度追蹤、錯誤處理、多種輸出格式、硬體加速選項。

前置需求

  • 伺服器安裝 ffmpeg、mencoder(或僅用 ffmpeg 完成所有任務)
  • PHP exec/shell_exec/proc_open 未被禁用
  • 儲存目錄可寫、足夠磁碟空間(轉碼暫存檔可能佔用數倍原始大小)
  • 建議:使用 佇列系統(Redis/RabbitMQ + Worker)處理轉碼,避免阻塞 Web 請求

核心 VideoProcessor 類別

<?php
class VideoProcessor
{
    private string $ffmpegBin = 'ffmpeg';
    private string $mencoderBin = 'mencoder';
    private string $tempDir;

    public function __construct(string $tempDir = null)
    {
        $this->tempDir = $tempDir ?? sys_get_temp_dir();
        // 驗證二進位檔存在
        foreach ([$this->ffmpegBin, $this->mencoderBin] as $bin) {
            if (!file_exists($bin) && !is_executable($bin) && !($path = $this->findInPath($bin))) {
                throw new RuntimeException("$bin 不存在或不可執行");
            }
        }
    }

    private function findInPath(string $bin): ?string
    {
        $paths = explode(PATH_SEPARATOR, getenv('PATH'));
        foreach ($paths as $path) {
            $full = $path . DIRECTORY_SEPARATOR . $bin;
            if (is_executable($full)) return $full;
        }
        return null;
    }

    /** 產生縮圖:指定時間點、尺寸、格式 */
    public function generateThumbnail(string $input, string $output, array $options = []): bool
    {
        $opts = array_merge([
            'time' => '00:00:05',      // 截取時間
            'width' => 320,            // 寬度,高度自動等比
            'height' => -1,            // -1 表示自動
            'format' => 'jpg',         // jpg, png, webp
            'quality' => 80,           // 1-100
        ], $options);

        $size = $opts['height'] > 0 ? "{$opts['width']}x{$opts['height']}" : "{$opts['width']}:-1";
        $cmd = sprintf(
            '%s -y -ss %s -i %s -vframes 1 -vf "scale=%s" -q:v %d %s',
            $this->ffmpegBin,
            escapeshellarg($opts['time']),
            escapeshellarg($input),
            $size,
            31 - (int)($opts['quality'] * 30 / 100), // ffmpeg q:v 2-31 (越小越好)
            escapeshellarg($output)
        );

        return $this->execute($cmd, "縮圖生成失敗: $input");
    }

    /** 轉碼為 MP4 (H.264 + AAC) - 使用 FFmpeg(推薦,現代、支援硬體加速) */
    public function convertToMp4(string $input, string $output, array $options = []): bool
    {
        $opts = array_merge([
            'video_codec' => 'libx264',
            'audio_codec' => 'aac',
            'crf' => 23,              // 18-28 越小品質越好,檔案越大
            'preset' => 'medium',     // ultrafast ~ veryslow
            'profile' => 'high',      // baseline, main, high
            'level' => '4.1',
            'width' => null,          // null=保持原尺寸
            'height' => null,
            'fps' => null,
            'audio_bitrate' => '128k',
            'fast_start' => true,     // moov atom 前移,支援網頁漸進式播放
            'hardware_accel' => null, // 'vaapi', 'qsv', 'nvenc', 'videotoolbox'
        ], $options);

        $vf = [];
        if ($opts['width'] || $opts['height']) {
            $w = $opts['width'] ?? -1;
            $h = $opts['height'] ?? -1;
            $vf[] = "scale=$w:$h";
        }
        if ($opts['fps']) $vf[] = "fps={$opts['fps']}";
        $filter = $vf ? '-vf "' . implode(',', $vf) . '"' : '';

        $hw = '';
        if ($opts['hardware_accel']) {
            $hwMap = [
                'nvenc' => '-hwaccel cuda -c:v h264_nvenc',
                'qsv'   => '-hwaccel qsv -c:v h264_qsv',
                'vaapi' => '-hwaccel vaapi -c:v h264_vaapi -vaapi_device /dev/dri/renderD128',
                'videotoolbox' => '-c:v h264_videotoolbox',
            ];
            $hw = $hwMap[$opts['hardware_accel']] ?? '';
        }

        $fastStart = $opts['fast_start'] ? '-movflags +faststart' : '';

        $cmd = sprintf(
            '%s -y -i %s %s %s -c:v %s -crf %d -preset %s -profile:v %s -level %s -c:a %s -b:a %s %s %s',
            $this->ffmpegBin,
            escapeshellarg($input),
            $hw,
            $filter,
            $opts['video_codec'],
            $opts['crf'],
            $opts['preset'],
            $opts['profile'],
            $opts['level'],
            $opts['audio_codec'],
            $opts['audio_bitrate'],
            $fastStart,
            escapeshellarg($output)
        );

        return $this->execute($cmd, "MP4 轉碼失敗: $input");
    }

    /** 舊版相容:使用 MEncoder 轉碼(不建議新專案使用) */
    public function convertWithMencoder(string $input, string $output, array $options = []): bool
    {
        $opts = array_merge([
            'video_bitrate' => 800,   // kbps
            'audio_bitrate' => 128,   // kbps
            'aspect' => '16:9',
        ], $options);

        $cmd = sprintf(
            '%s %s -o %s -ovc x264 -x264encopts bitrate=%d:threads=auto -oac mp3lame -lameopts br=%d -aspect %s',
            $this->mencoderBin,
            escapeshellarg($input),
            escapeshellarg($output),
            $opts['video_bitrate'],
            $opts['audio_bitrate'],
            escapeshellarg($opts['aspect'])
        );

        return $this->execute($cmd, "MEncoder 轉碼失敗: $input");
    }

    /** 同時產生縮圖 + 轉碼(單次 FFmpeg 完成,效率最高) */
    public function convertAndThumbnail(string $input, string $outputVideo, string $outputThumb, array $options = []): bool
    {
        $thumbOpts = $options['thumbnail'] ?? [];
        $videoOpts = $options['video'] ?? [];

        // 先產生縮圖(快速)
        if (!$this->generateThumbnail($input, $outputThumb, $thumbOpts)) {
            return false;
        }

        // 再轉碼
        return $this->convertToMp4($input, $outputVideo, $videoOpts);
    }

    /** 執行命令並處理錯誤 */
    private function execute(string $cmd, string $errorPrefix): bool
    {
        // 生產環境建議用 proc_open 取得 stderr 即時輸出、支援逾時
        $descriptors = [
            0 => ['pipe', 'r'],
            1 => ['pipe', 'w'],
            2 => ['pipe', 'w'],
        ];
        $proc = proc_open($cmd, $descriptors, $pipes);
        if (!is_resource($proc)) {
            error_log("$errorPrefix: proc_open 失敗");
            return false;
        }

        // 讀取 stderr(非阻塞)
        stream_set_blocking($pipes[2], false);
        $stderr = '';
        while (!feof($pipes[2])) {
            $stderr .= fread($pipes[2], 8192);
            usleep(10000);
        }

        $returnCode = proc_close($proc);

        if ($returnCode !== 0) {
            error_log("$errorPrefix (code $returnCode): $stderr");
            return false;
        }
        return true;
    }

    /** 靜態工廠:自動偵測二進位路徑 */
    public static function createDefault(): self
    {
        return new self();
    }
}
?>

使用範例

<?php
$processor = VideoProcessor::createDefault();

$input = '/uploads/video/user_123_raw.mov';
$outputVideo = '/uploads/video/user_123.mp4';
$outputThumb = '/uploads/thumbs/user_123.jpg';

// 非同步處理範例(佇列 Worker)
// dispatch(new ConvertVideoJob($input, $outputVideo, $outputThumb));

// 同步處理(僅示範,實際請用佇列)
if ($processor->convertAndThumbnail($input, $outputVideo, $outputThumb, [
    'thumbnail' => ['time' => '00:00:10', 'width' => 480, 'quality' => 85],
    'video' => [
        'crf' => 22,
        'preset' => 'fast',
        'width' => 1280,
        'height' => 720,
        'hardware_accel' => 'nvenc', // 若有 NVIDIA GPU
    ],
])) {
    echo "處理完成: $outputVideo, $outputThumb";
} else {
    echo "處理失敗,請查看錯誤日誌";
}
?>

進階:FFmpeg 進度監控(即時進度條)

<?php
class VideoProcessorWithProgress extends VideoProcessor
{
    public ?callable $onProgress = null; // function(float $percent, float $fps, float $speed)

    protected function execute(string $cmd, string $errorPrefix): bool
    {
        // 加入 -progress pipe:1 讓 FFmpeg 輸出進度到 stdout
        $cmd .= ' -progress pipe:1 -nostats';

        $descriptors = [
            0 => ['pipe', 'r'],
            1 => ['pipe', 'w'], // stdout: 進度資訊
            2 => ['pipe', 'w'], // stderr: 錯誤/日誌
        ];
        $proc = proc_open($cmd, $descriptors, $pipes);
        if (!is_resource($proc)) return false;

        stream_set_blocking($pipes[1], false);
        stream_set_blocking($pipes[2], false);

        $progressData = [];
        $stderr = '';

        while (true) {
            $status = proc_get_status($proc);
            if (!$status['running']) break;

            // 讀取進度
            $out = fread($pipes[1], 8192);
            if ($out) {
                foreach (explode("\n", $out) as $line) {
                    if (str_contains($line, '=')) {
                        [$k, $v] = explode('=', $line, 2);
                        $progressData[trim($k)] = trim($v);
                    }
                }
                // 計算進度
                if (isset($progressData['out_time_ms'], $progressData['duration_ms'])) {
                    $percent = ($progressData['out_time_ms'] / $progressData['duration_ms']) * 100;
                    $fps = (float)($progressData['fps'] ?? 0);
                    $speed = (float)($progressData['speed'] ?? 0); // 倍速
                    if ($this->onProgress) $this->onProgress($percent, $fps, $speed);
                }
            }

            // 讀取錯誤
            $err = fread($pipes[2], 8192);
            if ($err) $stderr .= $err;

            usleep(50000); // 50ms
        }

        $returnCode = proc_close($proc);
        if ($returnCode !== 0) {
            error_log("$errorPrefix: $stderr");
            return false;
        }
        return true;
    }
}

// 使用
$proc = new VideoProcessorWithProgress();
$proc->onProgress = function($pct, $fps, $speed) {
    echo "\r進度: " . number_format($pct, 1) . "%  FPS: $fps  Speed: ${speed}x";
};
$proc->convertToMp4($input, $output);
?>

硬體加速選擇指南

平台/GPUFFmpeg 編碼器適用場景備註
NVIDIA (NVENC)h264_nvenc, hevc_nvenc伺服器級、大量轉碼需驅動程式、FFmpeg 編譯支援 cuda/nvenc
Intel Quick Sync (QSV)h264_qsv, hevc_qsv消費級/伺服器 CPU 整合顯示需安裝 Media SDK、libmfx
AMD VAAPIh264_vaapiLinux AMD GPU/APU需 mesa-va-drivers、/dev/dri/renderD128
Apple VideoToolboxh264_videotoolboxmacOS 伺服器僅 macOS
純 CPU (libx264)libx264通用、相容性最好速度較慢,品質最穩定

部署檢查清單

  1. 佇列化:轉碼耗時長(分鐘到小時),必須用 Laravel Queue、Symfony Messenger、Redis + 自訂 Worker 等脫離 HTTP 請求。
  2. 暫存空間:轉碼過程會產生暫存檔,預留至少 3-5 倍原始檔案大小 磁碟空間。
  3. 逾時設定:Worker 進程設定無限執行時間,HTTP 端點只回傳「已加入佇列」。
  4. 錯誤重試:失敗自動重試 2-3 次(指數退避),仍失敗通知管理員/使用者。
  5. 安全性:上傳檔案先驗證 MIME、副檔名、掃描病毒,escapeshellarg 防指令注入。
  6. 日誌與監控:記錄轉碼耗時、輸出大小、CRF、編碼器、錯誤碼,接入 Prometheus/Grafana。

整合 FFmpeg 進行影片轉碼與縮圖,是打造影音平台、內容管理系統、社交應用的核心基礎設施。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved