收发器 · RtpTransceiver 与 Unified Plan

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

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

收发器 · RtpTransceiver 与 Unified Plan

媒体协商谈出了「每路媒体用什么编解码器、朝哪个方向收发」。承载这「一路媒体」的核心抽象,是 RtpTransceiver(收发器)——现代 WebRTC「Unified Plan」模型的基石。这一章读 pc/rtp_transceiver.cc,搞懂它和它管的 RtpSender / RtpReceiver。

Unified Plan:一条 m-line,一个收发器

先讲模型。WebRTC 历史上有过两种把「轨道」映射到「SDP m-line」的方案,如今标准是 Unified Plan:

Unified Plan:一个 RtpTransceiver ⟷ 一条 m-line ⟷ 一路双向媒体。 每个收发器捆绑一个 RtpSender(发什么)和一个 RtpReceiver(收什么),对应 SDP 里的一条媒体行。要发一路视频 + 收一路视频,就是一个视频收发器;再加一路音频,就再加一个音频收发器。收发器数量 = m-line 数量,一一对应,清清爽爽。

(对比早已废弃的 Plan B:它把同类型的多路轨道塞进一条 m-line,靠 SSRC 区分,协商起来别扭。Unified Plan 是 W3C 规范采纳的方案,RtpTransceiver 正是它的 C++ 落地。)

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

// 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() —— 这一路的发送器和接收器。下一批会细看 RtpSender(挂着要发的轨道 + 编码参数)和 RtpReceiver(吐出收到的远端轨道)。

方向:五种取值

一路媒体是发、是收、还是双向?由 RtpTransceiverDirection 决定。它的五个真实取值(api/rtp_transceiver_direction.h):

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

前四个直接对应 SDP 里的 a=sendrecv / a=sendonly / a=recvonly / a=inactive——方向的协商,就是这个枚举和 SDP 属性的互译。kStopped 则表示这个收发器彻底停用(对应 m-line 端口置 0)。

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

RtpTransceiver 有两个方向,新手极易混淆,但源码把它们分得很清(pc/rtp_transceiver.h):

direction() 是「你想要的」,current_direction() 是「协商后实际生效的」——两者可能不同。

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

为什么要分?因为方向是要双方同意的。你想 kSendRecv,但对端只肯 kRecvOnly(它不发),协商后 current_direction() 就会是 kSendOnly(你发它收)。「我想怎样」和「最终怎样」在分布式协商里天然是两回事——源码用两个字段如实建模。改了 direction() 后,必须再跑一轮协商,current_direction() 才会跟上。这也是为什么改方向要触发重新协商(renegotiation)。

RtpTransmissionManager:统管收发器

单个收发器讲完,谁来管理这一堆收发器 / 发送器 / 接收器?就是 PeerConnection 的 rtp_manager_ 成员——RtpTransmissionManager(pc/rtp_transmission_manager.cc)。它持有当前所有的收发器列表,并落地那两个 API:

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

协商时,SdpOfferAnswerHandler 就是遍历 RtpTransmissionManager 里的收发器,把每个映射成一条 m-line 生成 SDP;反过来收到对端 SDP,又据此增删 / 匹配收发器。收发器是「应用的轨道」与「SDP 的 m-line」之间的枢纽。

图表加载中…

小结与下一章

RtpTransceiver 是 Unified Plan 的基石:一个收发器 = 一条 m-line = 一路双向媒体,捆绑一个 RtpSender + 一个 RtpReceiver,由 mid() 绑定到 m-line。方向 RtpTransceiverDirection 五值(sendrecv/sendonly/recvonly/inactive/stopped)直译 SDP 方向属性;direction()(期望)与 current_direction()(协商后实际)是两回事,改方向要重新协商。RtpTransmissionManager(rtp_manager_)统管所有收发器,落地 AddTrack / AddTransceiver,是「轨道」与「m-line」的枢纽。

会话的媒体面协商清楚了。但协商出的 ICE 凭据、DTLS 参数、BUNDLE 分组,要真正落到底层传输上——这是 transport_controller_ 即 JsepTransportController 的活。下一章读它,以及 BUNDLE 怎么让多路媒体共用一条传输。👉 JsepTransportController 与 BUNDLE。

参考资料

  • [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 模型。

本页目录