FLV (Flash Video) 文件由 **Header (9-13 bytes)** + **Body (連續 Tags)** 組成。每個 Tag 包含:
視頻時長信息通常位於 **第一個 Script Data Tag (Type=18)** 中的 onMetaData 事件裡 (duration 字段),或通過遍歷所有視頻 Tag 取**最大 Timestamp** 推算 (適用於無元數據文件)。
FLV,跳過 Header 體 (含可選擴展字段)。duration。PrevTagSize 跳轉至下一 Tag (或直接 fseek 跳過 Data)。<?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 "解析失敗";
}
unpack('N', ...) (32-bit) 或手動位運算 (24-bit) 處理。onMetaData 為 ECMA Array,包含 duration (Number)、width/height 等。代碼實現簡易查找,複雜文件建議引入 sabre/amf。ffprobe -v error -show_entries format=duration -of csv=p=0 input.flv,支持所有容器格式、編碼、元數據,準確度最高,推薦生產使用。$getID3->analyze($file)['playtime_seconds']。$ffmpeg = new FFmpeg(); $probe = $ffmpeg->probe($file); $duration = $probe->getDuration();。通過直接讀取 FLV 二進制流,解析 Tag 結構與 AMF 元數據,可在無外部依賴環境下準確獲取視頻時長。代碼展示了大端序解析、位運算組合時間戳、簡易 AMF0 遍歷等核心技術。生產系統建議優先調用 FFprobe 或成熟庫,保證格式兼容性與維護效率。