# SdpOfferAnswerHandler · 协商的驾驶舱

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

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/webrtc/offer-answer-handler
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# SdpOfferAnswerHandler · 协商的驾驶舱

`PeerConnection` 的 [`sdp_handler_` 成员](https://daiw.org/manual/webrtc/peer-connection-object)，类型是 `SdpOfferAnswerHandler`。它是**会话协商的驾驶舱**——[JSEP 状态机](https://daiw.org/manual/webrtc/sdp-and-jsep)的司机。它有多重要？看体量：`pc/sdp_offer_answer.cc` **6422 行**，是整个 `pc/` 目录里最庞大的文件。这一章不逐行啃它，而是抓住它的**骨架**：入口、串行化、以及「应用一个描述」到底做了什么。

## 四个入口

`SdpOfferAnswerHandler` 对外的核心方法，就是[通话流程](https://daiw.org/manual/webrtc/call-lifecycle)里那几个 API 的落地（`pc/sdp_offer_answer.h`）：

```cpp
// 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 描述](https://daiw.org/manual/webrtc/sdp-and-jsep)，`SetLocalDescription` / `SetRemoteDescription` **应用**一个描述并推进状态机，`signaling_state()` 报告[当前状态](https://daiw.org/manual/webrtc/sdp-and-jsep)。这些方法都在 signaling 线程上、且**异步**——结果通过 observer 回调（还记得[线程模型](https://daiw.org/manual/webrtc/threading-model)里回调多在 signaling 线程吗）。

## OperationsChain：协商必须排队

这里有个精妙又关键的设计。协商操作**绝不能并发**——你不能一边 `setLocalDescription` 还没做完，一边又 `setRemoteDescription` 插进来，否则[状态机](https://daiw.org/manual/webrtc/sdp-and-jsep)会乱套。WebRTC 用一个 **`OperationsChain`** 强制它们**排队串行执行**。`pc/sdp_offer_answer.h` 里的成员：

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

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

  为什么非排队不可？因为一次 `setRemoteDescription` 可能触发一连串异步动作（[建传输](https://daiw.org/manual/webrtc/source-map)、[配收发器](https://daiw.org/manual/webrtc/offer-answer-handler)……），中途绝不能被另一个协商操作打断。`OperationsChain` 把「看似可以随便调的 API」变成「井然有序的单队列」——这正好呼应[线程模型那章](https://daiw.org/manual/webrtc/threading-model)的教训：**WebRTC 里「顺序」是被精心设计出来的，不是天然的**。这也是 W3C 规范要求的「操作串行化」在 C++ 侧的实现。
</Callout>

## 应用一个描述：ApplyLocalDescription 做了什么

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

```cpp
// 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](https://daiw.org/manual/webrtc/sdp-and-jsep) 和「本地 / 远端」把 [`SignalingState`](https://daiw.org/manual/webrtc/sdp-and-jsep) 往前推（如 `kStable` → `kHaveLocalOffer`）。推之前先**校验**：当前状态允不允许这个操作？比如在 `kHaveLocalOffer` 时又来一个本地 offer，就会被拒。
2. **下推到传输** —— 把协商出的 ICE 凭据、DTLS 指纹与角色、[BUNDLE](https://daiw.org/manual/webrtc/source-map) 分组，交给 [`transport_controller_`](https://daiw.org/manual/webrtc/peer-connection-object)（`JsepTransportController`），让它去[建立 ICE/DTLS](https://daiw.org/manual/webrtc/source-map)。`ApplyLocalDescription` 的 `.cc` 里就 `#include "pc/jsep_transport_controller.h"` 并直接调它。
3. **下推到收发器** —— 把每条 m-line 协商出的**方向**（sendrecv 等）和**编解码器**，落到 [`rtp_manager_`](https://daiw.org/manual/webrtc/peer-connection-object) 管理的 `RtpSender` / `RtpReceiver` 上，决定这路媒体到底发不发、收不收、用什么编解码。

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

## 一张流程图

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

```mermaid
flowchart TD
    API["SetLocalDescription() / SetRemoteDescription()<br/>(signaling 线程,异步)"]
    API -->|"挂到队列"| CHAIN["OperationsChain<br/>(串行:上一个做完才做下一个)"]
    CHAIN --> APPLY["ApplyLocalDescription / ApplyRemoteDescription"]
    APPLY --> ST["① UpdateSessionState<br/>校验 + 推进信令状态机"]
    APPLY --> TC["② 下推传输<br/>JsepTransportController(ICE/DTLS/BUNDLE)"]
    APPLY --> RTP["③ 下推收发器<br/>RtpTransmissionManager(方向/编解码)"]
    APPLY --> CB["observer 回调(成功/失败)"]
```

## 为什么它有 6400 行

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

## 小结与下一章

`SdpOfferAnswerHandler`（`pc/sdp_offer_answer.cc`，6422 行，`pc/` 最大文件）是**协商的驾驶舱**：四个入口（`CreateOffer`/`CreateAnswer`/`SetLocalDescription`/`SetRemoteDescription`）经 **`OperationsChain` 串行排队**（协商绝不并发）；核心 `ApplyLocalDescription` 干三件事——**校验并推进[信令状态机](https://daiw.org/manual/webrtc/sdp-and-jsep)**、**下推 [ICE/DTLS 传输](https://daiw.org/manual/webrtc/peer-connection-object)**、**下推[收发器](https://daiw.org/manual/webrtc/peer-connection-object)的方向与编解码**。协商的实质，是把 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」—— 应用描述与状态迁移的规范。
