🎬 第二章 RTSP流媒体协议
📄 一句话概括

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 秒实验

  1. 把第 5 步的 PAUSE 改成「在 SETUP 之后立刻 PAUSE」,看是不是回 455——这就是「状态不合法」。
  2. 在 TEARDOWN 之后再补一条 PLAY,看回 455 还是 454——取决于服务器实现,两种都可能。
  3. 把 PAUSE 换成 RECORD,看它会走到哪个分支——默认落到 ok 分支,这就是「服务器没实现也不报错」的隐患。

常见误区

  1. 「PAUSE 等于停止」——PAUSE 保留会话和端口,资源一直占着。关窗口要发 TEARDOWN。
  2. 「TEARDOWN 不发也行」——靠超时要等 60 秒,摄像头并发只有几路,会「退出了却连不上」。
  3. 「455 和 454 差不多」——454 是会话没了要重 SETUP,455 是会话还在但这一步不该发。处理方式完全不同。
自测三问(点开对答案) 1. 哪些方法是可逆的? —— **PLAY 和 PAUSE 可以任意来回。**SETUP/TEARDOWN 是单向的一对。 2. PAUSE 之后 PLAY,服务器为什么要在 RTP-Info 里给新时间戳? —— **因为暂停期间墙钟在走、媒体时间该停。**不更新就会音画不同步。 3. 455 和 454 分别意味着什么? —— **455:会话在,但这个方法在当前状态下不允许。454:会话根本不存在(没带、抄错、已超时)。**

相关笔记