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

拉起一路 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 秒实验

  1. 把 DESCRIBE 删掉直接 SETUP,看还能不能播——实际服务器大多会回 454 或者 455,因为没有 SDP 就不知道 trackID。
  2. 把 PLAY 的 Session 删掉,看返回什么——这是自己写客户端最常见的错。
  3. 把 timeout=60 改成 timeout=5,然后在 PLAY 之后 sleep(6) 再发请求,看服务器是不是回 454。

常见误区

  1. 「四步缺一步也能播」——某些服务器做了容错,但标准流程不能省。省掉 OPTIONS 会漏掉能力探测,省掉 DESCRIBE 会拿不到 trackID。
  2. 「PLAY 之后就结束了」——PLAY 只让服务器开始发,会话一直在,直到 TEARDOWN 或超时。
  3. 「TEARDOWN 不发也没事」——靠超时回收要等 60 秒,高并发下端口会被占满,新会话建不起来。
自测三问(点开对答案) 1. 四步各自的输出是什么? —— **OPTIONS 给方法列表,DESCRIBE 给 SDP,SETUP 给 Transport + Session,PLAY 给 RTP-Info。** 2. 为什么 SETUP 可能要做两次? —— **因为音视频是两条轨道。**每条 `m=` 行都要单独 SETUP,但它们共用同一个 Session。 3. PLAY 之后多久不发请求会话会被拆? —— **看 SETUP 响应里的 `timeout`,通常是 60 秒。**所以心跳间隔要远小于它。

相关笔记