# 一次通话的一生 · 从 createOffer 到媒体流

> 把整个教程的骨架先走一遍——一次 WebRTC 通话从无到有：建工厂、加轨道、createOffer 生成 SDP、交换、ICE 打通、DTLS 握手拿密钥、SRTP 媒体开跑。每一步都对应源码里一个具体的对象，这一章把它们串成一条线。

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

# 一次通话的一生 · 从 createOffer 到媒体流

有了[源码地图](https://daiw.org/manual/webrtc/source-map)，这一章把**动态过程**走一遍：一次通话从建立到媒体流动，**依次发生了什么、每一步由哪个对象负责**。这是整个教程的骨架——后面每个部分，都是在放大这条线上的某一个环节。

## PeerConnection 是个「上帝对象」

先认识主角。项目文档 `g3doc/peer_connection.md` 直言不讳：

> The PeerConnection is, to a large extent, a **"God object"** - most things that are done in WebRTC require a PeerConnection.

它几乎什么都管。但它并非一个巨型泥球，而是**把职责拆给一组各司其职的下属对象**，这些对象都由 `PeerConnection` 拥有、与它同生共死（源自 `g3doc/peer_connection.md`）：

| 下属对象 | 职责 | 本教程 |
| --- | --- | --- |
| `SdpOfferAnswerHandler` | 用 SDP 和对端**协商配置**（提议 / 应答） | 第 1 部分 |
| `RtpTransmissionManager` | 管理 `RtpSender` / `RtpReceiver` / `RtpTransceiver` 列表 | 第 1 部分 |
| `JsepTransportController` | 配置底层传输（ICE / DTLS）的细节 | 第 1–3 部分 |
| `DataChannelController` | 管理数据通道与 `SctpTransport` | 第 8 部分 |
| `Call` | 管理整体通话状态与媒体流 | 第 9 部分 |
| `RtcStatsCollector` | 按需从各对象收集统计 | 第 11 部分 |

记住这张表——下面的每一步，都会点到其中某个对象。

## 七步，从无到有

一次通话的建立，可以拆成七步。前三步是**应用代码主动调 API**，后四步是**库在底下自动完成**。

### 第 1 步：建工厂，建连接

一切从 `PeerConnectionFactory` 开始。它持有[三大线程](https://daiw.org/manual/webrtc/threading-model)和各种工厂，用一次、复用多次（文档建议**避免创建多个工厂**，费资源）。然后由它造出 `PeerConnection`：

```cpp
// 工厂造连接(简化):传入配置与依赖(含观察者)
RTCErrorOr<scoped_refptr<PeerConnectionInterface>> pc =
    factory->CreatePeerConnectionOrError(config, std::move(dependencies));
```

`PeerConnectionObserver`（依赖的一部分）是**回调入口**：ICE 候选、连接状态变化、远端轨道到达，都通过它通知应用。

### 第 2 步：加媒体

把要发送的音视频轨道挂上去，用 `AddTrack` 或 `AddTransceiver`（现代 Unified Plan 下更推荐后者）。这些方法真实存在于 `api/peer_connection_interface.h`：

```cpp
// api/peer_connection_interface.h(节选签名)
virtual RTCErrorOr<scoped_refptr<RtpSenderInterface>>
    AddTrack(scoped_refptr<MediaStreamTrackInterface> track, ...);
virtual RTCErrorOr<scoped_refptr<RtpTransceiverInterface>>
    AddTransceiver(scoped_refptr<MediaStreamTrackInterface> track, ...);
```

每个 transceiver 对应 SDP 里的一个「m-line」（媒体行），背后由 `RtpTransmissionManager` 记录。

### 第 3 步：createOffer，生成 SDP

调 `CreateOffer`，库会**审视当前有哪些轨道、支持哪些编解码器**，生成一段 **SDP（会话描述协议）** 文本——它列出「我要发什么媒体、用什么编解码器、我的 ICE 与 DTLS 参数是什么」：

```cpp
// api/peer_connection_interface.h:异步,结果回调给 observer
virtual void CreateOffer(CreateSessionDescriptionObserver* observer,
                         const RTCOfferAnswerOptions& options);
```

生成 SDP 的活，由 `SdpOfferAnswerHandler` 联合 `media_session.cc`（第 1 部分细讲）完成。拿到 offer 后，调 `SetLocalDescription` **把它设为本地描述**——这一步**同时触发 ICE 开始采集候选**。

### 第 4 步：信令交换（应用的活）

<Callout type="info">
  **WebRTC 不管信令。** 把本地的 SDP（以及随后陆续冒出的 ICE 候选）**传给对端**，是**应用自己的责任**——用 WebSocket、HTTP、甚至二维码都行。WebRTC 只负责「生成描述」和「消化描述」，中间那条传话的路，标准**故意不定义**，留给应用。所以你在任何 WebRTC 项目里，都会看到一个自建的「信令服务器」——它不是 WebRTC 的一部分。

  对端收到 offer 后，对称地做：`SetRemoteDescription(offer)` → `CreateAnswer` → `SetLocalDescription(answer)`，再把 answer 传回来。本端 `SetRemoteDescription(answer)`。至此**提议 / 应答（offer/answer）完成**，双方就「发什么、怎么发」达成一致。
</Callout>

### 第 5 步：ICE 打通一条路

SDP 交换的同时，双方的 ICE 引擎（`P2PTransportChannel`）在**并行**地干一件事：**在两个可能藏在 NAT 后的端点之间，找到一条能通的路**。它收集本地候选（主机地址、STUN 反射地址、TURN 中继地址），和对端候选**两两配对**，发 STUN 连通性检查探测，挑出一条最优通路。连通后，`PeerConnectionObserver` 收到连接状态变为 `connected`。这是[第 2 部分](https://daiw.org/manual/webrtc/source-map)整整一部分的内容。

### 第 6 步：DTLS 握手，拿到密钥

路通了，但还不能传媒体——**得先加密**。双方在这条 ICE 通路上做一次 **DTLS 握手**（`DtlsTransport`），彼此验证证书指纹（指纹已写在 SDP 里，防中间人），并**协商出一组密钥**。这组密钥不直接加密媒体，而是用来**导出 SRTP 的密钥**（DTLS-SRTP）。第 3 部分主角。

### 第 7 步：SRTP 媒体开跑

密钥就位，媒体终于可以流动了。发送侧：编码器出帧 → [RTP 打包](https://daiw.org/manual/webrtc/source-map) → **SRTP 加密** → 经 ICE 选定的通路发出。接收侧反向：收包 → SRTP 解密 → 抖动缓冲 → 解码 → 渲染。与此同时，**RTCP** 反馈在双向流动——报告丢了哪些包、估计的带宽是多少、时间戳如何对齐，[拥塞控制](https://daiw.org/manual/webrtc/source-map)据此实时调整码率。通话，就此建立。

## 一张时序图

把七步画成时序，更直观（左右各一个端点，中间的信令服务器是**应用自建**的）：

```mermaid
sequenceDiagram
    participant A as 端点 A
    participant S as 信令服务器(应用自建)
    participant B as 端点 B
    A->>A: 建工厂 · 建 PeerConnection · 加轨道
    A->>A: createOffer → setLocalDescription
    A->>S: 发送 offer(SDP)
    S->>B: 转交 offer
    B->>B: setRemoteDescription → createAnswer → setLocalDescription
    B->>S: 发送 answer(SDP)
    S->>A: 转交 answer
    A->>A: setRemoteDescription
    par ICE 连通性检查(并行进行)
        A-->>B: STUN 探测(候选两两配对)
        B-->>A: STUN 响应 → 选定通路
    end
    A->>B: DTLS 握手(验证指纹,协商密钥)
    B->>A: DTLS 完成 → 导出 SRTP 密钥
    A->>B: SRTP 加密的 RTP 媒体流
    B->>A: RTP 媒体流 + RTCP 反馈(双向)
```

## 这条线，就是整个教程

回头看，这七步几乎**一一对应**教程的各个部分：会话协商（第 1）、ICE（第 2）、DTLS/SRTP（第 3）、RTP/RTCP（第 4）、拥塞控制（第 5）、以及媒体两端的视频 / 音频管线（第 6/7）、数据通道（第 8）。**每个部分，都是把这条线上的一个环节，放大到源码级去看。** 这一章是导航图，后面是逐段的实地勘探。

## 小结与下一章

一次通话七步走：**建工厂 / 连接 → 加媒体 → createOffer 生成 SDP → 信令交换（应用负责）→ ICE 打通路 → DTLS 握手拿密钥 → SRTP 媒体开跑**。`PeerConnection` 是「上帝对象」，把职责拆给 `SdpOfferAnswerHandler`（协商）、`RtpTransmissionManager`（收发器）、`JsepTransportController`（传输）、`DataChannelController`（数据）、`Call`（媒体流）、`RtcStatsCollector`（统计）。信令**不属于** WebRTC。

这套协作跑在多线程上，而正确性高度依赖「谁在哪个线程上跑」。下一章拆开 WebRTC 的**三大线程模型**——不懂它，读任何模块都会困惑。👉 [三大线程模型](https://daiw.org/manual/webrtc/threading-model)。

## 参考资料

- [1] libwebrtc 源码.`g3doc/peer_connection.md`（「上帝对象」与下属对象分工）。
- [2] libwebrtc 源码.`api/peer_connection_interface.h`（`CreateOffer` / `AddTrack` / `AddTransceiver` 等签名）。
- [3] IETF RFC 8829「JavaScript Session Establishment Protocol （JSEP）」—— 提议 / 应答模型。
