SdpOfferAnswerHandler · 协商的驾驶舱
pc/sdp_offer_answer.cc 有 6400 多行,是 pc/ 里最庞大的文件——一次通话的协商大脑。这一章读它的真实结构:CreateOffer/SetLocalDescription 等入口如何经 OperationsChain 串行化,ApplyLocalDescription 又如何推进信令状态机、把协商结果下推给传输与收发器。
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)
// ...
}三步,对应三个下属对象:
- 推进状态机 ——
UpdateSessionState(pc/sdp_offer_answer.cc里真实存在)按 SdpType 和「本地 / 远端」把SignalingState往前推(如kStable→kHaveLocalOffer)。推之前先校验:当前状态允不允许这个操作?比如在kHaveLocalOffer时又来一个本地 offer,就会被拒。 - 下推到传输 —— 把协商出的 ICE 凭据、DTLS 指纹与角色、BUNDLE 分组,交给
transport_controller_(JsepTransportController),让它去建立 ICE/DTLS。ApplyLocalDescription的.cc里就#include "pc/jsep_transport_controller.h"并直接调它。 - 下推到收发器 —— 把每条 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」—— 应用描述与状态迁移的规范。