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 定义的,说白了就是一问一答。

流程是这样的:

  1. 发起方(Offerer) 创建一份 Offer SDP,描述自己支持的能力。
  2. 通过信令通道(比如 WebSocket)把 Offer 发给 应答方(Answerer)
  3. 应答方收到 Offer 后,创建一份 Answer SDP,表示「我接受你的哪些能力,我这边怎么配合」。
  4. 应答方把 Answer 发回给发起方。
  5. 双方各自设置本地和远端描述,协商完成。

嗯,这里要注意: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():设置本端生成的 SDP
  • setRemoteDescription():设置对端发来的 SDP

这两个方法接受一个 RTCSessionDescription 对象,或者直接传一个包含 typesdp 字段的普通对象。

// 方式一:使用 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 完整的协商流程图

下面我用一张图把整个流程串起来,你一看就明白了:

发起方 (Offerer) 应答方 (Answerer) 1. 创建 Offer 2. setLocalDescription(offer) Offer SDP 3. setRemoteDescription(offer) 4. 创建 Answer 5. setLocalDescription(answer) Answer SDP 6. setRemoteDescription(answer) ✅ 协商完成

这张图我画得比较清楚。你看,整个流程就是:创建 → 发送 → 接收 → 回应 → 设置,一共六步,缺一不可。

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