一次通话的一生 · 从 createOffer 到媒体流
把整个教程的骨架先走一遍——一次 WebRTC 通话从无到有:建工厂、加轨道、createOffer 生成 SDP、交换、ICE 打通、DTLS 握手拿密钥、SRTP 媒体开跑。每一步都对应源码里一个具体的对象,这一章把它们串成一条线。
一次通话的一生 · 从 createOffer 到媒体流
有了源码地图,这一章把动态过程走一遍:一次通话从建立到媒体流动,依次发生了什么、每一步由哪个对象负责。这是整个教程的骨架——后面每个部分,都是在放大这条线上的某一个环节。
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 开始。它持有三大线程和各种工厂,用一次、复用多次(文档建议避免创建多个工厂,费资源)。然后由它造出 PeerConnection:
// 工厂造连接(简化):传入配置与依赖(含观察者)
RTCErrorOr<scoped_refptr<PeerConnectionInterface>> pc =
factory->CreatePeerConnectionOrError(config, std::move(dependencies));PeerConnectionObserver(依赖的一部分)是回调入口:ICE 候选、连接状态变化、远端轨道到达,都通过它通知应用。
第 2 步:加媒体
把要发送的音视频轨道挂上去,用 AddTrack 或 AddTransceiver(现代 Unified Plan 下更推荐后者)。这些方法真实存在于 api/peer_connection_interface.h:
// 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 参数是什么」:
// api/peer_connection_interface.h:异步,结果回调给 observer
virtual void CreateOffer(CreateSessionDescriptionObserver* observer,
const RTCOfferAnswerOptions& options);生成 SDP 的活,由 SdpOfferAnswerHandler 联合 media_session.cc(第 1 部分细讲)完成。拿到 offer 后,调 SetLocalDescription 把它设为本地描述——这一步同时触发 ICE 开始采集候选。
第 4 步:信令交换(应用的活)
WebRTC 不管信令。 把本地的 SDP(以及随后陆续冒出的 ICE 候选)传给对端,是应用自己的责任——用 WebSocket、HTTP、甚至二维码都行。WebRTC 只负责「生成描述」和「消化描述」,中间那条传话的路,标准故意不定义,留给应用。所以你在任何 WebRTC 项目里,都会看到一个自建的「信令服务器」——它不是 WebRTC 的一部分。
对端收到 offer 后,对称地做:SetRemoteDescription(offer) → CreateAnswer → SetLocalDescription(answer),再把 answer 传回来。本端 SetRemoteDescription(answer)。至此提议 / 应答(offer/answer)完成,双方就「发什么、怎么发」达成一致。
第 5 步:ICE 打通一条路
SDP 交换的同时,双方的 ICE 引擎(P2PTransportChannel)在并行地干一件事:在两个可能藏在 NAT 后的端点之间,找到一条能通的路。它收集本地候选(主机地址、STUN 反射地址、TURN 中继地址),和对端候选两两配对,发 STUN 连通性检查探测,挑出一条最优通路。连通后,PeerConnectionObserver 收到连接状态变为 connected。这是第 2 部分整整一部分的内容。
第 6 步:DTLS 握手,拿到密钥
路通了,但还不能传媒体——得先加密。双方在这条 ICE 通路上做一次 DTLS 握手(DtlsTransport),彼此验证证书指纹(指纹已写在 SDP 里,防中间人),并协商出一组密钥。这组密钥不直接加密媒体,而是用来导出 SRTP 的密钥(DTLS-SRTP)。第 3 部分主角。
第 7 步:SRTP 媒体开跑
密钥就位,媒体终于可以流动了。发送侧:编码器出帧 → RTP 打包 → SRTP 加密 → 经 ICE 选定的通路发出。接收侧反向:收包 → SRTP 解密 → 抖动缓冲 → 解码 → 渲染。与此同时,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 的三大线程模型——不懂它,读任何模块都会困惑。👉 三大线程模型。
参考资料
- [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)」—— 提议 / 应答模型。