FLV 容器深度解析:文件头、Tag 结构与 AMF 编码
FLV 这个格式,说实话,在流媒体圈子里混久了,你很难绕开它。虽然现在 HLS、DASH 大行其道,但 FLV 在直播场景下,尤其是配合 HTTP-FLV 协议时,延迟优势非常明显。我最早接触 FLV 是在做 RTMP 推流的时候,那时候被它的 Tag 结构折腾得不轻。今天咱们就把 FLV 的底裤扒干净,从文件头到 Tag,再到 AMF 编码,一次性说透。
FLV 文件头:开头的 9 个字节
每个 FLV 文件,开头都是固定的 9 个字节。你拿十六进制编辑器打开任何一个 FLV 文件,看到的头几个字节一定是 46 4C 56,也就是 ASCII 的 "FLV"。这没什么好说的,文件签名。
| 偏移 | 大小(字节) | 字段 | 说明 |
|---|---|---|---|
| 0 | 3 | Signature | 固定为 "FLV"(0x46 0x4C 0x56) |
| 3 | 1 | Version | 版本号,目前主流是 1(0x01) |
| 4 | 1 | TypeFlags | 音频/视频标志位 |
| 5 | 4 | DataOffset | 文件头长度,固定为 9 |
这里有个细节,TypeFlags 这个字节,它的 bit 0 表示是否有视频,bit 2 表示是否有音频。我见过一些不规范的 FLV 文件,这两个位都置 0,结果播放器直接黑屏。嗯,遇到这种文件,你得做好容错处理。
核心要点:FLV 文件头只有 9 字节,解析时务必校验 Signature 和 DataOffset。DataOffset 虽然固定为 9,但有些工具会写入 9 以外的值,建议以实际读取为准。
Tag 结构:FLV 的基本单元
文件头之后,就是一个个的 Tag 了。FLV 的 Tag 分为三种:音频 Tag、视频 Tag 和脚本 Tag。每个 Tag 都有一个 11 字节的头部,然后是负载数据。
Tag 头的结构是这样的:
| 偏移 | 大小(字节) | 字段 | 说明 |
|---|---|---|---|
| 0 | 1 | TagType | 8=音频,9=视频,18=脚本 |
| 1 | 3 | DataSize | 负载数据长度(大端) |
| 4 | 3 | Timestamp | 时间戳(毫秒,大端) |
| 7 | 1 | TimestampExtended | 时间戳扩展位 |
| 8 | 3 | StreamID | 流 ID,固定为 0 |
时间戳这里有个坑。Timestamp 只有 3 个字节,最大只能表示 0xFFFFFF,也就是约 4.6 小时。超过这个时间怎么办?TimestampExtended 这个字节就是干这个用的。实际时间戳 = (TimestampExtended << 24) | Timestamp。我曾经在做一个 24 小时直播录制项目时,发现回放文件在 4 小时左右画面卡住了,排查了半天才发现是时间戳溢出,没有正确处理扩展位。
注意:每个 Tag 之后,还有一个 4 字节的 PreviousTagSize,表示前一个 Tag 的总大小(包括 Tag 头)。这个字段用于文件随机访问定位。解析时不要漏掉它,否则后续 Tag 位置全错。
音频 Tag 详解
音频 Tag 的负载数据,第一个字节是音频信息字节,后面才是真正的音频数据。
这个字节的结构:
- 高 4 位:音频编码类型。0 = PCM,1 = ADPCM,2 = MP3,10 = AAC
- 中 2 位:采样率。0 = 5.5kHz,1 = 11kHz,2 = 22kHz,3 = 44kHz
- 低 1 位:采样精度。0 = 8bit,1 = 16bit
- 最低 1 位:声道数。0 = 单声道,1 = 立体声
对于 AAC 编码,音频数据的前两个字节是 AACPacketType。0 表示 AAC 序列头(包含 AudioSpecificConfig),1 表示 AAC 原始数据。这个区分很重要,解码器必须先拿到序列头才能正常解码。
经验之谈:解析 AAC 音频 Tag 时,如果遇到 AACPacketType 为 0 的 Tag,一定要把 AudioSpecificConfig 缓存下来。后续的 AAC 原始帧解码需要依赖这个配置信息。我习惯在解析器里维护一个 aac_config 的缓冲区,专门存这个。
视频 Tag 详解
视频 Tag 的负载数据,第一个字节是视频信息字节,结构如下:
- 高 4 位:帧类型。1 = 关键帧,2 = 内部帧,3 = 丢弃帧,4 = 生成关键帧
- 低 4 位:编码类型。2 = H.263,3 = 屏幕视频,4 = VP6,7 = AVC/H.264
对于 H.264 编码,第二个字节是 AVCPacketType。0 表示 AVC 序列头(包含 SPS/PPS),1 表示 AVC NALU,2 表示 AVC 结束序列。
AVC 序列头后面跟着的是 AVCDecoderConfigurationRecord,里面包含了 SPS 和 PPS 数据。这个结构在 H.264 解码初始化时是必须的。
// AVCDecoderConfigurationRecord 结构示意
struct AVCDecoderConfigurationRecord {
uint8_t configurationVersion; // 固定 1
uint8_t AVCProfileIndication; // Profile
uint8_t profile_compatibility; // 兼容性标志
uint8_t AVCLevelIndication; // Level
uint8_t lengthSizeMinusOne; // NALU 长度字段字节数 - 1
uint8_t numOfSequenceParameterSets; // SPS 数量
// SPS 数据
uint16_t sequenceParameterSetLength;
uint8_t* sequenceParameterSetNALUnit;
uint8_t numOfPictureParameterSets; // PPS 数量
// PPS 数据
uint16_t pictureParameterSetLength;
uint8_t* pictureParameterSetNALUnit;
};
关键点:视频关键帧的帧类型是 1,非关键帧是 2。在转码或切片时,关键帧是随机访问点。如果你要做视频拼接或剪辑,必须从关键帧开始切,否则画面会花掉。
脚本 Tag 与 AMF 编码
脚本 Tag 的 TagType 是 18。它的负载数据采用 AMF(Action Message Format)编码。AMF 是 Adobe 定义的一种序列化格式,用于在 Flash 和服务器之间交换数据。
FLV 中的脚本 Tag 通常包含一个 "onMetaData" 命令,里面存放了视频的元数据信息,比如时长、宽度、高度、帧率、码率等。
AMF 编码的基本类型:
| 类型标记 | 类型名称 | 说明 |
|---|---|---|
| 0x00 | Number | IEEE 754 双精度浮点数(8 字节) |
| 0x01 | Boolean | 布尔值(1 字节) |
| 0x02 | String | 字符串(2 字节长度 + 数据) |
| 0x03 | Object | 键值对集合,以 0x000009 结束 |
| 0x04 | MovieClip | 已废弃,基本不用 |
| 0x05 | Null | 空值 |
| 0x06 | Undefined | 未定义 |
| 0x07 | Reference | 引用类型 |
| 0x08 | ECMAArray | 混合数组(4 字节长度 + 键值对) |
| 0x0A | StrictArray | 严格数组(4 字节长度 + 元素列表) |
| 0x0B | Date | 日期类型 |
| 0x0C | LongString | 长字符串(4 字节长度 + 数据) |
解析 onMetaData 时,通常是一个 ECMAArray(0x08),里面包含多个键值对。常见的元数据字段有:
- duration:视频时长(秒)
- width:视频宽度(像素)
- height:视频高度(像素)
- videodatarate:视频码率(kbps)
- framerate:视频帧率(fps)
- audiodatarate:音频码率(kbps)
- audiosamplerate:音频采样率(Hz)
- audiosamplesize:音频采样精度(bit)
- stereo:是否立体声(布尔值)
避坑指南:我曾经遇到过一个 FLV 文件,它的 onMetaData 里 duration 字段写的是 0。播放器拿到这个值后,进度条直接废了。后来我养成了一个习惯:如果元数据里的 duration 为 0 或缺失,就通过最后一个视频 Tag 的时间戳来推算时长。另外,有些编码器会把元数据写在文件末尾,解析时要注意。
FLV 整体结构图
下面这张图展示了 FLV 文件的完整结构,从文件头到 Tag 序列,一目了然。
这张图把 FLV 的层级关系讲得很清楚。文件头后面跟着 PreviousTagSize(第一个值为 0),然后是 Tag1、PreviousTagSize、Tag2…… 如此循环直到文件末尾。每个 Tag 内部又包含 11 字节的 Tag 头和负载数据。
解析 FLV 的常见坑点
做 FLV 解析器的时候,有几个地方特别容易出问题,我列出来供你参考:
- 时间戳乱序:B 帧的存在会导致 Tag 的时间戳不是严格递增的。解析时不要假设时间戳单调递增,否则在重排或切片时会出错。
- Tag 大小不匹配:DataSize 字段与实际负载大小不一致。有些编码器会写错这个值,建议以实际读取为准,或者做校验。
- AMF 解析边界:脚本 Tag 的 AMF 数据可能包含嵌套对象,解析时要注意递归深度。我见过一个恶意构造的 FLV 文件,AMF 嵌套了 100 层,直接导致解析栈溢出。
- 缺少序列头:H.264 和 AAC 的序列头(SPS/PPS 和 AudioSpecificConfig)必须在第一个数据帧之前出现。如果缺失,解码器无法初始化。
严重警告:千万不要在生产环境中直接使用 memcpy 或 fread 不加校验地读取 FLV 数据。恶意构造的 FLV 文件可能包含超长的 DataSize,导致缓冲区溢出。务必做边界检查。
好了,FLV 容器的核心内容就这些。文件头、Tag 结构、AMF 编码,这三块搞明白了,FLV 解析对你来说就是小菜一碟。下次遇到 FLV 文件打不开或者播放异常,你知道该从哪里下手排查了。