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

PHP 結合 FFmpeg 取得影片縮圖與總長度完整實作

     編輯:PHP 相關知識

影音平台核心需求:上傳後自動產生縮圖、顯示總長度。本文示範 PHP 呼叫 FFmpeg (CLI) 非同步處理,含參數詳解、安全防命令注入、錯誤處理、佇列架構建議,並提供純 PHP 替代方案 (getID3)。

一、前置需求

  1. 伺服器安裝 FFmpeg:apt install ffmpeg / brew install ffmpeg / Windows 下載靜態編譯版
  2. PHP 能執行 exec()/shell_exec()/proc_open() (未被 disable_functions 限制)
  3. 上傳目錄可寫、FFmpeg 可讀取來源檔案

二、核心工具類封裝

<?php
namespace App\Media;

class FFmpegProcessor {
    private string $ffmpegPath;
    private string $ffprobePath;
    private int $timeout;

    public function __construct(string $ffmpegPath = 'ffmpeg', string $ffprobePath = 'ffprobe', int $timeout = 300) {
        $this->ffmpegPath = $ffmpegPath;
        $this->ffprobePath = $ffprobePath;
        $this->timeout = $timeout;

        // 驗證指令存在
        if (!$this->commandExists($ffmpegPath)) {
            throw new RuntimeException("FFmpeg 不存在: $ffmpegPath");
        }
        if (!$this->commandExists($ffprobePath)) {
            throw new RuntimeException("FFprobe 不存在: $ffprobePath");
        }
    }

    private function commandExists(string $cmd): bool {
        $result = @shell_exec("which " . escapeshellarg($cmd) . " 2>/dev/null");
        return !empty(trim($result));
    }

    /**
     * 取得影片資訊 (時長、解析度、編碼、位元率等)
     *
     * @param string $inputPath 影片絕對路徑
     * @return array ['duration' => 秒數(float), 'width', 'height', 'codec', 'bitrate', 'format', ...]
     * @throws RuntimeException
     */
    public function getVideoInfo(string $inputPath): array {
        if (!file_exists($inputPath) || !is_readable($inputPath)) {
            throw new InvalidArgumentException("檔案不存在或不可讀: $inputPath");
        }

        // 使用 ffprobe JSON 輸出,易於解析
        $cmd = sprintf(
            '%s -v quiet -print_format json -show_format -show_streams %s',
            escapeshellarg($this->ffprobePath),
            escapeshellarg($inputPath)
        );

        $output = $this->executeCommand($cmd);
        $data = json_decode($output, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new RuntimeException("ffprobe JSON 解析失敗: " . json_last_error_msg());
        }

        $videoStream = null;
        $audioStream = null;
        foreach ($data['streams'] ?? [] as $stream) {
            if (($stream['codec_type'] ?? '') === 'video' && !$videoStream) {
                $videoStream = $stream;
            }
            if (($stream['codec_type'] ?? '') === 'audio' && !$audioStream) {
                $audioStream = $stream;
            }
        }

        $format = $data['format'] ?? [];

        return [
            'duration'   => (float)($format['duration'] ?? $videoStream['duration'] ?? 0),
            'width'      => (int)($videoStream['width'] ?? 0),
            'height'     => (int)($videoStream['height'] ?? 0),
            'video_codec'=> $videoStream['codec_name'] ?? null,
            'audio_codec'=> $audioStream['codec_name'] ?? null,
            'bitrate'    => (int)($format['bit_rate'] ?? 0),
            'format'     => $format['format_name'] ?? null,
            'size'       => (int)($format['size'] ?? 0),
            'fps'        => $this->parseFps($videoStream['r_frame_rate'] ?? '0/0'),
            'raw'        => $data, // 完整資料供進階用途
        ];
    }

    private function parseFps(string $fpsStr): float {
        if (str_contains($fpsStr, '/')) {
            [$num, $den] = explode('/', $fpsStr);
            return $den != 0 ? (float)$num / (float)$den : 0;
        }
        return (float)$fpsStr;
    }

    /**
     * 擷取縮圖 (關鍵幀)
     *
     * @param string $inputPath  來源影片路徑
     * @param string $outputPath 輸出圖片路徑 (副檔名決定格式 .jpg/.png/.webp)
     * @param float  $time       擷取時間點 (秒),預設 1 秒
     * @param int    $width      輸出寬度 (高度自動等比),0 保持原尺寸
     * @return bool 成功與否
     */
    public function extractThumbnail(
        string $inputPath,
        string $outputPath,
        float $time = 1.0,
        int $width = 320
    ): bool {
        if (!file_exists($inputPath)) {
            throw new InvalidArgumentException("來源檔案不存在: $inputPath");
        }

        // 確保輸出目錄存在
        $outDir = dirname($outputPath);
        if (!is_dir($outDir)) {
            mkdir($outDir, 0755, true);
        }

        $scaleFilter = $width > 0 ? "scale=$width:-1" : "scale=iw:ih";

        $cmd = sprintf(
            '%s -y -ss %.3f -i %s -vframes 1 -vf %s %s',
            escapeshellarg($this->ffmpegPath),
            $time,
            escapeshellarg($inputPath),
            escapeshellarg($scaleFilter),
            escapeshellarg($outputPath)
        );

        $this->executeCommand($cmd);

        return file_exists($outputPath) && filesize($outputPath) > 0;
    }

    /**
     * 批次擷取多張縮圖 (如每 10 秒一張、或固定張數均勻分佈)
     */
    public function extractThumbnailsInterval(
        string $inputPath,
        string $outputDir,
        float $interval = 10.0,
        int $width = 320,
        string $prefix = 'thumb_'
    ): array {
        $info = $this->getVideoInfo($inputPath);
        $duration = $info['duration'];
        if ($duration <= 0) return [];

        $count = max(1, (int)($duration / $interval));
        $outputs = [];

        for ($i = 1; $i <= $count; $i++) {
            $time = min($i * $interval, $duration - 0.5);
            $outputPath = $outputDir . '/' . $prefix . str_pad($i, 3, '0', STR_PAD_LEFT) . '.jpg';
            if ($this->extractThumbnail($inputPath, $outputPath, $time, $width)) {
                $outputs[] = $outputPath;
            }
        }
        return $outputs;
    }

    /**
     * 執行指令並處理錯誤
     */
    private function executeCommand(string $cmd): string {
        // proc_open 可分離 stdout/stderr,取得 exit code
        $descriptors = [
            1 => ['pipe', 'w'], // stdout
            2 => ['pipe', 'w'], // stderr
        ];
        $proc = proc_open($cmd, $descriptors, $pipes);
        if (!is_resource($proc)) {
            throw new RuntimeException("無法啟動程序: $cmd");
        }

        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[1]);
        fclose($pipes[2]);

        $returnCode = proc_close($proc);

        if ($returnCode !== 0) {
            throw new RuntimeException(
                "指令執行失敗 (exit $returnCode): $cmd\nStderr: $stderr"
            );
        }
        return $stdout;
    }
}

三、使用範例

<?php
require_once 'vendor/autoload.php';

use App\Media\FFmpegProcessor;

$processor = new FFmpegProcessor('/usr/bin/ffmpeg', '/usr/bin/ffprobe');

$videoPath = '/var/www/uploads/video_123.mp4';
$thumbPath = '/var/www/uploads/thumbs/video_123.jpg';

try {
    // 1. 取得資訊
    $info = $processor->getVideoInfo($videoPath);
    echo "時長: {$info['duration']} 秒\n";
    echo "解析度: {$info['width']}x{$info['height']}\n";
    echo "編碼: {$info['video_codec']}\n";
    echo "位元率: " . number_format($info['bitrate'] / 1000) . " kbps\n";

    // 2. 擷取縮圖 (第 5 秒,寬 480px)
    if ($processor->extractThumbnail($videoPath, $thumbPath, 5.0, 480)) {
        echo "縮圖產生成功: $thumbPath\n";
    }

    // 3. 批次擷取 (每 30 秒一張)
    $thumbs = $processor->extractThumbnailsInterval($videoPath, '/var/www/uploads/thumbs/video_123/', 30.0, 320);
    echo "共產生 " . count($thumbs) . " 張縮圖\n";

} catch (Exception $e) {
    error_log("FFmpeg 處理失敗: " . $e->getMessage());
    echo "處理失敗: " . $e->getMessage();
}

四、安全性:防命令注入

所有使用者輸入路徑/參數必須經 escapeshellarg() 處理,如上方程式碼所示。嚴禁**直接字串拼接**:

// ❌ 極度危險
$cmd = "ffmpeg -i $userInput -ss $time ...";

// ✅ 正確
$cmd = sprintf('ffmpeg -i %s -ss %s ...', escapeshellarg($userInput), escapeshellarg($time));
  • 檔案路徑:escapeshellarg($path)
  • 數值參數:escapesharg((string)$num) 或 (float)$num 確保型別
  • 禁用 shell_exec 改用 proc_open 避免 Shell 解析

五、效能與架構建議

場景建議方案
少量上傳、即時需求同步呼叫 (上方範例),設定合理 timeout
大量上傳、長影片佇列非同步:上傳後寫入 Redis/RabbitMQ,Worker 處理
多規格轉碼 (HLS/DASH)FFmpeg 複雜濾鏡圖、或使用 PHP-FFMpeg 物件導向封裝
雲端儲存 (S3/OSS)預簽名 URL 下載 → 處理 → 上傳回雲端,或掛載 FUSE
無法安裝 FFmpeg (共享主機)純 PHP 解析:getID3 (僅資訊,無法擷取縮圖/轉碼)

六、純 PHP 替代方案:getID3 (僅資訊讀取)

composer require james-heinrich/getid3

<?php
require_once 'vendor/autoload.php';
$getID3 = new getID3;
$info = $getID3->analyze('/path/video.mp4');
echo "時長: " . $info['playtime_seconds'] . " 秒\n";
echo "解析度: {$info['video']['resolution_x']}x{$info['video']['resolution_y']}\n";
echo "編碼: {$info['video']['codec']}\n";
// 無法擷取縮圖、轉碼

七、常見錯誤排查

錯誤原因解決
sh: ffmpeg: command not foundPATH 不含 ffmpeg、未安裝指定絕對路徑 /usr/bin/ffmpeg 或安裝
Permission deniedPHP 使用者無執行權限chmod +x /usr/bin/ffmpeg、檢查 disable_functions
輸出檔案 0 bytes時間點超過影片長度、關鍵幀間距大調整 -ss 時間、嘗試 -ss 00:00:01 格式
記憶體耗盡大檔案處理、佇列堆積限制並發 Worker 數、分片處理

八、總結

PHP 整合 FFmpeg 是影音處理標準做法:ffprobe 取資訊 (JSON)、ffmpeg 擷取縮圖/轉碼。關鍵在於 escapeshellarg() 防注入、proc_open 錯誤處理、佇列非同步架構。無法安裝 FFmpeg 環境可用 getID3 讀取資訊,但無法處理影音內容。建立健全的 MediaProcessor 服務類,即可支撐上傳、轉碼、縮圖、串流等完整影音工作流。

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