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

PHP 解析 FLV 視頻時長:基於二進制流讀取的完整實現

     編輯:PHP 綜合知識

FLV 文件結構基礎

FLV (Flash Video) 文件由 **Header (9-13 bytes)** + **Body (連續 Tags)** 組成。每個 Tag 包含:

  • Tag Header (11 bytes):Type (1B) + DataSize (3B) + Timestamp (3B) + TimestampExtended (1B) + StreamID (3B)。
  • Tag Data:音頻/視頻/腳本數據。
  • PrevTagSize (4 bytes):前一個 Tag 總大小 (含 Header+Data),用於反向求索。

視頻時長信息通常位於 **第一個 Script Data Tag (Type=18)** 中的 onMetaData 事件裡 (duration 字段),或通過遍歷所有視頻 Tag 取**最大 Timestamp** 推算 (適用於無元數據文件)。

核心解析邏輯

  1. 讀取 Header,驗證簽名 FLV,跳過 Header 體 (含可選擴展字段)。
  2. 循環讀取 Tag Header (先讀 11 字節,解析 DataSize、Timestamp)。
  3. 若為 Script Tag (Type=18) 且為首個,嘗試解析 AMF 編碼的 duration。
  4. 否則記錄所有視頻 Tag (Type=9) 的 Timestamp,取最大值。
  5. 根據 PrevTagSize 跳轉至下一 Tag (或直接 fseek 跳過 Data)。
  6. 返回時長 (毫秒 → 秒)。

完整 PHP 實現 (無第三方依賴)

<?php
/**
 * 獲取 FLV 視頻時長 (秒,浮點數)
 * @param string $filePath
 * @return float|false 失敗返回 false
 */
function getFlvDuration(string $filePath) {
    if (!file_exists($filePath)) return false;
    $fp = fopen($filePath, 'rb');
    if (!$fp) return false;

    // 1. 讀取 Header (9 bytes 固定 + 可選擴展)
    $header = fread($fp, 9);
    if (strlen($header) !== 9 || substr($header, 0, 3) !== 'FLV') {
        fclose($fp);
        return false;
    }
    $flags = ord($header[4]);
    $hasAudio = ($flags & 0x04) !== 0;
    $hasVideo = ($flags & 0x01) !== 0;
    $headerSize = 9;
    // 跳過擴展區域 (DataOffset 指向 Body 起始)
    $dataOffset = unpack('N', fread($fp, 4))[1]; // Big-Endian uint32
    if ($dataOffset > $headerSize) {
        fseek($fp, $dataOffset, SEEK_SET);
    }

    $duration = 0.0;
    $firstScriptTag = true;

    // 2. 循環解析 Tag
    while (!feof($fp)) {
        $tagHeader = fread($fp, 11);
        if (strlen($tagHeader) !== 11) break; // 文件截斷

        $tagType = ord($tagHeader[0]);
        $dataSize = (ord($tagHeader[1]) << 16) | (ord($tagHeader[2]) << 8) | ord($tagHeader[3]); // 24-bit BE
        $timestamp = (ord($tagHeader[4]) << 16) | (ord($tagHeader[5]) << 8) | ord($tagHeader[6]); // 24-bit BE
        $timestampExt = ord($tagHeader[7]); // 延伸 8-bit
        $timestamp = ($timestampExt << 24) | $timestamp; // 完整 32-bit 毫秒

        // 更新最長時間戳 (視頻 Tag)
        if ($tagType === 9) { // Video
            $duration = max($duration, $timestamp / 1000.0);
        }

        // 嘗試從首個 Script Tag 解析 duration (AMF0)
        if ($firstScriptTag && $tagType === 18) { // Script Data
            $firstScriptTag = false;
            $data = fread($fp, $dataSize);
            $metaDuration = parseAmfDuration($data);
            if ($metaDuration !== null) {
                fclose($fp);
                return $metaDuration; // 元數據更準確
            }
            // 解析失敗,繼續按 Timestamp 統計
            fseek($fp, -$dataSize, SEEK_CUR); // 回退重讀 Data 以便跳過
        }

        // 跳過 Tag Data + PrevTagSize (4 bytes)
        fseek($fp, $dataSize + 4, SEEK_CUR);
    }

    fclose($fp);
    return $duration > 0 ? $duration : false;
}

/**
 * 簡易 AMF0 解析:查找 onMetaData -> duration
 * 僅處理常見結構,生產建議使用 SabreAMF 等成熟庫
 */
function parseAmfDuration(string $data): ?float {
    // 查找 "onMetaData" 字符串 (AMF0 String: 0x02 + 16-bit len + bytes)
    $pos = strpos($data, "\x02\x00\x0AonMetaData"); // 簡化匹配
    if ($pos === false) return null;
    $pos += 12; // 跳過字符串頭
    // 接應為 ECMA Array (0x08) 或 Object (0x03)
    if ($pos >= strlen($data)) return null;
    $marker = ord($data[$pos++]);
    if ($marker !== 0x08 && $marker !== 0x03) return null; // 非數組/對象

    // 簡易遍歷鍵值對查找 "duration"
    while ($pos < strlen($data)) {
        // Key: String (0x02 + len + bytes)
        if ($pos + 2 > strlen($data)) break;
        if (ord($data[$pos]) !== 0x02) break;
        $keyLen = (ord($data[$pos+1]) << 8) | ord($data[$pos+2]);
        $pos += 3;
        if ($pos + $keyLen > strlen($data)) break;
        $key = substr($data, $pos, $keyLen);
        $pos += $keyLen;
        if ($key !== 'duration') {
            // 跳過 Value (簡略:根據類型標記跳過)
            if ($pos >= strlen($data)) break;
            $valType = ord($data[$pos]);
            $pos = skipAmfValue($data, $pos);
            continue;
        }
        // Value: Number (0x00 + 8-byte IEEE754 double)
        if ($pos >= strlen($data) || ord($data[$pos]) !== 0x00) return null;
        $pos++;
        if ($pos + 8 > strlen($data)) return null;
        $bytes = substr($data, $pos, 8);
        $duration = unpack('d', strrev($bytes))[1]; // Big-Endian double
        return $duration;
    }
    return null;
}

function skipAmfValue(string $data, int $pos): int {
    if ($pos >= strlen($data)) return $pos;
    $type = ord($data[$pos]);
    switch ($type) {
        case 0x00: return $pos + 9;      // Number (1+8)
        case 0x01: return $pos + 2;      // Boolean (1+1)
        case 0x02:                       // String (1+2+len)
            if ($pos + 2 >= strlen($data)) return strlen($data);
            $len = (ord($data[$pos+1]) << 8) | ord($data[$pos+2]);
            return $pos + 3 + $len;
        case 0x03: case 0x08: case 0x0A: // Object/ECMA Array/Strict Array - 簡略跳過
            return strlen($data); // 實際需遞歸解析,此處直接跳至末尾
        default: return $pos + 1;
    }
}

// 使用
$seconds = getFlvDuration('video.flv');
if ($seconds !== false) {
    echo "時長: " . gmdate('H:i:s', $seconds) . " ($seconds 秒)";
} else {
    echo "解析失敗";
}

關鍵技術點說明

  • 大端序讀取:FLV 所有多字節整數均為 Big-Endian,PHP unpack('N', ...) (32-bit) 或手動位運算 (24-bit) 處理。
  • Timestamp 擴展:3 字節基礎時間戳 + 1 字節擴展 = 32-bit 毫秒,最大約 49.7 天,長視頻需注意回卷。
  • AMF0 解析:onMetaData 為 ECMA Array,包含 duration (Number)、width/height 等。代碼實現簡易查找,複雜文件建議引入 sabre/amf。
  • 性能考量:僅順序讀取 Tag Header,不載入完整文件,內存佔用 O(1),適合大文件。

現代替代方案

  • FFprobe (FFmpeg):ffprobe -v error -show_entries format=duration -of csv=p=0 input.flv,支持所有容器格式、編碼、元數據,準確度最高,推薦生產使用。
  • getID3() PHP 庫:純 PHP 解析 30+ 格式,含 FLV,API 友好:$getID3->analyze($file)['playtime_seconds']。
  • Imagick/FFmpeg PHP 擴展:$ffmpeg = new FFmpeg(); $probe = $ffmpeg->probe($file); $duration = $probe->getDuration();。

總結

通過直接讀取 FLV 二進制流,解析 Tag 結構與 AMF 元數據,可在無外部依賴環境下準確獲取視頻時長。代碼展示了大端序解析、位運算組合時間戳、簡易 AMF0 遍歷等核心技術。生產系統建議優先調用 FFprobe 或成熟庫,保證格式兼容性與維護效率。

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