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

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

  1. 把 sid 的取值改成 r1.split("Session: ")[1].strip()(不切分号),再发一次 PLAY,看是不是也回 454——这就是最常见的那个坑。
  2. 把 client_port 改成 5002-5003 再跑,看服务器是不是照样抄回来——端口是客户端定,服务器不管。
  3. 把 CSeq 两条都设成 1,体会「重复 CSeq」在某些服务器上会被断连。

常见误区

  1. 「Session 就是 TCP 连接」——连接断了会话还在,重连带原 Session 还能接着播。这是两层东西。
  2. 「音视频是两条会话」——同一个 Session,两个 trackID。SETUP 两次但 Session 号一样。
  3. 「client_port 和 server_port 是一回事」——前者客户端定、后者服务器定,方向反了就是收不到数据。
自测三问(点开对答案) 1. Session 值里带 `;timeout=60`,存的时候怎么处理? —— **切掉分号后面的部分,只留编号本体。**整串抄回去会得到 454。 2. CSeq 的作用是什么? —— **把响应和请求配对。**流水线场景下响应可能乱序回来,只能靠它。 3. 一次会话占几个端口?谁定? —— **四个。客户端那对由客户端定,服务器那对由服务器定。**偶数给 RTP、下一个奇数给 RTCP。

相关笔记