17、FLV封装格式:FLV头部、Tag类型、AAC Sequence Header、AVC Sequence Header
聊到音视频推流,FLV 这个格式是绕不开的。为什么?因为 RTMP 协议底层用的就是 FLV 封装。说白了,你往服务器推流的时候,数据就是按照 FLV 的格式一层层打包好再发出去的。
我记得刚接触推流那会儿,总觉得 FLV 是个老掉牙的格式,Adobe 家的东西嘛。但后来发现,它在直播场景下简直太实用了——结构简单、解析快、延迟低。嗯,今天我们就把它掰开揉碎了看看。
FLV 文件结构概览
一个 FLV 文件,其实就三大部分:
- FLV 头部(Header)——告诉解析器:这是个 FLV 文件,里面有什么类型的流
- FLV 主体(Body)——由一堆 Tag 组成,每个 Tag 就是一帧数据
- FLV 尾部——其实没有专门的尾部,最后一个 Tag 读完就结束了
我习惯把 FLV 想象成一个集装箱。头部是箱体上的标签,Tag 就是里面一个个码放整齐的包裹。每个包裹有自己的类型——是视频、音频还是脚本数据。
核心要点:FLV 是流式格式,不需要像 MP4 那样先读到 moov box 才能播放。这也是它适合直播的根本原因。
FLV 头部(Header)详解
头部只有 9 个字节,结构非常紧凑。我直接贴出结构体定义:
typedef struct {
uint8_t signature[3]; // "FLV" 固定值
uint8_t version; // 版本号,目前是 0x01
uint8_t typeFlags; // 流类型标志位
uint32_t dataOffset; // 头部长度,固定为 9
} FLV_HEADER;
这里有个细节我想强调一下——typeFlags 这个字节。它的 bit 0 表示是否有音频流,bit 2 表示是否有视频流。举个例子:
0x05(二进制 0000 0101)——有音频 + 有视频0x01(二进制 0000 0001)——只有音频0x04(二进制 0000 0100)——只有视频
我在项目中遇到过一个问题:推流端只推了视频,但 FLV header 里把音频标志位也置了。结果拉流端一直等音频数据,画面卡住不动。排查了半天才发现是这里写错了。所以,写 header 的时候一定要跟实际流类型对应上。
FLV Tag 类型
头部之后就是 Tag 了。每个 Tag 由两部分组成:
- Tag 头部——11 字节,包含类型、数据大小、时间戳、流 ID
- Tag 数据——根据类型不同,结构也不同
Tag 类型用 1 个字节表示,一共三种:
| 类型值 | 名称 | 说明 |
|---|---|---|
| 0x08 | 音频 Tag | 音频数据,如 AAC、MP3 |
| 0x09 | 视频 Tag | 视频数据,如 H.264、H.265 |
| 0x12 | 脚本 Tag | 元数据,如分辨率、帧率、duration |
你想想看,解析器拿到一个 Tag,先看第一个字节就知道该走哪条解析路径了。这种设计简单粗暴,但非常高效。
个人习惯:我在写推流代码时,会把脚本 Tag 放在第一个视频 Tag 之前。这样拉流端一收到 FLV 就能立刻拿到元数据,不用等关键帧。
AAC Sequence Header
音频 Tag 里,如果是 AAC 编码,数据部分又分两种:
- AAC Sequence Header——音频编码配置信息
- AAC Raw Data——实际的音频帧数据
AAC Sequence Header 的结构其实就是 AudioSpecificConfig。它包含:
- 音频编码类型(AAC-LC、HE-AAC 等)
- 采样率(44100、22050 等)
- 声道数(单声道、立体声)
它的二进制结构长这样:
AudioSpecificConfig {
uint5_t audioObjectType; // AAC 类型,如 2 表示 AAC-LC
uint4_t samplingFrequencyIndex; // 采样率索引
uint4_t channelConfiguration; // 声道配置
// 后面还有 GASpecificConfig,但直播场景通常只用到前面这些
}
为什么需要这个 header?因为解码器必须知道这些参数才能正确解码。我刚开始做的时候,以为直接丢 AAC 裸数据就行,结果解码器一直报错。后来才发现,必须先发一个 Sequence Header 告诉解码器「我要用 AAC-LC、44100Hz、双声道来解码了」。
注意:Sequence Header 只发一次,在第一个音频 Tag 里发送。后续的音频 Tag 只包含 AAC Raw Data。如果推流过程中重复发送 Sequence Header,某些解码器会重置状态,导致音频卡顿。
AVC Sequence Header
视频 Tag 里,如果是 H.264 编码,同样有两种类型:
- AVC Sequence Header——视频编码配置信息
- AVC NALU——实际的视频帧数据
AVC Sequence Header 包含的是 AVCDecoderConfigurationRecord。它里面封装了:
- 配置版本号(固定为 1)
- AVC Profile(如 Baseline、Main、High)
- Profile 兼容性
- AVC Level(如 3.0、4.0)
- SPS(序列参数集)
- PPS(图像参数集)
结构体定义如下:
typedef struct {
uint8_t configurationVersion; // 0x01
uint8_t AVCProfileIndication; // 如 0x42 = Baseline
uint8_t profile_compatibility; // 通常为 0x40
uint8_t AVCLevelIndication; // 如 0x1E = Level 3.0
uint8_t lengthSizeMinusOne; // NALU 长度字段占几个字节
uint8_t numOfSequenceParameterSets; // SPS 数量
// SPS 数据
uint16_t sequenceParameterSetLength;
uint8_t* sequenceParameterSetNALUnit;
uint8_t numOfPictureParameterSets; // PPS 数量
// PPS 数据
uint16_t pictureParameterSetLength;
uint8_t* pictureParameterSetNALUnit;
} AVCDecoderConfigurationRecord;
这里有个坑,我曾经踩过——lengthSizeMinusOne 这个字段。它表示每个 NALU 前面用几个字节表示长度。比如值为 3,表示用 4 个字节(3+1)表示 NALU 长度。如果推流端和拉流端对这个字段理解不一致,视频就会花屏或者直接黑屏。
避坑指南:我曾经在对接某款编码器时,发现它输出的 AVC Sequence Header 里 SPS 和 PPS 的顺序是反的。大部分解码器能容忍,但有些严格的解码器直接罢工。所以,建议在解析时做一下校验,确保 SPS 在前、PPS 在后。
核心逻辑流程图
下面我用一张图把 FLV 的封装流程串起来,方便你理解整体脉络:
从这张图可以看得很清楚:FLV 文件就是 Header 后面跟一串 Tag,每个 Tag 后面跟一个 PreviousTagSize。这个 PreviousTagSize 的作用是方便解析器做随机访问——跳到一个位置后,通过它就能知道上一个 Tag 占了多少字节。
总结一下
FLV 封装格式的核心就这些内容。我个人觉得,理解 FLV 的关键在于把握两点:
- 头部的标志位——决定了后面会出现什么类型的 Tag
- Sequence Header 的作用——它是解码器正常工作的前提,必须正确构造
嗯,如果你在写推流代码时遇到花屏、无声、解码失败这些问题,十有八九就是 Sequence Header 没写对。回头检查一下 SPS、PPS 和 AudioSpecificConfig 的二进制内容,问题基本都能解决。
一个小技巧:用 Wireshark 抓包看 RTMP 推流数据,可以直接看到 FLV 的二进制结构。我调试时经常这么干,比看代码直观多了。
公众号:蓝海资料掘金营,微信deep3321