12、Offer/Answer模型:创建Offer、创建Answer、设置本地描述、设置远程描述
WebRTC 的通信建立,说白了就是两个浏览器互相「握手」的过程。这个握手,靠的就是 Offer/Answer 模型。我刚开始接触时也觉得这名字挺唬人,其实它就是个协商机制——双方商量好用什么编解码器、用什么网络端口、走什么传输协议。
今天我们就把它拆开揉碎了讲清楚。你跟着我走一遍,保证能搞明白。
12.1 什么是 Offer/Answer 模型?
Offer/Answer 模型,是 WebRTC 建立连接的核心流程。它基于 SDP(Session Description Protocol)协议工作。SDP 说白了就是一个文本格式的「通信配置单」,里面写满了双方的能力信息。
流程很简单:
- 发起方(Caller)生成一个 Offer,描述自己「能做什么」
- 接收方(Callee)收到 Offer 后,生成一个 Answer,描述自己「能做什么」
- 双方各自把对方的描述设置到自己的 PeerConnection 里
嗯,这里要注意:Offer 和 Answer 都是 SDP 字符串。它们通过信令服务器中转,不经过 WebRTC 的媒体通道。
12.2 创建 Offer
创建 Offer 是发起方的第一步。我习惯在创建 PeerConnection 之后,立刻调用 createOffer() 方法。
const pc = new RTCPeerConnection(configuration);
// 创建 Offer
const offer = await pc.createOffer();
console.log('Offer SDP:', offer.sdp);
// 设置本地描述
await pc.setLocalDescription(offer);
console.log('本地描述已设置');
createOffer() 会生成一个包含当前媒体信息的 SDP 对象。它包含:
- 音频/视频的编解码器列表(如 H264、VP8、Opus)
- 媒体流的 SSRC 标识
- ICE 候选信息(如果已收集)
- DTLS 指纹(用于加密协商)
我在项目中遇到过一个问题:创建 Offer 时如果还没添加媒体轨道,生成的 SDP 里可能没有媒体描述。所以我的习惯是,先调用 addTrack() 或 addTransceiver(),再创建 Offer。
pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true }) 来强制生成接收音视频的 Offer,即使本地没有添加轨道。
12.3 创建 Answer
接收方收到 Offer 后,需要创建 Answer。流程和创建 Offer 类似,但用的是 createAnswer() 方法。
// 假设收到了远程的 Offer SDP
const remoteOffer = new RTCSessionDescription({ type: 'offer', sdp: offerSdp });
// 设置远程描述
await pc.setRemoteDescription(remoteOffer);
// 创建 Answer
const answer = await pc.createAnswer();
console.log('Answer SDP:', answer.sdp);
// 设置本地描述
await pc.setLocalDescription(answer);
console.log('Answer 已设置');
这里有个关键点:必须先设置远程描述,再创建 Answer。为什么?因为 createAnswer() 需要知道对方的能力,才能生成一个「双方都能接受」的配置。
我曾经踩过一个坑:在设置远程描述之前就调用了 createAnswer(),结果生成的 Answer 里编解码器列表是空的。嗯,那次排查花了我半天时间。
12.4 设置本地描述(setLocalDescription)
setLocalDescription() 的作用,是把生成的 Offer 或 Answer 告诉 PeerConnection 内部。它让 WebRTC 引擎知道:「这就是我对外宣称的能力」。
设置本地描述后,PeerConnection 会开始:
- 收集 ICE 候选(如果还没收集完)
- 准备 DTLS 握手
- 启动媒体编码器/解码器
代码很简单:
// 设置本地描述
await pc.setLocalDescription(offer);
// 或者
await pc.setLocalDescription(answer);
注意,setLocalDescription() 是异步的。它返回一个 Promise,等它 resolve 之后,本地描述才算真正生效。
pc.localDescription 获取当前的本地 SDP 对象。调试时我经常打印它来检查 SDP 内容。
12.5 设置远程描述(setRemoteDescription)
setRemoteDescription() 是接收对方 SDP 的入口。它告诉 PeerConnection:「对方的能力是这样的,你按这个来协商」。
设置远程描述后,PeerConnection 会:
- 解析对方的 SDP,提取编解码器、SSRC、指纹等信息
- 开始 ICE 候选匹配
- 准备接收媒体流
// 设置远程描述
const remoteSdp = new RTCSessionDescription({
type: 'offer', // 或者 'answer'
sdp: receivedSdpString
});
await pc.setRemoteDescription(remoteSdp);
这里有个容易搞混的地方:type 字段必须和 SDP 类型一致。如果你收到的是 Offer,就传 type: 'offer';收到的是 Answer,就传 type: 'answer'。传错了会报错。
12.6 完整流程示例
把上面几个步骤串起来,就是一个完整的 Offer/Answer 流程。我画了一张图,帮你理清逻辑:
你看,整个流程就是来回交换 SDP。发起方先出 Offer,接收方回 Answer。双方各设置一次本地描述、一次远程描述,就完成了协商。
12.7 常见问题与避坑指南
我做了这么多年 WebRTC,Offer/Answer 这块踩过的坑不少。挑几个典型的说说:
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 创建 Offer 时 SDP 为空 | 没有添加媒体轨道或 Transceiver | 先调用 addTrack() 或 addTransceiver() |
| 设置远程描述失败 | SDP 类型不匹配(offer/answer 搞反) | 检查 type 字段,确保和 SDP 内容一致 |
| Answer 创建后协商失败 | 远程描述未设置就创建 Answer | 先 setRemoteDescription,再 createAnswer |
| ICE 候选不匹配 | SDP 中缺少 ICE 候选信息 | 使用 Trickle ICE,边收集边发送候选 |
12.8 小结
Offer/Answer 模型,说白了就是 WebRTC 的「握手协议」。它不负责传数据,只负责商量好怎么传。你只要记住四个方法:
createOffer()— 发起方生成 OffercreateAnswer()— 接收方生成 AnswersetLocalDescription()— 告诉引擎「我是什么能力」setRemoteDescription()— 告诉引擎「对方是什么能力」
顺序别搞反,类型别搞错,基本就稳了。嗯,下一节我们聊聊 ICE 候选收集,那是打通 P2P 通道的关键一步。
公众号:蓝海资料掘金营,微信deep3321