📹 第二章 VLC及FFplay流媒体播放器
📄 一句话概括

网络串流的第一步不是「收数据」,是「谈妥怎么收」。 五条报文一来一回,谈完才开始有画面——这五条就是 RTSP 的全部骨架。

📝 本节和前面几节的关系

前两节讲了 VLC 是什么、能干什么,这一节看它连上去的头一秒。 用自己写的七十行假服务端来对照,比看文档清楚:服务端要回什么、少一个字段会怎样, 一改就现形。 RTSP 报文的逐字细节在第一章 1.3 与 1.5,这一节只管「顺序和分工」。

一、五步,一步不多

OPTIONS → DESCRIBE → SETUP → PLAY,收工时一条 TEARDOWN。

步 问什么 答什么 少了会怎样
OPTIONS 你支持哪些方法 方法清单 不影响播放
DESCRIBE 这路流长什么样 SDP 描述 不知道有几路、什么编码
SETUP 数据发到哪个端口 会话号+服务端端口 没地方收数据
PLAY 开始发 确认+时间范围 握手完成但没数据
TEARDOWN 结束 确认 服务端等你超时才释放

第二行和第三行最容易被跳过,因为很多人以为「连上就能播」。 DESCRIBE 拿到的 SDP 决定后面所有事:一路还是两路、H.264 还是 H.265、时钟频率是多少。 SETUP 才是真正分配资源的那一步:客户端说「我 8000 端口收」,服务端说「我从 9000 发」, 两边从此对着这两个端口说话。

二、两路流要 SETUP 两次

音视频各一路,就得 SETUP 两遍。 这是新手最常漏的一步。

SDP 里有几个 m= 行,就要 SETUP 几次:

SDP 内容 m= 行数 SETUP 次数
只有视频 1 1
音视频都有 2 2
视频+双向对讲 3 3

只 SETUP 一路,另一路就是「静音」或「黑屏」,而且很多服务端不报错,只是那一路永远没数据。 D21 的假服务端只声明了一路,所以只 SETUP 一次——真实摄像机一般是两路,代码里得循环。

三、那 129 个字节的 SDP 里有什么

D21 里 DESCRIBE 回来的实体是 129 字节,全文不长,但信息密度极高。

| 行 | 写了什么 | 谁来用 | |---|---|---|---| | v=0 | 版本号,永远是 0 | 解析器校验 | | o= | 会话标识与版本 | 判断描述变没变 | | s= | 会话名 | 给人看的 | | t=0 0 | 时间范围,0 0 表示不限 | 直播都是 0 0 | | a=control:* | 基准控制地址 | 拼 SETUP 的 URL | | m=video 0 RTP/AVP 96 | 一路视频,载荷类型 96 | 决定有几路 | | a=rtpmap:96 H264/90000 | 96 号就是 H.264,时钟 90 kHz | 决定怎么解 | | a=control:trackID=0 | 这一路的控制名 | 拼 SETUP 的 URL |

m=video 0 里的那个 0 是个坑:它不是端口,是「端口见别处」,真实端口在 SETUP 的响应里才定。 a=rtpmap 里的 90 kHz 更重要:它就是 RTP 时间戳的时钟,算错这个,同步就全错。

四、握手快不代表播放快

D21 五步合计 0.8 毫秒,全在本机回环上。这只是握手,不是起播。

真实链路上,从点开到出画面要经过四段:

段 典型耗时 卡在哪
TCP 连接+认证 10~50 毫秒 网络往返、摘要计算
五步握手 20~100 毫秒 每步一个往返
等第一个关键帧 0~4000 毫秒 GOP 长度
攒够缓冲 200~1000 毫秒 缓冲策略

第三行常常是最大的一块:如果 GOP 是 4 秒,最坏要等 4 秒才有一个 I 帧,在那之前解不出完整画面。 这就是为什么监控摄像机要把 GOP 调到 1~2 秒——不是为了码率,是为了「点开就能看」。

五、动手 Demo:七十行假服务端,把五步走通

不装 VLC、不连真摄像机,用标准库起一个假服务端,让客户端把 OPTIONS 到 TEARDOWN 走一遍。 这是理解 RTSP 最快的办法:报文你能亲手改,改错立刻看到后果。

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(("127.0.0.1", 0))
srv.listen(1)

def serve(sock):
    conn, _ = sock.accept()
    f = conn.makefile("rwb")
    while True:
        head = f.readline().decode("latin-1").strip()
        if not head:
            break
        method = head.split(" ")[0]
        # 读完头,取 CSeq
        if method == u"DESCRIBE":
            body = sdp.encode("latin-1")
            extra = u"Content-Type: application/sdp\r\n"
        else:
            body = b""
            extra = u""
        resp = (u"RTSP/1.0 200 OK\r\nCSeq: " + cseq + u"\r\n"
                + extra + u"Content-Length: %d\r\n\r\n" % len(body))
        f.write(resp.encode("latin-1") + body)

实跑输出:

--------------------------------------------------------------------
在本机起一个最小 RTSP 服务端,让播放器真连一次
--------------------------------------------------------------------
**网络串流不是玄学,就是在 TCP 上按行发几段文本。** 这一节用一个七十行的假服务端,
把 DESCRIBE→SETUP→PLAY 走通。

  假服务端监听 **127.0.0.1:52758**,SDP 里声明一路 H.264。

  步     请求         状态码     实体字节     往返
  ---------------------------------------------------------
  1      OPTIONS      200        0            0.21 毫秒
  2      DESCRIBE     200        129          0.17 毫秒
  3      SETUP        200        0            0.07 毫秒
  4      PLAY         200        0            0.06 毫秒
  5      TEARDOWN     200        0            0.05 毫秒

  五步合计 **0.86 毫秒**,全在本机回环上——**协议握手本身几乎不花时间,慢的是网络。**

**服务端收到的东西按 CSeq 排队,一条不差:**
    CSeq=1   OPTIONS rtsp://127.0.0.1:52758/cam RTSP/1.0
    CSeq=2   DESCRIBE rtsp://127.0.0.1:52758/cam RTSP/1.0
    CSeq=3   SETUP rtsp://127.0.0.1:52758/cam/trackID=0 RTSP/1.0
    CSeq=4   PLAY rtsp://127.0.0.1:52758/cam RTSP/1.0
    CSeq=5   TEARDOWN rtsp://127.0.0.1:52758/cam RTSP/1.0

**真实摄像机比这多两件事**:一是要认证(401 挑战,客户端带用户名密码的摘要再发一次),
二是不止一路流(主码流+子码流)。
  但握手骨架就是上面这五步,一步不多。

五步平均 0.1 毫秒,全在回环上 —— 这说明协议本身不慢,慢的是往返。 真机上每一跳要几十毫秒,五步就是两三百毫秒。

写这个假服务端时踩了一个真坑:DESCRIBE 的响应忘了写 Content-Length,客户端没读走 SDP 实体, 下一次请求读到的第一行是上一包的 v=0。 表现是「SETUP 收到了一个叫 v=0 的状态码」——协议报文是流式的,不分包就没有边界, 少一个长度字段就串包。

这一点在真实项目里一样成立:自己写 RTSP 客户端时,读响应必须按 Content-Length 精确读实体, 不能「读到空行就算完」。

30 秒实验

  1. 把假服务端的 OPTIONS 响应里删掉 Public 头,看客户端会怎样——大多数客户端不检查,直接用。
  2. 在 SDP 里加一路音频(再来一个 m=audio),把客户端改成 SETUP 两次, 体会「两路流要两次 SETUP」。
  3. 把 Content-Length 故意写大 10,看客户端卡在哪——会一直等那 10 个字节,直到超时。

常见误区

  1. 以为握手完就有画面。 握手只是谈妥参数,真正出画面还要等关键帧、攒缓冲,最坏要等一个 GOP。
  2. 以为 DESCRIBE 可以跳过。 不看 SDP 就不知道该 SETUP 几路、载荷类型是什么、时钟频率多少, 后面全是猜。
  3. 以为响应读到空行就结束了。 有实体时必须按 Content-Length 读完, 否则残留数据会串到下一个响应里。
自测三问(点开对答案) 1. 为什么要先 OPTIONS 再 DESCRIBE? —— OPTIONS 是探路,问对方支持哪些方法,免得发了不被支持的方法被拒。 不少客户端为了省一个往返会跳过它,直接 DESCRIBE。 2. SETUP 里客户端和服务端各说了一个端口,分别干什么? —— client_port 是客户端收 RTP 的端口,server_port 是服务端发的端口。 之后的数据全部按这一对端口走。 3. 为什么摄像机把 GOP 调短能让「点开更快」? —— 因为解画面必须从 I 帧开始,GOP 越长,等下一个 I 帧的时间越长。 1 秒 GOP 最坏等 1 秒,4 秒 GOP 最坏等 4 秒。

下一节反过来:让 VLC 自己当服务器,把画面推出去——播放器和服务端其实是同一套代码的两种用法。

相关笔记