RTSP 只有四个概念:URL、Session、CSeq、Transport。 前三个决定「跟谁说话、我是谁、这是第几句」,第四个决定「数据往哪个门进」。
2.3 走完了四步流程,这一节把流程里反复出现的四个词钉死。 这四个词搞混一个,客户端就连不上——而且报错信息往往不指向真正的原因。
一、URL:指向哪一路流
RTSP 的 URL 长这样:rtsp://192.168.1.64:554/Streaming/Channels/101。
结构和 HTTP 的 URL 一模一样:协议、主机、端口、路径。
三个容易踩的点:
① 端口可以省略,省略就是 554。有些设备改成 8554,就必须写出来。
② 路径区分大小写,而且是设备厂家自定义的。海康是 /Streaming/Channels/101,大华是 /cam/realmonitor?channel=1,写错了就是 404。
③ SETUP 时要在后面拼 trackID,变成 .../101/trackID=0。这是 RTSP 特有的写法,HTTP 里没有对应物。
所以「一个 URL」在 RTSP 里其实有两个形态:
会话级 URL(DESCRIBE 用)和轨道级 URL(SETUP/PLAY 用)。
后者是前者加上 SDP 里的 a=control 值。
二、Session:这一次会话的身份证
Session 是 RTSP 区别于 HTTP 的核心。
它是一串十六进制字符(常见 8 位,如 1A2B3C4D),由服务器在 SETUP 的响应里分配。
它的作用只有一个:让服务器在收到一条请求时,知道「这是哪条会话的」。 因为一台服务器上可能同时有几百条会话,每条对应不同的客户端、不同的流位置。
三个必须记住的细节:
① 值后面可能带参数:1A2B3C4D;timeout=60。存的时候要把 ; 后面的切掉,只留号码本体。
很多人把整串抄回去,服务器就认不出来——这就是 454 的第一大来源。
② 会话会超时:timeout 秒内没请求就作废。作废后带老 Session 来,就是 454。
③ 一个会话可以有多个轨道:音视频共享同一个 Session,只是 trackID 不同。所以别以为「两路流就有两个 Session」。
三、CSeq:配对用的序号
CSeq 是「命令序列号」,从 1 开始,每条请求加一。 服务器必须把它原样抄进响应。
为什么需要它?因为 RTSP 允许流水线。 客户端可以连着发 OPTIONS、DESCRIBE、SETUP 而不用等响应,三条响应回来的顺序不一定和发出顺序一致。 这时候唯一的配对依据就是 CSeq。
实践中的三条规矩: ① 严格递增,不要跳也不要复用。 有些服务器(尤其是国产 NVR)看到重复 CSeq 会直接断连。 ② 响应到了先查 CSeq,对不上就丢掉这一条,别解析。 ③ 不要用 0——很多库把 0 当作「未初始化」。
顺便说一个坑:CSeq 是「每条连接」还是「每个会话」计数的?标准没强制。 实际设备里两种都有,所以最稳的做法是「一条连接内单调递增」,重连之后从头开始。
四、Transport:四个端口的来历
Transport 是 SETUP 独有的头域,也是唯一一个「客户端和服务器各写一半」的头域。
拆开看它的字段:
① RTP/AVP:用 RTP over UDP 传,AVP 是音视频profile。要 TCP 就写 RTP/AVP/TCP。
② unicast/multicast:单播还是组播。直播分发偶尔用组播,点播几乎全是单播。
③ client_port=5000-5001:客户端收数据的两个端口,偶奇成对。
④ server_port=6000-6001:服务器发数据的两个端口,由服务器填。
⑤ interleaved=0-1:走 TCP 时的通道号,代替端口。
这里最容易错的一点:client_port 是「客户端说、服务器抄」,而 server_port 是「服务器说、客户端记」。
方向反了,就会去错端口听,表现为「会话建起来了但一帧都收不到」。
五、动手 Demo:四个概念在一个真实 SETUP 里各占什么位置
跑一次 SETUP,把响应里的 Transport 和 Session 拆开; 然后故意带一个错的 Session 去 PLAY,看 454 是怎么回来的。
import socket, threading
SID = "1A2B3C4D"
def handle(txt):
head = txt.split("\r\n")
meth, url, _ = head[0].split(" ")
hd = dict(l.split(": ", 1) for l in head[1:] if ": " in l)
cseq = hd.get("CSeq", "0")
if meth == "SETUP":
cp = hd.get("Transport", "").split("client_port=")[-1]
return ("RTSP/1.0 200 OK\r\nCSeq: %s\r\n"
"Transport: RTP/AVP;unicast;client_port=%s;"
"server_port=6000-6001\r\n"
"Session: %s;timeout=60\r\n\r\n" % (cseq, cp, SID))
sess = hd.get("Session", "").split(";")[0]
if sess != SID:
return "RTSP/1.0 454 Session Not Found\r\nCSeq: %s\r\n\r\n" % cseq
return "RTSP/1.0 200 OK\r\nCSeq: %s\r\nSession: %s\r\n\r\n" % (cseq, SID)
def serve(sock):
for _ in range(2):
c, _ = sock.accept()
c.sendall(handle(c.recv(8192).decode()).encode())
c.close()
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()
def rtsp(text):
s = socket.create_connection(("127.0.0.1", port), 3.0)
s.sendall(text.encode())
r = s.recv(8192).decode()
s.close()
return r
r1 = rtsp("SETUP rtsp://127.0.0.1/live/test/trackID=0 RTSP/1.0\r\n"
"CSeq: 1\r\nTransport: RTP/AVP;unicast;"
"client_port=5000-5001\r\n\r\n")
print(r1.strip())
sid = r1.split("Session: ")[1].split(";")[0]
print("剥掉 ;timeout 之后的 Session = %r" % sid)
r2 = rtsp("PLAY rtsp://127.0.0.1/live/test RTSP/1.0\r\n"
"CSeq: 2\r\nSession: DEADBEEF\r\n\r\n")
print("带错 Session 的 PLAY -> %s" % r2.split("\r\n")[0])
实跑输出:
--------------------------------------------------------------------
四个重要概念——搞混任何一个都会连不上
--------------------------------------------------------------------
① SETUP 换回来的两样东西
头域 值 作用
--------------------------------------------------------------------------------
Transport client_port=5000-5001 客户端在这两个端口收 RTP/RTCP
Session 1A2B3C4D 这次会话的身份证,后面每条请求都要带
② 不带 Session 会怎样
**实测:带了一个错的 Session 号,服务器回 `RTSP/1.0 454 Session Not Found`。**
**因为服务器按 Session 找上下文,找不到就不知道你要播哪一路。**
**这也是 RTSP 和 HTTP 最大的区别:它有「会话」这个概念。**
③ 四个概念一句话
概念 一句话
-----------------------------------------------------
URL 指向哪一路流,形如 rtsp://host/path
Session 这一次连接的身份证,由 SETUP 分配
CSeq 请求序号,响应原样带回,用来配对
Transport 用什么协议传、两边各用哪个端口
**Transport 里的 `client_port=5000-5001` 是两个端口:**
**偶数给 RTP(画面),下一个奇数给 RTCP(统计与反馈)。**
**所以一次 RTSP 会话实际占用四个端口:客户端两个、服务器两个。**
服务器把 client_port 抄回来了,又补上 server_port——一条会话的四个端口到这里就齐了。
Session: 1A2B3C4D;timeout=60 里,真正的编号只有 1A2B3C4D。代码里必须切掉分号后面那段。
带 DEADBEEF 去 PLAY,服务器回 454。它不是「密码错了」,是「我不认识这个会话」。
30 秒实验
- 把
sid的取值改成r1.split("Session: ")[1].strip()(不切分号),再发一次 PLAY,看是不是也回 454——这就是最常见的那个坑。 - 把
client_port改成5002-5003再跑,看服务器是不是照样抄回来——端口是客户端定,服务器不管。 - 把
CSeq两条都设成1,体会「重复 CSeq」在某些服务器上会被断连。
常见误区
- 「Session 就是 TCP 连接」——连接断了会话还在,重连带原 Session 还能接着播。这是两层东西。
- 「音视频是两条会话」——同一个 Session,两个 trackID。SETUP 两次但 Session 号一样。
- 「client_port 和 server_port 是一回事」——前者客户端定、后者服务器定,方向反了就是收不到数据。
自测三问(点开对答案)
1. Session 值里带 `;timeout=60`,存的时候怎么处理? —— **切掉分号后面的部分,只留编号本体。**整串抄回去会得到 454。 2. CSeq 的作用是什么? —— **把响应和请求配对。**流水线场景下响应可能乱序回来,只能靠它。 3. 一次会话占几个端口?谁定? —— **四个。客户端那对由客户端定,服务器那对由服务器定。**偶数给 RTP、下一个奇数给 RTCP。- ☐ (本节完,下一节 2.5 RTSP重要方法)