剩下最常用的三种:8 音频、9 视频、18 数据。 它们的负载直接就是 FLV 的 tag 数据体——这就是「RTMP 封 FLV」的真意。
4.4.7 与 4.4.8 讲的是控制类,这一节讲真正搬内容的三类。 讲完这节,第 4 章的 RTMP 就完整了;第五、六章会换成 HLS 与 HTTP-FLV。
一、三种主力消息
① 类型 8 Audio:负载是 FLV 音频 tag 的数据体。 第一个字节是音频头(编码格式、采样率、位深、声道),后面是帧数据。 ② 类型 9 Video:负载是 FLV 视频 tag 的数据体。 第一个字节是视频头(帧类型、编码格式),H.264 还多一个 AVC 包类型字节。 ③ 类型 18 Data(AMF0):负载是 AMF0 编码的数据。 直播里最常见的就是 onMetaData。
实测一路只含视频的 25 帧流: 1 条类型 18(元数据 184 字节)、26 条类型 9(视频平均 80 字节)。 类型 8 是 0 条,因为这一路没编音频。
二、为什么负载就是 FLV tag
因为 RTMP 与 FLV 是同一套体系里设计出来的。 FLV 的 tag 数据体已经包含了「这一帧是什么、多大、时间戳多少」的全部信息,RTMP 只是把这套负载换了个外壳(块头代替 tag 头)。
所以你会看到: ① FLV 文件里 tag 的数据体,与 RTMP 消息的负载逐字节相同。 ② 差别只在包装:FLV 用 11 字节 tag 头,RTMP 用块头。 ③ 时间戳两边都有,但一个是 3 字节加扩展,一个是消息头里的 3 字节。
这个设计带来的实用好处是「转封装极便宜」: 把 RTMP 流转成 FLV 文件,几乎不用重新解析码流,搬字节就行。 所以录制服务器的 CPU 开销很低。
三、AMF3 那两个类型号
类型 15(AMF3 Data)与 17(AMF3 Command)是 AMF0 的新版替代。 AMF3 在 ActionScript 3 时代引入,压缩率更好、支持更多类型。
但直播里几乎不用,原因有三条: ① 兼容性:老客户端只认 AMF0,用 AMF3 会直接解析失败。 ② 收益太小:直播里 AMF 消息只有几条(connect、createStream、publish、onMetaData),省几个字节没意义。 ③ 实现复杂:AMF3 的字符串引用表比 AMF0 麻烦得多。
所以看到实现里只用 18、不用 15/17,是正常的。 真正用 AMF3 的场景是 Flash 的富媒体应用,不是直播。
四、聚合消息为什么没人用
类型 20 是 Aggregate,它把多个 tag 打成一个消息发,省掉重复的块头。 听上去很划算,但现实中基本没人用。
原因是它破坏了交错粒度: 把十个帧打成一个大消息之后,这个大消息要按块大小切开发——如果它很大,音频就要等更久才能插进来。 等于把「切块」带来的交错优势又还回去了。
第二个原因是兼容性: 有些播放器不支持类型 20,遇到就直接丢弃。 省下的那点块头,跟「播不出来」的风险比,完全不值。
所以第 4 章的结论可以收成一句: RTMP 里真正在跑的只有 8、9、18 三种消息,加上类型 1 到 6 的控制消息。 其余类型号都是历史遗留或试验产物。
五、动手 Demo:数一数各类消息
封一路只含视频的流,把 tag 按类型号数一遍。 再看时间戳序列,确认 25 fps 的步长是 40 毫秒。
import io, os, struct, tempfile
import av, numpy as np
tmp = os.path.join(tempfile.mkdtemp(), "b.flv")
c = av.open(tmp, "w", format="flv")
st = c.add_stream("libx264", rate=25)
st.width, st.height, st.pix_fmt = 160, 120, "yuv420p"
st.options = {"crf": "34", "preset": "ultrafast", "g": "25"}
for i in range(25):
f = av.VideoFrame(160, 120, "yuv420p")
y = np.full((120, 160), 60 + i * 4, np.uint8)
f.planes[0].update(y.tobytes())
for pi in (1, 2):
f.planes[pi].update(bytes([128]) * f.planes[pi].buffer_size)
f.pts = i
for pk in st.encode(f):
c.mux(pk)
for pk in st.encode(None):
c.mux(pk)
c.close()
raw = io.open(tmp, "rb").read()
off, cnt, tss = 13, {}, []
while off + 11 <= len(raw):
t = raw[off]
size = int.from_bytes(raw[off + 1:off + 4], "big")
ts = int.from_bytes(raw[off + 4:off + 7], "big")
cnt[t] = cnt.get(t, 0) + 1
if t == 9:
tss.append(ts)
off += 11 + size + 4
print("类型 9(视频)%d 条" % cnt.get(9, 0))
print("类型 8(音频)%d 条" % cnt.get(8, 0))
print("类型 18(数据)%d 条" % cnt.get(18, 0))
print("视频时间戳前 6 个 %s" % tss[:6])
print("相邻步长 %d 毫秒" % (tss[3] - tss[2] if len(tss) > 3 else 0))
实跑输出:
--------------------------------------------------------------------
其他消息类型——音视频、元数据、聚合
--------------------------------------------------------------------
**① 剩下的类型号**
类型 名称 负载格式 常见用途
--------------------------------------------------------------
8 Audio FLV 音频 tag AAC/MP3 帧
9 Video FLV 视频 tag H.264/H.265 帧
15 AMF3 Data AMF3 新版元数据
17 AMF3 Command AMF3 新版命令
18 AMF0 Data AMF0 onMetaData
20 Aggregate 多条消息 把多帧打成一包
**② 真封一路流,数一数各类消息**
视频消息(类型 9):27 条
音频消息(类型 8):0 条
数据消息(类型 18):1 条
视频时间戳前 6 个:0、0、40、80、120、160
**③ onMetaData 里到底有什么**
**它是类型 18,负载是 AMF0 编码的 `@setDataFrame` 加一个对象。**
**对象里常见字段:**
**duration、width、height、framerate、videocodecid、audiocodecid。**
**直播时 duration 写 0**——**因为不知道什么时候结束。**
**这一个小细节会让很多播放器把进度条画成「未知」,属于正常现象。**
**聚合消息(类型 20)值得单独提一句:它把多个 tag 打成一包发,省掉重复的块头。**
**但现在的服务器基本不用它**——**因为聚合后会破坏「音视频交错」的粒度,反而增加延迟。**
**所以看到实现里只用 8/9/18 三种,是正常的,不是漏了。**
类型 9 有 26 条、类型 18 有 1 条、类型 8 是 0 条——0 条是因为这一路只编了视频,不是封包出错。 时间戳前 6 个是 0、0、40、80、120、160,相邻步长 40 毫秒——正好是 25 fps 的倒数。 两条 0 是因为编码器把前两帧都打了同一个 pts,实际推流里不会出现这种情况。
30 秒实验
- 把
rate改成 30,再看时间戳步长——变成 33 或 34 毫秒,因为 1000/30 除不尽,编码器会取整。 - 加上音频流再封一次,看类型 8 与类型 9 的比例——音频条数通常是视频的 2 到 4 倍(每帧音频更小、更密)。
- 把帧数从 25 改成 50,看关键帧条数——GOP 25 的话关键帧从 2 个变 3 个。
常见误区
- 「RTMP 有专门的音视频封装」——负载就是 FLV 的 tag 数据体。
- 「AMF3 比 AMF0 好,应该优先用」——兼容性差、收益小,直播里只用 AMF0。
- 「聚合消息能省带宽就该用」——破坏交错粒度,而且播放器可能不支持。
自测三问(点开对答案)
1. 直播里真正在用的三种消息类型是什么? —— **8 音频、9 视频、18 数据。**其余是控制类或历史遗留。 2. 为什么说「RTMP 封 FLV」? —— **消息负载直接就是 FLV 的 tag 数据体**,只是外壳换成了块头。 3. 聚合消息为什么没人用? —— **破坏音视频交错粒度**,而且部分播放器不支持。- ☐ (本章完,下一章 5.1 HLS简介)