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

我们前面写的服务器「把话说完了」,但一帧画面都没发。 VLC 一行命令就能补上这一段:读文件、编 RTP、在 8554 端口开 RTSP 服务。

📝 本节和前面几节的关系

2.6.2 讲了服务器要做哪些事,这一节先看一个「现成的」「做到什么程度」。 真正能上生产的服务器在第 8~12 章:Live555、EasyDarwin、Nginx、SRS、ZLMediaKit。

一、最小实现缺的那一半

把前面七个 Demo 的服务器拼起来,它已经能跑通四步握手、能分配端口、能回 Session。 但它缺了最关键的一半:PLAY 之后什么都不发。

真实的 RTSP 服务器在 PLAY 之后要做的事,比握手复杂得多: ① 打开媒体源,解封装成一路视频轨道。 ② 按时间戳切片,把每一帧切成若干个 RTP 包(一帧常切成几个到几十个,第 3 章讲)。 ③ 按节奏发包,不能一口气全推出去——要按 25fps 的节拍送。 ④ 处理丢包反馈,RTCP 收到 NACK 要重传。 ⑤ 会话结束就停,别把线程泄漏在那里。

这五件事里,第 ① 和 ② 需要编解码知识,第 ③ 需要定时器,第 ④ 需要理解 RTCP。 所以「写一个能握手的 RTSP 服务器」是几小时的事,「写一个能稳定发流的」是几个月的事。

二、VLC 一行命令当服务器

VLC 其实内置了一个 RTSP 服务器,只是没人这么用它。命令是: vlc -vvv test.mp4 --sout "#rtp{sdp=rtsp://:8554/test}"

拆开看这三段: ① -vvv:开三级日志。调不通的时候这一项最关键,它会把你发出去的每一条 RTSP 报文都打出来。 ② test.mp4:输入。可以是文件、摄像头、甚至另一路网络流。 ③ --sout "#rtp{...}":输出链。rtp 表示输出 RTP,sdp=rtsp://:8554/test 表示「顺便开一个 RTSP 服务,路径叫 test」。

客户端就用 vlc rtsp://127.0.0.1:8554/test 或 ffplay 拉。 如果要转码,往输出链里加 transcode{vcodec=h264,vb=800}。

三、它和我们的最小实现差在哪

把能力列成表,差别一目了然:

① 四步握手:都有。这部分是协议,谁写都一样。 ② 发 RTP 包:最小实现没有,VLC 有。这是分水岭。 ③ 转码:最小实现没有,VLC 有(--sout-transcode)。 ④ 多路复用:最小实现没有,VLC 有——一路输入可以同时输出 RTSP、HTTP、文件,这靠 --sout 里串联多个输出实现。

所以「RTSP 服务器」这个词有两层意思,必须分清: 一层是「把协议对话走完」,一层是「真的把媒体流送出去」。 我们写的是第一层,VLC 两样都有,第 8~12 章的专用服务器两样都有还更抗压。

四、什么时候能用 VLC,什么时候不能

VLC 当服务器,只适合三种场景: ① 自测:验证自己写的客户端能不能拉流。这最有用。 ② 演示:给客户演示「这一路流是好的」。 ③ 单路长期跑:一台机器一路流,跑几天没问题。

不能用的场景同样明确: ① 并发上不去:几十路就开始掉帧,因为它不是为多路设计的。 ② 没有鉴权:任何人都能拉,没有用户、没有 token。 ③ 没有录制、没有转发、没有集群:这三样是生产必须的。 ④ 崩溃会带走全部会话:一个进程扛所有,没有隔离。

一句话结论:VLC 是「调试用的服务器」,不是「生产用的服务器」。 调试时它省下的时间很值,生产时它省下的钱会以事故的形式还回去。

五、动手 Demo:我们的最小服务器发了多少字节,VLC 会发多少

真跑四步握手,把每一步的请求字节和响应字节都记下来。 看到「响应最大也才 280 字节」,就能明白为什么「不发流的服务器」这么省。

import socket, threading

SDP = ("v=0\r\ns=Live\r\nm=video 0 RTP/AVP 96\r\n"
       "a=rtpmap:96 H264/90000\r\na=control:trackID=0\r\n")
SID = "1A2B3C4D"
LOG = []

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")
    if meth == "DESCRIBE":
        body = "RTSP/1.0 200 OK\r\nCSeq: %s\r\nContent-Type: application/sdp\r\n" \
               "Content-Length: %d\r\n\r\n%s" % (cseq, len(SDP), SDP)
    elif meth == "SETUP":
        body = ("RTSP/1.0 200 OK\r\nCSeq: %s\r\n"
                "Transport: RTP/AVP;unicast;client_port=5000-5001;"
                "server_port=6000-6001\r\nSession: %s;timeout=60\r\n\r\n" % (cseq, SID))
    else:
        body = "RTSP/1.0 200 OK\r\nCSeq: %s\r\nSession: %s\r\n\r\n" % (cseq, SID)
    LOG.append((meth, len(txt.encode()), len(body.encode())))
    return body

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)
for i, m in enumerate(("OPTIONS", "DESCRIBE", "SETUP", "PLAY"), 1):
    s.sendall(("%s rtsp://127.0.0.1/live/test RTSP/1.0\r\nCSeq: %d\r\n\r\n"
                % (m, i)).encode())
    s.recv(8192)
s.close()

for m, up, down in LOG:
    print("%-9s 上行 %3d 字节  下行 %3d 字节" % (m, up, down))
print("总计 上行 %d 字节  下行 %d 字节"
      % (sum(x[1] for x in LOG), sum(x[2] for x in LOG)))
print("如果真发 2 Mbps 的流,PLAY 之后每秒是 %d 字节" % (2 * 1000 * 1000 // 8))

实跑输出:

--------------------------------------------------------------------
VLC 当 RTSP 服务器——它比最小实现多了【真的发包】
--------------------------------------------------------------------
① 我们的最小服务器:四步都通了,但只回了文本

  请求         请求字节     响应字节     状态
  ----------------------------------------------------------
  OPTIONS      56           115          RTSP/1.0 200 OK
  DESCRIBE     57           280          RTSP/1.0 200 OK
  SETUP        54           130          RTSP/1.0 200 OK
  PLAY         72           122          RTSP/1.0 200 OK

  **注意响应都很小(几百字节)——因为 PLAY 之后它【什么都不发】。**
**一个真正的 RTSP 服务器在 PLAY 之后要持续往客户端端口发 RTP 包。**

② VLC 的 RTSP 服务器模式

  📋 **在装了 VLC 的机器上(Linux 或 Windows 都行):**
  `vlc -vvv test.mp4 --sout "#rtp{sdp=rtsp://:8554/test}"`

  **这条命令做三件事:**
**读 test.mp4 → 编成 RTP 包 → 在 8554 端口开一个 RTSP 服务。**
  **客户端用 `vlc rtsp://127.0.0.1:8554/test` 或 ffplay 都能拉。**

③ 它和我们的最小实现差在哪

  能力          本 Demo 的服务器     VLC
  -------------------------------------------------------------
  四步握手      有                   有
  发 RTP 包     没有                 有
  转码          没有                 有(--sout-transcode)
  多路复用      没有                 有(一路输入多种输出)

  **所以「RTSP 服务器」这个词有两层意思:**
**一层是把协议对话走完(我们这里做的),**
  **另一层是真的把媒体流送出去(VLC、Live555、EasyDarwin 做的)。**
**第 8、9 章讲的就是后一层。**

  ⚠️ **VLC 的 RTSP 服务只适合调试与演示**——**并发几十路就顶不住,也没有鉴权、没有录制。**
  **生产环境要用 Live555、EasyDarwin、ZLMediaKit 这类专门的服务器。**

四步加起来下行 647 字节,还不够一帧画面。这就是「只握手不发流」和「真服务器」的差距。 PLAY 之后如果真发 2 Mbps 的流,一秒钟就是 250 KB——是这四条响应总量的 386 倍。 所以判断一个服务器是不是真的在工作,别看握手,看 PLAY 之后有没有字节流出来。

30 秒实验

  1. 在装了 VLC 的机器上跑 vlc -vvv test.mp4 --sout "#rtp{sdp=rtsp://:8554/test}",再用我们的客户端去拉,看 PLAY 之后是不是真的有字节进来。
  2. 把 --sout 换成 "#rtp{sdp=rtsp://:8554/test}:#std{access=file,mux=ts,dst=out.ts}",一路输入同时出两路,体会「多路复用」。
  3. 加 --sout-transcode 把码率压到 800 kbps,再用客户端看 SDP 里的 rtpmap 变没变。

常见误区

  1. 「能握手就是 RTSP 服务器」——握手只是协议对话。不发 RTP 就只是个对话机器人,客户端拿到的是空流。
  2. 「VLC 能当生产服务器」——并发几十路就掉帧,没有鉴权、没有录制、没有集群。演示可以,上线不行。
  3. 「sdp=rtsp://:8554/test 里的 test 是文件名」——它是 RTSP 的路径名,随便起,和输入文件无关。
自测三问(点开对答案) 1. 最小 RTSP 服务器缺的是哪一半? —— **PLAY 之后真发 RTP 包那一半。**握手是协议,发包是媒体处理。 2. `--sout "#rtp{sdp=rtsp://:8554/test}"` 里三段各是什么? —— **输入文件、输出链 `--sout`、输出模块 `rtp` 加 SDP 路径。**路径可以随便起。 3. 为什么 VLC 不能上生产? —— **并发差、没鉴权、没录制、没集群、没有进程隔离。**它定位是播放器,服务器模式只是附带的调试功能。

相关笔记