SDP 就是一份纯文本的「说明书」,告诉客户端这路流有几条轨道、什么编码、怎么解。 里面最容易被忽略的一个数字是时钟频率:它决定了时间戳怎么换算成秒。
2.3 说过 DESCRIBE 会带回一份 SDP,这一节把这份文本逐行拆开。 第 3 章的 RTP 时间戳、第 13 章的 WebRTC 协商,用的都是同一套 SDP。
一、它是一份纯文本的说明书
SDP 的全名是 Session Description Protocol,但它其实不是协议,是一种「文本格式」。 它不规定怎么传,只规定「怎么描述」。
格式规则简单到极点:
① 一行一个字段,形如 名字=值,中间没有空格。
② 名字是单个小写字母:v、o、s、c、t、m、a。
③ 行的顺序有讲究:v o s 必须在最前,t 在 m 之前,a 可以出现在任何位置,作用域跟着它前面的那个 m 走。
④ 换行用 \r\n,不是 \n。这一点在手工构造 SDP 时经常踩。
它就是「一段可以被程序读的广告词」。 服务器用 DESCRIBE 把这段广告词发给你,你照着它去 SETUP、去解包。 所以 SDP 一旦不完整,后面全错——而这恰恰是最常见的故障源之一。
二、九个字段,三个必须看
一份典型 SDP 大概十行,字段有九个,但真正必须看懂的只有三个:
① m= 媒体行:m=video 0 RTP/AVP 96四个字段分别是:媒体类型、端口、传输协议、载荷类型。
有几个 m= 就有几条轨道,也就要 SETUP 几次。
② a=rtpmap:a=rtpmap:96 H264/90000说清「96 号载荷是什么编码、时钟频率多少」。
没有它,96 这个数字毫无意义——因为 96 是动态分配的。
③ a=control:a=control:trackID=0这条轨道的名字,SETUP 时要拼到 URL 后面。
漏了它,SETUP 就不知道建哪条轨道的通道。
其余六个了解一下就够:
v=0 版本号永远是 0;o= 发起者与版本;s= 会话名(随便写);c= 连接地址;t=0 0 表示一直有效;a=fmtp 编码的额外参数。
三、m= 一行里的四个字段
m=video 0 RTP/AVP 96 这行只有四个字段,但每一个都有坑:
① 媒体类型:video、audio、application(比如文字、白板)。一路流可以同时有 video 和 audio 两个 m= 行。
② 端口:这里写 0 很常见,意思是「端口由 Transport 头协商,别在这里定」。老式 SDP 可能写具体端口,但对 RTSP 来说 0 是对的。
③ 传输协议:RTP/AVP 表示 RTP over UDP。要 TCP 就写 RTP/AVP/TCP。
注意这个值和 SETUP 的 Transport 头必须一致,不一致就 461。
④ 载荷类型:96。这是最容易错的一个。
0~95 是固定分配(0=PCMU、26=JPEG、34=H.263),96~127 是动态的,必须由 a=rtpmap 定义。
所以看到 m=video 0 RTP/AVP 96 而找不到 a=rtpmap:96,基本可以判定这份 SDP 是坏的。
有些廉价摄像头还真会漏这一行——现象是客户端能连上,但解不出画面。
四、时钟频率:时间戳的尺子
a=rtpmap:96 H264/90000 里那个 90000 是最容易被跳过的数字,但它决定了时间戳怎么算。
含义是:一秒钟对应 90000 个时间戳单位。 所以一个单位约 11.1 微秒,一帧(按 25fps)是 3600 个单位。
为什么是 90000? 因为它能被常见的帧率整除:90000/25 = 3600,90000/30 = 3000,90000/24 = 3750。 整除了,每帧的时间戳就是整数,不会有累积误差。
视频用 90000,音频用 48000(对应采样率)。
两个流的时间戳不是同一个尺子,同步时必须先换算。
换算公式很简单:秒 = 时间戳 / 时钟频率。
这一条在第 3 章讲 RTP 时间戳时会反复用到。
还有一个副作用值得知道:RTP 头里的时间戳是 32 位无符号数,最大 4294967295。 除以 90000,约 47721 秒,也就是 13.26 小时就绕回一圈。 所以长时间直播必然遇到「时间戳回绕」,客户端必须处理——只比较差值,不比较大小。
五、动手 Demo:SDP 逐行拆解:一份纯文本说清怎么解
拿一份真实的 SDP,逐行分类、逐字段拆开,最后算一下时钟频率带来的时间戳回绕。 重点是三行:m=、a=rtpmap、a=control。
SDP = ("v=0\r\n"
"o=- 0 0 IN IP4 127.0.0.1\r\n"
"s=Live\r\n"
"c=IN IP4 0.0.0.0\r\n"
"t=0 0\r\n"
"a=control:*\r\n"
"m=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\n"
"a=control:trackID=0\r\n")
NAME = {"v": "version", "o": "origin", "s": "session name",
"c": "connection", "t": "timing", "m": "media",
"a": "attribute"}
for line in SDP.split("\r\n"):
if not line:
continue
k, v = line.split("=", 1)
print("%-2s %-13s %s" % (k, NAME[k], v))
m = [l for l in SDP.split("\r\n") if l.startswith("m=")]
print("轨道数 = %d" % len(m))
if m:
mt, port, proto, pt = m[0][2:].split(" ")
print("类型=%s 端口=%s 传输=%s 载荷=%s" % (mt, port, proto, pt))
CLK = 90000
print("一帧 25fps 的时间戳步长 = %d" % (CLK // 25))
print("32 位时间戳约 %.2f 小时绕回" % (2 ** 32 / CLK / 3600))
print("一帧在墙上时间占 %.1f 毫秒" % (1000 / 25))
实跑输出:
--------------------------------------------------------------------
SDP——一份纯文本,说清这路流怎么解
--------------------------------------------------------------------
① 真从 SETUP 之前的 DESCRIBE 里拿一份 SDP
| v=0
| o=- 0 0 IN IP4 127.0.0.1
| s=Live
| c=IN IP4 0.0.0.0
| t=0 0
| a=control:*
| m=video 0 RTP/AVP 96
| a=rtpmap:96 H264/90000
| a=fmtp:96 packetization-mode=1;profile-level-id=42e01f
| a=control:trackID=0
② 每一行是什么意思
行 名字 意思
--------------------------------------------------------------------------------
v=0 version SDP 版本,永远是 0
o= origin 会话的发起者与版本号
s= session name 会话名,随便写
c= connection 媒体连接地址(这里 0.0.0.0 表示看 Transport)
t= timing 会话起止时间,0 0 表示一直有效
a=control attribute 控制 URL 的后缀
m= media 媒体行:类型、端口、传输协议、载荷类型
a=rtpmap attribute 载荷类型对应哪个编码与时钟频率
a=fmtp attribute 编码的额外参数
③ 从一行 m= 里读出三件事
字段 值 含义
---------------------------------------------------------------
媒体类型 video video(还有 audio/application)
端口 0 0 = 由 Transport 头协商决定
传输协议 RTP/AVP RTP/AVP = RTP over UDP
载荷类型 96 96 = 动态载荷(H.264 常用 96~127)
④ 时钟频率决定了时间戳怎么算
**`a=rtpmap:96 H264/90000` 里的 90000 是时钟频率:**
**一秒对应 90000 个时间戳单位,一个单位约 11.1 微秒。**
**所以 RTP 头里 32 位的时间戳,约 13 小时就绕回一圈。**
**这一条非常重要**:**音视频同步就是靠两个流各自的时钟频率,换算到同一个时间轴上来对齐。**
**视频 90000、音频 48000 是常见组合,第 3 章会用到。**
九个字段里,a 出现了三次,每次作用域不同——a=control:* 是会话级的,后面两个 a= 是 m=video 这一条轨道的。
m=video 0 RTP/AVP 96 里端口是 0,意思是「端口等 SETUP 再协商」,不是「用 0 端口」。
90000 除以 25 得 3600——整数,没有余数。这就是选 90000 的原因:常见帧率都能整除。
30 秒实验
- 加一条
m=audio 0 RTP/AVP 97和a=rtpmap:97 MPEG4-GENERIC/48000/2,看轨道数变成 2——这就是音视频双轨的 SDP。 - 把
a=rtpmap:96 H264/90000删掉再跑,体会「96 成了没有定义的数字」——客户端此时不知道该用什么解码器。 - 把时钟频率改成
1000,看「绕回时间」变成几小时——这就是为什么不能随便改时钟频率。
常见误区
- 「SDP 是一种传输协议」——它是文本格式,不传任何东西。它靠 RTSP(或 SIP、HTTP)捎带过去。
- 「96 就是 H.264」——96 是动态载荷,具体是什么由
a=rtpmap说。同一份 SDP 里 96 可以是任何编码。 - 「时钟频率就是帧率」——90000 是时间戳的刻度,帧率是每秒多少帧。90000 除以 25 才得到每帧的步长。
自测三问(点开对答案)
1. SDP 里必须看懂的三行是什么? —— **`m=`(几路、什么载荷)、`a=rtpmap`(载荷对应的编码与时钟频率)、`a=control`(trackID,SETUP 要拼)。** 2. 为什么视频的时钟频率选 90000? —— **因为能被 24/25/30 这些常见帧率整除**,每帧时间戳是整数,无累积误差。 3. 32 位时间戳按 90000 的时钟多久绕回? —— **约 13.26 小时。**长时间直播必然遇到,只能比差值不比大小。- ☑ (第二章完;下一章 3.1.1 RTP格式)