SdpOfferAnswerHandler · 协商的驾驶舱

pc/sdp_offer_answer.cc 有 6400 多行,是 pc/ 里最庞大的文件——一次通话的协商大脑。这一章读它的真实结构:CreateOffer/SetLocalDescription 等入口如何经 OperationsChain 串行化,ApplyLocalDescription 又如何推进信令状态机、把协商结果下推给传输与收发器。

作者 David更新于 第 10 篇(共 112 篇)

SdpOfferAnswerHandler · 协商的驾驶舱

PeerConnection 的 sdp_handler_ 成员,类型是 SdpOfferAnswerHandler。它是会话协商的驾驶舱——JSEP 状态机的司机。它有多重要?看体量:pc/sdp_offer_answer.cc 6422 行,是整个 pc/ 目录里最庞大的文件。这一章不逐行啃它,而是抓住它的骨架:入口、串行化、以及「应用一个描述」到底做了什么。

四个入口

SdpOfferAnswerHandler 对外的核心方法,就是通话流程里那几个 API 的落地(pc/sdp_offer_answer.h):

// pc/sdp_offer_answer.h(节选)
void CreateOffer(CreateSessionDescriptionObserver* observer, ...);
void CreateAnswer(CreateSessionDescriptionObserver* observer, ...);
void SetLocalDescription(...);        // 多个重载
void SetRemoteDescription(...);
PeerConnectionInterface::SignalingState signaling_state() const override;

CreateOffer / CreateAnswer 生成一个 SDP 描述,SetLocalDescription / SetRemoteDescription 应用一个描述并推进状态机,signaling_state() 报告当前状态。这些方法都在 signaling 线程上、且异步——结果通过 observer 回调(还记得线程模型里回调多在 signaling 线程吗)。

OperationsChain:协商必须排队

这里有个精妙又关键的设计。协商操作绝不能并发——你不能一边 setLocalDescription 还没做完,一边又 setRemoteDescription 插进来,否则状态机会乱套。WebRTC 用一个 OperationsChain 强制它们排队串行执行。pc/sdp_offer_answer.h 里的成员:

// pc/sdp_offer_answer.h
scoped_refptr<OperationsChain> operations_chain_ RTC_GUARDED_BY(signaling_thread());

每个公共的 CreateOffer() / CreateAnswer() / SetLocalDescription() / SetRemoteDescription(),都不是立即执行,而是被「挂」到 operations_chain_ 上排队。 源码注释(pc/sdp_offer_answer.h)写明这些操作「are chained onto the operations_chain_」。这条链保证:上一个协商操作彻底完成(含它的异步回调)之前,下一个不会开始。

为什么非排队不可?因为一次 setRemoteDescription 可能触发一连串异步动作(建传输、配收发器……),中途绝不能被另一个协商操作打断。OperationsChain 把「看似可以随便调的 API」变成「井然有序的单队列」——这正好呼应线程模型那章的教训:WebRTC 里「顺序」是被精心设计出来的,不是天然的。这也是 W3C 规范要求的「操作串行化」在 C++ 侧的实现。

应用一个描述:ApplyLocalDescription 做了什么

真正的重头戏,是应用一个描述。以 SetLocalDescription 为例,它内部走到 ApplyLocalDescription(pc/sdp_offer_answer.cc),这才是「让协商结果生效」的地方。抓住它的三件大事:

// pc/sdp_offer_answer.cc:ApplyLocalDescription 的骨架(极简示意)
RTCError SdpOfferAnswerHandler::ApplyLocalDescription(...) {
  RTC_DCHECK_RUN_ON(signaling_thread());     // 必须在 signaling 线程
  // ① 校验 + 推进状态机
  UpdateSessionState(type, cricket::CS_LOCAL, ...);
  // ② 把协商好的媒体「下推」给传输控制器(配置 ICE/DTLS)
  //    transport_controller()->...  (BUNDLE、DTLS 角色、ICE 参数)
  // ③ 把媒体方向/编解码结果落到收发器(rtp_manager 的 sender/receiver)
  //    ...
}

三步,对应三个下属对象:

  1. 推进状态机 —— UpdateSessionState(pc/sdp_offer_answer.cc 里真实存在)按 SdpType 和「本地 / 远端」把 SignalingState 往前推(如 kStable → kHaveLocalOffer)。推之前先校验:当前状态允不允许这个操作?比如在 kHaveLocalOffer 时又来一个本地 offer,就会被拒。
  2. 下推到传输 —— 把协商出的 ICE 凭据、DTLS 指纹与角色、BUNDLE 分组,交给 transport_controller_(JsepTransportController),让它去建立 ICE/DTLS。ApplyLocalDescription 的 .cc 里就 #include "pc/jsep_transport_controller.h" 并直接调它。
  3. 下推到收发器 —— 把每条 m-line 协商出的方向(sendrecv 等)和编解码器,落到 rtp_manager_ 管理的 RtpSender / RtpReceiver 上,决定这路媒体到底发不发、收不收、用什么编解码。

ApplyRemoteDescription(处理对端描述)结构对称。这就是「协商」二字的实质:把一段 SDP 文本,翻译成对传输层和收发器的一系列配置动作。

一张流程图

把入口、串行化、应用三件事串起来:

图表加载中…

为什么它有 6400 行

因为魔鬼全在细节。这个文件要正确处理 JSEP 的一切边角:rollback 回滚、双方抢话(glare)、ICE 重启、BUNDLE 分组协商、RTP 头扩展的取舍、m-line 的增删与匹配(Unified Plan)、数据通道 m-line、每个状态下允许哪些操作的校验…… 每一个都是真实网络里会遇到的情况。协商看似只是「交换两段文本」,但把它做对、做全,需要 6400 行——这也是为什么会话建立值得单独一个部分来讲。

小结与下一章

SdpOfferAnswerHandler(pc/sdp_offer_answer.cc,6422 行,pc/ 最大文件)是协商的驾驶舱:四个入口(CreateOffer/CreateAnswer/SetLocalDescription/SetRemoteDescription)经 OperationsChain 串行排队(协商绝不并发);核心 ApplyLocalDescription 干三件事——校验并推进信令状态机、下推 ICE/DTLS 传输、下推收发器的方向与编解码。协商的实质,是把 SDP 文本翻译成对下属对象的配置。

到这里,会话建立的主干——工厂、上帝对象、SDP/JSEP、协商驾驶舱——已经打通。第 1 部分接下来会继续深入这条主干上的关键节点:媒体协商(media_session.cc 怎么给编解码器取交集)、收发器(RtpTransceiver/Sender/Receiver 的 Unified Plan 模型)、传输控制器(JsepTransportController 与 BUNDLE)、数据通道与统计收集。(第 1 部分陆续更新。)

参考资料

  • [1] libwebrtc 源码.pc/sdp_offer_answer.h / .cc(SdpOfferAnswerHandler、operations_chain_、ApplyLocalDescription、UpdateSessionState)。
  • [2] libwebrtc 源码.api/rtc_error.h(RTCError 返回值风格)、rtc_base/operations_chain.h(串行化机制)。
  • [3] IETF RFC 8829「JSEP」—— 应用描述与状态迁移的规范。

本页目录