4. 媒体协商 SDP 详解:SDP 是什么、Offer/Answer 模型、创建 Offer 和 Answer、设置本地与远端描述
好,咱们今天来聊聊 WebRTC 里最绕不开的一个话题——SDP。
说实话,我刚开始接触 WebRTC 的时候,看到 SDP 那一大段文本,第一反应是:这什么鬼?一堆看不懂的字段,像天书一样。后来踩了不少坑,才慢慢摸清楚它的门道。
你想想看,两个浏览器要建立连接,总得先互相打个招呼吧?「嘿,我支持 H264 编码,我这边网络端口是 50000,你能接吗?」——这个打招呼的过程,就是媒体协商。而 SDP,就是他们用来打招呼的「语言」。
4.1 SDP 到底是什么?
SDP 全称 Session Description Protocol,会话描述协议。名字挺唬人,说白了就是一段文本,描述了你这边「能做什么」和「想怎么做」。
它不负责传输数据,只负责描述数据。就像你去餐厅点菜,菜单上的文字描述的是菜品的口味和做法,而不是菜本身。SDP 就是 WebRTC 的「菜单」。
核心要点:SDP 描述的是媒体能力,包括:
- 支持的编解码器(比如 H264、VP8、VP9)
- 网络信息(IP 地址、端口号)
- 传输协议(比如 RTP、SRTP)
- 媒体类型(音频、视频、数据通道)
我个人的习惯是,把 SDP 理解成一份「简历」。你把自己的能力写清楚,对方看了之后决定要不要跟你「合作」。
4.2 Offer/Answer 模型
WebRTC 的媒体协商,遵循的是 Offer/Answer 模型。这个模型是 RFC 3264 定义的,说白了就是一问一答。
流程是这样的:
- 发起方(Offerer) 创建一份 Offer SDP,描述自己支持的能力。
- 通过信令通道(比如 WebSocket)把 Offer 发给 应答方(Answerer)。
- 应答方收到 Offer 后,创建一份 Answer SDP,表示「我接受你的哪些能力,我这边怎么配合」。
- 应答方把 Answer 发回给发起方。
- 双方各自设置本地和远端描述,协商完成。
嗯,这里要注意:Offer 和 Answer 不是随便写的。Answer 必须基于 Offer 来生成,不能自己凭空捏造。比如 Offer 说支持 H264 和 VP8,Answer 只能从这两个里选,不能说「我偏要用 VP9」。
避坑指南:我曾经在项目中遇到过一个问题——Offer 里写了三个编解码器,Answer 只回了一个,结果视频死活出不来。后来发现是 Answer 里漏了 payload type 的映射。所以,一定要检查 Answer 里的编解码器参数是否和 Offer 对应。
4.3 创建 Offer 和 Answer
在浏览器里,创建 Offer 和 Answer 是通过 RTCPeerConnection 对象来完成的。代码其实不复杂,但顺序很重要。
4.3.1 创建 Offer
const pc = new RTCPeerConnection(config);
// 先添加媒体轨道
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// 创建 Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令发送给远端
sendToSignalingServer(offer);
这里有个细节:createOffer() 之前,一定要先 addTrack() 或者 addTransceiver()。为什么?因为 Offer 是根据你添加的媒体流来生成的。你没加视频轨道,Offer 里就不会有视频相关的描述。
4.3.2 创建 Answer
远端收到 Offer 后,创建 Answer:
// 远端收到 Offer
const offer = receiveFromSignalingServer();
const pc = new RTCPeerConnection(config);
await pc.setRemoteDescription(new RTCSessionDescription(offer));
// 创建 Answer
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// 把 Answer 发回给发起方
sendToSignalingServer(answer);
你看,流程是对称的。发起方先设本地,再发 Offer;应答方先设远端,再创建 Answer,再设本地。
重要提醒:千万不要在 setRemoteDescription 之前调用 createAnswer。我见过有人这么写,结果 Answer 里全是空的。因为 createAnswer 需要依赖远端 Offer 的信息,没有 Offer,它不知道要生成什么。
4.4 设置本地与远端描述
设置描述有两个方法:
setLocalDescription():设置本端生成的 SDPsetRemoteDescription():设置对端发来的 SDP
这两个方法接受一个 RTCSessionDescription 对象,或者直接传一个包含 type 和 sdp 字段的普通对象。
// 方式一:使用 RTCSessionDescription 对象
const desc = new RTCSessionDescription({
type: 'offer',
sdp: 'v=0\r\no=- ...'
});
await pc.setRemoteDescription(desc);
// 方式二:直接传对象(浏览器会自动转换)
await pc.setRemoteDescription({
type: 'offer',
sdp: 'v=0\r\no=- ...'
});
我个人更推荐方式二,少写一行代码,而且不容易出错。
4.5 完整的协商流程图
下面我用一张图把整个流程串起来,你一看就明白了:
这张图我画得比较清楚。你看,整个流程就是:创建 → 发送 → 接收 → 回应 → 设置,一共六步,缺一不可。
4.6 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 创建 Offer 时 SDP 为空 | 没有先添加媒体轨道 | 先调用 addTrack() 或 addTransceiver() |
| Answer 里缺少编解码器 | Offer 和 Answer 的 payload type 不匹配 | 检查双方 SDP 中的 rtpmap 字段 |
| setRemoteDescription 报错 | SDP 格式错误或 type 字段不对 | 确保 type 是 'offer' 或 'answer',sdp 是完整字符串 |
| 协商完成后媒体不传输 | ICE 候选者没有交换 | 检查 ICE 流程是否完整,候选者是否送达 |
我的经验:调试 SDP 问题的时候,最笨但最有效的方法就是——把两边的 SDP 打印出来,逐行对比。我曾经花了一整个下午,最后发现是 Offer 里少了一个 a=rtcp-fb 行,导致远端不认识 NACK 包。所以,别怕看 SDP 文本,看多了你就习惯了。
好了,关于 SDP 和 Offer/Answer 模型,今天就聊到这里。这部分内容虽然看起来枯燥,但它是 WebRTC 的基石。你把这个搞懂了,后面 ICE 连接、数据通道那些,理解起来就轻松多了。
公众号:蓝海资料掘金营,微信deep3321