RTSP 的方法分三种可逆性:只读的、可来回切的、拆了就回不去的。 PLAY 和 PAUSE 能任意来回,TEARDOWN 不行——这条差别决定了客户端状态机怎么画。
2.4 把四个概念钉死了,这一节看这些概念是被哪些「动作」改动的。 2.6.1 的状态机就是把这一节的方法画成图。
一、方法按「改不改状态」分三类
别按字母顺序记,按「它会不会改状态」分三类最省事:
① 只读(不改任何状态) OPTIONS 问能力、DESCRIBE 取 SDP。调一百次也不影响会话,可以随便重试。 ② 可来回切(改传输状态,但会话还在) PLAY 开始/继续发流,PAUSE 停发但保留会话与端口。这两个是双向的:PAUSE 之后能再 PLAY,PLAY 之后能再 PAUSE。 ③ 单向(一建一拆) SETUP 建通道、TEARDOWN 拆通道。TEARDOWN 之后想再播,必须重新 SETUP 拿新 Session,回不到从前。
还有一个特殊方法 RECORD,方向是反的——它让客户端往服务器【发】流。 监控的录像回传、部分 NVR 的上传走的就是它。 配套的还有 ANNOUNCE,用来先把 SDP 告诉服务器。
二、PLAY 与 PAUSE:唯一可逆的一对
这一对是 RTSP 最有价值的地方,也是它比 HTTP 强的根本。
PAUSE 做的事很轻:服务器停止发 RTP,但会话、端口、播放位置全保留。 再 PLAY 时从停下的地方接着走,不重头、不重新协商。
这带来一个必须注意的细节:时间戳。
PAUSE 期间时间在流逝,但媒体时间不该跟着走。
所以恢复 PLAY 时,服务器要在 RTP-Info 里给出新的起始序号和时间戳,客户端按它重新对齐音画。
如果服务器忘了更新,恢复后就会出现音画不同步——这是不少廉价设备的通病。
PAUSE 还有一个常见误用:拿它当「停止」。 PAUSE 不释放端口、不删会话,服务器那边的资源一直占着。 用户关窗口时该发的是 TEARDOWN,不是 PAUSE。
三、TEARDOWN:拆了就回不去
TEARDOWN 是「终止」,做三件事:停发、释放端口、删会话。 做完之后带老 Session 来一律 454。
它不是必需的——不发也能播,靠 60 秒超时服务器自己回收。 但那是「等着别人打扫」,代价是端口被白占一分钟。
什么场景下这条代价很痛?
① 高频重连:客户端每隔几秒重连一次,端口很快耗尽。
② 端口范围受限:有些服务器的 server_port 只能从固定区间取,几百个会话就把区间用光了。
③ 摄像头有连接数上限:不少 IPC 只允许 4~6 路并发,不发 TEARDOWN 会让你「明明退出了却连不上」。
所以规矩很简单:catch 到任何退出路径都要发 TEARDOWN。 哪怕服务器不回响应,发出去也比不发好。
四、方法在哪个状态下可用
发错方法不会「没反应」,会得到一个明确的错误:455 Method Not Valid in This State。
几个典型的非法组合: ① 没 SETUP 就 PLAY → 455。没有传输通道,不知道往哪发。 ② TEARDOWN 之后又 PLAY → 454(会话没了)或者 455。 ③ Ready 状态下发 PAUSE → 455。因为 Ready 的含义是「没在播」,没在播就没什么可暂停的。
455 和 454 的区别要分清: 454 是「会话不存在」,455 是「会话在,但这个动作现在不能做」。 前者要重新 SETUP,后者只是流程走错了。
把这一节和 2.6.1 的状态机连起来看就通了: 状态机的每一条边,标注的就是「在哪个状态能发哪个方法」。 方法表是静态的许可,状态机是动态的路径。
五、动手 Demo:PLAY、PAUSE、PLAY、TEARDOWN,看会话状态怎么走
一条连接上按顺序发七个方法,每步记下响应码和当前会话状态。 重点看第 4~6 步:PAUSE 之后还能不能再 PLAY。
import socket, threading
SID = "1A2B3C4D"
STATE = ["未建立"]
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")
bad = "RTSP/1.0 455 Method Not Valid in This State\r\nCSeq: %s\r\n\r\n" % cseq
ok = "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" % cseq
if meth == "SETUP":
STATE[0] = "已就绪"
return ok + "Session: %s\r\n\r\n" % SID
if meth == "PLAY":
if STATE[0] not in ("已就绪", "暂停"):
return bad
STATE[0] = "播放中"
return ok + "Session: %s\r\n\r\n" % SID
if meth == "PAUSE":
if STATE[0] != "播放中":
return bad
STATE[0] = "暂停"
return ok + "Session: %s\r\n\r\n" % SID
if meth == "TEARDOWN":
STATE[0] = "已拆除"
return ok + "Session: %s\r\n\r\n" % SID
return ok + "\r\n"
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)
STEPS = ("OPTIONS", "DESCRIBE", "SETUP", "PLAY", "PAUSE", "PLAY", "TEARDOWN")
for i, meth in enumerate(STEPS, 1):
extra = "Session: %s\r\n" % SID if meth in ("PLAY", "PAUSE", "TEARDOWN") else ""
s.sendall(("%s rtsp://127.0.0.1/live/test RTSP/1.0\r\nCSeq: %d\r\n%s\r\n"
% (meth, i, extra)).encode())
r = s.recv(8192).decode().split("\r\n")[0]
print("%d %-9s %-42s 状态=%s" % (i, meth, r, STATE[0]))
s.close()
实跑输出:
--------------------------------------------------------------------
重要方法——PLAY 之后还能 PAUSE,这是 RTSP 的本事
--------------------------------------------------------------------
① 这里真跑了一遍 PLAY → PAUSE → PLAY → TEARDOWN
次序 方法 响应 会话状态
------------------------------------------------------
1 OPTIONS RTSP/1.0 200 OK 未建立
2 DESCRIBE RTSP/1.0 200 OK 未建立
3 SETUP RTSP/1.0 200 OK 已就绪
4 PLAY RTSP/1.0 200 OK 播放中
5 PAUSE RTSP/1.0 200 OK 暂停
6 PLAY RTSP/1.0 200 OK 播放中
7 TEARDOWN RTSP/1.0 200 OK 已拆除
② 每个方法改的是「哪一层」
方法 改什么 能不能撤回
--------------------------------------------------------------------
OPTIONS 什么都不改,只问能力 —
DESCRIBE 什么都不改,只取 SDP —
SETUP 建一条传输通道,分配 Session 用 TEARDOWN 拆
PLAY 让 RTP 开始发 用 PAUSE 停
PAUSE 停发但保留会话 用 PLAY 继续
TEARDOWN 拆会话、释放端口 不能,要重新 SETUP
**PLAY 和 PAUSE 可以来回切,这是 RTSP 比 HTTP 强的地方——**
**HTTP 的 GET 是「取一次就完」,没有「停在半路再继续」的概念。**
**监控里拖动时间轴回放、会议里静音暂停,用的都是这一套。**
第 4 步 PLAY 进「播放中」,第 5 步 PAUSE 进「暂停」,第 6 步 PLAY 又回「播放中」——这一对照,可逆性就看见了。
第 7 步 TEARDOWN 之后状态是「已拆除」,回不到「已就绪」,这就是单向。
注意服务器是靠 STATE 这个变量判断的,它没有重看 Session——真实服务器两个都查。
30 秒实验
- 把第 5 步的 PAUSE 改成「在 SETUP 之后立刻 PAUSE」,看是不是回 455——这就是「状态不合法」。
- 在 TEARDOWN 之后再补一条 PLAY,看回 455 还是 454——取决于服务器实现,两种都可能。
- 把
PAUSE换成RECORD,看它会走到哪个分支——默认落到ok分支,这就是「服务器没实现也不报错」的隐患。
常见误区
- 「PAUSE 等于停止」——PAUSE 保留会话和端口,资源一直占着。关窗口要发 TEARDOWN。
- 「TEARDOWN 不发也行」——靠超时要等 60 秒,摄像头并发只有几路,会「退出了却连不上」。
- 「455 和 454 差不多」——454 是会话没了要重 SETUP,455 是会话还在但这一步不该发。处理方式完全不同。
自测三问(点开对答案)
1. 哪些方法是可逆的? —— **PLAY 和 PAUSE 可以任意来回。**SETUP/TEARDOWN 是单向的一对。 2. PAUSE 之后 PLAY,服务器为什么要在 RTP-Info 里给新时间戳? —— **因为暂停期间墙钟在走、媒体时间该停。**不更新就会音画不同步。 3. 455 和 454 分别意味着什么? —— **455:会话在,但这个方法在当前状态下不允许。454:会话根本不存在(没带、抄错、已超时)。**- ☐ (本节完,下一节 2.6.1 客户端状态机)