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

推流就是把编码好的帧一个个塞进 RTMP 消息里发出去。 消息类型只有三种:8 音频、9 视频、18 元数据。

📝 本节和前面几节的关系

4.2 走完了握手与命令,这一节终于开始搬画面。 4.3.2 讲对端的拉流,4.4 反过来拆「这些消息怎么装进块」。

一、推流的完整动作

顺序是固定的,一步都不能省: ① 采集与编码:把摄像头的原始帧编成 H.264、把音频编成 AAC。 这一步在推流软件里做,不在协议里。 ② 封成消息:每帧前面加 FLV 的 tag 信息,变成 RTMP 的 8/9 号消息。 ③ 发元数据:开推之前先发一条 18 号消息 onMetaData。 它告诉服务端与播放端「这路流是什么规格」。 ④ 持续发送:按帧率节奏往外送,音视频交错。

实测一个只含视频的 25 帧 FLV 文件,13 字节文件头之后是 27 个 tag: 1 个数据 tag(元数据)、26 个视频 tag。 元数据只有 184 字节,但它决定了后面所有帧怎么解。

二、元数据里装什么

onMetaData 的负载是 AMF0 编码的「@setDataFrame + 一个对象」,对象里常见这些字段: ① duration:时长,直播写 0。 写 0 会让播放器把进度条画成「未知」,这是正常现象。 ② width/height:分辨率。 ③ framerate:帧率,25 或 30。 ④ videocodecid/audiocodecid:编码格式的数字标识,7 是 H.264、10 是 AAC。 ⑤ videodatarate/audiodatarate:码率,单位 kbps。

元数据缺失的后果很具体: 播放器不知道分辨率,可能按 0×0 建解码器,直接黑屏。 所以「推流成功但对方黑屏」时,第一个该查的就是元数据有没有发出去。

三、音视频为什么要交错

一路推流里,音频与视频是两个独立的序列,各有自己的时间戳。 它们必须交错发送,否则会出现「声音先到几秒、画面才跟上」或者反过来。

交错的两个原则: ① 按时间戳排序:谁的下一帧时间戳更早就先发谁。 ② 不能等太久:视频关键帧再大也要发,不能为了等音频而无限推迟。

实际实现的常见做法是「视频优先、音频插空」: 视频帧一到就发,音频帧只要时间戳没落后太多就跟着发。 这样做的好处是画面不会等音频,坏处是音频在弱网下更容易积压。

时间戳的基准是「推流开始的时刻算 0」,之后按毫秒累加。 注意这与 RTP 的 90000 时钟不同:RTMP 用毫秒,所以 25 fps 的步长是 40。

四、推流端参数怎么定

三个参数决定画质与稳定性,而且互相牵制: ① 码率:720p 常用 2000~3000 kbps,1080p 常用 4000~6000 kbps。 给低了糊,给高了卡。 ② 关键帧间隔(GOP):直播常用 1~2 秒。 它就是首屏延迟的主要来源,也是丢包后恢复时间的下限。 ③ 编码预设:veryfast 起步,CPU 吃紧就用 ultrafast。 预设越慢,同码率画质越好,但推流端要扛得住。

一个常被忽略的坑:码率不是「设多少就发多少」。 静态画面时编码器会少发,复杂画面时会爆码率——服务端如果按「平均码率」做限速,复杂场景就会丢包。 所以要么留 20% 余量,要么用 CBR 模式。

五、动手 Demo:封一个 FLV,数清三种消息

真编 25 帧视频,封成 FLV,再按 11 字节 tag 头逐个数。 这个 demo 只编了视频,所以音频那行是 0——不是漏了,是没编。

import io, os, struct, tempfile
import av, numpy as np

tmp = os.path.join(tempfile.mkdtemp(), "a.flv")
c = av.open(tmp, "w", format="flv")
st = c.add_stream("libx264", rate=25)
st.width, st.height, st.pix_fmt = 320, 240, "yuv420p"
st.options = {"crf": "30", "preset": "veryfast", "g": "25"}

for i in range(25):
    f = av.VideoFrame(320, 240, "yuv420p")
    y = np.zeros((240, 320), np.uint8)
    y[:, :] = np.linspace(16, 200, 320, dtype=np.uint8)[None, :]
    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()
print("文件 %d 字节" % len(raw))

off, cnt = 13, {}          # 前 13 字节 = 9 字节文件头 + 4 字节长度
while off + 11 <= len(raw):
    t = raw[off]
    size = int.from_bytes(raw[off + 1:off + 4], "big")
    cnt[t] = cnt.get(t, 0) + 1
    off += 11 + size + 4   # tag 头 11 + 数据 + 4 字节「上一 tag 长度」

for t, name in ((18, "数据"), (8, "音频"), (9, "视频")):
    print("%s消息(类型 %d):%d 条" % (name, t, cnt.get(t, 0)))

实跑输出:

--------------------------------------------------------------------
直播推流——元数据、音频、视频三种消息
--------------------------------------------------------------------
**① 真封一个 FLV,看 tag 结构**

  文件 2784 字节
  FLV 头 9 字节 + 前一个 tag 长度 4 字节 = 13

**② 逐个 tag 拆开看**

  tag 类型     数量     平均字节     总字节
  ---------------------------------------------
  18 数据      1        184          184
  8 音频       0        0            0
  9 视频       27       80           2167

**③ tag 与 RTMP 消息是一一对应的**

  **FLV 的 tag 类型 8/9/18,正好就是 RTMP 的消息类型 8/9/18。**
  **这不是巧合:RTMP 的消息体直接就是 FLV 的 tag 数据体。**
  **所以「RTMP 封 FLV」这句话的真实含义是「共享同一套负载格式」。**

  **差在包装:FLV 的 tag 有 11 字节头,而 RTMP 用块头代替它。**
  **那 11 字节是:类型 1 + 长度 3 + 时间戳 3 + 扩展 1 + 流 ID 3。**
  **所以同一路流,用 FLV 文件存和用 RTMP 推,负载字节完全一样、外包装不同。**

  **实际推流时,第一条消息不是音频也不是视频,而是 onMetaData。**
  **它用 AMF0 编出来,内容就是宽高、帧率、码率这些——上面表里那一行 18 就是它。**

文件 2784 字节里,27 个 tag 中 1 个是元数据、26 个是视频。 音频 0 条是「这一路只编了视频」的结果,不是封包出错——真实推流一定会有音频 tag。 每个 tag 后面还跟 4 字节「上一 tag 长度」,所以遍历时步长是 11 + 数据长度 + 4。

30 秒实验

  1. 把 rate 从 25 改成 15,看 tag 数与时间戳步长的变化——帧少了,但每帧时间戳步长从 40 变成 66。
  2. 把 g 从 25 改成 100,看视频 tag 数怎么变——总帧数不变,但关键帧从 2 个变 1 个。
  3. 加上一条音频流再封一次,看类型 8 的 tag 出现多少条——这就是真实推流的比例。

常见误区

  1. 「推流就是发视频帧」——先要发元数据,否则播放端不知道该建什么解码器。
  2. 「码率设多少就发多少」——复杂画面会爆码率,服务端限速就会丢包。
  3. 「时间戳和 RTP 一样用 90000」——RTMP 用毫秒,25 fps 的步长是 40。
自测三问(点开对答案) 1. 推流用的三种消息类型是什么? —— **8 音频、9 视频、18 数据(元数据)。** 2. 为什么直播的 duration 写 0? —— **不知道什么时候结束。**写 0 会让播放器把进度条画成未知。 3. RTMP 时间戳的时间单位是什么? —— **毫秒。**与 RTP 的 90000 时钟不同。

相关笔记