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 秒实验
- 换素材:拿一个 720p 的文件,看 SPS 字节数变不变(分辨率不一样,SPS 长度会变)。
- 只写 SPS 不写 PPS:把
a=fmtp里的第二段删掉,观察 SDP 少多少字节。 - 改端口:把
m=video 0的 0 改成 5004,看客户端是否还发 SETUP。
常见误区
- 服务器自己编 Transport。必须原样回声,否则协商失败。
- SDP 里不写 sprop-parameter-sets。H.264 解不出来(8.2.3 有实测)。
- 以为
m=里的端口是固定的。写 0 表示现谈,写死反而不灵活。
自测三问(点开对答案)
1. RTSP 服务器最少要做哪三件事? —— 回 SDP、把 Transport 原样回声、给 Session 号。 2. interleaved=0-1 是什么意思? —— 两个通道号,0 走 RTP、1 走 RTCP,数据走 RTSP 那条 TCP 连接。 3. Content-Base 有什么用? —— 它是相对地址的基准,决定后面 SETUP 与 PLAY 的 URI 怎么拼。- ☐ (本节完,下一节 8.1.3 FFmpeg 实现 RTSP 直播拉流)