3.2 读懂 SDP 报文 本节摘要:SDP(Session Description Protocol,会话描述协议)是协商的合同文本:一串以短字母开头的行,记录双方的能力与约定。它看起来像密文,结构其实高度规整——会话层定全局,每个媒体行管一路流,属性行写细节。本节用一份带注解的真实报文带你按图索骥,看完你就能从协商日志里一眼定位「方向不对、编解码缺失、带宽没约束」这类问题行。 读完本节你能 阅读完本节,你应当能够: 说出 SDP 的分层结构:会话级字段与媒体级字段的分工; 读懂 m 行、c 行、a 行的含义,尤其方向属性 sendrecv、sendonly、recvonly、inactive; 从编解码属性行里确认双方最终选定的编码与载荷类型;
本节摘要:SDP(Session Description Protocol,会话描述协议)是协商的合同文本:一串以短字母开头的行,记录双方的能力与约定。它看起来像密文,结构其实高度规整——会话层定全局,每个媒体行管一路流,属性行写细节。本节用一份带注解的真实报文带你按图索骥,看完你就能从协商日志里一眼定位「方向不对、编解码缺失、带宽没约束」这类问题行。
阅读完本节,你应当能够:
排查协商问题时,日志里通常都会打出 offer 与 answer 全文,可惜大部分人的目光会直接跳到最后一行报错。这很可惜:报错只说「失败」,SDP 却写着「为什么失败」。一次典型的例子——产品要求「观众只看不说」,测试却发现观众端也在上行视频流量,回头看 answer 里视频方向是 sendrecv,一行就说明了问题。
SDP 的可读性差有两个原因:一是行首缩写语焉不详(m 是 media、a 是 attribute),二是内容混排了协议历史包袱。但它有个可爱的性质:绝大多数行是浏览器自动生成的,你只需要看懂十来种。本节就把这十来种一次讲完。
先看一份砍掉冗余行的真实 offer 骨架,注释即解读:
v=0 // 协议版本,恒为 0 o=- 46117314 2 IN IP4 127.0.0.1 // 会话标识:id 与版本号,重协商时版本递增 s=- // 会话名,WebRTC 固定为 - t=0 0 // 会话有效期,WebRTC 固定为 0 0 a=group:BUNDLE 0 1 // 复用声明:媒体行 0 与 1 共用一条传输通道 a=msid-semantic: WMS local-stream // 媒体流标识语义,关联轨道分组 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 // 媒体行 1:音频,候选端口 9,载荷 111 与 103 c=IN IP4 0.0.0.0 // 连接地址占位,实际地址由候选行提供 a=rtcp:9 IN IP4 0.0.0.0 // RTCP 通道端口(配合复用实际同端口) a=sendrecv // 方向:既发也收 a=mid:0 // 媒体行编号,与 BUNDLE 声明对应 a=rtpmap:111 opus/48000/2 // 载荷 111 是 Opus,48 千赫双声道 a=fmtp:111 minptime=10;useinbandfec=1 // 编解码参数:FEC 已启用 a=ssrc:1001 cname:abc123 // 同步源标识与规范名 m=video 9 UDP/TLS/RTP/SAVPF 96 98 // 媒体行 2:视频,载荷 96 与 98 a=sendrecv // 方向同上 a=rtcp-mux // RTP 与 RTCP 复用同一端口 a=rtpmap:96 VP8/90000 // 载荷 96 是 VP8 a=rtcp-fb:96 nack // 支持 NACK 重传反馈 a=rtcp-fb:96 nack pli // 支持关键帧请求 a=rtcp-fb:96 goog-remb // 支持带宽估计反馈
按块拆解。会话级头部(v、o、s、t、group)只有几行,需要盯的是 o 行第二段的版本号——每次重协商必须递增,如果日志里两次 offer 版本相同,说明有描述被重复发送,正是 3.1 节那个「answer 迟到」案例的病根。每个 m 行开启一个媒体块:一行一路媒体(音频一路、视频一路),载荷数字是编解码的代号,rtpmap 行把代号翻译成人话。块内 a 行是细节的全部:方向、参数、反馈能力,逐行罗列。
方向属性值得单独背下来,它是业务正确性的第一道闸:
| 属性 | 含义 | 典型场景 |
|---|---|---|
| sendrecv | 既发送也接收 | 会议里所有人的音频 |
| sendonly | 只发不收 | 主播推流端 |
| recvonly | 只收不发 | 观众端 |
| inactive | 该路媒体静默 | 暂时关闭的轨道 |
answer 的生成规则是「逐项裁剪」:offer 列出的是「我能做的全部」,answer 勾选的是「这次就用这些」。方向上 answer 只能收窄不能放宽——你 sendonly 过去,对方最多回 recvonly,绝不可能变成双向。编解码上 answer 从 offer 的清单里挑交集,顺序即偏好,第一项通常是最终生效项。

把 SDP 当作协商结果的「对账单」来用,是它最大的工程价值。三个常用对账场景:
场景一:确认轨道方向。产品语义(观众只收)要落实为 addTrack 的决策:观众端不 addTrack、只 transceive 收。上线前抓一份 answer,检查视频块方向是否 recvonly,一眼定案。场景二:确认编解码。想在低端设备上强制 VP8 而非 H264,就检查 answer 里 rtpmap 顺序;发现生效编码不符,回头调 setCodecPreferences。场景三:确认抗弱网开关。useinbandfec 决定音频抗丢包,nack 与 pli 决定视频恢复手段——这些行缺失,第 6 章的恢复机制就无从发力。
💡 关键直觉:SDP 是浏览器替你写的协商纪要,业务配置有没有生效,别猜,打开纪要对账。
背景:某直播产品的观众端页面被运维发现存在上行视频流量,在弱网地区造成观感卡顿,与「观众只收不发」的产品预期不符。
操作:工程师导出观众端的 offer 与 answer 对账:offer 视频块方向 sendrecv,answer 也是 sendrecv。追查代码发现观众端为了「预留未来连麦能力」,无差别执行了 addTrack(空轨道也加了),浏览器据此把方向谈成了双向。修复分两步:观众端改为 addTransceiver 声明 recvonly;服务端在信令层按角色校验方向,发现观众端 offer 里出现 send 方向立即告警。
结果:观众端上行视频流量归零,弱网首帧时间明显改善;连麦能力后续上线时,只需在观众升格为主播的流程里补一次重协商,方向按需放宽。
解读:协商是「轨道决定方向」——浏览器忠实地把你的轨道结构翻译成 SDP,而不是反过来替你实现产品语义。想控制方向,控制轨道结构;想审计方向,读 SDP。这条单向因果想通了,「为什么产品配置不生效」一类问题就都有了排查起点。
变式:多方会议里同一份方法论可以继续用:SFU 侧下发的 offer 通常把上行块设为 recvonly、下行块设为 sendonly,接入排障时先对这份「角色合同」,方向不对先查角色,再查轨道,最后才怀疑网络。层层收窄,比漫无目的地抓包快得多。