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 文件结构全景图 FLV 文件头(9 字节) PreviousTagSize(4 字节,值为 0) Tag 1(音频/视频/脚本) 11 字节 Tag 头 + 负载数据 PreviousTagSize(4 字节) Tag 2(音频/视频/脚本) 11 字节 Tag 头 + 负载数据 ... 最后一个 Tag + PreviousTagSize 文件结束

这张图把 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 文件打不开或者播放异常,你知道该从哪里下手排查了。