🎬 第四章 RTMP流媒体协议
📄 一句话概括

剩下最常用的三种: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 秒实验

  1. 把 rate 改成 30,再看时间戳步长——变成 33 或 34 毫秒,因为 1000/30 除不尽,编码器会取整。
  2. 加上音频流再封一次,看类型 8 与类型 9 的比例——音频条数通常是视频的 2 到 4 倍(每帧音频更小、更密)。
  3. 把帧数从 25 改成 50,看关键帧条数——GOP 25 的话关键帧从 2 个变 3 个。

常见误区

  1. 「RTMP 有专门的音视频封装」——负载就是 FLV 的 tag 数据体。
  2. 「AMF3 比 AMF0 好,应该优先用」——兼容性差、收益小,直播里只用 AMF0。
  3. 「聚合消息能省带宽就该用」——破坏交错粒度,而且播放器可能不支持。
自测三问(点开对答案) 1. 直播里真正在用的三种消息类型是什么? —— **8 音频、9 视频、18 数据。**其余是控制类或历史遗留。 2. 为什么说「RTMP 封 FLV」? —— **消息负载直接就是 FLV 的 tag 数据体**,只是外壳换成了块头。 3. 聚合消息为什么没人用? —— **破坏音视频交错粒度**,而且部分播放器不支持。

相关笔记