第 2 章:SDP——媒体协商的艺术
本章解决的痛点:编码器选 G.711 还是 OPUS?为什么通话中点击"保持"对方听不到音乐?双方都说支持视频,为什么通话只有音频?理解 SDP 的 offer/answer 模型,是掌控媒体质量与兼容性的关键。
2.1 SDP 协议基础
2.1.1 SDP 是什么?
SDP(Session Description Protocol,会话描述协议) 定义于 RFC 4566,它本身不负责传输,只是用一种标准格式描述多媒体会话的参数。SIP 消息的消息体(Body)通常就是 SDP。
类比:
- SIP = 打电话的动作(拨号、接听、挂断)
- SDP = 双方确认通话细节(用什么语言、什么音量、是否开视频)
2.1.2 SDP 文本格式解析
SDP 是纯文本格式,由一系列 type=value 的行组成,type 必须是小写字母:
v=0 ← 协议版本,目前固定为 0
o=alice 2890844526 2890844526 IN IP4 pc33.example.com ← 会话发起者
s=- ← 会话名称(- 表示无)
i=A conversation about SDP ← 会话信息(可选)
u=http://example.com/sdp ← URI(可选)
e=alice@example.com ← 邮箱(可选)
p=+1 123 456 7890 ← 电话(可选)
c=IN IP4 pc33.example.com ← 连接信息(全局或按媒体)
b=AS:64 ← 带宽信息(可选)
t=0 0 ← 活动时间(0 0 表示永久)
r=7d 1h 0 25h ← 重复时间(可选)
m=audio 49170 RTP/AVP 0 8 ← 媒体描述:音频,端口,协议,格式列表
a=rtpmap:0 PCMU/8000 ← 属性:格式 0 对应 PCMU,8kHz
a=rtpmap:8 PCMA/8000 ← 属性:格式 8 对应 PCMA,8kHz
a=sendrecv ← 属性:双向收发
m=video 51372 RTP/AVP 31 ← 第二个媒体:视频
a=rtpmap:31 H261/90000 ← 属性:格式 31 对应 H.261
2.1.3 必需字段详解
v=(Version) 协议版本,目前唯一有效值是 0。
o=(Origin)
o=<username> <session-id> <session-version> <nettype> <addrtype> <unicast-address>
username: 发起者用户名,-表示无session-id: 唯一会话标识,通常是 NTP 时间戳session-version: 会话版本,每次修改 SDP 时递增nettype: 网络类型,IN= Internetaddrtype: 地址类型,IP4或IP6unicast-address: 发起者的 IP 地址或主机名
s=(Session Name) 会话名称,每个 SDP 必须有一个且仅有一个。如果不需要,用 -。
t=(Timing)
t=<start-time> <stop-time>
NTP 时间戳,0 0 表示会话是永久的(常见于电话呼叫)。
m=(Media)
m=<media> <port> <proto> <fmt> ...
media:audio,video,text,application,messageport: 传输端口(RTP 端口)proto: 传输协议,RTP/AVP(UDP)、RTP/SAVP(SRTP)、udp、TCPfmt: 媒体格式列表,数字对应 RTP payload type
一个 SDP 可以有多个 m= 行,表示多路媒体(如音频 + 视频)。
2.1.4 属性行(a=)——SDP 的扩展机制
a= 是 SDP 中最灵活的部分,常见属性:
| 属性 | 含义 | 示例 |
|---|---|---|
rtpmap | RTP 负载类型映射 | a=rtpmap:0 PCMU/8000 |
fmtp | 格式特定参数 | a=fmtp:97 profile-level-id=42e01f |
sendrecv | 双向收发(默认) | a=sendrecv |
sendonly | 仅发送 | a=sendonly(hold 状态) |
recvonly | 仅接收 | a=recvonly |
inactive | 不发送也不接收 | a=inactive |
ptime | 打包时长(毫秒) | a=ptime:20 |
maxptime | 最大打包时长 | a=maxptime:60 |
crypto | SRTP 加密密钥 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 ... |
candidate | ICE 候选地址 | a=candidate:1 1 UDP 2130706431 192.168.1.1 ... |
rtcp | RTCP 端口(非 RTP+1 时) | a=rtcp:53020 |
2.2 offer/answer 模型——谁来决定用什么编解码?
2.2.1 RFC 3264 协商流程
SDP 本身不定义如何协商,RFC 3264 定义了 offer/answer 模型:
- offer:主动方发送 SDP,列出自己支持的所有选项(编解码、端口、IP 等)
- answer:被动方回复 SDP,从 offer 中选择自己支持的选项
- 协商结果:双方都认可的参数组合
关键规则:
- answer 必须包含与 offer 数量相同的
m=行(按顺序对应) - answer 的
m=格式列表必须是 offer 的子集(只能删减,不能添加) - answer 的
rtpmap必须映射 offer 中声明的 payload type - 第一个共同支持的格式将被使用(顺序很重要!)
2.2.2 协商实例:Alice 呼叫 Bob
Alice 的 offer(INVITE):
v=0
o=alice 2890844526 0 IN IP4 alice.example.com
s=-
c=IN IP4 alice.example.com
t=0 0
m=audio 49170 RTP/AVP 0 8 96
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:96 opus/48000/2
a=ptime:20
Alice 支持:PCMU (G.711 μ-law)、PCMA (G.711 A-law)、OPUS。她希望用 OPUS(放在最后,但实际优先看对方支持什么)。
Bob 的 answer(200 OK):
v=0
o=bob 2890844730 0 IN IP4 bob.example.com
s=-
c=IN IP4 bob.example.com
t=0 0
m=audio 3456 RTP/AVP 0 96
a=rtpmap:0 PCMU/8000
a=rtpmap:96 opus/48000/2
a=ptime:20
Bob 支持 PCMU 和 OPUS,不支持 PCMA。最终双方使用 PCMU(因为 0 在 96 之前,且双方支持)。
如果 Bob 只想用 OPUS:
m=audio 3456 RTP/AVP 96 0
把 96 放在第一位,双方就会协商使用 OPUS。
2.2.3 FreeSWITCH 的协商策略
FreeSWITCH 通过 vars.xml 或 SIP Profile 配置支持的编解码器顺序:
<!-- conf/vars.xml -->
<X-PRE-PROCESS cmd="set" data="global_codec_prefs=OPUS,PCMU,PCMA,G729"/>
<X-PRE-PROCESS cmd="set" data="outbound_codec_prefs=OPUS,PCMU,PCMA"/>
协商逻辑:
- FS 作为 UAC(发起呼叫):按
outbound_codec_prefs顺序发送 offer - FS 作为 UAS(接收呼叫):按
global_codec_prefs顺序选择 answer - 如果双方没有共同编解码器 →
488 Not Acceptable Here
2.2.4 协商失败案例分析
案例 1:无共同编解码器
Alice offer: m=audio 10000 RTP/AVP 96
a=rtpmap:96 opus/48000/2
Bob answer: m=audio 20000 RTP/AVP 0
a=rtpmap:0 PCMU/8000
Bob 的 answer 中 m= 的格式列表是 0,但 offer 中没有 0,只有 96。这是非法 answer,Alice 应回复 488 或忽略。
案例 2:payload type 冲突
Alice: a=rtpmap:96 opus/48000/2
Bob: a=rtpmap:96 G729/8000
双方都用了 96,但映射到不同编解码器!这会导致音频完全错乱。
解决方案:FS 会自动进行 payload type 重写(rewrite) ,在 SDP 中统一映射。
2.3 媒体方向属性——hold/resume 的 SDP 实现
2.3.1 四种方向属性
| 属性 | 含义 | 应用场景 |
|---|---|---|
sendrecv | 双向收发 | 正常通话(默认值) |
sendonly | 仅发送,不接收 | 保持通话但只听,或单向广播 |
recvonly | 仅接收,不发送 | 保持通话但只发静音,或录音端 |
inactive | 既不发送也不接收 | 完全暂停,节省带宽 |
2.3.2 保持(Hold)的 SDP 流程
当你点击"保持"按钮:
步骤 1:Re-INVITE with sendonly
INVITE sip:bob@example.com SIP/2.0
...
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendonly ← 告诉对方:我还在发,但不想收你的了
a=inactive ← 或完全不收发
步骤 2:对方回复 200 OK with recvonly
SIP/2.0 200 OK
...
m=audio 3456 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=recvonly ← 好的,我只收不发
步骤 3:保持音乐(MOH) FS 检测到 sendonly 后,会向被保持方播放保持音乐(Music on Hold)。音乐通过 RTP 发送到对方的 recvonly 端口。
2.3.3 为什么点了"保持"对方却听不到音乐?
常见故障排查:
-
SDP 方向错误:检查 Re-INVITE 是否真的包含
a=sendonly,而不是a=inactivesendonly= 我方发音乐,对方收inactive= 完全静音
-
RTP 流向错误:音乐应该发到对方的 RTP 端口,检查 SDP 中的
c=和m= -
MOH 配置问题:FreeSWITCH 的保持音乐配置
<!-- conf/autoload_configs/switch.conf.xml --> <param name="hold-music" value="local_stream://moh"/>检查
local_stream是否正常加载:fs_cli> show local_stream -
防火墙阻断:MOH 的 RTP 流可能走不同端口,检查防火墙规则
2.3.4 恢复通话(Resume)
再次 Re-INVITE,将方向改回 sendrecv:
INVITE sip:bob@example.com SIP/2.0
...
a=sendrecv ← 恢复正常双向通话
2.4 带宽管理与 QoS
2.4.1 b= 行:带宽声明
SDP 支持两种带宽类型:
b=CT:1000 ← Conference Total,整个会议的总带宽
b=AS:64 ← Application Specific,单个媒体的带宽
b=TIAS:64000 ← Transport Independent Application Specific(精确到 bps)
编解码器带宽参考:
| 编解码器 | 采样率 | 打包时长 | 码率 | 实际带宽(含 RTP/UDP/IP 头) |
|---|---|---|---|---|
| G.711 (PCMU/PCMA) | 8kHz | 20ms | 64 kbps | ~80 kbps |
| G.729 | 8kHz | 20ms | 8 kbps | ~24 kbps |
| OPUS | 48kHz | 20ms | 可变 6-510 kbps | 通常 20-40 kbps |
| iLBC | 8kHz | 30ms | 13.33 kbps | ~30 kbps |
2.4.2 ptime:打包时长的艺术
ptime 定义每个 RTP 包包含多少毫秒的音频数据:
| ptime | 每包采样数 | 包率 | 延迟 | 适用场景 |
|---|---|---|---|---|
| 10ms | 80 | 100 pkt/s | 低 | 实时交互、低延迟网络 |
| 20ms | 160 | 50 pkt/s | 中 | 默认推荐,平衡延迟与开销 |
| 40ms | 320 | 25 pkt/s | 高 | 高丢包网络,减少包头开销 |
| 60ms | 480 | 16.7 pkt/s | 很高 | 卫星链路、极端弱网 |
计算示例:
- G.711,ptime=20ms:每秒 50 个包,每个包 160 字节音频数据 + 12 字节 RTP 头 + 8 字节 UDP 头 + 20 字节 IP 头 = 200 字节/包 → 80 kbps
- G.711,ptime=40ms:每秒 25 个包,每个包 320 + 40 = 360 字节 → 72 kbps(节省 10% 带宽)
2.4.3 QoS 标记:DSCP/ToS
在 c= 行后可以添加网络类型,配合 SIP/SDP 指示 QoS:
c=IN IP4 192.168.1.100
b=AS:64
a=dscp:ef ← Expedited Forwarding,语音优先
FreeSWITCH 配置(sip_profiles/internal.xml):
<param name="rtp-ip-tos" value="184"/> <!-- DSCP EF = 10111000 = 184 -->
<param name="sip-ip-tos" value="184"/>
2.5 多路媒体与流标识
2.5.1 音频 + 视频的 SDP 结构
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
m=video 51372 RTP/AVP 31 32
a=rtpmap:31 H261/90000
a=rtpmap:32 MPV/90000
两个 m= 行表示两路独立的媒体流,分别有独立的端口和编解码器。
2.5.2 流复用:bundle
WebRTC 常用 BUNDLE 扩展,将音频和视频复用到同一端口:
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=mid:0
...
m=video 9 UDP/TLS/RTP/SAVPF 96
a=mid:1
...
a=group:BUNDLE 0 1 ← 将 mid 0 和 1 捆绑到同一传输
2.5.3 SSRC 与 CNAME
在 SDP 中声明 RTP 流的同步源标识:
a=ssrc:123456789 cname:user@example.com
a=ssrc:123456789 msid:stream1 track1
2.6 实战:分析一次 SDP 协商故障
场景:FreeSWITCH 与某网关互通,呼叫失败
抓包看到的 offer(FS 发出):
m=audio 26748 RTP/AVP 102 0 8 9 103 101
a=rtpmap:102 opus/48000/2
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:9 G722/8000
a=rtpmap:103 telephone-event/48000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
a=ptime:20
a=sendrecv
answer(网关回复):
m=audio 10000 RTP/AVP 18 0 8
a=rtpmap:18 G729/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=sendrecv
问题分析:
- FS 不支持 G.729(没有授权)
- 网关首选 G.729(payload 18 放在第一位)
- 双方共同支持的是 PCMU (0) 和 PCMA (8)
- 但网关的 answer 格式列表是
18 0 8,FS 应该选择 0 或 8
实际结果:FS 回复 488 Not Acceptable Here
根因:网关 SDP 格式有误,或 FS 配置禁用了 G.711
解决:检查 FS 的 global_codec_prefs,确保包含 PCMU/PCMA:
<X-PRE-PROCESS cmd="set" data="global_codec_prefs=OPUS,G722,PCMU,PCMA"/>
本章小结
- SDP 描述会话参数,SIP 传输 SDP。看懂 SDP 是排查"没声音、音质差、视频不显示"等问题的基本功。
- offer/answer 模型决定编解码选择:格式列表的顺序决定优先级,第一个共同支持的格式将被使用。
- 方向属性(sendrecv/sendonly/recvonly/inactive)控制媒体流:hold/resume 的本质是 Re-INVITE + 方向修改。
- ptime 影响延迟与带宽:默认 20ms 是最佳平衡点,弱网可增大到 40ms。
- 多路媒体用多个 m= 行,WebRTC 的 BUNDLE 扩展可复用传输。
练习题
- Alice 支持 OPUS、G.711,Bob 只支持 G.711。如果 Alice 的 offer 中格式列表是
m=audio 10000 RTP/AVP 96 0,而 Bob 的 answer 是m=audio 20000 RTP/AVP 0 96,最终协商使用什么编解码?为什么? - 通话中保持音乐的 SDP 流程是怎样的?如果 answer 方回复
a=inactive而不是a=recvonly,会发生什么? - G.711 的 payload type 0 和 8 分别代表什么?在跨国互通中有什么注意事项?
- 解释
a=rtpmap:96 opus/48000/2中每个字段的含义。为什么是 48000 而不是 8000?
下一章预告:RTP/RTCP——语音数据的搬运工。我们将深入解析 RTP 包头、抖动缓冲、丢包补偿,以及如何用工具分析通话质量。