WebRTC 的源码架构是那种你读十遍都会觉得自己才刚开始懂的东西。用 API 写一个视频通话 Demo 很容易,但老手和新人的差距,往往就体现在问题发生的那一刻,你能不能准确说出“这一层到底发生了什么”。这篇文章我不会带你一行行念代码,而是把 WebRTC 源码当成一张地图,从整体分层、数据流、线程模型、编译调试到问题排查,讲清楚架构设计背后的逻辑,以及我实际读源码、改源码时踩过的坑。如果你正准备啃 WebRTC 源码,或者已经读过一部分但感觉一团乱麻,这篇文章应该能帮你把骨架搭起来。
1. 源码整体分层与模块地图
WebRTC 源码的规模摆在那里,一上来就扎进某个.cc文件里是愚蠢的。我自己的习惯是,先搞清楚“谁依赖谁”,把层次画出来,再挑感兴趣的链路往下钻。整个工程的模块关系,本质上可以压缩成一句话:底层提供能力,上层组织调度,外部只碰一个门面。
1.1 底层能力:线程、网络与基础工具
先看最底下的一层,这是整个 WebRTC 能跑起来的地基。源码里的rtc_base目录承担了这个角色。它提供的不只是std::string这种工具函数,而是整个运行时的骨骼——线程、锁、网络套接字封装、时间戳、随机数、日志、JSON 解析等等。
很多人忽略这一点,直接去看音视频引擎的代码,然后被各种PostTask和线程跳转搞得晕头转向。我的建议是,先读rtc_base里的thread和network相关代码,把一个 WebRTC 线程到底长什么样搞清楚,再往上层走。你在pc、call、media这些目录里看到的线程跳转,底层几乎都是rtc::Thread::PostTask那一套。掌握了这个基础,后面看任何模块都会顺畅很多。
1.2 核心引擎:媒体、传输与信令的边界
再往上一层,是media和modules这两个目录。media主要定义媒体流的抽象类型,比如音频源、视频源、轨道这些概念;modules才是真正的引擎所在地。
modules/audio_coding管音频的编解码和网络适配,modules/video_coding管视频的编解码、抖动缓冲和丢包处理,modules/audio_processing管回声消除、降噪、自动增益这些前处理,modules/pacing管发送节奏的控制,modules/rtp_rtcp管 RTP/RTCP 协议的实现。这些模块之间基本没有互相依赖的关系,它们各自独立工作,只通过上层的call层进行协作。
传输相关的代码在p2p和pc目录里。p2p实现了 ICE 代理、STUN/TURN 客户端这些连接建立的底层逻辑;pc目录则是 PeerConnection 的真正实现所在,它把媒体引擎和传输层串起来,向上层暴露统一接口。信令的部分在pc里的sdp相关代码中,负责解析和生成 SDP 描述。
1.3 门面层:PeerConnection 与 PeerConnectionFactory
最上面的门面是api目录里的PeerConnectionFactoryInterface和PeerConnectionInterface。你写应用时调用的所有 WebRTC API,本质上都是围绕这两个抽象展开的。
PeerConnectionFactory 负责工厂化地创建各种对象——音频源、视频源、编解码器、PeerConnection 实例等等。它需要你把网络线程、worker 线程、信令线程的配置传进去,这也是 WebRTC “线程模型先行” 的一个明显信号。PeerConnection 则负责维护端到端的多媒体会话——处理 offer/answer、管理 ICE 状态机、维护媒体发送和接收通道。整个源码架构里,你几乎可以找到所有功能模块最终汇入 PeerConnection 的痕迹。
这四个层级的关系,我用一个简单的文本图来表示(WebRTC 源码里没有这种“架构图”文件,但读一段时间源码后,你脑子里自然会长出这个东西):
应用层 API (addTrack / createOffer / onicecandidate ...) | 会话层 PeerConnection (pc/ 目录) | 呼叫与媒体控制层 (call/ 目录) | 传输层: ICE / DTLS / SRTP (p2p/, transport/) 媒体层: 采集/编码/RTP/抖动缓冲/解码 (modules/, media/) | 基础层: 线程/网络套接字/工具库 (rtc_base/)理解了这张图之后,再回头读任何一段源码,你都会很清楚它所在的位置和它需要向谁负责。
2. 核心模块的职责拆解与关键源码路径
分层只是第一步,真正让你在源码里“找得到北”的,是把关键模块接下来对号入座。我挑几个对理解整个架构最有帮助的模块,拆一下它们各自干了什么。
2.1 PeerConnection:整个会话的调度中枢
在pc/peer_connection.cc里,你可以看到这个“中枢”是怎么统筹全局的。它维护的成员很多,但核心职责可以概括成三块:媒体协商、连接状态管理、数据流转发。
媒体协商是 offer/answer 的 SDP 交换过程。你会在源码里看到CreateOffer/SetLocalDescription/SetRemoteDescription这一串调用,它们最终会走到pc/sdp_offer_answer.cc里的协商逻辑。这里的核心是SdpOfferAnswerHandler类。WebRTC 把信令协商的复杂度全部收敛到这个类里面——它的职责是把你传进去的 SDP 解析成内部描述结构,和本地能力做比对,最终决定使用哪些编解码器、哪些网络参数。
连接状态管理则是 ICE 状态机。PeerConnection内部聚合了一个JsepTransportController,它管理着 DTLS 传输、ICE 传输链路。当你看到oniceconnectionstatechange回调触发时,背后就是这条链路的某个状态字段发生了变化,然后把事件抛给PeerConnection的观察者。
数据流转发则包括音频、视频轨道的数据走向和接收。PeerConnection把从应用层传入的MediaStreamTrackInterface绑定到发送通道(sender)上,并且把接收到的远端轨道通过OnTrack回调抛给应用层。在源码里,你会看到它维护着一个发送者和接收者的集合,每种媒体类型有一套独立的通道机制。
2.2 音频引擎:不那么显眼但绝不简单
视频的曝光度高,但音频引擎在源码里的复杂度一点不输视频。核心在modules/audio_coding目录。
音频发送端的主要链路是:麦克风采集 → 音频前处理(AudioProcessing)→ 编码(AudioEncoder)→ 封装 RTP 包 → 交给网络发送。这里面你会在AudioProcessing类里看到回声消除、降噪、自动增益等一组子模块。比较关键的一个点是:回声消除必须在参考信号和采集信号之间做一个时间对齐,这个对齐逻辑在modules/audio_processing里有一套非常精细的延迟估计器。如果不理解这一块,以后走查音频相关问题时基本无从下手。
音频接收端的主要链路是:收到 RTP 包 → NetEQ(网络抖动缓冲与解码器)→ 播放。modules/audio_coding/neteq是音频接收侧的灵魂所在,它同时兼顾了抖动缓冲、丢包隐藏、解码重采样等任务。你在音频通话里遇到的声音断续问题、音调变化问题,根源几乎都在 NetEQ 的参数配置和状态机行为上。
2.3 视频引擎:编码器管理、发送节奏与接收缓冲
视频引擎在modules/video_coding里,核心链路相对直观:采集 → 编码 → 发送;接收 → 解码 → 渲染。
发送侧最值得关注的类是VideoStreamEncoder。它负责维护一组编码器,根据网络状况动态调整目标码率和分辨率。源码里的VideoStreamEncoder::OnBitrateChanged就是码率自适应策略的实际执行入口。另一个比较核心的类是PacedSender(位于modules/pacing),它的任务是“平滑发送”——避免突发流量打爆网络。你会在里面看到 token bucket 算法的实现,以及优先级队列的设计。如果不做平滑发送,视频帧往往非常大,突发发送会带来严重的网络抖动和丢包。
接收侧的核心是FrameBuffer(位于modules/video_coding/frame_buffer2),它负责对乱序到达的帧进行重排,等待可解码帧完整后交给解码器。这里有一个关键设计:WebRTC 不是逐帧直接解码,而是基于帧组(frame group)的完整性来调度解码。当关键帧前的某帧缺失时,FrameBuffer 会等待或请求关键帧,而不是硬着头皮解码出残缺画面。理解这一点,你能解释很多“视频卡在首帧黑屏”的现象。
2.4 实时传输:RTP/RTCP、ICE、DTLS 的分工
传输层在 WebRTC 架构里是相对独立的。RTP/RTCP 的实现在modules/rtp_rtcp,ICE 和 DTLS 的实现在p2p/目录。它们之间的协作关系是:RTP 负责媒体数据,RTCP 负责控制信息(如丢包反馈、接收质量统计),ICE 负责候选路径的发现与连通性检测,DTLS 则负责把传输变成加密安全的通道。
读这一块源码时,一个容易绕晕的地方是SRTP(安全实时传输协议)和 DTLS 之间的关系。简单说,DTLS 握手成功后生成 SRTP 密钥材料,之后所有 RTP/RTCP 包都通过 SRTP 加密。在源码里,这个逻辑在pc/dtls_transport.cc里能看得很清楚:它持有SrtpTransport的实例,握手完成后去做SetRtpParams。理解了这条链,你就明白为什么 WebRTC 不单独支持“RTMP 那种明文传输”——安全层是内建的,不是后来附加的。
3. 数据流视角:从采集到渲染的完整路线
光看模块地图,你看的是静态结构。真正让你把架构“读活”的,是一条数据的流动路线。我拿视频链路举例,从摄像头到对端屏幕,走一遍源码路径,你就能把前面拆开的模块串起来。
3.1 发送链路:采集、前处理与编码调度
视频采集走的是设备抽象层。在桌面端,采集实现一般在modules/video_capture,它通过操作系统提供的摄像头接口拿到原始帧。然后,帧被送进一个叫VideoBroadcaster的门面分发器——器如其名,它把每一帧同时广播给所有订阅者(通常是编码器,也可能是本地预览)。
编码器入口在VideoStreamEncoder。这里接收的是未压缩帧,它会做三件事:其一,根据当前 CPU 负载做帧率降级(Adaptation);其二,根据码率控制目标做质量降级;其三,把帧交给具体的VideoEncoder实现进行编码。这里有个容易误解的地方:分辨率降级并不会把帧缩小再送进编码器,而是通过请求采集端改变采集分辨率,或跳过部分帧来实现。源码里的VideoStreamEncoder::OnFrame能看到这套判断逻辑。
编码后的字节流被包装成 RTP 包,交给RtpSenderEgress(位于modules/rtp_rtcp)进行最终发送。但要注意,这里有一个PacedSender在中间把关。PacedSender维护一个发送队列,按照设定好的节流参数(比如网络估计出的发送带宽)决定“每 5 毫秒最多发多少字节”。如果中间没有这一层,一帧 1080p 的数据会一次性冲出去,网络直接抖动成心脏病。
3.2 接收链路:抖动缓冲、重排序与解码渲染
接收侧收到 RTP 包后,第一站是RtpReceiver。它把包拆开,根据负载类型找到对应的接收通道。视频包进入FrameBuffer(帧缓冲),音频包进入 NetEQ。
FrameBuffer的核心工作是维护一个待处理的帧集合。它会做重排序、丢包检测和帧完整性判断。对视频来说,因为存在帧间参考关系(P 帧依赖前面的帧),WebRTC 不会把单独一个帧立即丢给解码器,而是等“组成一个可解码的最小结构”再下发。这个最小结构在源码里叫DecodableFrameInfo。如果一直等不到关键帧,帧缓冲会触发关键帧请求——通过发送 PLI(视频丢包指示)消息让对端重新发关键帧。这条路径在modules/video_coding/frame_buffer2和rtcp_receiver之间能串起来。
解码完成后的帧通过回调交给渲染层。在media/base/video_broadcaster和video_renderer相关代码中,你会看到一个非常“生产环境”的设计:渲染器是推模式还是拉模式取决于实现。有的平台希望 WebRTC 主动送帧,有的平台则希望 WebRTC 等它来取。这个抽象在VideoSinkInterface里定义得非常清晰——任何对象只要实现OnFrame方法,就可以作为视频帧的消费者。
3.3 一条链路的完整源码文件清单
为了方便你按图索骥,我把上面这条视频发送链路涉及的源码文件列成一个常用清单。你在读源码时,沿着这个清单往下走,基本能把整条链路走完:
| 链路阶段 | 源码路径(常见位置,具体以你使用的分支为准) | 核心类/文件 |
|---|---|---|
| 设备采集 | modules/video_capture/ | VideoCaptureAdapter |
| 帧分发 | media/base/video_broadcaster.cc | VideoBroadcaster |
| 流编码调度 | modules/video_coding/video_stream_encoder.cc | VideoStreamEncoder |
| 码率控制 | call/rtp_video_sender.cc | RtpVideoSender |
| 平滑发送 | modules/pacing/paced_sender.cc | PacedSender |
| RTP打包发送 | modules/rtp_rtcp/source/rtp_sender_egress.cc | RtpSenderEgress |
| 网络传输 | p2p/base/basic_ice_agent.cc | BasicIceAgent |
| 接收分发 | modules/rtp_rtcp/source/rtp_receiver.cc | RtpReceiver |
| 视频帧缓冲 | modules/video_coding/frame_buffer2.cc | FrameBuffer |
| 解码后渲染 | media/base/video_broadcaster.cc | VideoBroadcaster |
注意:不同分支下目录结构会有调整,但核心类的名字基本稳定。建议先锁定一个 release 分支阅读,避免 master 上频繁变动带来的干扰。
4. 线程模型与任务调度机制
不夸张地说,WebRTC 源码里 70% 的“看不懂”和“改崩了”,都源于对线程模型的不了解。它是你绕不过去的一关,而且一旦跨过去,后面读代码的速度会提升很多。
4.1 三大线程:信令线程、工作线程、网络线程
WebRTC 内部明确规定了三条线程的角色,源码里对应的就是你在 API 层传入的那三个线程参数。
信令线程(signaling thread)负责处理所有与 PeerConnection 状态机相关的操作,比如CreateOffer、SetLocalDescription、触发OnIceCandidate回调。它的核心要求是“快速返回,不要阻塞”——任何耗时操作都不能直接站在信令线程上做,否则整个会话控制就会被卡住。你在源码的pc/peer_connection.cc里会发现大量if (!thread_->IsCurrent())之类的检查,目的就是保证所有状态修改都发生在正确的线程上。
工作线程(worker thread)承担媒体处理的繁重任务。编码器初始化、帧率适应、发送带宽估计投票等都在这个线程上执行。call/目录下的Call类就运行在 worker 线程上。这里有个实际遇到的问题:如果你在回调里做了太重的 UI 操作,会导致媒体处理变慢,进而影响编码帧率——因为 worker 线程被占用了。
网络线程(network thread)处理所有 I/O:UDP 收发、ICE 连接性检测、STUN 请求、RTP 包的最终发送。它也是三条线程里最敏感的:任何在拼包(比如重组分片包)环节的阻塞,都会直接表现为网络丢包和抖动。源码里你可以看到一个类叫NetworkThread,它内部维护一个 SocketServer 来管理所有 FD。这层的实现贴近操作系统的异步 I/O 模型,读起来会稍微费劲,但它的价值在于理解“为什么网络相关代码如此强调不要阻塞”。
4.2 通过 PostTask 理解任务跨线程调度
跨线程调度是源码阅读必须掌握的手段,核心工具是rtc::Thread::PostTask或rtc::Thread::Invoke。前者是异步投递,后者是阻塞同步执行(执行完才返回)。代码里能频繁看到这种模式:
worker_thread_->PostTask([encoder = std::move(encoder)]() { // 在 worker 线程上做编码器创建工作 });这种写法的好处有两个:一是天然解决了数据竞争的很多问题——不同线程不会同时访问同一结构;二是让代码的意图非常明确——看到这段代码,你就知道当前上下文不行,必须跳到 worker 线程里去操作那个对象。
但代价也很明显:追踪调用栈变得非常困难。你看到一个对象的方法,但要知道它当前跑在哪个线程,本身就需要很强的上下文关联能力。我读源码时常用的一个方法是:看到PostTask就记录“从哪来的,去哪的”,一遍下来基本能在脑子里画出线程间通信的路径图。源码里也有一个辅助检查,用RTC_DCHECK_RUN_ON(thread)在 debug 模式下校验当前线程。你可以用这个宏反向确定一段逻辑应该运行在哪个线程。
4.3 锁的粒度:为什么不能全局一把锁
刚读源码时,看到一个对象内部既有锁又有 PostTask,那你可能会觉得“这不重复吗”?其实它们的适用场景不同。PostTask 解决的是“换线程执行”,而锁解决的是“同一个对象在不同线程上被同时访问”,两条路径是互补的。
WebRTC 的设计原则是:尽量通过线程模型来避免锁,而不是用锁来硬撑。所有对象先确定归属线程,由归属线程执行,这样很多成员变量根本不需要加锁。只有少数必须跨线程访问的场景(比如统计信息的读取、状态标志的查询)才使用锁,而且会把锁的粒度控制得很小。你如果自己在改 WebRTC 或者二次开发,记住这条经验:先想清楚哪个线程该访问这个对象,如果用锁才能修好,大概率设计上就是不合理的。
5. 源码级编译调试与辅助工具链
读源码最痛苦的不是读不懂,而是验证不了。看文档百遍不如自己改一行代码跑一遍。WebRTC 的源码编译体系比较厚重,但规则是死的,按流程走很快就能上手。
5.1 构建系统配置与关键参数
WebRTC 使用 GN 管理构建配置,配合 Ninja 执行实际编译。为了快速迭代验证,我一般会编译一个精简目标,而不是整个工程全量编译。如果你主要关注音频视频链路,可以只编译peerconnection_client或者rtc_unittests这些测试程序。
构建之前需要补一下参数。is_debug决定是否是调试版本,is_asan可以在内存问题排查时开启地址消毒器,rtc_use_h264决定是否编入 H.264 支持。如果你只做架构阅读和模块验证,强烈建议用调试版本,并且开启rtc_enable_protobuf和rtc_include_tests,方便后续跑单元测试。
提示:WebRTC 工程庞大,首次编译花的时间很长。建议在
args.gn里只开测试目标,或者用--rtc_dcheck_always_on这样的参数做快速验证,避免每次改动都要跑整个构建图。
5.2 日志系统:WebRTC 的“探头”
WebRTC 自带一套日志系统,比你在代码里随便加printf要强大得多。核心宏是RTC_LOG,支持从LS_SENSITIVE到LS_ERROR的分级输出,可以按文件、模块或严重程度进行过滤。
我排查架构问题时最常用的是这条命令:
./out/debug/peerconnection_client --log-file=/tmp/webrtc.log --log-severity=LS_VERBOSE日志里最关键的几个关键字是:IceCandidatePair(查看候选对选择结果)、BWE(带宽估计的更新)、VideoStreamEncoder(编码参数调整)、PacedSender(发送节奏)。你可以通过--v参数调整特定模块的详细级别,比如--vmodule=paced_sender=3会把 PacedSender 的日志调到最详细。这样你就能看到某个具体实现在运行时到底在做什么,很多源码“为什么这样写”的问题都能在日志里找到答案。
5.3 调试场景:用 GDB 追踪关键调用链
如果你需要在源码里精确定位某个参数的传递路径,我通常会用 GDB 设置条件断点。比如我想看带宽估计更新时 PacedSender 的调用来源,可以这样:
(gdb) break modules/pacing/paced_sender.cc:120 (gdb) condition 1 bitrate_bps > 300000 (gdb) run等断点命中后,用bt查看完整调用栈,基本能看到一条清晰的调度链:某个带宽估计模块 → 码率控制器 → PacedSender。这个调用栈就是源码里“谁调用了谁”的最直接证据。实际去跑一遍,比你对着静态代码猜半天要靠谱得多。
6. 常见问题排查与源码定位心得
架构读到最后,终究要落到解决实际问题。我挑几个在 WebRTC 开发里经常碰到的问题,从源码角度给出定位思路,这是纯经验部分,常规文档里很少写这么细。
6.1 连不上:卡在 ICE 状态机
表现是双方都拿了候选,但连接一直停在connecting,最后超时。从源码层面,这个问题要按两条路径分开查。第一是 STUN 绑定请求是否正常收发。在p2p/stun_port.cc里,StunPort负责发送 STUN 请求并处理返回,如果这里出现“请求发不出”或“响应解析失败”,多半是本机网络限制或 TURN 服务器配置问题。第二条路径是检查 ICE 候选对的排序。在p2p/ice_transport.cc里有一个候选对优先级排序的函数,它计算的是nomination和priority。如果你的两个端使用的 ICE 版本不一致,可能导致选择出的候选对被判为不合法。排查时我一般先看日志里的IceCandidatePair行,如果显示state=SUCCEEDED但还是没连通,那就是后面的 DTLS 或 SRTP 环节出问题了。
6.2 黑屏:首帧迟迟不出来或花屏
视频黑屏问题,最典型的两个源码原因是:其一,接收侧FrameBuffer一直凑不齐关键帧。比如丢包导致关键帧数据缺失,而发送侧又没收到 PLI 请求,就会一直卡在黑屏。这种情况在modules/video_coding/frame_buffer2.cc的日志里表现为FrameBuffer::FindFrameToDecode返回值频繁为nullptr。其二,对帧缓冲的超时配置不合理。默认情况下FrameBuffer会在一定时间后强制丢弃旧帧,但如果配置的等待窗口太长,播放延迟就会被拉得非常高。
如果你看到的是花屏而不是黑屏,那大概率是解码器输出了不完整的帧。这时候先看video_receive_stream里的syncable配置,如果音视频同步机制把视频帧缓存拉得太久,也容易出现“视频看起来卡但不黑屏”的假象。
6.3 回声:回声消除的参考链路没对上
回声问题涉及modules/audio_processing里的回声消除器。它的算法本身很成熟,大量回声问题其实是参考信号和采集信号没对齐。从源码角度,你要检查三处:一是AudioProcessing是否在播放远端音频时也收到了参考信号(set_stream_delay是否正确设置);二是延迟估计模块是否因为缓冲区不足而误判了延迟;三是回声消除器的启用状态,但注意不要随意关闭,它是最后一个兜底防线。实际中见过很多“回声没人修所以直接关掉”的操作,看似解决了问题,其实只是把架构问题掩盖了。源码里ApmDataDumper可以导出调试转储文件,用外部工具分析比肉眼猜延迟要靠谱得多。
6.4 音频断断续续:抖动缓冲的“抗压”能力
音频断续,最直接的原因是到达时间抖动太大,超过 NetEQ 的缓冲能力。调整参数是最容易的事,但你要理解 NetEQ 的工作机制才能调得准。在modules/audio_coding/neteq里,NetEq用了一个自适应算法来动态改变缓冲区大小,而不是固定不变。它的目标是“在丢包率和延迟之间取得平衡”。源码里有一个SetMinimumDelay的入口,你可以通过它设置播放延迟的下限。如果你发现延迟已经很高但依旧断续,那问题就不在缓冲了,而在接收端速度太快——可能是外部设备(如蓝牙耳机)的时钟漂移导致采样率不匹配。这部分代码在modules/audio_device/里,可以用ALSA或CoreAudio相关代码来检查设备句柄的参数配置。
6.5 速度慢:RTP 分片和重组消耗过多 CPU
视频通话 CPU 占用过高,很多时候不是编解码本身的问题,而是 RTP 分片重组逻辑的开销。WebRTC 在发送端会把一帧视频切成多个 RTP 包,接收端再拼回来。如果网络丢包较多,接收侧会反复收到部分包,重组失败的包会被丢弃,这个过程的消耗会被放大。源码里modules/rtp_rtcp/source/rtp_packet_reassembler就是做这件事的。排查思路是先看日志里的DiscardedPackets统计,如果丢包重组失败率很高,网络层问题优先,然后再去优化代码实现。还有一种 CPU 是无谓的——不同方式打包导致memcpy过多。源码里RtpPacketToSend的缓冲区复用策略,如果你在二次开发时把帧拷来拷去,性能损耗会很可观。
7. 最后再分享一点我的个人感受
WebRTC 的源码架构读到这里,你已经比大部分只“使用” WebRTC 的人站得高了一截。我个人体会,读它最大的好处不是能用更多 API,而是遇到问题时能快速判断“这是谁的锅”:是信令协商,是 ICE 建立,是媒体编码,是传输抖动,还是设备适配。这个判断力,是任何官方文档和博客都给不了你的。
如果让我给一个简单的阅读路线图,我建议你把顺序定成:rtc_base(线程机制)→api(接口定义)→pc(PeerConnection 实现)→call(Call 类调度)→modules(具体媒体引擎)→p2p(ICE 逻辑),然后再回头精读modules里的具体模块。这个路线比较符合架构的依赖方向,读的时候不会出现“这个类从哪冒出来”的困惑。
还有一点想特别提醒:如果你是二次开发,尽量不要绕过 WebRTC 的线程模型去“优化”,比如用全局锁去保护某个模块的状态。我见过太多“用大锁解决问题,结果引入更多死锁”的案例。WebRTC 这套线程模型是经过大量生产环境验证的,你的改动如果让它需要大锁才能正常工作,通常说明有更好的设计路径。
最后再分享一个小技巧:改代码之前,先看对应模块的 unit test。WebRTC 的测试量很大,测试代码本身就是用法的权威说明。有时候源码里某个类的行为你看半天看不懂,一跑它对应的 test,立刻明白“原来这个坑是这么填上的”。这种通过测试反向理解源设计的方法,也是我读大型 C++ 项目时最常用的招数,分享给你,希望能帮你在 WebRTC 源码的弯弯绕绕里少走几步冤枉路。