拉起一路 RTSP 流只需要四步:OPTIONS、DESCRIBE、SETUP、PLAY。 四步走完,客户端手上多了三样东西:SDP、Transport、Session——缺一样都播不了。
2.2 把报文格式拆完了,这一节把四条报文串成一条时间线。 第 8 章的 openRTSP 客户端、第 10~12 章的服务器,走的都是这四步。
一、四步各解决什么
这四步不是随便排的,每一步的输出是下一步的输入:
① OPTIONS:问「你会什么」。输出是一串方法名。作用是确认这台设备支持 PLAY,不然后面几步白做。
② DESCRIBE:要一份 SDP。输出是「有几路流、什么编码、trackID 是多少」。没有它,SETUP 时不知道该拼哪个 trackID。
③ SETUP:建传输通道。输出是 Transport(往哪发)和 Session(你是谁)。有几路流就要 SETUP 几次——音视频分开就是两次。
④ PLAY:开始发流。输出是 RTP-Info,告诉你从哪个序号、哪个时间戳开始。
四步之间有严格的依赖: 不 DESCRIBE 就不知道 trackID,不 SETUP 就没有 Session,没有 Session 就 PLAY 不动。 这条链断在哪一步,后面的全废。
二、DESCRIBE 带回一份 SDP
DESCRIBE 是整个流程里信息量最大的一步。 它的响应体是一份 SDP 文本,通常几百字节到几 KB。
SDP 里必须看的三处:
① m=video 0 RTP/AVP 96:有一路视频,用 RTP 传,载荷类型 96(动态)。
② a=rtpmap:96 H264/90000:96 号载荷就是 H.264,时钟频率 90000。
③ a=control:trackID=0:这条轨道的名字,SETUP 时要拼到 URL 后面。
96 这个数字有讲究:
0~95 是固定分配(比如 0 是 PCMU、26 是 JPEG),96~127 是动态的,具体是什么必须由 a=rtpmap 说清。
所以看到 96 却找不到 rtpmap,就说明 SDP 不完整,后面必错。
还有一路音频的话,SDP 里会多一组 m=audio,而且 a=control:trackID=1。
这时要做两次 SETUP,拿两个 Session 值——是同一条会话下的两条轨道,所以 Session 号相同,只是 URL 的 trackID 不同。
三、SETUP 换回两样东西
SETUP 是唯一一个「双向都带信息」的步骤:
客户端说:Transport: RTP/AVP;unicast;client_port=5000-5001。意思是「用 UDP 单播,我在 5000 收 RTP、5001 收 RTCP」。
服务器答:Transport: RTP/AVP;unicast;client_port=5000-5001;server_port=6000-6001,外加一个 Session: 1A2B3C4D;timeout=60。
注意服务器把客户端的端口原样抄回来了,再补上自己那一对(server_port)。所以一次会话四个端口,到这里就全定了。
timeout=60 是会话的寿命。
60 秒内没有任何请求,服务器就主动拆会话、释放那对端口。
客户端必须在这个窗口内发心跳(GET_PARAMETER 或 OPTIONS),否则播着播着就断。
四、TEARDOWN 与超时
正常的收尾是 TEARDOWN。 它不像 PLAY 那样可以反悔——拆了就没了,想再播必须重新 SETUP、拿新的 Session。
TEARDOWN 要做的事有三件:
① 服务器停发 RTP,把那条通道的两端都关掉。
② 释放 server_port 那一对端口,让别的会话能用。
③ 从会话表里删掉这一条,之后带这个 Session 号来都是 454。
但现实里最多的收尾不是 TEARDOWN,是「什么都不做」。 客户端直接关掉 socket,服务器靠 60 秒超时自己回收。 这在高并发下会出问题——端口被占着的 60 秒里,新连接可能建不起来。
所以写客户端的规矩是:退出前一定发 TEARDOWN,然后才 close。 哪怕服务器不响应,也要发出去——这是「礼貌」也是「性能」。
五、动手 Demo:四步真跑一遍,看每一步换回什么
一台最小服务器加一个按顺序走四步的客户端,每步都把关键头域提出来。 跑完你会看到 Session 号从无到有,RTP 端口从空到定。
import socket, threading
SDP = ("v=0\r\ns=Live\r\nc=IN IP4 0.0.0.0\r\n"
"m=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=control:trackID=0\r\n")
SID = "1A2B3C4D"
def handle(txt):
head = txt.split("\r\n")
meth = head[0].split(" ")[0]
hd = dict(l.split(": ", 1) for l in head[1:] if ": " in l)
cseq = hd.get("CSeq", "0")
base = "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" % cseq
if meth == "OPTIONS":
return base + "Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN\r\n\r\n"
if meth == "DESCRIBE":
return (base + "Content-Type: application/sdp\r\n"
"Content-Length: %d\r\n\r\n%s" % (len(SDP), SDP))
if meth == "SETUP":
return (base + "Transport: RTP/AVP;unicast;client_port=5000-5001;"
"server_port=6000-6001\r\n"
"Session: %s;timeout=60\r\n\r\n" % SID)
return base + "Session: %s\r\nRTP-Info: url=trackID=0;seq=1000\r\n\r\n" % SID
def serve(sock):
c, _ = sock.accept()
buf = b""
while True:
d = c.recv(8192)
if not d:
return
buf += d
while b"\r\n\r\n" in buf:
req, buf = buf.split(b"\r\n\r\n", 1)
c.sendall(handle(req.decode()).encode())
srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen(4)
port = srv.getsockname()[1]
threading.Thread(target=serve, args=(srv,), daemon=True).start()
s = socket.create_connection(("127.0.0.1", port), 3.0)
URL = "rtsp://127.0.0.1/live/test"
for i, meth in enumerate(("OPTIONS", "DESCRIBE", "SETUP", "PLAY", "TEARDOWN"), 1):
u = URL if meth != "SETUP" else URL + "/trackID=0"
extra = "Session: %s\r\n" % SID if meth in ("PLAY", "TEARDOWN") else ""
s.sendall(("%s %s RTSP/1.0\r\nCSeq: %d\r\n%s\r\n" % (meth, u, i, extra)).encode())
r = s.recv(8192).decode()
first = r.split("\r\n")[0]
keys = [l for l in r.split("\r\n") if l.startswith(("Session", "Transport", "Content-Type"))]
print("%d %-9s %-22s %s" % (i, meth, first, " | ".join(keys)))
s.close()
实跑输出:
--------------------------------------------------------------------
RTSP 交互流程——四步拉起一路流
--------------------------------------------------------------------
① 四步各自解决什么
步 请求 拿到什么 下一步靠它
-----------------------------------------------------------------------------
1 OPTIONS 服务器支持的方法列表 确认能做下面几步
2 DESCRIBE SDP:有几路流、什么编码 知道 trackID
3 SETUP Transport + Session 号 知道往哪发、认哪次会话
4 PLAY RTP-Info:起始序号与时间戳 开始收包
② 真实跑一遍的报文与状态码
次序 请求 响应 关键头域
----------------------------------------------------------
1 OPTIONS RTSP/1.0 200 OK —
2 DESCRIBE RTSP/1.0 200 OK Content-Type
3 SETUP RTSP/1.0 200 OK Transport
4 PLAY RTSP/1.0 200 OK Session
5 TEARDOWN RTSP/1.0 200 OK Session
③ DESCRIBE 拿回来的 SDP
| v=0
| o=- 0 0 IN IP4 127.0.0.1
| s=Live
| c=IN IP4 0.0.0.0
| t=0 0
| a=control:*
| m=video 0 RTP/AVP 96
| a=rtpmap:96 H264/90000
| a=fmtp:96 packetization-mode=1;profile-level-id=42e01f
| a=control:trackID=0
**四步走完,客户端手上有了三样东西:SDP(怎么解)、Transport(往哪收)、Session(我是谁)。**
**所以 RTSP 是「先谈好再传」——谈的过程全是文本,传的过程全是二进制。**
五条请求全部 200,但从第三条开始才有 Session——这就是「SESSION 是 SETUP 造的」。
Transport 只在 SETUP 出现一次,后面 PLAY 和 TEARDOWN 直接带 Session 就够了。
DESCRIBE 那一条带了 Content-Type,因为它是唯一有响应体的步骤。
30 秒实验
- 把 DESCRIBE 删掉直接 SETUP,看还能不能播——实际服务器大多会回 454 或者 455,因为没有 SDP 就不知道 trackID。
- 把 PLAY 的
Session删掉,看返回什么——这是自己写客户端最常见的错。 - 把
timeout=60改成timeout=5,然后在 PLAY 之后sleep(6)再发请求,看服务器是不是回 454。
常见误区
- 「四步缺一步也能播」——某些服务器做了容错,但标准流程不能省。省掉 OPTIONS 会漏掉能力探测,省掉 DESCRIBE 会拿不到 trackID。
- 「PLAY 之后就结束了」——PLAY 只让服务器开始发,会话一直在,直到 TEARDOWN 或超时。
- 「TEARDOWN 不发也没事」——靠超时回收要等 60 秒,高并发下端口会被占满,新会话建不起来。
自测三问(点开对答案)
1. 四步各自的输出是什么? —— **OPTIONS 给方法列表,DESCRIBE 给 SDP,SETUP 给 Transport + Session,PLAY 给 RTP-Info。** 2. 为什么 SETUP 可能要做两次? —— **因为音视频是两条轨道。**每条 `m=` 行都要单独 SETUP,但它们共用同一个 Session。 3. PLAY 之后多久不发请求会话会被拆? —— **看 SETUP 响应里的 `timeout`,通常是 60 秒。**所以心跳间隔要远小于它。- ☐ (本节完,下一节 2.4 RTSP重要概念)