跳到主要内容

libwebrtc源码导读

面向需要深入定制 libwebrtc 的工程人员。读完之后应该能:在 Chromium 主仓 src/ 之外独立 checkout webrtc,定位到任意一个对外 API 的实际执行路径,知道改哪个文件、改完之后会影响谁,以及在什么场景下不应该自己动手改而应该走上层封装。

本文基于 branch-heads/m120 起到 main 的代码组织方式。M96 之前的目录结构(如 webrtc/ 顶层目录、talk/ 残留)不再讨论。


1. 为什么要读 libwebrtc 源码

读源码不是目的,定制才是。读源码值得做的场景:

  • 需要替换默认 BWE(GCC/BBR),接入私有拥塞控制算法
  • 需要接入硬件编码器(Intel QSV、NVENC、苹果 VideoToolbox 私有路径)或自研编码器
  • 需要在 SFU 或 MCU 上裁剪 PeerConnection 层,只保留 RTP/RTCP/Transport
  • 需要修复 m120 之前已确认但官方未 backport 的 bug(典型如 NACK 风暴、PLI 抑制)
  • 需要把 libwebrtc 接入非标网络层(QUIC datagram、私有 UDP 协议、卫星链路)

不值得读源码的场景:

  • 只是想调一个回声消除参数:用 AudioProcessing::Config 即可
  • 只是想看 SDP 协商:直接看 pc/sdp_offer_answer.cc 入口已经够,不需要全栈通读
  • 只是排查丢包率高:先看 getStats() 出来的 inboundRtp/outboundRtp 字段,不要直接进 transport 层
  • 上层框架(react-native-webrtc、aiortc、Pion)的功能问题:在框架层修,不要往下钻

失败时的样子:花两周读完 modules/audio_processing,最后发现业务问题在 JS 层 getUserMedia 的 constraints 没传对。


2. 获取源码与构建环境

mkdir webrtc-checkout && cd webrtc-checkout
fetch --nohooks webrtc
gclient sync
cd src
git checkout -b m120 branch-heads/m120
gclient sync -D
gn gen out/Default --args='is_debug=false rtc_include_tests=false use_custom_libcxx=false'
ninja -C out/Default

fetch 会拉 chromium/tools/depot_tools 体系、buildtools/third_party/,整体大约 12 GB。gclient sync -D 在切换分支后必须执行,否则 third_party/abseil-cpp 等子模块会停留在旧 commit,编译期符号会错乱。

什么时候不适用 depot_tools:在企业内网且无法访问 chromium.googlesource.com 时。这种情况只能镜像整个 chromium DEPS 树到内网 GitLab,单独 git clone webrtc 仓库是构建不起来的,因为 DEPS 里钉死了上百个第三方依赖的 hash。


3. 源码目录结构

下面是 m120 分支顶层 src/ 中与 webrtc runtime 强相关的目录树。build/buildtools/testing/tools_webrtc/ 是构建/测试支撑,不展开。

src/
├── api/ 对外 C++ 接口层,业务唯一应该 include 的目录
│ ├── peer_connection_interface.h
│ ├── create_peerconnection_factory.h
│ ├── audio_codecs/ 音频编解码工厂接口
│ ├── video_codecs/ 视频编解码工厂接口
│ ├── transport/ network_control / network_state_predictor 等
│ ├── task_queue/ TaskQueueFactory 接口
│ ├── rtp_parameters.h RtpEncodingParameters / RtpCodecCapability
│ └── stats/ RTCStats 类型定义

├── pc/ PeerConnection 层实现,SDP / Transceiver / DataChannel
│ ├── peer_connection.cc/h
│ ├── peer_connection_factory.cc/h
│ ├── sdp_offer_answer.cc/h O/A 状态机
│ ├── rtp_transceiver.cc/h
│ ├── rtp_sender.cc / rtp_receiver.cc
│ ├── jsep_transport_controller.cc/h
│ ├── dtls_transport.cc/h
│ ├── srtp_transport.cc/h
│ └── data_channel_controller.cc/h

├── call/ Call/Stream 层,承接媒体管线与传输
│ ├── call.cc/h Call::CreateAudio/VideoSendStream
│ ├── rtp_transport_controller_send.cc/h BWE/Pacer 的宿主
│ ├── rtp_video_sender.cc/h
│ ├── audio_send_stream.cc / audio_receive_stream.cc
│ ├── video_send_stream.cc / video_receive_stream2.cc
│ └── flexfec_receive_stream_impl.cc

├── media/ MediaChannel 抽象,桥接 pc 层与 call 层
│ ├── base/ MediaChannel / Codec / VideoSinkInterface
│ ├── engine/ WebRtcVoiceEngine / WebRtcVideoEngine
│ └── sctp/ SctpTransport (DataChannel 底层)

├── modules/ 所有可独立测试的算法/协议模块
│ ├── audio_processing/ APM:AEC3 / NS / AGC2 / HPF
│ ├── audio_coding/ NetEq、ACM、Opus/G711 包装
│ ├── audio_device/ ADM 各平台实现 (CoreAudio/ALSA/AAudio/OpenSL)
│ ├── audio_mixer/
│ ├── video_capture/
│ ├── video_coding/ VCM、Encoder/Decoder 包装、JitterBuffer、FrameBuffer3
│ ├── video_processing/
│ ├── rtp_rtcp/ RTP 打包、RTCP 反馈、SR/RR/REMB/TWCC
│ ├── pacing/ PacedSender / TaskQueuePacedSender
│ ├── congestion_controller/
│ │ ├── goog_cc/ GCC v2 (delay-based + loss-based)
│ │ └── pcc/ 实验性 PCC
│ ├── remote_bitrate_estimator/
│ ├── desktop_capture/
│ └── third_party/ 模块内部的第三方 (g711/g722/fft4g 等)

├── p2p/ ICE/STUN/TURN 与 Transport 抽象
│ ├── base/ Port / Connection / IceTransportInternal
│ ├── client/ BasicPortAllocator
│ └── stunprober/

├── rtc_base/ 通用基础设施:线程、socket、log、加密、TaskQueue
│ ├── thread.h rtc::Thread (libjingle 遗产)
│ ├── task_queue* TaskQueue 抽象 + 各平台实现
│ ├── network.cc / socket_* 网络枚举与跨平台 socket
│ ├── ssl_* BoringSSL 包装
│ └── system_wrappers (旧)

├── system_wrappers/ CPU 频率、时钟、字段试验 (FieldTrial)
├── audio/ AudioState / channel_send / channel_receive (call 层使用)
├── video/ VideoStreamEncoder / VideoReceiveStream2 内部细节
├── logging/ rtc_event_log + protobuf schema
├── stats/ RTCStatsCollector
├── sdk/ 平台 SDK:android/、objc/、windows/
├── third_party/ abseil / boringssl / libvpx / openh264 / pffft / opus
└── examples/ peerconnection_client / peerconnection_server / AppRTCMobile

理解这棵树的关键是分层:

  • api/ 是契约。业务只能 include api/。改 api/ 视为 ABI 变更
  • pc/ + media/engine/ 实现 W3C PeerConnection 语义
  • call/ + audio/ + video/ 实现媒体管线 + 传输调度
  • modules/ 是可独立替换的算法层(编解码、APM、NetEq、Pacer、BWE)
  • p2p/ + rtc_base/ 是基础网络与系统抽象

什么时候适用这个分层:你只想改一个 BWE 参数 → modules/congestion_controller/goog_cc/,不要碰 pc/。 什么时候不适用:要做"仅传输"裁剪 → 反而要绕开 pc/ 直接用 call/ 接口。


4. 核心对象关系

口径上的几个常见混淆:

  • Call 不是每路通话一个,而是每个 PeerConnection 一个;多路 PC 之间不共享 BWE,除非传入相同的 RtpTransportControllerSendFactory
  • RtpTransceiver 是 W3C 概念,每个 transceiver 内部有一对 RtpSender / RtpReceiver,二者共用 SSRC 池但走不同的 SRTP 方向
  • MediaChannelpc/media/engine/)是历史产物,处于 pccall 之间,承担 SDP 协商出来的 codec/RTP header extension/SSRC 到 Call 层 Stream 的翻译
  • JsepTransportController 一个 PC 一份,里面按 BUNDLE group 维护 DtlsTransport;m100 之后 BUNDLE 默认 max-bundle,所有 m= 行复用同一个 ICE/DTLS

失败时的样子:在 SFU 里给每个客户端 new 一个 PeerConnection,又共享 Thread,导致 Call 内部的 worker_thread 抢占严重,BWE 估计完全失真。


5. 关键调用链

5.1 创建 PeerConnection

PeerConnectionFactory::CreatePeerConnectionOrError 的关键点:Call 是在工厂这一层创建的,但寿命绑在 PeerConnection 上。network_thread / worker_thread / signaling_thread 由工厂决定,PC 不能再换。

5.2 CreateOffer → SetLocalDescription → ICE → DTLS → SRTP → 媒体流

每一步可能失败的样子:

  • CreateOffer 阶段:transceiver 没有 codec 交集(典型场景:只支持 H264 但 Builtin 工厂没注册 H264) → SDP 里没有 m-line payload,远端直接 reject
  • ICE 阶段:选不到 candidate pair → IceConnectionState 卡在 checking;常见原因是 TURN 凭据过期、对端在对称 NAT 但没配 TURN
  • DTLS 阶段:fingerprint 不匹配 → OnDtlsHandshakeError;常见原因是中间节点重写了 SDP 但没改 fingerprint
  • SRTP 阶段:远端 profile 不一致(AES_CM_128_HMAC_SHA1_80 vs AEAD_AES_256_GCM) → SrtpTransport::ProtectRtp 静默丢包
  • 媒体流阶段:codec 编码失败但帧仍在产生 → VideoStreamEncoder::EncodeVideoFrame 频繁回退到 FrameDropper

6. PeerConnection::CreateOffer 真实代码片段

下面摘自 m120 pc/peer_connection.ccpc/sdp_offer_answer.cc。代码经过裁剪,保留主干。

// pc/peer_connection.cc
void PeerConnection::CreateOffer(CreateSessionDescriptionObserver* observer,
const RTCOfferAnswerOptions& options) {
RTC_DCHECK_RUN_ON(signaling_thread());
sdp_handler_->CreateOffer(observer, options);
}
// pc/sdp_offer_answer.cc (节选)
void SdpOfferAnswerHandler::CreateOffer(
CreateSessionDescriptionObserver* observer,
const RTCOfferAnswerOptions& options) {
RTC_DCHECK_RUN_ON(signaling_thread());
// 1. 校验状态
if (!ExpectSetLocalDescription(SessionDescription::Type::kOffer)) {
PostCreateSessionDescriptionFailure(observer, ...);
return;
}
// 2. 把 transceiver 列表转成 MediaSessionOptions
cricket::MediaSessionOptions session_options;
GetOptionsForOffer(options, &session_options);
// 3. 真正生成 SDP
webrtc_session_desc_factory_->CreateOffer(observer, options, session_options);
}

webrtc_session_desc_factory_->CreateOffer 内部会调到 cricket::MediaSessionDescriptionFactory::CreateOffer,这一层负责把 transceiver 翻译成 m-line + codec preferences + RTP header extensions + ICE ufrag/pwd + DTLS fingerprint。

为什么这么设计:W3C JSEP 定义了三个状态机(PC state、Signaling state、ICE state),而 SDP 是平台无关字符串协议。SdpOfferAnswerHandler 把这两个世界隔开 — 上面是 W3C 状态,下面是 cricket 历史 SDP 工厂。

什么时候不适用:不要在 MediaSessionDescriptionFactory 里做业务定制(如硬编码私有 a= 行)。正确的做法是用 SetCodecPreferences 或在 OnSuccess 回调里改 SDP 字符串。


7. Call::CreateAudioSendStream 与媒体管线

// call/call.cc (节选)
AudioSendStream* Call::CreateAudioSendStream(
const AudioSendStream::Config& config) {
RTC_DCHECK_RUN_ON(worker_thread_);
EnsureStarted();

AudioSendStream* send_stream = new AudioSendStreamImpl(
clock_, &audio_state_->audio_processing()->config(),
worker_thread_, network_thread_,
transport_send_->packet_router(),
transport_send_->GetBandwidthObserver(),
bitrate_allocator_.get(),
event_log_, call_stats_->AsRtcpRttStats(),
suspended_rtp_state, config);
ConfigureSync(config.sync_group);
audio_send_ssrcs_[config.rtp.ssrc] = send_stream;
send_stream->RegisterWithTransport(transport_send_->packet_router());
return send_stream;
}

关键约束:

  • 必须在 worker_thread_ 上调用,否则 RTC_DCHECK_RUN_ON 触发崩溃(debug 构建)或竞态(release)
  • transport_send_->packet_router() 是所有 send stream 共享的 PacketRouter,BWE 通过它统一调度
  • bitrate_allocator_ 决定多路 stream 的总带宽分配。新增 send stream 后,旧 stream 的目标码率会被重新计算

8. VideoStreamEncoder 与编码线程

VideoStreamEncodervideo/ 目录里最重要的对象,负责把原始 VideoFrame 喂给具体的 VideoEncoder 实现,并处理:

  • 自适应分辨率/帧率(OveruseFrameDetectorResourceAdaptationModule
  • 多 stream simulcast 编码
  • 长时间缓冲区控制
  • 编码统计与 EncoderInfo 反馈给 BWE
// video/video_stream_encoder.cc (节选)
void VideoStreamEncoder::OnFrame(Timestamp post_time,
bool queue_overload,
const VideoFrame& video_frame) {
RTC_DCHECK_RUN_ON(encoder_queue_);
// 1. 计算 input fps,更新自适应模块
input_state_provider_.OnFrameSizeObserved(video_frame.size());
// 2. 走自适应裁剪(缩分辨率/降帧率)
VideoFrame out_frame = MaybeCropAndResizeVideoFrame(video_frame, ...);
// 3. 调具体 VideoEncoder
EncodeVideoFrame(out_frame, post_time);
}

这里 encoder_queue_TaskQueueBase*,独立于 worker_thread,每个 send stream 一条。这意味着:

  • 编码不会阻塞 BWE/Pacer
  • 但如果你在编码器实现里做了同步 IO(如把帧 dump 到磁盘),整条 queue 阻塞,输入帧会被 FrameDropper 丢弃

什么时候不适用:不要在 VideoEncoder::Encode 里做 std::this_thread::sleep_forpthread_cond_wait 之类的同步等待。正确做法是在自己的 worker 线程里编码,Encode 立刻返回 WEBRTC_VIDEO_CODEC_OK,编完用 EncodedImageCallback::OnEncodedImage 通知。


9. TaskQueue 与线程模型

webrtc 有两套并行的线程抽象:

  • rtc::Thread(旧,rtc_base/thread.h):libjingle 遗产,Signaling/Worker/Network 三大线程是 rtc::Thread
  • TaskQueueBase(新,api/task_queue/):每个独立模块自己的 queue,如 encoder_queue、pacer_queue、stats_queue、network_thread 上的 socket queue
// api/task_queue/task_queue_base.h (节选)
class TaskQueueBase {
public:
virtual void PostTask(absl::AnyInvocable<void() &&> task) = 0;
virtual void PostDelayedTask(absl::AnyInvocable<void() &&> task,
TimeDelta delay) = 0;
virtual void Delete() = 0;
static TaskQueueBase* Current();
};

TaskQueueFactoryCreatePeerConnectionFactoryDependencies::task_queue_factory 注入。默认实现按平台不同:

  • Linux/Android:TaskQueueLibevent(基于 libevent)
  • macOS/iOS:TaskQueueGcd(GCD)
  • Windows:TaskQueueWin(IOCP + 线程池)
  • 跨平台调试:TaskQueueStdlib(基于 std::thread)

为什么要替换 TaskQueueFactory:在嵌入式系统上要把所有 webrtc 任务集中到自己的事件循环里(如 boost::asio、folly::IOExecutor),减少线程数。

什么时候不适用:在桌面 Chromium 嵌入场景下不要替换。Chromium 已经把 TaskQueueFactory 桥接到 base::SequenceManager,自己再换一层会破坏 trace 与 thread checker。


10. JsepTransportController / DtlsTransport / IceTransport 协作

// pc/jsep_transport_controller.cc (节选)
RTCError JsepTransportController::MaybeCreateJsepTransport(
bool local, const cricket::ContentInfo& content_info,
const cricket::SessionDescription& description) {
if (GetJsepTransportByName(content_info.name)) return RTCError::OK();

// 1. 创建 ICE
std::unique_ptr<cricket::IceTransportInternal> ice =
CreateIceTransport(content_info.name, /*rtcp=*/false);
// 2. 创建 DTLS(包住 ICE)
std::unique_ptr<DtlsTransportInternal> dtls =
CreateDtlsTransport(content_info, std::move(ice));
// 3. 用 DTLS 派生 SRTP keys
std::unique_ptr<DtlsSrtpTransport> srtp_transport =
CreateDtlsSrtpTransport(content_info.name, dtls.get(), nullptr);

auto jsep_transport = std::make_unique<cricket::JsepTransport>(
content_info.name, certificate_, std::move(ice), nullptr,
std::move(dtls), nullptr, std::move(srtp_transport),
/*sctp_transport=*/nullptr, [this] { UpdateAggregateStates_n(); });
jsep_transports_by_name_[content_info.name] = std::move(jsep_transport);
return RTCError::OK();
}

P2PTransportChannel 是 ICE 实现,承担 candidate pair 选举、STUN binding、连续连通性检查、ICE restart。所有 ICE 事件经 signal_* 回调通知 DtlsTransportJsepTransportController

DtlsTransport 内部状态机:new → connecting → connected → closed/failed。一旦 OnDtlsHandshakeError 触发,整个 BUNDLE group 的 transport 全部失败,PC 进入 failed

失败时的样子:在弱网下 ICE 反复 restart,DTLS 也跟着 restart;DTLS 重协商会清空 SRTP key,导致播放端听到一段空白。这是已知问题,可以通过 IceConfig.continual_gathering_policy = GATHER_CONTINUALLY + surface_ice_candidates_on_ice_transport_type_changed 缓解。


11. 如何 Fork 定制

定制原则:永远在自己的 fork 上以 patch / cherry-pick 方式维护改动,不要直接基于 branch-heads/m120 写新代码后 force push。这样在 backport 到 m126、m132 时才有路径。

11.1 自定义编码器注册

不要去改 media/engine/internal_encoder_factory.cc。正确做法是在创建 PeerConnectionFactory 时传入自己的 VideoEncoderFactory

class MyVideoEncoderFactory : public VideoEncoderFactory {
public:
std::vector<SdpVideoFormat> GetSupportedFormats() const override {
return {SdpVideoFormat("H264", H264Params()), SdpVideoFormat("AV1")};
}
std::unique_ptr<VideoEncoder> Create(
const Environment& env, const SdpVideoFormat& format) override {
if (format.name == "H264") return std::make_unique<MyHwH264Encoder>(env);
if (format.name == "AV1") return std::make_unique<MyAv1Encoder>(env);
return nullptr;
}
};

PeerConnectionFactoryDependencies deps;
deps.video_encoder_factory = std::make_unique<MyVideoEncoderFactory>();
auto factory = CreateModularPeerConnectionFactory(std::move(deps));

为什么这么做:VideoEncoderFactoryapi/ 公共契约,跨版本稳定;改 internal_encoder_factory.cc 在每次 rebase 都会冲突。

什么时候不适用:仅当你需要让"内置 SW 编码器"也走你的路径,比如做编码 fallback,那要写一个 WrappingVideoEncoderFactory,先 try 自家硬编,失败再委托给 InternalEncoderFactory

11.2 自定义网络层

替换 rtc::PacketSocketFactory

class MySocketFactory : public rtc::PacketSocketFactory {
rtc::AsyncPacketSocket* CreateUdpSocket(...) override { return new MyUdpSocket(...); }
...
};

PeerConnectionDependencies pc_deps(observer);
pc_deps.allocator = std::make_unique<cricket::BasicPortAllocator>(
network_manager.get(), my_socket_factory.get(),
/*turn_customizer=*/nullptr);

如果要替换得更深(不走 UDP/TCP,比如走自研可靠 datagram),需要实现 IceTransportInternal 自己。这是侵入性最强的定制路径,要 override JsepTransportController::CreateIceTransport,多数情况不建议。

11.3 自定义 BWE

api/transport/network_control.h 提供 NetworkControllerFactoryInterface

class MyBweFactory : public NetworkControllerFactoryInterface {
std::unique_ptr<NetworkController> Create(NetworkControllerConfig config) override {
return std::make_unique<MyBweController>(config);
}
TimeDelta GetProcessInterval() const override { return TimeDelta::Millis(25); }
};

deps.network_controller_factory = std::make_unique<MyBweFactory>();

你的 NetworkController 会接收 TransportPacketsFeedbackRoundTripTimeUpdateTargetTransferRate 等事件,输出 NetworkControlUpdate(pacer rate、target rate、congestion window)。GoogCC 也是这个接口的实现,可以参考 modules/congestion_controller/goog_cc/goog_cc_network_control.cc

什么时候不适用:你只想"调阈值"。GoogCC 已经暴露大量 FieldTrial 参数(WebRTC-Bwe-LossBasedBweV2WebRTC-Bwe-MaxRttLimit),先用 FieldTrial。

11.4 调试 Trace 开关

webrtc 有四套日志/追踪:

  • RTC_LOG(LS_INFO):源码内日志,编译期通过 rtc_enable_logging=true 控制
  • RTC_HISTOGRAM_*:UMA 直方图,桌面端会聚合到 Chromium 的 metrics
  • RtcEventLog:二进制 protobuf 事件流,是排查 BWE/RTP/RTCP 的金标准
  • WebRTC-* FieldTrial:开关式实验

打开 RtcEventLog:

auto event_log = std::make_unique<RtcEventLogOutputFile>("/tmp/webrtc.log", 100*1024*1024);
peer_connection->StartRtcEventLog(std::move(event_log), /*output_period_ms=*/5000);

之后用 out/Default/event_log_visualizer --plot=all /tmp/webrtc.log 可以画出码率/RTT/丢包/PacingDelay 时间序列图,是定制 BWE 时唯一靠谱的工具。


12. 反模式

  • 直接改 m96 / m100 分支不打补丁、不维护 cherry-pick 链。三个月后想升级到 m132 时无法迁移,最终只能停在旧版本,CVE 也修不上。
  • 改公共头文件(api/)后只重编 webrtc,不重编上层 SDK。ABI 不兼容,运行时随机崩溃在 libwebrtc.solibsdk.so 边界。改 api/ 后必须 gn clean out/ && ninja -C out/Default,并触发上游全量重编。
  • Encode() / OnFrame() / 任意 TaskQueue 任务里做长时间同步等待。会拖死整条 queue,编码降级到 0 fps,BWE 误判为高延迟反复降码率。
  • 跨线程裸调 PeerConnection 方法PeerConnection 大多数方法绑定 signaling_thread(),跨线程调用时 debug 构建直接 DCHECK,release 构建是数据竞争。要走 signaling_thread()->PostTask(...)
  • 删除 RTC_DCHECK_RUN_ON 而不是定位线程错误。这条 check 是设计意图的体现,删了等于在 release 上引入隐藏 bug。
  • 替换 TaskQueueFactory 时让所有 queue 共享同一条线程。Pacer 和 Encoder 互相阻塞,体感是高码率档突然全部丢帧。
  • 改 SRTP profile 协商顺序但不改 BoringSSL ciphers。本地能握手,跨厂商互通失败。
  • 在 SFU 里复用同一个 Call 实例处理多客户端Call 里的 bitrate_allocator_ 假设所有 stream 共享带宽,对应不上多客户端独立网络条件。每个客户端必须独立 PC + 独立 Call。
  • BuiltinAudioEncoderFactory 又自定义 Opus 实现,不做 wrapping。会出现两份 Opus 同时注册,SDP 协商挑哪个不确定。

13. 阅读路径推荐

按目标倒推:

  • 想读懂 SDP 协商:pc/peer_connection.cc::SetLocalDescriptionpc/sdp_offer_answer.ccpc/media_session.cc
  • 想读懂 ICE:p2p/base/p2p_transport_channel.cc::SortConnectionsAndUpdateStatep2p/base/connection.cc::Pingp2p/base/basic_ice_controller.cc
  • 想读懂 BWE:modules/congestion_controller/goog_cc/goog_cc_network_control.cc::OnTransportPacketsFeedbackdelay_based_bwe.ccloss_based_bwe_v2.cc
  • 想读懂 NetEq(音频抖动缓冲):modules/audio_coding/neteq/neteq_impl.cc::GetAudiodecision_logic.cc::GetDecision
  • 想读懂视频接收:video/video_receive_stream2.cc::OnRtpPacketmodules/video_coding/frame_buffer3.cc::InsertFramemodules/video_coding/decoder_database.cc
  • 想读懂线程模型:rtc_base/thread.cc::ProcessMessages + api/task_queue/task_queue_base.h + 平台 task_queue_* 实现

每条路径建议带着一个 RtcEventLog + 一个 RTC_LOG_THREAD_BLOCK_COUNT debug 构建一起读,否则只看代码很容易把异步流程脑补成同步。


14. 权威资料

核对日期:2026-06-22