🎬 第二章 RTSP流媒体协议
📄 一句话概括

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 秒实验

  1. 加一条 m=audio 0 RTP/AVP 97 和 a=rtpmap:97 MPEG4-GENERIC/48000/2,看轨道数变成 2——这就是音视频双轨的 SDP。
  2. 把 a=rtpmap:96 H264/90000 删掉再跑,体会「96 成了没有定义的数字」——客户端此时不知道该用什么解码器。
  3. 把时钟频率改成 1000,看「绕回时间」变成几小时——这就是为什么不能随便改时钟频率。

常见误区

  1. 「SDP 是一种传输协议」——它是文本格式,不传任何东西。它靠 RTSP(或 SIP、HTTP)捎带过去。
  2. 「96 就是 H.264」——96 是动态载荷,具体是什么由 a=rtpmap 说。同一份 SDP 里 96 可以是任何编码。
  3. 「时钟频率就是帧率」——90000 是时间戳的刻度,帧率是每秒多少帧。90000 除以 25 才得到每帧的步长。
自测三问(点开对答案) 1. SDP 里必须看懂的三行是什么? —— **`m=`(几路、什么载荷)、`a=rtpmap`(载荷对应的编码与时钟频率)、`a=control`(trackID,SETUP 要拼)。** 2. 为什么视频的时钟频率选 90000? —— **因为能被 24/25/30 这些常见帧率整除**,每帧时间戳是整数,无累积误差。 3. 32 位时间戳按 90000 的时钟多久绕回? —— **约 13.26 小时。**长时间直播必然遇到,只能比差值不比大小。

相关笔记