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 的媒体通道。

核心要点: Offer/Answer 模型只负责「协商」,不负责「传输」。协商完成后,媒体数据才通过 ICE 打通的通道传输。

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 里编解码器列表是空的。嗯,那次排查花了我半天时间。

警告: 创建 Answer 时,不要手动修改 SDP 字符串。除非你非常清楚自己在做什么。WebRTC 的协商逻辑很复杂,手动修改容易导致协商失败。

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'。传错了会报错。

调试技巧: 如果设置远程描述失败,先检查 SDP 字符串是否完整。有时候信令传输过程中会截断 SDP,导致解析失败。我习惯在发送前对 SDP 做 Base64 编码,接收后再解码。

12.6 完整流程示例

把上面几个步骤串起来,就是一个完整的 Offer/Answer 流程。我画了一张图,帮你理清逻辑:

发起方(Caller) 接收方(Callee) 1. 创建 PeerConnection 添加媒体轨道 2. createOffer() 生成 Offer SDP 3. setLocalDescription(offer) 设置本地描述 Offer SDP 4. setRemoteDescription(offer) 设置远程描述 5. createAnswer() 生成 Answer SDP 6. setLocalDescription(answer) 设置本地描述 Answer SDP 7. setRemoteDescription(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,边收集边发送候选
曾经踩过的坑: 有一次我在生产环境发现,部分用户的 Offer/Answer 协商总是失败。排查了半天,发现是信令服务器把 SDP 中的换行符给吞了。从那以后,我所有 SDP 传输都做 Base64 编码,再也没出过问题。

12.8 小结

Offer/Answer 模型,说白了就是 WebRTC 的「握手协议」。它不负责传数据,只负责商量好怎么传。你只要记住四个方法:

  • createOffer() — 发起方生成 Offer
  • createAnswer() — 接收方生成 Answer
  • setLocalDescription() — 告诉引擎「我是什么能力」
  • setRemoteDescription() — 告诉引擎「对方是什么能力」

顺序别搞反,类型别搞错,基本就稳了。嗯,下一节我们聊聊 ICE 候选收集,那是打通 P2P 通道的关键一步。

个人习惯: 我每次写 Offer/Answer 逻辑时,都会在关键步骤加日志。比如打印 SDP 的前 200 个字符,确认类型和内容是否正确。调试时能省不少时间。

公众号:蓝海资料掘金营,微信deep3321