# 收发器 · RtpTransceiver 与 Unified Plan

> Unified Plan 下，一个 RtpTransceiver 就是一条 m-line、一路双向媒体，把一个发送器和一个接收器捆在一起。这一章读 pc/rtp_transceiver.cc：方向枚举（sendrecv/sendonly/…）、desired 与 current 两种方向的区别、mid 如何绑定 m-line，以及 RtpTransmissionManager 怎么统管这些收发器。

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

# 收发器 · RtpTransceiver 与 Unified Plan

[媒体协商](https://daiw.org/manual/webrtc/media-session)谈出了「每路媒体用什么编解码器、朝哪个方向收发」。承载这「一路媒体」的核心抽象，是 **`RtpTransceiver`（收发器）**——现代 WebRTC「Unified Plan」模型的基石。这一章读 `pc/rtp_transceiver.cc`，搞懂它和它管的 `RtpSender` / `RtpReceiver`。

## Unified Plan：一条 m-line，一个收发器

先讲模型。WebRTC 历史上有过两种把「轨道」映射到「[SDP m-line](https://daiw.org/manual/webrtc/sdp-and-jsep)」的方案，如今标准是 **Unified Plan**:

<Callout type="info">
  **Unified Plan：一个 `RtpTransceiver` ⟷ 一条 m-line ⟷ 一路双向媒体。** 每个收发器**捆绑一个 `RtpSender`（发什么）和一个 `RtpReceiver`（收什么）**，对应 SDP 里的一条媒体行。要发一路视频 + 收一路视频，就是一个视频收发器；再加一路音频，就再加一个音频收发器。**收发器数量 = m-line 数量**，一一对应，清清爽爽。

  （对比早已废弃的 Plan B：它把同类型的多路轨道塞进**一条** m-line，靠 SSRC 区分，协商起来别扭。Unified Plan 是 W3C 规范采纳的方案，`RtpTransceiver` 正是它的 C++ 落地。）
</Callout>

`pc/rtp_transceiver.h` 里，`RtpTransceiver` 的接口清楚地体现了这个「捆绑」：

```cpp
// pc/rtp_transceiver.h(节选)
class RtpTransceiver : public RtpTransceiverInterface {
  std::optional<std::string> mid() const override;                 // 绑定的 m-line id
  scoped_refptr<RtpSenderInterface> sender() const override;       // 发送器
  scoped_refptr<RtpReceiverInterface> receiver() const override;   // 接收器
  RtpTransceiverDirection direction() const override;              // 期望方向
  // ...
};
```

- **`mid()`** —— media ID，把这个收发器**绑定到 SDP 的某条 m-line**（SDP 里的 `a=mid:` 就是它）。协商后才确定。
- **`sender()` / `receiver()`** —— 这一路的发送器和接收器。[下一批](https://daiw.org/manual/webrtc/transceivers)会细看 `RtpSender`（挂着要发的轨道 + 编码参数）和 `RtpReceiver`（吐出收到的远端轨道）。

## 方向：五种取值

一路媒体是发、是收、还是双向？由 `RtpTransceiverDirection` 决定。它的五个真实取值（`api/rtp_transceiver_direction.h`）：

```cpp
// api/rtp_transceiver_direction.h
enum class RtpTransceiverDirection {
  kSendRecv,   // 双向:既发又收
  kSendOnly,   // 只发
  kRecvOnly,   // 只收
  kInactive,   // 不发不收(占位)
  kStopped,    // 已停止(收发器废弃)
};
```

前四个**直接对应 [SDP 里的 `a=sendrecv` / `a=sendonly` / `a=recvonly` / `a=inactive`](https://daiw.org/manual/webrtc/sdp-and-jsep)**——方向的协商，就是这个枚举和 SDP 属性的互译。`kStopped` 则表示这个收发器彻底停用（对应 m-line 端口置 0）。

## 期望方向 vs 协商方向：一个关键区分

`RtpTransceiver` 有**两个**方向，新手极易混淆，但源码把它们分得很清（`pc/rtp_transceiver.h`）：

<Callout type="info">
  **`direction()` 是「你想要的」，`current_direction()` 是「协商后实际生效的」——两者可能不同。**

  - **`direction()`(desired)** —— 应用**设定的期望**。你调 `SetDirection(kSendRecv)` 说「我想双向」，它立刻改变，但**还没生效**。
  - **`current_direction()`(current)** —— 一轮 [offer/answer 协商](https://daiw.org/manual/webrtc/offer-answer-handler)**完成后**，双方实际达成的方向。只有它才代表媒体真正在怎么流。
  - 还有个 **`fired_direction()`** —— 上一次触发给事件的方向，用于正确地激发 `track` 事件。

  为什么要分？因为方向是**要双方同意的**。你想 `kSendRecv`，但对端只肯 `kRecvOnly`（它不发），协商后 `current_direction()` 就会是 `kSendOnly`（你发它收）。**「我想怎样」和「最终怎样」在分布式协商里天然是两回事**——源码用两个字段如实建模。改了 `direction()` 后，必须再跑一轮协商，`current_direction()` 才会跟上。这也是为什么改方向要触发[重新协商（renegotiation）](https://daiw.org/manual/webrtc/offer-answer-handler)。
</Callout>

## RtpTransmissionManager：统管收发器

单个收发器讲完，谁来**管理这一堆**收发器 / 发送器 / 接收器？就是 [`PeerConnection` 的 `rtp_manager_` 成员](https://daiw.org/manual/webrtc/peer-connection-object)——**`RtpTransmissionManager`**（`pc/rtp_transmission_manager.cc`）。它持有当前所有的收发器列表，并落地那两个 API：

- **`AddTrack(track)`** —— 找一个合适的、还没绑轨道的收发器复用，或**新建**一个收发器来发这条轨道，返回它的 `RtpSender`。
- **`AddTransceiver(...)`** —— 直接**新建**一个收发器（可以只收不发，常用于「我准备好接收」的场景）。

[协商时](https://daiw.org/manual/webrtc/offer-answer-handler)，`SdpOfferAnswerHandler` 就是遍历 `RtpTransmissionManager` 里的收发器，把每个映射成一条 m-line 生成 SDP；反过来收到对端 SDP，又据此增删 / 匹配收发器。**收发器是「应用的轨道」与「SDP 的 m-line」之间的枢纽**。

```mermaid
flowchart TD
    RM["RtpTransmissionManager(rtp_manager_)"]
    RM --> T1["RtpTransceiver #1(mid=0)<br/>direction: sendrecv"]
    RM --> T2["RtpTransceiver #2(mid=1)<br/>direction: recvonly"]
    T1 --> S1["RtpSender(发:本地轨道 + 编码参数)"]
    T1 --> R1["RtpReceiver(收:远端轨道)"]
    T2 --> R2["RtpReceiver(收:远端轨道)"]
    T1 -.对应.-> M1["SDP m-line #0"]
    T2 -.对应.-> M2["SDP m-line #1"]
```

## 小结与下一章

**`RtpTransceiver` 是 Unified Plan 的基石**：一个收发器 = 一条 [m-line](https://daiw.org/manual/webrtc/sdp-and-jsep) = 一路双向媒体，捆绑一个 `RtpSender` + 一个 `RtpReceiver`，由 `mid()` 绑定到 m-line。方向 `RtpTransceiverDirection` 五值（sendrecv/sendonly/recvonly/inactive/stopped）直译 SDP 方向属性；**`direction()`（期望）与 `current_direction()`（协商后实际）是两回事**，改方向要重新协商。[`RtpTransmissionManager`](https://daiw.org/manual/webrtc/peer-connection-object)（`rtp_manager_`）统管所有收发器，落地 `AddTrack` / `AddTransceiver`，是「轨道」与「m-line」的枢纽。

会话的**媒体面**协商清楚了。但协商出的 ICE 凭据、DTLS 参数、BUNDLE 分组，要真正落到底层传输上——这是 [`transport_controller_`](https://daiw.org/manual/webrtc/peer-connection-object) 即 `JsepTransportController` 的活。下一章读它，以及 BUNDLE 怎么让多路媒体共用一条传输。👉 [JsepTransportController 与 BUNDLE](https://daiw.org/manual/webrtc/jsep-transport-controller)。

## 参考资料

- [1] libwebrtc 源码.`pc/rtp_transceiver.h` / `.cc`（`RtpTransceiver`、`direction` / `current_direction`、`mid` / `sender` / `receiver`）。
- [2] libwebrtc 源码.`api/rtp_transceiver_direction.h`（方向枚举）、`pc/rtp_transmission_manager.h`（收发器统管）。
- [3] IETF RFC 8829「JSEP」、W3C webrtc-pc —— RtpTransceiver 与 Unified Plan 模型。
