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

PHP 解析 FLV 檔案獲取影片時長:二進制 Header 解析實現

     編輯:PHP 相關知識

FLV (Flash Video) 格式雖已淡出主流,但在舊系統遷移、影片資產審計、轉碼前置檢查場景仍需解析時長。本文實現純 PHP 解析 FLV Header 與 Tag 結構,提取 duration (毫秒),無需 FFmpeg 等外部工具。

FLV 檔案結構簡述

[FLV Header: 9-13 bytes]
  Signature: 'FLV' (3 bytes)
  Version: 1 byte
  Flags: 1 byte (audio/video)
  DataOffset: 4 bytes (UI32)

[Previous Tag Size: 4 bytes] = 0

[Tag 1]
  Tag Header: 11 bytes
    Type: 1 byte (8=audio, 9=video, 18=script)
    DataSize: 3 bytes (UI24)
    Timestamp: 3 bytes (UI24)
    TimestampExtended: 1 byte
    StreamID: 3 bytes (always 0)
  Tag Data: DataSize bytes
[Previous Tag Size: 4 bytes] = Tag Header + Tag Data size

[Tag 2] ...
...
[EOF]

關鍵點:Script Data Tag (onMetaData)

  • 類型 18 (0x12),通常為第 1 或第 2 個 Tag
  • 包含 AMF 編碼的中繼資料:duration、width、height、framerate、videodatarate 等
  • 若無 Script Tag,則掃描所有 Video Tag 的 Timestamp 取最大值

完整解析類實現

<?php
/**
 * FLV 時長解析器
 * 純 PHP 實現,無外部依賴
 */
class FLVDurationParser
{
    private string $filePath;
    private resource $fp;
    private int $fileSize;
    private array $header = [];
    
    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
    }
    
    public function getDuration(): float
    {
        if (!file_exists($this->filePath)) {
            throw new Exception("File not found: {$this->filePath}");
        }
        
        $this->fileSize = filesize($this->filePath);
        $this->fp = fopen($this->filePath, 'rb');
        if (!$this->fp) {
            throw new Exception("Cannot open file: {$this->filePath}");
        }
        
        try {
            $this->parseHeader();
            $duration = $this->parseTags();
            return $duration / 1000.0; // 返回秒數 (float)
        } finally {
            fclose($this->fp);
        }
    }
    
    private function parseHeader(): void
    {
        $header = fread($this->fp, 13);
        if (strlen($header) < 9) throw new Exception('Invalid FLV header');
        
        $signature = substr($header, 0, 3);
        if ($signature !== 'FLV') throw new Exception('Not an FLV file');
        
        $this->header['version'] = ord($header[3]);
        $this->header['flags'] = ord($header[4]);
        $this->header['data_offset'] = $this->readUI32(substr($header, 5, 4));
        
        // 移至第一個 Tag (跳過 PreviousTagSize0 = 4 bytes)
        fseek($this->fp, $this->header['data_offset'], SEEK_SET);
    }
    
    private function parseTags(): int
    {
        $maxTimestamp = 0;
        $foundMetadata = false;
        
        while (!feof($this->fp) && ftell($this->fp) < $this->fileSize) {
            $tagHeader = fread($this->fp, 11);
            if (strlen($tagHeader) < 11) break;
            
            $tagType = ord($tagHeader[0]);
            $dataSize = $this->readUI24(substr($tagHeader, 1, 3));
            $timestamp = $this->readUI24(substr($tagHeader, 4, 3));
            $timestampExt = ord($tagHeader[7]);
            $timestamp = ($timestampExt << 24) | $timestamp; // UI32
            $streamId = $this->readUI24(substr($tagHeader, 8, 3)); // 應為 0
            
            $tagDataStart = ftell($this->fp);
            $tagDataEnd = $tagDataStart + $dataSize;
            
            // Script Data Tag (18) - 嘗試提取 onMetaData duration
            if ($tagType === 18 && !$foundMetadata) {
                $metadataDuration = $this->parseScriptData($dataSize);
                if ($metadataDuration > 0) {
                    fclose($this->fp);
                    return $metadataDuration; // 直接返回,最準確
                }
                $foundMetadata = true;
            }
            
            // Video Tag (9) - 記錄最大時間戳作為備選
            if ($tagType === 9) {
                if ($timestamp > $maxTimestamp) $maxTimestamp = $timestamp;
            }
            
            // Audio Tag (8) - 同理
            if ($tagType === 8) {
                if ($timestamp > $maxTimestamp) $maxTimestamp = $timestamp;
            }
            
            // 跳過 Tag Data
            fseek($this->fp, $tagDataEnd, SEEK_SET);
            
            // 跳過 Previous Tag Size (4 bytes)
            fseek($this->fp, 4, SEEK_CUR);
        }
        
        return $maxTimestamp; // 毫秒
    }
    
    /**
     * 解析 Script Data (AMF0) 尋找 duration
     * 簡易實現:尋找 "duration" 字串後的 8 字節 double
     */
    private function parseScriptData(int $dataSize): int
    {
        $data = fread($this->fp, $dataSize);
        if (strlen($data) !== $dataSize) return 0;
        
        // 尋找 "onMetaData" 字串 (AMF0 字串格式: 0x02 + length(UI16) + bytes)
        // 簡化:直接搜尋 "duration" 關鍵字
        $pos = strpos($data, 'duration');
        if ($pos !== false) {
            // duration 通常緊接著:字串結束後為 AMF0 Number (0x00 + 8 bytes double)
            $numPos = $pos + 8; // "duration" 長度 8
            // 往後找 0x00 (AMF0 Number 標識)
            for ($i = $numPos; $i < min($numPos + 10, $dataSize); $i++) {
                if (ord($data[$i]) === 0x00) {
                    // 讀取 8 字節 double (大端序)
                    if ($i + 8 < $dataSize) {
                        $doubleBytes = substr($data, $i + 1, 8);
                        $duration = $this->readDoubleBE($doubleBytes);
                        return (int)round($duration * 1000); // 秒轉毫秒
                    }
                }
            }
        }
        
        // 兜底:尋找第一個 double 值 (假設為 duration)
        // 實際 AMF0 解析較複雜,建議生產環境用 php-amf 或 ffprobe
        return 0;
    }
    
    // 工具方法:大端序讀取
    private function readUI24(string $bytes): int
    {
        return (ord($bytes[0]) << 16) | (ord($bytes[1]) << 8) | ord($bytes[2]);
    }
    
    private function readUI32(string $bytes): int
    {
        return (ord($bytes[0]) << 24) | (ord($bytes[1]) << 16) | (ord($bytes[2]) << 8) | ord($bytes[3]);
    }
    
    private function readDoubleBE(string $bytes): float
    {
        // PHP unpack 大端序 double
        $arr = unpack('d', strrev($bytes)); // x86 小端序,需反轉
        return $arr[1] ?? 0.0;
    }
    
    // 靜態快捷方法
    public static function getDuration(string $filePath): float
    {
        return (new self($filePath))->getDuration();
    }
    
    public static function getFormattedDuration(string $filePath): string
    {
        $seconds = self::getDuration($filePath);
        $h = (int)($seconds / 3600);
        $m = (int)(($seconds % 3600) / 60);
        $s = (int)($seconds % 60);
        return sprintf('%02d:%02d:%02d', $h, $m, $s);
    }
}

使用範例

<?php
require 'FLVDurationParser.php';

$flvFile = 'video/sample.flv';

try {
    // 獲取秒數 (float)
    $durationSec = FLVDurationParser::getDuration($flvFile);
    echo "Duration: {$durationSec} 秒\n";
    
    // 獲取格式化 HH:MM:SS
    $formatted = FLVDurationParser::getFormattedDuration($flvFile);
    echo "Formatted: {$formatted}\n";
    
    // 批次處理
    $files = glob('videos/*.flv');
    foreach ($files as $file) {
        $dur = FLVDurationParser::getFormattedDuration($file);
        echo basename($file) . " => $dur\n";
    }
    
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}

效能與限制

指標表現
解析速度~5-20 MB/s (純 PHP 循環),100MB 檔案約 5-10 秒
記憶體常數級 (僅緩衝 Tag Data),不載入整檔
準確度有 onMetaData 時極高;無則依賴最大 Timestamp (可能比實際略長)
支援格式標準 FLV (H.263/VP6/H.264 + MP3/AAC)
不支援加密 FLV、F4V (MP4 容器)、Live FLV (無限流)

生產環境建議:改用 FFprobe

function getDurationFFprobe(string $file): float
{
    $cmd = "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 " . escapeshellarg($file);
    $output = shell_exec($cmd);
    return (float)trim($output);
}

// 或使用 PHP-FFmpeg 庫
// $probe = FFMpeg\FFProbe::create();
// $duration = $probe->format($file)->get('duration');

優勢:支援所有容器格式、準確度 100%、解析速度快 (只讀 Header)、C 語言實現穩定。

適用場景判斷

  • ✅ **純 PHP 方案**:無法安裝 FFmpeg 的共享主機、快速原型、CI/CD 輕量檢查、舊系統遷移腳本
  • ❌ **不建議**:高併發 API、大檔案 (GB 級)、需精準關鍵幀資訊、現代專案 (MP4/WebM 為主)

FLV 解析是理解二進制容器格式、AMF 編碼、大端序處理的優秀練習。但實際專案中,**FFprobe 是標準答案**,純 PHP 實現僅作為受限環境的備選或教學用途。

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