RTMP 从头到尾只用一条 TCP 连接,控制命令与媒体数据全挤在里面。 FFmpeg 里没有叫 rtmp 的封装格式——封装用 flv,传输靠 rtmp:// 前缀。
8.5 已经演示过 RTMP 握手,这一节把剩下的几层补齐。 8.6.3 与 8.6.4 再把推流端与播放端接上。
一、五层结构
RTMP 不是单一协议,而是五层叠起来的东西。
层 干什么 字节
TCP 连接 先建一条长连接,一直不关 — 握手 交换版本号与随机数 1537 + 3073 chunk 分块 把长消息切成小段 每段最多 128 AMF0 命令 connect、createStream、publish 几十字节 FLV 标签 音频、视频、元数据 11 + N + 4
跟 RTSP 最大的不同:RTMP 只有一条 TCP 连接。 RTSP 是控制走 554、媒体另开 RTP 通道;RTMP 把两件事塞进同一条线, 靠 chunk 里的消息类型区分——这条线既是指挥部,也是运输队。
二、首次建连要花多少字节
把三种协议的「第一次握手」摆在一起比,差距很直观。
协议 首次建连字节 说明
HTTP 73 一条请求,答完就完 RTSP 477 四问答(OPTIONS 到 PLAY) RTMP 6146 握手 1537 + 3073 + 1536
RTMP 贵得多,因为它一次建连之后就不再断开。 这 6 KB 只付一次,后面几百帧都复用这条连接。 摊到一小时直播上基本看不见,所以「贵」在这里不是缺点。
反过来说:连接数少是 RTMP 的优点,也是它的瓶颈。 每个观众占一条常连接,服务器上万观众就是上万条—— 这也是后来 HLS 与 HTTP-FLV 能流行起来的原因。
三、FFmpeg 里没有 rtmp 这个格式
这一条几乎人人踩过:写 format="rtmp" 会直接报错。
格式名 在不在
flv 有 rtmp 没有 mpegts 有 hls 有 rtsp 有
本机 libav 一共 414 个格式,rtmp 这个名字不在里面。
因为 RTMP 在 FFmpeg 里被拆成两半:
封装用 flv,传输靠 URL 里的 rtmp:// 前缀。
所以正确写法是 -f flv rtmp://127.0.0.1/live/stream。
-f flv 说的是「里面装什么」,rtmp:// 说的是「怎么送」。
把两者混成一件事,就会写出根本不存在的东西。
四、FLV 的头只有 13 个字节
FLV 的文件头极短,因为设计目标是「边收边解」。
偏移 字段 本例
0 到 2 签名 FLV 3 版本 0x01 4 标志位 0x01(有视频) 5 到 8 数据偏移 9 9 到 12 PreviousTagSize0 0
标志位是个位掩码:0x01 有视频、0x04 有音频,两个都有就是 0x05。
头后面那 4 个字节是 PreviousTagSize0,值恒为 0——
它没有实际数据,只是为了让「每个标签前面都有一条长度记录」这个格式整齐。
所以第一个真正的标签从第 13 个字节开始。
第一个标签永远是 0x12(脚本),装的是 onMetaData: 宽高、帧率、码率、时长都写在这里,播放器靠它先知道「这是什么」。
五、chunk 的头有多大
一条命令不可能只有一个 chunk,它会被切成 128 字节一段。
拿一条 300 字节的 connect 命令算一遍:
项 值
消息长度 300 字节 chunk 大小 128 字节 切成几段 3 段 第一段头 12 字节 后续每段头 1 字节 实际发出 314 字节
第一段的头最胖,因为它要把消息长度、类型、流号一次说清。 后面的段只带 1 个字节,意思是「我还是上一段那条消息」。
314 减 300 等于 14,比裸数据多了不到 5%。 听着不多,但建连阶段全是几十字节的小命令, 128 的默认值会让开销比例飙到三成以上。
所以连上之后双方会协商一个更大的 chunk size(常见 4096), 开销立刻降到几乎看不见——这是 RTMP 省字节的关键一步。
六、动手 Demo:查格式清单、拆 FLV 头、算 chunk 开销
下面这段先翻一遍 libav 的格式清单,确认 rtmp 不在里面; 再把 50 帧封成 FLV,把前 13 个字节逐字段读出来。
import io, struct
from fractions import Fraction
import av
names = list(av.formats_available)
print(u"格式总数 %d" % len(names))
for k in ("flv", "rtmp", "mpegts", "hls", "rtsp"):
print(u" %-8s %s" % (k, "有" if k in names else "没有"))
try:
o = av.open("rtmp://127.0.0.1:19999/live/x", "w", format="rtmp")
o.close()
except Exception as e:
print(u"format=rtmp -> %s" % type(e).__name__)
buf = io.BytesIO()
o = av.open(buf, "w", format="flv")
st = o.add_stream("libx264", rate=Fraction(25, 1))
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
st.bit_rate = 400000
c, k = av.open("s7.mp4"), 0
for f in c.decode(video=0):
for q in st.encode(f):
o.mux(q)
k += 1
if k >= 50:
break
for q in st.encode(None):
o.mux(q)
o.close()
c.close()
d = buf.getvalue()
print(u"FLV %d 字节" % len(d))
print(u" 签名 %s" % d[:3].decode("ascii"))
print(u" 版本 0x%02X" % d[3])
print(u" 标志位 0x%02X" % d[4])
print(u" 偏移 %d" % struct.unpack(">I", d[5:9])[0])
print(u" Tag0 %d" % struct.unpack(">I", d[9:13])[0])
print(u" 首标签 0x%02X" % d[13])
msg, chunk = 300, 128
parts = (msg + chunk - 1) // chunk
print(u"%d 字节切 %d 段,共发 %d 字节"
% (msg, parts, msg + 12 + (parts - 1)))
实跑输出:
--------------------------------------------------------------------
8.6.1 RTMP 简介
--------------------------------------------------------------------
**① RTMP 分五层,一层一层往下扒**
层 干什么 字节
------------------------------------------------------------------
TCP 连接 先建一条长连接,一直不关 —
握手 交换版本号与随机数 1537 + 3073
chunk 分块 把长消息切成小段 每段最多 128
AMF0 命令 connect、createStream、publish 几十字节
FLV 标签 音频、视频、元数据 11 + N + 4
**跟 RTSP 最大的不同:RTMP 只有一条 TCP 连接,**
**控制命令与媒体数据全挤在这条线上,靠 chunk 里的消息类型区分。**
**② 三家的首次建连开销对比**
协议 首次建连字节 说明
-------------------------------------------------------
HTTP 73 一条请求,答完就完
RTSP 477 四问答(OPTIONS 到 PLAY)
RTMP 6146 握手 1537 + 3073 + 1536
**RTMP 贵得多,因为它一次建连之后就不再断开——**
**这 6 KB 只付一次,后面几百帧都复用这条连接。**
**数字看着吓人,摊到一小时直播上基本看不见。**
**③ 实测:本机 libav 里有没有 rtmp 这个格式**
格式名 在不在
----------------------
flv 有
rtmp 没有
mpegts 有
hls 有
segment 有
rtsp 有
rtp 有
**一共 414 个格式,`rtmp` 这个名字不在里面。**
**因为 RTMP 在 FFmpeg 里不是一个「封装格式」,而是一套协议:**
**封装用 `flv`,传输靠 URL 里的 `rtmp://` 前缀。**
**所以写 `format="rtmp"` 会直接抛 `ValueError`——名字根本不存在。**
**正确的写法是 `format="flv"`,地址写 `rtmp://`。**
**④ 实测:50 帧封成 FLV 有多大,头长什么样**
项 值
---------------------------------------------
FLV 总字节 30271
封装耗时 56 ms
文件头 FLV
版本 0x01
标志位 0x01(有视频)
数据偏移 9
头 13 字节 464c5601010000000900000000
**「FLV」三个字母开头,版本 1,标志位 0x01 表示「只有视频没有音频」,**
**数据偏移 9 表示「头后面有 9 个字节才开始第一个标签」。**
**头之后紧跟 4 字节的 PreviousTagSize0,值恒为 0——这是 FLV 的起手式。**
**所以第一个标签从第 13 个字节开始,不多不少。**
**⑤ chunk 的头有多大:一条 300 字节的命令要切几段**
项 值
---------------------------
消息长度 300 字节
chunk 大小 128 字节
切成几段 3 段
第一段头 12 字节
后续每段头 1 字节
实际发出 314 字节
**第一段的头最胖,因为它要把消息长度、类型、流号一次说清;**
**后面的段只带 1 个字节,说「我还是上一段那条消息」。**
**128 这个默认值很小,所以 RTMP 建连阶段会很啰嗦。**
**连上之后双方会协商一个更大的 chunk size(常见 4096),开销立刻降下来。**
rtmp 那一行标「没有」,format="rtmp" 也确认抛 ValueError。
头 13 个字节读出来是「FLV + 01 + 01 + 00000009 + 00000000」, 第一个标签的类型是 0x12,不是视频——很多人以为第一段就是画面。
30 秒实验
- 把标志位那个字节改成 0x05:看解复用器会不会以为「有音频」。
- 把
format="flv"去掉只留rtmp://:libav 会按扩展名硬猜,猜不对就报错。 - 把 chunk 从 128 改成 4096:算一遍 300 字节的命令还剩多少开销。
常见误区
- 以为 RTMP 是个封装格式。封装是 flv,RTMP 只是运输通道。
- 以为 FLV 第一个标签是视频。第一个永远是 0x12 脚本标签。
- 把
PreviousTagSize0当成有效数据。它恒为 0,只是占位。
自测三问(点开对答案)
1. RTMP 为什么要比 RTSP 多花十几倍字节建连? —— 它要建立一条长期复用的 TCP 连接,握手与随机数交换都在里面。 2. `-f flv rtmp://…` 两个部分各管什么? —— `-f flv` 管「里面装什么」,`rtmp://` 管「怎么送出去」。 3. 一条 300 字节的命令按 128 切,实际发多少字节? —— 314 字节:300 加第一段 12 字节头,加后两段各 1 字节。- ☐ 8.6.1 完,下一节 8.6.2 直播推流与拉流