🎬 第八章 FFmpeg命令行实现流媒体功能及直播应用
📄 一句话概括

四条请求、四个响应,加起来 477 字节,这就是一次完整的握手。 PLAY 的 URI 会变短,因为服务器给了 Content-Base。

📝 本节和前面几节的关系

前三节分别讲了协议、服务器、客户端,这一节把一次会话完完整整走一遍。 看的是「顺序、字节、时间」三件事,这三样决定了排查时该看哪里。

一、四问四答的字节账

一次拉流,客户端一共只发四条请求:

序 方法 字节 距上一条


1 OPTIONS 95 19.5 毫秒 2 DESCRIBE 121 0.3 毫秒 3 SETUP 141 1.1 毫秒 4 PLAY 120 0.7 毫秒

四条加起来 477 字节。 第一条那 19.5 毫秒里其实包含了 TCP 建连接的时间,后面三条都在毫秒级。

SETUP 最长(141 字节),因为它要带一整行 Transport。 这也说明:字节数基本跟着「信息量」走,越复杂的协商越长。

注意「距上一条」这一列,全部是亚毫秒到毫秒级。 所以协商本身不慢,慢的是后面的媒体与缓冲(8.1.3 的首帧 999 毫秒)。

二、PLAY 为什么换了 URI

四条请求的 URI 前三条一样,最后一条短一截:

请求 URI


OPTIONS rtsp://127.0.0.1:端口/live/streamid=0 DESCRIBE rtsp://127.0.0.1:端口/live/streamid=0 SETUP rtsp://127.0.0.1:端口/live/streamid=0 PLAY rtsp://127.0.0.1:端口/live/

不是客户端写错了,是服务器在 DESCRIBE 响应里给了 Content-Base: rtsp://…/live/。 规矩是:相对地址接到 Content-Base 后面。

所以 a=control:streamid=0 拼出来是 …/live/streamid=0, 而 PLAY 针对整个会话,只到 …/live/ 这一层。

这条规矩有实际后果:服务器换了路径,客户端不用改代码, 因为它每次都从 Content-Base 重新拼。 反过来说,Content-Base 写错一个斜杠,后面所有请求都会打在错的地方。

三、CSeq 与 Session

两个头分工明确:

头 谁生成 干什么


CSeq 客户端 请求与响应配对 Session 服务器 认出是哪个会话

CSeq 从 1 开始,一条加一,响应里必须原样抄回来。 RTSP 允许同时有好几条请求在飞,靠 CSeq 才不会把响应配错。

Session 是服务器编的,客户端只负责带。 它还有一个隐藏作用:超时计时。长时间不发请求,会话就过期了, 所以才需要 GET_PARAMETER 定期续命。

还有一个细节:每条请求都带 User-Agent: Lavf62.12.102。 Lavf 是 libavformat 的缩写,服务器日志里看到它,就知道对面是 ffmpeg 系。

四、一次完整会话的时序

把四个阶段的时间与字节放在一条时间轴上,就是这样:

时刻 发生什么 关键字段


0 TCP 连上 554 没有 约 20 毫秒 OPTIONS → 200 Public 列表 约 20 毫秒 DESCRIBE → 200 SDP、Content-Base 约 21 毫秒 SETUP → 200 Transport、Session 约 22 毫秒 PLAY → 200 Range: npt=0- 之后 RTP 开始流 序号、时间戳

注意最后一行:RTP 是在 PLAY 的响应之后才开始流的。 所以排查「没有画面」时,先确认 PLAY 有没有回 200,再看 RTP 有没有来。

Range: npt=0.000- 是「从现在开始播」的意思。 npt 是 normal play time,单位是秒;写 npt=10- 就是从那一段的第十秒开始。

直播流一般不接受 npt 回退——过去的画面已经没有了。 这也是为什么直播的进度条拖不动(8.3.3 还会再说一次这件事)。

五、动手 Demo:抓四条请求,量字节与间隔

下面这段代码起服务器、让 libav 来连,把收到的每条请求连字节数与到达时刻一起记下来。 最后把四条请求的 URI 排在一起,PLAY 那一行的差别就露出来了。

import socket, threading, time

BUF = []

def serve(sdp, port):
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(("127.0.0.1", port))
    srv.listen(1)
    c, _ = srv.accept()
    buf = b""
    t0 = time.time()
    while True:
        d = c.recv(4096)
        if not d:
            break
        buf += d
        while b"\r\n\r\n" in buf:
            head, buf = buf.split(b"\r\n\r\n", 1)
            txt = head.decode("utf-8", "replace")
            first = txt.split("\r\n")[0]
            BUF.append((first.split(" ")[0], first, len(txt) + 4,
                        (time.time() - t0) * 1000))
            # 这里按方法回 200,代码见生成器完整版
    c.close()

for m, first, n, ms in BUF:
    print(u"%-9s %4d 字节  %6.1f ms  %s" % (m, n, ms, first.split(" ")[1]))

实跑输出:

--------------------------------------------------------------------
8.1.4 RTSP 交互流程分析
--------------------------------------------------------------------

**① 四问四答,一条不多一条不少**

  序     方法         字节     距上一条
  -----------------------------------------
  1      OPTIONS      95       0.0 ms
  2      DESCRIBE     121      0.3 ms
  3      SETUP        141      0.6 ms
  4      PLAY         120      0.6 ms

**四条请求加起来 477 字节。**

**② 第一条与最后一条的 URI 不一样**

  请求         URI
  -------------------------------------------------------
  OPTIONS      rtsp://127.0.0.1:57432/live/streamid=0
  DESCRIBE     rtsp://127.0.0.1:57432/live/streamid=0
  SETUP        rtsp://127.0.0.1:57432/live/streamid=0
  PLAY         rtsp://127.0.0.1:57432/live/

**前三条都对着 `…/live/streamid=0`,PLAY 却对着 `…/live/`。**
**不是客户端乱写,是服务器在 DESCRIBE 的响应里给了 `Content-Base`。**

**规矩是:URL 以 `/` 结尾时,相对地址接在后面。**
**所以 `a=control:streamid=0` 拼到 `…/live/` 后面才是完整地址。**
**PLAY 是针对整个会话,所以只到目录那一层。**

**③ 每条请求都带 CSeq,用来配对响应**

**CSeq 从 1 开始,一条加一。响应里必须把它原样抄回来。**
**RTSP 允许同时在飞多条请求,靠 CSeq 才不会把响应配错。**

**④ 每条请求都带 User-Agent: Lavf62.12.102**

**`Lavf` 就是 libavformat 的缩写,后面是版本号。**
**所以服务器日志里看到 Lavf,就知道对面是 ffmpeg 系(ffplay 也一样)。**

四条请求的字节数是 95/121/141/120,加起来 477。 这个数看着不起眼,但它就是「一次握手的全部成本」。

时间那一列全在毫秒级,而且第一条明显最长—— 因为它前面还有一次 TCP 三次握手,那才是真正的固定成本。

最后打出来的 URI 会看到:前三条对着 streamid=0,PLAY 只到 live/。 这个差别是服务器用 Content-Base 定的,不是客户端乱写。

30 秒实验

  1. 改 CSeq 起点:让服务器回错 CSeq,看客户端多久才放弃。
  2. 去掉 Session 头:服务器不认会话,客户端会重发 SETUP。
  3. 把 Content-Base 指向别的路径:观察四条请求的 URI 怎么跟着变。

常见误区

  1. 以为 PLAY 的 URI 是客户端写错。是 Content-Base 决定的。
  2. 把 CSeq 当成序号无关紧要。它错了,响应就配不上。
  3. 以为直播也能 npt 回退。过去的数据已经没了,服务器只能拒绝。
自测三问(点开对答案) 1. 一次握手一共几条请求、多少字节? —— 四条:OPTIONS、DESCRIBE、SETUP、PLAY,合计 477 字节。 2. PLAY 的 URI 为什么比前三条短? —— 它针对整个会话,只到 Content-Base 那一层,前三条要指到具体那一路流。 3. RTP 从什么时候开始流? —— PLAY 回 200 之后。所以没画面要先查 PLAY。

相关笔记