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