消息头有四种格式:11、7、3、0 字节,逐级砍掉「与上一块相同」的字段。 六块连发,全用格式 0 要 72 字节头,按最优序列只用 30 字节。
4.4.3 拆完基本头,这一节拆消息头。 4.4.5 讲「时间戳超过 24 位」时插入的扩展字段。
一、四种格式各带什么
格式号写在基本头的高 2 位里,决定消息头长度: ① 格式 0(11 字节):时间戳 + 长度 + 类型 + 流 ID,全都要。 ② 格式 1(7 字节):时间戳 + 长度 + 类型,流 ID 沿用。 ③ 格式 2(3 字节):只有时间戳,长度、类型、流 ID 全沿用。 ④ 格式 3(0 字节):什么都不带,与上一块完全一致。
这套设计的核心是「增量编码」: 同一个流里连续发的帧,流 ID 恒为 1、类型恒为 9,那就只在第一块写一次。 之后的块只写真正变化的时间戳。
二、六块连发能省多少
假设连发六个视频帧,每帧正好一块: 第 1 块用格式 0:11 字节消息头 + 1 字节基本头 = 12。 第 2 块用格式 1:7 + 1 = 8,因为流 ID 没变。 第 3 块用格式 2:3 + 1 = 4,因为长度与类型也没变。 第 4、5 块用格式 3:0 + 1 = 1,只写一个基本头。 第 6 块用格式 2:4,因为它又要带一个新时间戳。
实测累计 30 字节,而六块全用格式 0 要 72 字节,省了 58%。 省下来的部分全在「重复信息」上:流 ID 四次没再写、类型五次没再写。
三、格式 3 的两种含义
这是 RTMP 里最有名的一个坑:格式 3 既表示「新消息」也表示「后续块」。 ① 出现在消息的第一块:意思是「这条新消息的所有字段都与上一条相同」。 时间戳按上一段的时间戳增量往后推。 ② 出现在同一条消息的中间块:意思是「接着上一块,载荷继续」。 这时候没有消息头,载荷直接跟在基本头后面。
区分办法只有一个:看当前 csid 上「这条消息收满了没有」。 收满了 → 是新消息;没满 → 是继续。 这个状态必须每个 csid 单独维护,混在一起就全乱。
四、用错格式的后果
格式 1/2/3 都依赖「上一次的值」,所以有两条硬约束: ① 一条消息的第一块必须是格式 0。 ② 一条新流的第一块也必须是格式 0。
违反了会怎样? 接收端不知道该沿用哪个值,只能拿「上一次碰巧的缓存」去凑。 表现是「大部分时候正常、偶尔画面撕裂」——这种 bug 最难查,因为它依赖发送顺序。
所以自己写 RTMP 客户端时,最省事的做法是: 每条消息的第一块一律用格式 0,同一条消息的后续块一律用格式 3。 不用格式 1 与 2,也能跑通,只是多几个字节。 等确定解析没问题了,再回来优化。
五、动手 Demo:11 字节怎么压到 0 字节
四种格式的长度对一遍,再算六块连发的累计开销。 最后看格式 3 的两种含义怎么区分。
fmt_len = {0: 11, 1: 7, 2: 3, 3: 0}
for f, n in fmt_len.items():
print("格式 %d → 消息头 %d 字节" % (f, n))
print("")
seq = [(0, 11), (1, 7), (2, 3), (3, 0), (3, 0), (2, 3)]
tot = 0
for i, (f, size) in enumerate(seq, 1):
tot += size + 1 # 每个块还有 1 字节基本头
print("第 %d 块 格式 %d 消息头 %d 字节 累计 %d" % (i, f, size, tot))
full = len(seq) * 12
print("")
print("全用格式 0 要 %d 字节,实际 %d 字节,省 %.0f%%"
% (full, tot, (full - tot) * 100.0 / full))
print("")
# 格式 3 的两种含义:靠「收满没满」区分
def kind(recv, need):
return "新消息" if recv >= need else "后续块"
print("已收 128 / 应收 128 →", kind(128, 128))
print("已收 64 / 应收 128 →", kind(64, 128))
实跑输出:
--------------------------------------------------------------------
块消息头——11 字节怎么压到 0 字节
--------------------------------------------------------------------
**① 四种格式,长度不同**
格式 字节 带哪些字段
----------------------------------------------------------
0 11 时间戳 + 长度 + 类型 + 流 ID,全都要
1 7 时间戳 + 长度 + 类型,流 ID 沿用
2 3 只有时间戳,长度类型流 ID 全沿用
3 0 什么都不带,与上一块完全相同
**这套设计的核心是「增量编码」:同一个流里连续发的帧,大部分字段是不变的。**
**比如视频帧流 ID 恒为 1、类型恒为 9,那就只在第一块写一次。**
**② 算一下能省多少**
假设连发六个视频帧,每帧正好一块,块流 ID 都是 4、类型都是 9。
第 1 块:格式 0,消息头 11 字节,加基本头共累计 12
第 2 块:格式 1,消息头 7 字节,加基本头共累计 20
第 3 块:格式 2,消息头 3 字节,加基本头共累计 24
第 4 块:格式 3,消息头 0 字节,加基本头共累计 25
第 5 块:格式 3,消息头 0 字节,加基本头共累计 26
第 6 块:格式 2,消息头 3 字节,加基本头共累计 30
六块全用格式 0 要 72 字节,实际只用 30 字节,省了 58%
**省下来的部分全在「重复信息」上:流 ID 四次没再写、类型五次没再写。**
**这就是为什么一路直播跑起来之后,绝大多数块都是格式 3,整条连接几乎只剩载荷在前面走。**
**③ 用错格式会怎样**
**格式 1/2/3 都依赖「上一次的值」,所以有两条硬约束:**
**① 一条消息的第一块必须是格式 0;② 一条新流的第一块也必须是格式 0。**
**违反了接收端就不知道该沿用哪个值,只能猜,一猜就错位。**
**还有一个隐蔽的坑:格式 3 有两种含义。**
**它既表示「新消息,字段全沿用」,也表示「后续块」。**
**前者出现在消息的第一块,后者出现在同一条消息的中间块。**
**区分办法是看「这条消息的长度是否已经收满」。**
**收满了就是新消息,没满就是继续。**
四种格式的长度是 11、7、3、0,每级砍掉一组字段。 六块累计 30 字节,全用格式 0 是 72 字节——省了 58%。直播跑起来之后,绝大多数块都是格式 3,就是这个原因。 格式 3 的两种含义只差一个判断:这个 csid 上的消息收满了没有。收满就是新消息,没满就是继续。
30 秒实验
- 把第二块也改成格式 0,看总字节涨到多少——多 10 字节,这就是「不做增量编码」的代价。
- 把最后一块改成格式 3,算时间戳还对不对——格式 3 会沿用上一段的增量,所以「第 6 块时间戳」会算错。
- 把
seq全换成格式 3,看总字节降到多少——6 字节,但接收端已经没法知道长度与类型了,这是不可解的。
常见误区
- 「消息头固定 11 字节」——四种格式,最短 0 字节。
- 「格式 3 只有一种含义」——既表示新消息,也表示后续块。
- 「一条消息的中间块可以用格式 1」——可以,但没人这么写,容易与「新消息」混淆。
自测三问(点开对答案)
1. 四种消息头格式各多少字节? —— **11、7、3、0。**逐级砍掉「与上一块相同」的字段。 2. 格式 3 的两种含义怎么区分? —— **看这条消息收满了没有。**收满是新消息,没满是后续块。 3. 为什么一条消息的第一块必须是格式 0? —— **因为格式 1/2/3 都依赖上一次的值**,没有「上一次」就只能全写。- ☐ (本节完,下一节 4.4.5 扩展时间戳)