ARTICLE · 1234422
WebRTC 源码地图:api、pc、call、audio、video、modules 分别负责什么?
做过音视频开发,再打开 WebRTC 源码,最先遇到的困难往往不是某个算法看不懂,而是不知道应该从哪里读起。
准备接入芯片的 H.264 编码器,搜索 encoder,会同时遇到 VideoEncoder、VideoStreamEncoder、VideoEncoderFactory。准备分析视频卡顿,video/、call/、modules/rtp_rtcp/ 和 modules/pacing/ 又都能找到相关代码。几个类都带着 Send,到底谁接收原始帧,谁处理编码结果,谁真正负责发包?
目录名能提供线索,但只有把目录、对象和数据流联系起来,才能找到问题的入口。
本文沿用嵌入式可视对讲这个场景:设备采集摄像头和麦克风数据,与远端交换音视频;视频使用平台编码能力,音频需要回声消除;网络会出现丢包和带宽波动。我们借这个场景说明源码各部分的职责,再把视频发送、音频收发和网络反馈三条路径连起来。
1. 先区分三件事:目录职责、对象关系、数据路径
先看一张职责图。

图 1:这张图用于回答“某类工作主要由谁负责”。它不是严格的 C++ 依赖图,也不表示每一帧都会依次经过图中的全部目录。
如果只记住六个目录,可以先建立下面这层认识。表中的入口是阅读起点,不是每个目录的完整文件清单。
api/ | peer_connection_interface.hmedia_stream_interface.h、video_codecs/video_encoder.h | |
pc/ | peer_connection.ccsdp_offer_answer.cc、rtp_transceiver.cc | |
call/ | call.hcall.cc、rtp_transport_controller_send.cc | |
audio/ | audio_transport_impl.ccaudio_send_stream.cc、channel_receive.cc | |
video/ | video_stream_encoder.ccvideo_send_stream_impl.cc、video_receive_stream2.cc | |
modules/ | audio_processing/audio_coding/neteq/、rtp_rtcp/、pacing/、congestion_controller/ |
这些职责可以从接口、装配代码和具体流实现交叉确认。它们存在协作和交叉,不是互不相干的六个子系统。
还必须补上三个目录。media/ 负责把上层媒体配置连接到音视频引擎,理解它才能把 pc/ 与 call/ 接起来;p2p/ 包含 ICE、候选连接及相关底层传输实现;rtc_base/ 提供线程、网络、日志等基础设施。视频帧、音频信号处理等公共能力也可能位于 api/、common_video/、common_audio/,不能用“所有算法都在 modules”作为搜索规则。
读源码时,建议给每一条箭头都问一句:它传递的究竟是什么?
创建或配置关系:工厂创建对象,协商结果设置参数。 媒体数据:PCM、原始视频帧、编码后的帧。 网络数据:RTP、RTCP 以及其他协议报文。 控制信息:目标码率、关键帧请求、网络状态、统计回调。
这四类关系在代码里可能都表现为一次函数调用,但含义不同。CreateVideoSendStream() 创建了发送流,并不意味着每张摄像头图像都要重新走一遍创建流程。api/ 中声明了 VideoEncoder,也不意味着一个视频帧先“流过 api 目录”再进入实现。
2. api:先看接入契约,再找实现
2.1 api 里有接口,也有参数和媒体类型
对于应用接入,api/ 是首要入口。PeerConnectionInterface 描述会话能力,MediaStreamTrackInterface 描述媒体轨,VideoFrame 描述视频帧,VideoEncoder 描述编码器需要实现的行为。它们解决的是“双方如何合作”,并不把所有执行细节放在同一个地方。
这里的“接口”也不只指 C++ 纯虚类。api/ 还包含配置结构、值类型、工厂入口及部分实现文件,例如 api/enable_media.cc。读目录时,不能把“头文件”和“公共接口”、“有 .cc 文件”和“内部实现”简单画等号。
官方 native-api.md 将 api/ 列为主要 API 目录,同时列出了一批历史 API 目录。文档也明确说明,名称包含 impl、internal 的内部对象不属于公共 API。对接产品时优先选择明确的接口;为了分析问题可以继续进入内部类,但应意识到这些实现更容易随版本变化。WebRTC 的原生接口也不代表永久不变的 C++ ABI。
2.2 以硬件视频编码器为例
平台有一个 H.264 编码 SDK,WebRTC 怎么使用它?先读两个文件就能找到接入方向:
api/video_codecs/video_encoder_factory.h:描述支持哪些编码格式,以及怎样创建编码器。api/video_codecs/video_encoder.h:描述编码器初始化、提交帧、返回结果和更新速率等行为。
本基线的工厂创建入口是 VideoEncoderFactory::Create(const Environment&, const SdpVideoFormat&)。具体编码器实现需要落实 InitEncode()、RegisterEncodeCompleteCallback()、Encode()、SetRates()、Release() 等接口约定。平台代码可以在这些入口中连接厂商 SDK,而 WebRTC 上层通过统一接口使用它。
这样分工以后,VideoStreamEncoder 可以决定什么时候编码、以什么目标码率编码,具体 VideoEncoder 则负责把原始帧变成指定格式的编码数据。硬件编码器不会因为被放进一个工厂,就自动具备弱网适配能力;它还必须正确执行速率变化、关键帧要求,并把真实的编码结果反馈回来。这一点后面还会沿发送路径继续说明。
这里也要区分两种接入方式:把原始帧交给 WebRTC 管理的编码器,与把外部已经编码好的帧注入发送端,是不同的路径。本基线存在 EncodedVideoFrameInjectorInterface,但它不是把任意 H.264 字节交给普通 VideoTrack 的同义写法。外部编码方案还需要处理协商格式、时间戳、码率联动和关键帧响应,不能靠跳过编码器接口就省掉这些工作。
2.3 配置与依赖,不是一回事
配置表达“想怎样运行”,例如服务器地址、策略和媒体参数;依赖提供“由谁来执行”,例如音频设备、编码器工厂、音频处理构造器。嵌入式移植最值得先找的是后者,而不是一开始就修改整个 video/ 或 audio/。
当前官方示例 examples/peerconnection/client/conductor.cc 的创建路线值得先读:准备 PeerConnectionFactoryDependencies,提供所需编解码工厂,调用 EnableMedia(deps),然后调用 CreateModularPeerConnectionFactory()。模块化依赖默认没有启用媒体,EnableMedia() 负责装配媒体能力;当前 APM 构造依赖使用的是 audio_processing_builder 字段。
模块化构造也给体积裁剪留下了空间,但“没有传某个对象”不等于已经完成二进制裁剪。真正减少固件体积,还要检查构建目标、链接依赖及实际产物。官方桌面示例适合学习对象怎样装配,它本身不代表某块嵌入式板卡的驱动和硬件编解码已经适配完成。
3. pc:把媒体能力、协商和传输组织成一次会话
3.1 PeerConnection 是会话协调入口
pc/ 中的 PeerConnection 实现了应用熟悉的会话操作。添加 Track、创建 Offer/Answer、设置本地和远端描述、接收远端 ICE 候选,最终都需要在这里协调媒体对象与传输状态。较大的工作又拆给 SdpOfferAnswerHandler、JsepTransportController 等对象完成。
因此,不宜把 pc/ 简写成“信令目录”。它处理协商,也连接实际传输;pc/rtp_transport.cc、pc/srtp_transport.cc 就涉及 RTP 路由与 SRTP 保护。而业务使用 WebSocket、MQTT 还是其他通道交换 SDP 和候选,并不是 PeerConnection 替应用规定的协议。
对可视对讲设备来说,按门铃、呼叫哪台手机、响铃多久、是否允许接听,是应用业务。PeerConnection 负责把已经确定要建立的实时连接组织起来。业务状态和媒体状态可以互相影响,但不能混成同一个状态机。
3.2 先分清几个相似的对象
PeerConnectionFactory | ||
MediaSource | ||
MediaStreamTrack | ||
MediaStream | ||
RtpSenderRtpReceiver | ||
RtpTransceiver |
这些对象的语义主要由 api/media_stream_interface.h 和 RTP sender、receiver、transceiver 接口定义,具体行为再进入 pc/ 实现。
例如,一条视频 Track 在多编码层或多路编码的配置下,可以关联多个编码和 RTP 流标识,所以不要从“只有一个 Track”直接推断“只有一个 SSRC”。同样,Transceiver 的 direction 表达期望方向,current_direction 表达已协商方向;刚设置了期望,不代表网络上已经立即改变了收发行为。
MediaStream 也不是音画同步器。把远端音轨和视频轨放进同一个容器,只是提供归组信息;真正播放到同一时间轴上,还涉及 RTP 时间戳映射、接收端时序和播放调度。后面分析同步问题时,仍要进入具体收流和渲染链路。
3.3 哪些问题先读 pc
如果出现“设置远端 SDP 失败”“协商完成却没有创建期望的媒体流”“切换方向后行为不符合预期”,先从 pc/ 看对象状态和参数落地,比直接进入编解码器更有效。
如果是“持续收到完整视频帧,但硬件解码器输出异常”,则应转向视频接收和平台解码实现。PeerConnection 仍然参与会话,但它已经不是最短的排查入口。这种按现象选入口的能力,比记住一串目录名更有用。
4. media 与 call:协商结果怎样变成真正的收发流
4.1 media 是不能漏掉的一层
从 pc/ 直接跳到 video/,经常会丢失一个关键问题:SDP 里协商出来的 codec、方向、RTP 扩展和流标识,是怎样变成音视频引擎参数的?这条连接需要看 media/。
在本基线中,RtpTransceiver::CreateChannel() 创建媒体收发 Channel,并构造 BaseChannel。BaseChannel 持有分开的 MediaSendChannelInterface 与 MediaReceiveChannelInterface,承担媒体参数应用、传输关联以及接收包分发等衔接工作。
继续进入 media/engine/webrtc_video_engine.cc 和 webrtc_voice_engine.cc,会看到 WebRtcVideoSendChannel、WebRtcVideoReceiveChannel 及对应的 Voice 收发 Channel。这些对象再通过 Call::CreateVideoSendStream()、CreateAudioSendStream() 等接口创建具体流,把配置落到运行实体上。
旧版本教程可能画的是另一套 Channel 类名。学习时可以保留“协商对象—媒体桥接—具体流”这层认识,但对照源码时必须以当前类型和实际调用为准。

图 2:箭头表示装配、关联或配置方向,不是每帧执行顺序;省略了部分代理、辅助对象和跨序列切换。DataChannel 有自己的 SCTP/DTLS 路径,不能硬套进音视频 Stream。
4.2 Call 管媒体会话,不负责业务呼叫
第一次看到 call/,很容易想到拨号、接听和挂断。源码里的 Call 更接近媒体会话引擎:创建和销毁音视频收发 Stream,接收媒体网络包,协调会话内共享的传输状态、带宽估计与码率分配。
例如,同一个会话同时发送麦克风音频和摄像头视频,不能让两条流各自认为“整条网络带宽都归自己”。共享估计、分配和发送调度正是理解 call/ 的价值所在。RtpTransportControllerSend 是继续追踪发送传输控制的重要入口。
这里的 AudioSendStream、VideoSendStream 与上一节的 MediaStream 不是同一个层次:前者是内部媒体处理链的流对象,后者是 API 层的 Track 容器。遇到名字中的 Stream,先看命名空间、所属接口和处理的数据,再判断含义。
通常,启用音视频的 PeerConnection 会对应一个 Call;但不要写成“任何 PeerConnection 都无条件包含 Call”。当前工厂的 CreateCall_s() 在没有媒体引擎或媒体工厂时可以返回空,纯 DataChannel 等没有启用媒体的组合需要单独理解。
读到这里,已经可以把创建路线连起来:工厂装配能力,PeerConnection 组织协商,媒体 Channel 应用配置,Call 创建并组织具体收发流。 接下来再看这些流运行起来以后,数据到底经过哪里。
5. video:沿一帧视频,把编码与发送连起来
5.1 原始帧、编码帧和 RTP 包是三种数据
摄像头交上来的原始图像,编码器输出的 H.264 等压缩数据,以及网络上发送的 RTP 包,分别处在不同阶段。一次原始帧输入可能产生一张编码帧,一张编码帧又可能拆成多个 RTP 包。丢包恢复和帧依赖判断因此也不能简单等同于“丢了一帧就重发一帧”。
在原始帧入口附近,先看 VideoTrackSourceInterface、AdaptedVideoTrackSource、VideoFrame 与 VideoTrack 的关系。平台采集适配代码负责把摄像头输出接到这些约定中,传递图像缓冲、尺寸、旋转和时间信息。适配层可以连接厂商采集接口,不要求所有设备都先走 WebRTC 自带的视频采集实现。
这里有一个很实际的区分:类名中有 Source,不代表它一定直接操作摄像头驱动。Source 可以包装设备、转发应用提供的帧,也可以是测试源。判断它做了什么,要继续看真正提交帧的代码。
5.2 VideoStreamEncoder 是编码调度者
video/video_stream_encoder.cc 是视频发送侧最值得反复阅读的文件之一。它连接源端输出与具体编码器,处理编码配置、帧处理节奏、编码队列、码率更新及资源适配协作。
实际路径中还有 frame_cadence_adapter 等辅助对象,所以不能把源端到 VideoStreamEncoder::OnFrame() 画成一个没有中间调度的同步调用。本文图中保留的是主要处理阶段;阅读实现时还要跟踪进入编码序列的任务。
当进入具体编码阶段,EncodeVideoFrame() 会调用 encoder_->Encode()。这里的 encoder_ 才是实现 VideoEncoder 契约的对象,它可以是内置软件实现,也可以是接入的平台实现。modules/video_coding/codecs/h264/h264_encoder_impl.cc 是内置 H.264 路线的阅读入口之一,不应把它当作所有硬编都必须修改的文件。

图 3:实线表示主要媒体数据方向,虚线表示控制反馈。图中省略部分封装、适配和任务队列切换;反馈抵达并不意味着硬件编码器已经立即执行完新设置。
5.3 编码完成以后,仍然有很长一段路径
编码结果通过回调返回到 VideoStreamEncoder::OnEncodedImage(),再交给它的编码结果接收者。沿当前实现继续读,可以找到 VideoSendStreamImpl::OnEncodedImage()、RtpVideoSender::OnEncodedImage(),以及 RTPSenderVideo::SendEncodedImage()。这条路线把“编码完成”连接到 RTP 封装。
RTP 封装之后还涉及发送调度、传输保护和网络发送。正常 WebRTC 媒体路径使用 SRTP 保护 RTP;DTLS 参与建立安全上下文,但媒体 RTP 不是被当成普通 DTLS 应用数据逐包包裹后发送。路径选择和底层收发再进入 ICE 相关传输。这部分协议关系已在第 2 篇展开,这里重点记住源码入口在 modules/pacing/、pc/ 和 p2p/ 分散协作。
对硬件编码来说,回调的执行位置尤其重要。VideoSendStreamImpl::OnEncodedImage() 的代码注释明确考虑了由编码器内部线程回调的情况。接入时需要按接口约定处理缓冲区和对象生命周期,不能默认“编码调用返回了,硬件也已经不再使用输入内存”。具体内存释放时机还取决于所接芯片 SDK 的约定。
5.4 视频接收不会经过 NetEq
接收端要先把 RTP 包重新组织成可解码的帧,再处理帧依赖、等待时机和解码。当前 RtpVideoStreamReceiver2 涉及包缓冲、H.26x 相关组帧处理和参考帧关系;VideoStreamBufferController 连接帧缓冲与解码时机,VideoReceiveStream2 组织接收、解码和后续输出。
这里容易被旧源码路径误导:本基线的通用 FrameBuffer 实现在 api/video/frame_buffer.{h,cc},不能只到旧版本的 modules/video_coding/frame_buffer2.h 中找。视频接收还需要同时考虑“包有没有收齐”“依赖帧是否可用”“何时值得解码”,这些工作分散在多个对象中。
因此,“WebRTC 有一个 jitter buffer,音视频都往里放”会造成误解。抗网络抖动是共同目标,音频与视频使用的缓冲单位、补偿手段和调度要求不同。视频帧缓冲与音频 NetEq 应分别建立模型。
6. audio:把设备、3A、编解码和播放接起来
6.1 audio 目录负责组织收发链
音频不能只盯着 3A 算法。对于双向对讲,设备是否按时交付录音,播放端是否连续拉取数据,采样率和声道数是否一致,都会影响最终结果。audio/ 的价值在于把这些步骤和媒体流组织起来。
几个常用对象可以先这样记:AudioTransportImpl 连接设备音频回调与内部处理;AudioSendStream 组织发送流;发送方向的 ChannelSend 衔接编码与 RTP;接收方向的 ChannelReceive 连接接收包、NetEq 和解码后的音频输出。
这里的 ChannelSend、ChannelReceive 位于 audio/,与前面 pc/ 的 BaseChannel、media/engine/ 的媒体 Channel 处于不同位置。看到相似名字时,不要省略所属文件和上下文。
6.2 先按 Native 默认音频路径理解
以下讨论的是所列源码中的 Native 音频接入路径。浏览器或其他集成方式可以另行组织音频设备、处理器和回调;例如源码对 Chromium 的 PullRenderData() 路径有专门说明,不能把所有集成方式都画成完全相同的音频链路。
录音侧,音频设备通过 RecordedDataIsAvailable() 提交 PCM。AudioTransportImpl 按需要进行声道、采样率等处理,并在配置了 APM 时完成采集侧处理,再把数据送往注册的音频发送者。后续由发送流和 ChannelSend 接入音频编码与 RTP 发送。
Native 这条回调路径以 10 ms 音频块为处理单位,但10 ms 处理块不等于一个 RTP 包固定包含 10 ms 音频。编码器可以累积输入,按其配置的帧长和打包行为产生输出。ChannelSend 的代码中,提交 10 ms 数据与最终编码回调、发包是分开的步骤。

图 4:箭头表示 PCM 或编码音频的主要数据方向。播放端由设备通过 NeedMorePlayData() 拉取,回调内部驱动混音;图中不能按箭头顺序理解成所有函数被主动依次调用。
6.3 AEC 为什么需要播放侧参考
3A 通常指回声消除 AEC、噪声抑制 NS 和自动增益控制 AGC。这些能力的主要实现在 modules/audio_processing/,其中 AEC3 的入口之一是 aec3/echo_canceller3.cc;audio/ 负责把音频链路连接到这些能力。
AEC 处理的是扬声器声音经过声学路径进入麦克风形成的回声。仅有麦克风 PCM 不足以描述这条路径,还需要知道播放侧送出了什么。沿默认 Native 播放路径看,NeedMorePlayData() 先从混音器取得 PCM,在配置 APM 时将混音结果送入反向处理路径,再按设备需求处理输出采样率。
所以,AEC 参考不是远端的 Opus 包,也不是随便取一条接收流的压缩数据。它应与实际送往播放链路的音频内容及其时序相匹配。如果平台又做了混音、增益或额外缓存,接入设计必须把这些变化和延迟纳入考虑;仅仅把 AEC 开关设成开启,不能证明参考路径已经接对。
这也是嵌入式对讲常见的排查顺序:先确认录音、播放和参考数据的来源及时间关系,再深入 AEC 算法。否则,很容易花很多时间调整算法参数,却一直没有修正错误的输入。
6.4 NetEq 不只是一个等包队列
接收侧,ChannelReceive 把压缩音频及相关包信息交给 NetEq。播放需要音频时,再向 NetEq 获取可输出的音频块。NetEq 包含包与音频缓冲、解码驱动、播放延迟管理及丢包补偿、时间尺度调整等协作逻辑,不能把它简化成“一个先入先出的 RTP 缓冲区”。
解码后的音频再参与混音和设备播放。这里的先后顺序必须讲清楚:常规路径不是先在 NetEq 外面把全部音频解码成 PCM,再把 PCM 放进一个叫 NetEq 的 jitter buffer;解码本身就是 NetEq 组织的工作之一。
同样,不要把所有接收端恢复机制都归到 NetEq。音频的重传请求跟踪还涉及 audio/nack_tracker.h 和 ChannelReceive;网络重传与播放补偿协作,但不属于同一个算法。当前音频发送也可以接入共用发送调度基础设施,是否按视频同样的节奏和预算处理音频,则有独立策略,不能写成“音频总绕过 pacer”或“音频与视频完全同样排队”。
6.5 VAD 要按具体用途找
VAD 是语音活动检测,回答当前音频是否包含语音或语音概率多大。AEC、NS、AGC 与 VAD 有关联,但各自解决的问题不同,不能把它们当作一个总开关。
源码中既有 common_audio/vad/ 的传统检测实现,也有音频处理中的检测器封装及 agc2/rnn_vad/ 等实现。某个内部模块用于增益判断的语音概率,并不自动成为应用层通用的“开始说话/结束说话”事件接口。分析 VAD 时,要先确定使用场景和当前调用者,再看输入要求、状态和输出语义。
本篇先把这些位置标出来。后续研究 3A、语音检测、双讲和静音策略时,就可以分别从处理链、输入输出和具体算法展开,不必把它们混在一篇里。
7. modules:把算法位置和使用者一起记
modules/ 很大,比较实用的读法是同时记录两件事:这里提供什么能力,上层谁在使用它。只记目录名,等到实际排查问题时仍然容易断链。
modules/audio_processing/ | AudioTransportImpl 或具体集成方的处理调用进入;先区分采集侧和播放参考侧 | |
modules/audio_coding/neteq/ | ChannelReceive 的插包、取音频路径进入;不是视频帧缓冲 | |
modules/audio_device/ | api/audio/audio_device.h 等文件;设备 SDK 不会自动被适配 | |
modules/video_coding/ | video/ 和 api/video/ | |
modules/rtp_rtcp/ | RtpVideoSender、音频 Channel 和传输控制器协作 | |
modules/pacing/ | RtpTransportControllerSend 和具体发送路径看接线;区分音频策略、探测和正常发送 | |
modules/congestion_controller/goog_cc/ | ||
modules/video_capture/ |
这些入口分别可在 APM、NetEq、设备、视频编码、RTP/RTCP、pacing 和 GoogCC 源码中核对。
这里还应分清三个概念。模块是代码组织和能力划分;接口是组件合作时的约定;算法是组件内部的处理方法。一个模块可能包含多种算法,一种功能也可能跨多个模块完成。
例如,丢包恢复涉及接收端发现缺包、反馈重传请求、发送端保存并重发历史包,以及接收端决定是否继续等待。这不可能只靠一个 Nack 类完成。带宽估计也不能单独决定画质,还要经过分配、编码目标、实际编码输出和发送队列。
模块化同样不等于可以随意复制一个目录单独编译。真实依赖由构建文件和接口使用关系决定,APM、NetEq、RTP 组件是否能以所需方式独立使用,需要按目标版本和产品边界验证。
8. 网络反馈怎样回到编码器:不要把 NACK、GCC 和关键帧请求混成一条链
前面一直沿着发送方向读。对弱网优化来说,另一半路线更重要:接收端反馈到达后,发送端究竟改变了什么?
8.1 同一类 RTCP 入口,后面有不同分支
在 modules/rtp_rtcp/source/rtcp_receiver.cc 中,TriggerCallbacksFromRtcpPacket() 对不同反馈触发不同观察者。对源码地图而言,至少要分清下面三路。
EncoderRtcpFeedbackVideoStreamEncoder::SendKeyFrame() | ||
RtpTransportControllerSend |
NACK 不等于让编码器重新编码那一帧,PLI/FIR 也不等于列出一批待重传 RTP 包。它们可以在同一次弱网事件中先后发生,但表达的是不同需求。关键帧请求还可能被节流,不是远端每请求一次就无条件立即编码一次。
8.2 GCC 给出网络控制结果,流和编码器再落实
沿传输反馈往下读,会进入 RtpTransportControllerSend 的反馈处理,再进入网络控制器。在常见 GoogCC 路线中,算法实现主要位于 modules/congestion_controller/goog_cc/。源码还包含实验配置和其他选择分支,因此定位实际控制器时,要同时检查创建工厂、依赖注入和 field trial,不能只凭某个目录存在就判断它在运行。
控制器输出还要经过会话和流级处理。Call::OnTargetTransferRate() 将网络估计交给码率分配器的 OnNetworkEstimateChanged(),再由分配逻辑通知各流。当前 VideoSendStreamImpl::OnBitrateUpdated() 会结合 RTP 发送侧计算和保护开销处理,再把相关结果交给 VideoStreamEncoder::OnBitrateUpdated();后者在编码序列上更新速率,最终通过 SetEncoderRates() 调用具体编码器的 SetRates()。
因此,网络估计值、分配给视频的目标、编码器的实际产出、网卡最终发出的速率,应该分别记录。 它们在统计口径、协议开销和时间窗口上不必相等。只看一个瞬时数字,很难判断问题究竟发生在估计、分配、编码还是排队阶段。
举一个排查例子:如果网络目标已经下降,但平台编码器连续输出大帧,队列仍可能增长。此时应核对 SetRates() 的实参、厂商 SDK 是否接受更新、什么时候生效,以及后续实际帧大小。如果这些环节没有证据,直接认定“GCC 算错了”往往为时过早。
8.3 BBR、FEC 和 QoS 在这张地图上怎样放
BBR 是拥塞控制方向的研究主题,但它不是本篇标准 GoogCC 路线里的一个必经模块。要讨论替换控制器,必须验证实际工厂、接口和实验开关;不能把“学习过 BBR”写成“WebRTC 默认用 BBR”。
FEC 与重传主要关注媒体恢复,但冗余与重传都消耗传输预算;pacing 安排发包时间,带宽估计和拥塞控制判断网络目标,编码适配再影响画质和输出量。这些机制需要联合分析,不能把某一个开关等同于完整的弱网优化方案。
工程里常说的 QoS,在本系列中可以理解为围绕清晰度、流畅度、实时性和资源开销进行的综合质量控制。它不是源码中一个叫 QoS 的总算法。后续研究每种机制时,都应在这张地图上说明:输入来自哪里,改变了哪个决策,效果由什么指标验证。
9. 目录找对以后,还要看线程和任务序列
9.1 不能再直接照搬“固定三个线程”的旧图
很多旧资料用 signaling、worker、network 三个线程介绍 WebRTC。作为历史职责划分,它有帮助;如果直接拿来解释当前每一个实例,就可能出错。
在本文固定提交中,便捷函数 CreatePeerConnectionFactory() 的线程参数已经只有 network_thread 和 signaling_thread;模块化依赖结构仍保留 worker_thread 字段,但已标记弃用。ConnectionContext 默认让 worker 与 network 共用,旧字段还有兼容处理;提供不同的 worker 时,当前实现会输出弃用错误日志,并非在这一处无条件拒绝创建。
这也不等于整个 WebRTC 只运行两个操作系统线程。编码、解码、音频设备以及平台 SDK 都可能有自己的任务序列和线程。职责名称、任务队列和物理线程数量,是三个需要分别确认的问题。
9.2 TaskQueue 约束执行顺序,不等于固定一个 CPU 核
TaskQueueBase 提供串行任务执行的约定,常规任务不会彼此重叠,普通 PostTask() 有相应的顺序保证;但接口不保证底层永远由同一个操作系统线程执行。延迟任务也不因此成为硬实时定时器。
所以,读到 PostTask() 之后,要继续读 lambda 或回调体,不能把“提交成功”当成“处理已经完成”。读到 BlockingCall(),要把它理解为同步调用:跨线程时,调用方等待目标线程执行完成;如果目标就是当前线程,则可以直接执行。再结合 RTC_DCHECK_RUN_ON、SequenceChecker 和代理类,确认哪些调用允许直接执行,哪些依赖特定序列。
对嵌入式平台,尤其应记录三件事:厂商回调在哪个线程执行;帧缓冲由谁持有、何时释放;关闭设备或挂断时,尚未完成的任务怎样退出。源码路径追对了,生命周期却理解错了,仍可能出现偶发花屏或崩溃。
9.3 Environment 是共用服务的传递方式
现在的接口中经常出现 Environment。它组织时钟、任务队列工厂、事件日志和 field trials 等共用服务,并让使用者携带这些依赖。它不是操作系统环境变量,也不是“把所有组件变成全局单例”。
看到 Environment,可以顺手记录这段代码使用的是哪个时钟、哪种任务队列、什么实验配置。分析延迟和拥塞控制时,这些信息往往比一个函数名更能解释实际行为。
10. 嵌入式开发中,怎样用这张地图定位问题
10.1 先把平台接入边界画清楚
对于摄像头和麦克风已经能正常工作的设备,可以先把平台能力与 WebRTC 的合作边界列出来。
VideoFrame、平台采集适配代码 | ||
VideoEncoderFactoryVideoEncoder | ||
AudioDeviceModule | ||
这些是接口和默认链路给出的接入方向,不是一份对所有芯片都相同的移植补丁。具体需要实现哪些能力,仍应以平台、编解码格式和目标构建为准。
10.2 五类现象,对应五个优先入口

图 5:每一行都是“现象—源码入口—需要取得的证据”。它给出优先检查方向,不把某种现象直接等同于唯一根因。
连接建立不起来。 先记录信令、ICE 和 DTLS 等状态变化,检查候选和所选路径,再进入 pc/ 与 p2p/。连接层尚未完成时,先调视频编码参数通常解释不了问题。
持续有 RTP 到达,却没有画面。 先确认包是否分发给正确的接收流,再看帧是否完整、依赖是否满足、解码器是否被调用、是否产生输出。依次经过 pc/ 的传输分发、视频 RTP 接收、帧缓冲和解码入口,能够避免把所有黑屏都归因于网络。
画面越来越迟,最后延迟很大。 需要分别测量采集等待、编码排队、编码耗时、发送队列、接收等待、解码与显示排队。VideoStreamEncoder、pacer 和接收帧缓冲只是其中几处;芯片 SDK 与显示系统的队列也必须观察。只把接收缓冲调小,不能消除发送端已经积累的延迟。
对讲有回声,调 AEC 参数没有明显变化。 先沿 AudioTransportImpl 和平台播放回调验证参考 PCM 是否正确,核对录放延迟、声道和平台额外处理,再进入 APM/AEC3。处理对象拿到什么数据,是判断算法效果的前提。
网络差了,画质或延迟却没有按预期变化。 同时记录网络控制目标、视频分配、SetRates() 参数、实际编码帧大小和发送队列。先找哪一环不再跟随,再决定分析 GCC、分配逻辑还是硬件编码器。
这里的日志和测量项是排查方法,不是本篇已经在某块板卡上测得的结果。做性能结论时,需要补上设备型号、编码配置、网络条件和观测时间窗口。
11. 一条可以照着走的源码阅读路线
11.1 第一遍:只回答“对象怎样装起来”
先读 native-api.md,确认公共接口的边界。然后读 api/peer_connection_interface.h,建立 Factory、PeerConnection 和依赖结构的基本认识。接着跟随当前 conductor.cc 的创建代码,进入 EnableMedia()、PeerConnectionFactory 和 ConnectionContext。
这一遍不要求读完每个函数。需要得到的是一张自己的创建记录:哪些依赖由应用提供,哪些对象内部创建,媒体能力在哪里启用,设备和编解码工厂怎样交进去。
11.2 第二遍:追一次媒体配置落地
从一次视频协商过程入手,跟踪视频 Transceiver 的 RtpTransceiver::CreateChannel()、BaseChannel 的内容设置、WebRtcVideoSendChannel 中的流创建,最后到 Call::CreateVideoSendStream()。每一层只记录输入参数、持有的对象及它接下来配置了谁。
下面的命令用于源码检索,假设当前目录是所选提交的 src 根目录。它们不会构建工程,也不意味着搜索结果中所有位置都处于同一条运行路径。
git rev-parse HEAD
rg -n 'EnableMedia|CreateModularPeerConnectionFactory' \
examples/peerconnection/client/conductor.cc api/enable_media.cc
rg -n 'CreateChannel|CreateMediaContentChannels' pc/rtp_transceiver.cc
rg -n 'SetLocalContent_w|SetRemoteContent_w' pc/channel.cc
rg -n 'CreateVideoSendStream|CreateAudioSendStream' media/engine call
如果一个调用查到的是接口,继续找实现;如果查到代理或 PostTask(),继续跟到实际执行体。不要因为 IDE 已经跳到一个同名方法,就假定找到了最终处理点。
11.3 第三遍:只追一帧,再补反馈
先追视频发送,找到 EncodeVideoFrame()、具体编码器的 Encode()、编码结果回调、VideoSendStreamImpl::OnEncodedImage() 和 RtpVideoSender::OnEncodedImage()。给每个观察点标清楚是原始帧、编码帧还是 RTP 包。
rg -n 'EncodeVideoFrame|encoder_->Encode|OnEncodedImage|SetEncoderRates' \
video/video_stream_encoder.cc
rg -n 'OnEncodedImage|OnBitrateUpdated' video/video_send_stream_impl.cc
rg -n 'OnEncodedImage|SendEncodedImage' \
call/rtp_video_sender.cc modules/rtp_rtcp/source/rtp_sender_video.cc
rg -n 'TriggerCallbacksFromRtcpPacket|OnReceivedNack' \
modules/rtp_rtcp/source/rtcp_receiver.cc
然后分别补两条反馈线:一条从网络控制结果到 VideoEncoder::SetRates(),另一条从 PLI/FIR 到 SendKeyFrame()。NACK 则单独追踪包重传,不要强行接到编码器重编码上。
音频再用相同方法追一次:录音回调在哪里进来,APM 在哪里处理,编码结果在哪里发送,接收包怎样进入 NetEq,播放拉取怎样驱动混音。看到 RecordedDataIsAvailable() 与 NeedMorePlayData() 的实际调用和数据以后,3A 与 NetEq 就不再只是两个目录名。















































































