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

RTSP 服务器只交三样东西:一份 SDP、一句原样回声的 Transport、一个会话号。 SDP 实测 262 字节,其中十个字节是 base64 膨胀出来的零头。

📝 本节和前面几节的关系

8.1.1 讲的是客户端视角,这一节换到服务器视角。 第三章 3.2.4 提过一句 VLC 可以当 RTSP 服务器,当时只用命令行说了结论; 这里把它拆开:服务器到底要回什么,客户端才肯继续。

一、服务器要做三件事

一台 RTSP 服务器,最少的职责只有三件:

职责 对应方法 回什么


报价单 DESCRIBE 一份 SDP 定管道 SETUP Transport 原样回声 认人 PLAY/后续请求 Session 号

三件事都不难,难在「不能自己发挥」: SETUP 的 Transport 必须把客户端发来的那行抄回去, 抄错一个字段,客户端当场就会认为协商失败。

这一节所有实验都跑在「自己写的最小服务器」上。 它只做协商、不送媒体,正好用来验证「服务器要回什么」。

二、SDP 逐行看

SDP 是纯文本,一行一个字段,格式是 类型=值。实测整份 262 字节:

行 字节 说明


v=0 5 版本,永远是 0 o=- 0 0 IN IP4 127.0.0.1 26 会话标识 s=No Name 11 会话名,随便写 c=IN IP4 127.0.0.1 20 媒体往哪连 t=0 0 7 起止时间,0 表示不限 a=tool:libavformat … 30 谁生成的 m=video 0 RTP/AVP 96 22 一路视频,载荷类型 96 a=rtpmap:96 H264/90000 24 96 就是 H.264 a=fmtp:96 … 95 参数集,最长的一行 a=control:streamid=0 22 这一路的相对地址

十行加起来正好 262 字节,跟节里说的数对得上。

最长的一行是 a=fmtp,95 字节,里面塞了 SPS 与 PPS 的 base64。 这一段叫 sprop-parameter-sets(简称 SPS/PPS 参数集): 没有它,解码器不知道画面多大、按什么表解码。

m= 那一行的 0 是端口号,写 0 表示「建会话时再商量」。 这就是 SETUP 存在的理由——端口不写在说明书里,而是要现谈。

三、SETUP 的两种 Transport

同一个客户端,换个传输方式,SETUP 那一行就完全不同:

客户端参数 SETUP 里的 Transport


rtsp_transport=tcp RTP/AVP/TCP;unicast;interleaved=0-1 rtsp_transport=udp RTP/AVP/UDP;unicast;client_port=9780-9781

TCP 那种把 RTP 塞进 RTSP 自己那条连接,叫 interleaved(交织)。 0-1 是两个通道号:0 走 RTP、1 走 RTCP。 数据在 TCP 上的格式是:一个美元符号 $、一个通道号、两字节长度、然后是 RTP 包。

UDP 那种给了两个端口号,9780 收 RTP、9781 收 RTCP。 这两个端口是客户端开的,服务器要往它们发包。

为什么很多人默认用 TCP:UDP 一旦被防火墙拦,客户端会「连上了但没画面」, 而且 RTSP 本身不重传,排查起来很难。 TCP 走的是同一条 554 连接,穿墙几乎百分之百成功。

四、Session 号与 Content-Base

服务器回的每条 200 响应里都带一个 Session: 12345678。 这个号是服务器自己编的,客户端不认识也无所谓——它只负责原样带回。

之后每条请求都带 Session:,服务器靠它认出「还是刚才那个人」。 没有它,两个客户端同时连上来就分不清谁是谁了。

另一个容易被忽略的头是 DESCRIBE 响应里的 Content-Base。 它给的是「以后所有相对地址的基准」,实测我们会回 rtsp://…/live/。 所以 SETUP 要去 …/live/streamid=0,而 PLAY 只到 …/live/(8.1.4 会看到这个差别)。

VLC 当服务器时,这一套它都替你做了,所以命令行只要一句 rtp{sdp=rtsp://:8554/live}。 自己写服务器就得一条条补齐——这也正是本节实验做的事。

五、动手 Demo:最小服务器要回什么

下面这段代码从素材里取出 SPS/PPS 拼一份真 SDP,逐行量字节; 再让 libav 用 tcp 与 udp 各连一次,抓 SETUP 里那行 Transport。

import av, base64, socket, struct, threading, time

def sps_pps(path):
    c = av.open(path)
    ed = c.streams.video[0].codec_context.extradata
    c.close()
    off = 5
    n = ed[off] & 0x1F
    off += 1
    sps = []
    for _ in range(n):
        L = struct.unpack(">H", ed[off:off + 2])[0]
        off += 2
        sps.append(ed[off:off + L])
        off += L
    m = ed[off]
    off += 1
    pps = []
    for _ in range(m):
        L = struct.unpack(">H", ed[off:off + 2])[0]
        off += 2
        pps.append(ed[off:off + L])
        off += L
    return sps, pps

sps, pps = sps_pps(u"s7.mp4")
b64 = base64.b64encode(sps[0]).decode()
print(u"SPS %d 字节 -> base64 %d 字符(%.2f 倍)"
      % (len(sps[0]), len(b64), len(b64) / float(len(sps[0]))))

实跑输出:

--------------------------------------------------------------------
8.1.2 VLC 作为 RTSP 流媒体服务器
--------------------------------------------------------------------

**① 服务器要交出的第一样东西:SDP**

  `v=0`
  `o=- 0 0 IN IP4 127.0.0.1`
  `s=No Name`
  `c=IN IP4 127.0.0.1`
  `t=0 0`
  `a=tool:libavformat 62.12.102`
  `m=video 0 RTP/AVP 96`
  `a=rtpmap:96 H264/90000`
  `a=fmtp:96 packetization-mode=1;sprop-parameter-sets=Z2QAC6zZQo35IQAAAwABAAADADIPFCmW,aOvgjLIs`
  `a=control:streamid=0`

  行                       字节     说明
  ----------------------------------------------------------------
  v=0                      5        版本
  o=- 0 0 IN IP4 127.…     26       会话标识
  s=No Name                11       会话名
  c=IN IP4 127.0.0.1       20       媒体连到哪
  t=0 0                    7        起止时间
  a=tool:libavformat …     30       谁造的
  m=video 0 RTP/AVP 96     22       一路视频,载荷类型 96
  a=rtpmap:96 H264/90…     24       96 对应 H.264,时钟 90 kHz
  a=fmtp:96 packetiza…     95       给解码器的参数串
  a=control:streamid=0     22       这一路的相对地址

**整份 SDP 共 262 字节。**

**② `sprop-parameter-sets` 是 SPS 与 PPS 的 base64**

  项               值
  ---------------------------------------------
  SPS 原始字节     24
  SPS base64       32 个字符
  膨胀比           1.33 倍(正好 4 除以 3)
  PPS 原始字节     6

**base64 每 3 字节变 4 个字符,所以 SDP 一定会比原数据胖三分之一。**
**这不是浪费,是为了让二进制能塞进纯文本协议里。**

**③ SETUP 那一句才是真正的「商量」**

  客户端参数             SETUP 里的 Transport 行
  ----------------------------------------------------------------------
  rtsp_transport=tcp     RTP/AVP/TCP;unicast;interleaved=0-1
  rtsp_transport=udp     RTP/AVP/UDP;unicast;client_port=23642-23643

**`RTP/AVP/TCP` 是把 RTP 塞进 RTSP 自己那条 TCP 连接(interleaved),**
**`client_port` 是客户端开的两个 UDP 端口(一个 RTP、一个 RTCP)。**

**服务器必须原样把这行抄回 200 响应里——抄错一个字段,客户端就不认。**
**所以「VLC 当服务器」这一步,实质就是「把客户端的 Transport 原样回声」。**

**④ 服务器还要给 Session 号**

**上面两次协商,服务器回的都是 `Session: 12345678`。**
**这个号是服务器自己编的,之后每条请求都要带 `Session:` 头,**
**服务器靠它认出「这是刚才那个会话」,不然 N 个客户端会串台。**

SPS 是 24 字节,base64 之后 32 个字符,正好 4 除以 3。

所以 SDP 里那段参数集一定比原数据胖三分之一—— base64 每 3 字节变 4 个字符,这是为了让二进制能塞进纯文本协议,不是浪费。

代码里那个 off = 5 是关键:AVCC 的头是「版本、档次、兼容、级别」四字节, 第五字节是「长度字段占几字节」,第六字节才是 SPS 的个数。 写错一位,取出来的就是垃圾,SDP 里的参数集也就废了。

30 秒实验

  1. 换素材:拿一个 720p 的文件,看 SPS 字节数变不变(分辨率不一样,SPS 长度会变)。
  2. 只写 SPS 不写 PPS:把 a=fmtp 里的第二段删掉,观察 SDP 少多少字节。
  3. 改端口:把 m=video 0 的 0 改成 5004,看客户端是否还发 SETUP。

常见误区

  1. 服务器自己编 Transport。必须原样回声,否则协商失败。
  2. SDP 里不写 sprop-parameter-sets。H.264 解不出来(8.2.3 有实测)。
  3. 以为 m= 里的端口是固定的。写 0 表示现谈,写死反而不灵活。
自测三问(点开对答案) 1. RTSP 服务器最少要做哪三件事? —— 回 SDP、把 Transport 原样回声、给 Session 号。 2. interleaved=0-1 是什么意思? —— 两个通道号,0 走 RTP、1 走 RTCP,数据走 RTSP 那条 TCP 连接。 3. Content-Base 有什么用? —— 它是相对地址的基准,决定后面 SETUP 与 PLAY 的 URI 怎么拼。

相关笔记