推流是四步:握手、connect、createStream、publish。 四步走完才能发 metadata 与音视频,顺序错了服务端直接踢。
11.5.1 把带 RTC 的版本编出来了,这一节先用普通的 RTMP 推。 11.5.3 换 WebRTC 播同一路流。
一、推流是四步
① 握手:发 C0+C1 共 1537 字节,收 S0+S1+S2 共 3073 字节。
② connect:带 app 名与 tcUrl,等一个 _result。
③ createStream:要一条流,回的 _result 里带流号。
④ publish:带流名,等 onStatus 里的 Publish.Start。
四步的顺序不能变,因为每一步都在给下一步准备参数: connect 决定 app,createStream 决定流号,publish 决定流名。 顺序错了服务端会直接断开,而且不会告诉你为什么。
握手这 3073 字节是双向对称的,这个数很好记: 1537 是 C0 一个字节加 C1 的 1536 字节,另一侧完全一样。
二、真跑一遍:每一步多少字节
这一节不是画流程图,是真起一个只负责接收的 RTMP 服务端, 把一段真编出来的 FLV 推上去,逐步记字节数。
实测下来:握手上下各 3073 字节,后面三条命令都是几十字节, 而视频消息一口气发了一万多字节。 整段上行 13749 字节,服务端只回了 232 字节。
这个比例说明推流的本质: 客户端说一百句,服务端回一句。 所以推流的瓶颈永远在推流端的上行带宽,不在服务端的响应。
三、这份数据说明了什么
两条结论特别值得记:
① 视频消息最大与最小差了 67 倍。 最大那条是关键帧(I 帧),小的是普通帧(P 帧)。 这意味着码率只是平均值,瞬时带宽要按关键帧算, 否则一到关键帧就卡。 ② 消息被切成 128 字节一块,块头开销占 4.1%。 握手之后一共 10676 字节,其中负载 10240 字节, 剩下 436 字节全是块头。
第二条有个反向的推论: 消息越大,块头摊得越薄;消息越小,块头占比越高。 所以 RTMP 在低码率、小帧的场景下效率最差——这也解释了为什么同一路流,音频单独推的时候开销比例会更高。
四、动手 Demo:推 RTMP 流——四步,一次真的推上去
起一个只接收的 RTMP 服务端,手工完成握手与三条命令, 再把整段 FLV 的 tag 按消息发过去。
# 1. 握手:发 C0+C1,收 S0+S1+S2
c.sendall(b"\x03" + c1) # C0 + C1(1536)
s1 = rd(c, 1537) # S0 + S1
rd(c, 1536) # S2
c.sendall(s1[1:]) # C2 = S1 回抄
# 2. connect
body = amf_str("connect") + amf_num(1) + amf_obj({
"app": "live", "tcUrl": "rtmp://127.0.0.1/live"})
c.sendall(chunk(3, 20, 0, 0, body))
# 3. createStream
c.sendall(chunk(5, 20, 0, 0,
amf_str("createStream") + amf_num(2) + amf_null()))
# 4. publish
c.sendall(chunk(5, 20, 1, 0,
amf_str("publish") + amf_num(3) + amf_null()
+ amf_str("livestream")))
# 5. metadata 与视频帧,逐个 tag 发
for t, ts, body in flv_tags(raw):
if t == 18:
c.sendall(chunk(4, 18, 1, ts, body)) # metadata
elif t == 9:
c.sendall(chunk(6, 9, 1, ts, body)) # 视频
实跑输出:
--------------------------------------------------------------------
推 RTMP 流——四步,一次真的推上去
--------------------------------------------------------------------
**① 推流是四步,一步都不能少**
步骤 发什么 等什么回应
-----------------------------------------------------------------
握手 C0+C1 共 1537 字节 S0+S1+S2 共 3073 字节
connect app 名与 tcUrl `_result`
createStream 要一条流 `_result` 带流号
publish 流名 `onStatus` 的 Start
**四步走完才能发 metadata 与音视频。**
**顺序错了服务端会直接踢掉,而且不会告诉你为什么。**
**② 真跑一遍,全程实测**
**先编一段 FLV,再起一个只负责接收的 RTMP 服务端,把它推上去。**
**每一步各花了多少字节:**
阶段 上行字节 下行字节
------------------------------------------
握手 3073 3073
connect 124 83
createStream 37 41
publish 45 108
metadata 197 0
视频 10273 0
合计 13749 232
消息类型 条数 说明
---------------------------------------------------------------
20 命令 3 connect、createStream、publish
18 metadata 1 onMetaData,尺寸与码率
9 视频 27 序列头 1 条 + 每帧 1 条 + 结束标记
**publish 的回应是 `NetStream.Publish.Start`,服务端认下了这条流。**
**整段上行 13749 字节,服务端只回了 232 字节。**
**推流是极度不对称的:客户端说一百句,服务端回一句。**
**③ 这份数据说明了什么**
**26 条视频消息里,最大 3198 字节、最小 48 字节,差了 67 倍。**
**最大那条是关键帧(I 帧),小的是普通帧(P 帧)。**
**这一点对带宽规划很关键:码率是平均值,**
**瞬时带宽要按关键帧算,否则一到关键帧就卡。**
**消息被切成了 128 字节一块:31 条消息里,25 条要切好几块。**
**握手之后一共发了 10676 字节,其中消息负载 10240 字节,**
**剩下的 436 字节全是块头的开销,占 4.1%。**
**这就是 RTMP 在高码率下效率不如裸 TCP 的原因:**
**消息越大,块头摊得越薄;消息越小,块头占比越高。**
握手上下各 3073 字节,三条命令加起来不到 250 字节。
视频一口气 10273 字节,上行的绝大部分都在这儿。
服务端全程只回了 232 字节,比例悬殊。
publish 的回应是 NetStream.Publish.Start,收到它才算推上去。
30 秒实验
- 把 publish 放到 createStream 前面,猜会怎样——服务端直接断开,因为它还不认识那个流号。
- 不发 metadata 只发视频,看还能不能播——多数播放器能播,但不知道尺寸,可能显示错位。
- 把
chunk size从 128 调大,猜块头占比怎么变——降下来,这正是 RTMP 支持协商块长的原因。
常见误区
- 「握手是单向的」——双向各 3073 字节,完全对称。
- 「推流带宽看平均值」——要看关键帧那一瞬间的峰值。
- 「服务端要回很多数据」——推流阶段只回几条应答。
自测三问(点开对答案)
1. 推流四步的顺序是什么? —— **握手、connect、createStream、publish。** 2. 握手一共多少字节? —— **单向 3073 字节**,双向各一份。 3. 为什么关键帧那一瞬间带宽最高? —— **I 帧不参考别的帧,数据量最大**,实测差 67 倍。- ☐ (本节完,下一节 11.5.3 WebRTC 播放视频流)