3. 多媒体基础

SDP,全称 Session Description Protocol,会话描述协议。很多人第一次看到 SDP 会觉得它就是个文本配置文件,没啥技术含量。嗯,我刚开始也这么想,直到有一次排查一个跨平台互通问题,发现双方 SDP 里一个 a=fmtp 参数写错了,导致视频花屏了整整两天……从那以后,我再也不敢小看这几百行文本了。

说白了,SDP 就是 WebRTC 通信的「自我介绍」。它告诉对方:我能发什么格式的音频、视频,我的网络地址在哪,我用什么编码器。没有它,两个浏览器根本没法握手。

3.1 SDP 的作用与结构

SDP 本身不负责传输媒体数据,它只做一件事——描述会话。一个典型的 SDP 文本长这样:

v=0
o=- 123456 2 IN IP4 192.168.1.100
s=-
t=0 0
m=audio 49170 RTP/AVP 96
a=rtpmap:96 OPUS/48000/2
a=fmtp:96 minptime=10;useinbandfec=1
m=video 49172 RTP/AVP 98
a=rtpmap:98 H264/90000
a=fmtp:98 profile-level-id=42e01f;packetization-mode=1

每一行都以一个单字符开头,后面跟等号。这些字符就是 SDP 的「行类型」。我习惯把它们分成三类:

行类型 含义 必选?
v= 协议版本,目前固定为 0 是
o= 会话发起者信息 是
s= 会话名称 是
t= 会话时间 是
m= 媒体描述 是
a= 属性行 否

你可能会问:为什么 s=- 里是个短横?因为 WebRTC 里会话名称不重要,RFC 允许用 - 占位。我见过不少新手在这里纠结,其实没必要。

核心要点:SDP 是文本格式,按行解析。每一行都独立,没有嵌套结构。解析器从上往下读,遇到 m= 就知道新媒体流开始了。

3.2 Offer/Answer 模型

WebRTC 的 SDP 交换不是单向的,而是双向协商。这就是 Offer/Answer 模型,定义在 RFC 3264 里。

流程很简单:

  1. A 方生成一个 Offer SDP,描述它想怎么通信
  2. B 方收到后,生成一个 Answer SDP,表示接受或调整
  3. 双方根据 Answer 调整自己的发送参数

我记得有一次调试一个 Android 和 iOS 互通的问题,Android 端 Offer 里写了 H264 和 VP8 两种编码,但 iOS 端 Answer 只回了 H264。结果 Android 端还在傻等 VP8 的包……这就是典型的 Answer 没被正确解析。

避坑指南:Offer 可以包含多个候选编码,但 Answer 只能选一个。如果 Answer 里出现了 Offer 没提的编码,那就是非法 SDP,直接拒绝。

为什么需要这个模型?因为 WebRTC 是 P2P 的,没有中心服务器帮你协调。双方必须自己商量好用什么编码、什么端口、什么网络协议。Offer/Answer 就是这套「商量」的标准化流程。

3.3 媒体描述行(m=行)解析

m= 行是 SDP 里最重要的行之一。它定义了一个媒体流的基本信息。格式如下:

m=<媒体类型> <端口> <传输协议> <格式列表>

举个例子:

m=audio 49170 RTP/AVP 96 97 98

拆开看:

  • 媒体类型:audio、video、application、text 等。WebRTC 里最常见的是 audio 和 video。
  • 端口:RTP 使用的 UDP 端口。注意,RTCP 端口默认是 RTP 端口 + 1。
  • 传输协议:WebRTC 里固定是 RTP/AVP 或 RTP/SAVPF。后者带 F,表示支持反馈机制,比如 NACK、PLI。
  • 格式列表:一组数字,每个数字对应一个编码格式。具体映射关系由 a=rtpmap 定义。

注意:端口号在 WebRTC 里其实不太重要,因为 ICE 会重新协商实际使用的端口。但 SDP 里必须写一个,通常写 9 或者 49170 这种占位值。我见过有人写 0,结果某些老版本 Chrome 直接报错。

一个 SDP 里可以有多个 m= 行,每个代表一个独立的媒体流。比如一个音频流加一个视频流:

m=audio 49170 RTP/SAVPF 96
m=video 49172 RTP/SAVPF 98 99

每个 m= 行后面跟着的属性行,只属于这个媒体流,直到下一个 m= 出现为止。

3.4 属性行(a=行)详解

a= 行是 SDP 里最灵活的部分。它用来补充描述媒体流的各种属性。我把它分成几类:

3.4.1 编码参数类

a=rtpmap:96 OPUS/48000/2
a=fmtp:96 minptime=10;useinbandfec=1
  • a=rtpmap:把 m= 行里的数字映射成具体编码。格式是 编码名/采样率/声道数。
  • a=fmtp:编码的特定参数。比如 Opus 的 minptime 表示最小打包时长,useinbandfec 表示是否启用内带 FEC。

我个人习惯把 a=fmtp 看作「编码器的配置文件」。不同厂商对同一编码的参数理解可能不同,这里最容易出兼容性问题。

3.4.2 网络传输类

a=candidate:1 1 UDP 2122252543 192.168.1.100 49170 typ host
a=ice-ufrag:abc123
a=ice-pwd:xyz789
  • a=candidate:ICE 候选地址,告诉对方可以从哪些 IP 和端口连过来。
  • a=ice-ufrag 和 a=ice-pwd:ICE 认证用的用户名和密码。

经验之谈:我曾经遇到一个问题,双方 ICE 候选地址都正确,但就是连不上。最后发现是 a=ice-ufrag 里有个不可见字符。SDP 是纯文本,任何不可见字符都会导致解析失败。建议拿到 SDP 后先用 hexdump 看一眼。

3.4.3 媒体控制类

a=sendrecv
a=recvonly
a=sendonly
a=inactive

这些属性控制媒体流的传输方向。WebRTC 里默认是 sendrecv,但有些场景只需要单向传输,比如屏幕共享时接收端设为 recvonly。

3.4.4 其他常用属性

属性行 作用
a=group:BUNDLE audio video 把多个媒体流捆绑到同一个传输通道
a=msid 媒体流标识,关联到具体的 MediaStream
a=ssrc 同步源标识,每个 RTP 流都有唯一的 SSRC
a=extmap RTP 头部扩展,用于传递时间戳、层信息等

这里我想特别提一下 a=group:BUNDLE。这个属性在 WebRTC 里几乎必用。它让音频和视频走同一个 UDP 端口,大幅减少 NAT 穿透的难度。没有它,每个媒体流都要单独打洞,成功率会低很多。

总结一下:SDP 的 a= 行是「自由发挥」的地方。RFC 定义了一些标准属性,但厂商也可以自定义(以 a=x- 开头)。解析时遇到不认识的行,直接忽略就好,不要抛异常。

3.5 SDP 整体结构图

下面这张图展示了 SDP 从会话层到媒体层的完整结构。你可以看到 m= 行如何分割不同的媒体流,以及 a= 行如何附着在对应的媒体流上。

SDP 协议结构图 SDP 协议结构图 会话层(Session Level) v=0 | o=- ... | s=- | t=0 0 媒体流 1(Media Stream 1) m=audio 49170 RTP/SAVPF 96 a=rtpmap:96 OPUS/48000/2 a=fmtp:96 minptime=10;useinbandfec=1 媒体流 2(Media Stream 2) m=video 49172 RTP/SAVPF 98 99 a=rtpmap:98 H264/90000 a=rtpmap:99 rtx/90000 每个 m= 行定义一个新的媒体流,其后的 a= 行只属于该媒体流

从图里可以清楚看到:会话层在最上面,包含全局信息。下面每个 m= 行开启一个新的媒体流,后面跟着一堆 a= 行。解析器就是按这个顺序逐行处理的。

实用技巧:调试 SDP 问题时,我习惯先看 m= 行数量对不对,再看每个 m= 后面的 a=rtpmap 是否匹配。如果双方 m= 行数不一致,那肯定是 Offer/Answer 没对齐。

好了,SDP 的核心内容就这些。它不复杂,但细节很多。尤其是 a= 行,不同场景下会有不同的组合。多抓包、多对比,慢慢就能摸清规律。