网络串流的第一步不是「收数据」,是「谈妥怎么收」。 五条报文一来一回,谈完才开始有画面——这五条就是 RTSP 的全部骨架。
前两节讲了 VLC 是什么、能干什么,这一节看它连上去的头一秒。 用自己写的七十行假服务端来对照,比看文档清楚:服务端要回什么、少一个字段会怎样, 一改就现形。 RTSP 报文的逐字细节在第一章 1.3 与 1.5,这一节只管「顺序和分工」。
一、五步,一步不多
OPTIONS → DESCRIBE → SETUP → PLAY,收工时一条 TEARDOWN。
| 步 | 问什么 | 答什么 | 少了会怎样 |
|---|---|---|---|
| OPTIONS | 你支持哪些方法 | 方法清单 | 不影响播放 |
| DESCRIBE | 这路流长什么样 | SDP 描述 | 不知道有几路、什么编码 |
| SETUP | 数据发到哪个端口 | 会话号+服务端端口 | 没地方收数据 |
| PLAY | 开始发 | 确认+时间范围 | 握手完成但没数据 |
| TEARDOWN | 结束 | 确认 | 服务端等你超时才释放 |
第二行和第三行最容易被跳过,因为很多人以为「连上就能播」。 DESCRIBE 拿到的 SDP 决定后面所有事:一路还是两路、H.264 还是 H.265、时钟频率是多少。 SETUP 才是真正分配资源的那一步:客户端说「我 8000 端口收」,服务端说「我从 9000 发」, 两边从此对着这两个端口说话。
二、两路流要 SETUP 两次
音视频各一路,就得 SETUP 两遍。 这是新手最常漏的一步。
SDP 里有几个 m= 行,就要 SETUP 几次:
| SDP 内容 | m= 行数 | SETUP 次数 |
|---|---|---|
| 只有视频 | 1 | 1 |
| 音视频都有 | 2 | 2 |
| 视频+双向对讲 | 3 | 3 |
只 SETUP 一路,另一路就是「静音」或「黑屏」,而且很多服务端不报错,只是那一路永远没数据。 D21 的假服务端只声明了一路,所以只 SETUP 一次——真实摄像机一般是两路,代码里得循环。
三、那 129 个字节的 SDP 里有什么
D21 里 DESCRIBE 回来的实体是 129 字节,全文不长,但信息密度极高。
| 行 | 写了什么 | 谁来用 |
|---|---|---|---|
| v=0 | 版本号,永远是 0 | 解析器校验 |
| o= | 会话标识与版本 | 判断描述变没变 |
| s= | 会话名 | 给人看的 |
| t=0 0 | 时间范围,0 0 表示不限 | 直播都是 0 0 |
| a=control:* | 基准控制地址 | 拼 SETUP 的 URL |
| m=video 0 RTP/AVP 96 | 一路视频,载荷类型 96 | 决定有几路 |
| a=rtpmap:96 H264/90000 | 96 号就是 H.264,时钟 90 kHz | 决定怎么解 |
| a=control:trackID=0 | 这一路的控制名 | 拼 SETUP 的 URL |
m=video 0 里的那个 0 是个坑:它不是端口,是「端口见别处」,真实端口在 SETUP 的响应里才定。
a=rtpmap 里的 90 kHz 更重要:它就是 RTP 时间戳的时钟,算错这个,同步就全错。
四、握手快不代表播放快
D21 五步合计 0.8 毫秒,全在本机回环上。这只是握手,不是起播。
真实链路上,从点开到出画面要经过四段:
| 段 | 典型耗时 | 卡在哪 |
|---|---|---|
| TCP 连接+认证 | 10~50 毫秒 | 网络往返、摘要计算 |
| 五步握手 | 20~100 毫秒 | 每步一个往返 |
| 等第一个关键帧 | 0~4000 毫秒 | GOP 长度 |
| 攒够缓冲 | 200~1000 毫秒 | 缓冲策略 |
第三行常常是最大的一块:如果 GOP 是 4 秒,最坏要等 4 秒才有一个 I 帧,在那之前解不出完整画面。 这就是为什么监控摄像机要把 GOP 调到 1~2 秒——不是为了码率,是为了「点开就能看」。
五、动手 Demo:七十行假服务端,把五步走通
不装 VLC、不连真摄像机,用标准库起一个假服务端,让客户端把 OPTIONS 到 TEARDOWN 走一遍。 这是理解 RTSP 最快的办法:报文你能亲手改,改错立刻看到后果。
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(("127.0.0.1", 0))
srv.listen(1)
def serve(sock):
conn, _ = sock.accept()
f = conn.makefile("rwb")
while True:
head = f.readline().decode("latin-1").strip()
if not head:
break
method = head.split(" ")[0]
# 读完头,取 CSeq
if method == u"DESCRIBE":
body = sdp.encode("latin-1")
extra = u"Content-Type: application/sdp\r\n"
else:
body = b""
extra = u""
resp = (u"RTSP/1.0 200 OK\r\nCSeq: " + cseq + u"\r\n"
+ extra + u"Content-Length: %d\r\n\r\n" % len(body))
f.write(resp.encode("latin-1") + body)
实跑输出:
--------------------------------------------------------------------
在本机起一个最小 RTSP 服务端,让播放器真连一次
--------------------------------------------------------------------
**网络串流不是玄学,就是在 TCP 上按行发几段文本。** 这一节用一个七十行的假服务端,
把 DESCRIBE→SETUP→PLAY 走通。
假服务端监听 **127.0.0.1:52758**,SDP 里声明一路 H.264。
步 请求 状态码 实体字节 往返
---------------------------------------------------------
1 OPTIONS 200 0 0.21 毫秒
2 DESCRIBE 200 129 0.17 毫秒
3 SETUP 200 0 0.07 毫秒
4 PLAY 200 0 0.06 毫秒
5 TEARDOWN 200 0 0.05 毫秒
五步合计 **0.86 毫秒**,全在本机回环上——**协议握手本身几乎不花时间,慢的是网络。**
**服务端收到的东西按 CSeq 排队,一条不差:**
CSeq=1 OPTIONS rtsp://127.0.0.1:52758/cam RTSP/1.0
CSeq=2 DESCRIBE rtsp://127.0.0.1:52758/cam RTSP/1.0
CSeq=3 SETUP rtsp://127.0.0.1:52758/cam/trackID=0 RTSP/1.0
CSeq=4 PLAY rtsp://127.0.0.1:52758/cam RTSP/1.0
CSeq=5 TEARDOWN rtsp://127.0.0.1:52758/cam RTSP/1.0
**真实摄像机比这多两件事**:一是要认证(401 挑战,客户端带用户名密码的摘要再发一次),
二是不止一路流(主码流+子码流)。
但握手骨架就是上面这五步,一步不多。
五步平均 0.1 毫秒,全在回环上 —— 这说明协议本身不慢,慢的是往返。 真机上每一跳要几十毫秒,五步就是两三百毫秒。
写这个假服务端时踩了一个真坑:DESCRIBE 的响应忘了写 Content-Length,客户端没读走 SDP 实体,
下一次请求读到的第一行是上一包的 v=0。
表现是「SETUP 收到了一个叫 v=0 的状态码」——协议报文是流式的,不分包就没有边界,
少一个长度字段就串包。
这一点在真实项目里一样成立:自己写 RTSP 客户端时,读响应必须按 Content-Length 精确读实体,
不能「读到空行就算完」。
30 秒实验
- 把假服务端的 OPTIONS 响应里删掉
Public头,看客户端会怎样——大多数客户端不检查,直接用。 - 在 SDP 里加一路音频(再来一个
m=audio),把客户端改成 SETUP 两次, 体会「两路流要两次 SETUP」。 - 把
Content-Length故意写大 10,看客户端卡在哪——会一直等那 10 个字节,直到超时。
常见误区
- 以为握手完就有画面。 握手只是谈妥参数,真正出画面还要等关键帧、攒缓冲,最坏要等一个 GOP。
- 以为 DESCRIBE 可以跳过。 不看 SDP 就不知道该 SETUP 几路、载荷类型是什么、时钟频率多少, 后面全是猜。
- 以为响应读到空行就结束了。 有实体时必须按 Content-Length 读完, 否则残留数据会串到下一个响应里。
自测三问(点开对答案)
1. 为什么要先 OPTIONS 再 DESCRIBE? —— OPTIONS 是探路,问对方支持哪些方法,免得发了不被支持的方法被拒。 不少客户端为了省一个往返会跳过它,直接 DESCRIBE。 2. SETUP 里客户端和服务端各说了一个端口,分别干什么? —— client_port 是客户端收 RTP 的端口,server_port 是服务端发的端口。 之后的数据全部按这一对端口走。 3. 为什么摄像机把 GOP 调短能让「点开更快」? —— 因为解画面必须从 I 帧开始,GOP 越长,等下一个 I 帧的时间越长。 1 秒 GOP 最坏等 1 秒,4 秒 GOP 最坏等 4 秒。下一节反过来:让 VLC 自己当服务器,把画面推出去——播放器和服务端其实是同一套代码的两种用法。
- ☐ (本节完,下一节 02.01.04 VLC作为流媒体服务器)