来源:飞一样的成长
原文:https://mp.weixin.qq.com/s/9AZnOR-UuvwUXQVi7kDfug
一块可视对讲板卡,已经能从摄像头取得图像,能用硬件编码器输出 H.264,也能通过声卡录音和播放。接下来,把这些能力放进一次跨网络的实时通话,还需要解决不少问题:双方怎样找到彼此,怎样确认使用同一种编码格式,网络突然变差时怎样维持画面,收到的数据应该等多久再播放,两边同时说话时怎样避免回声。
这些问题横跨设备、媒体处理、网络传输和业务控制。WebRTC 把其中大量实时通信能力组织在一起,但完整产品仍需要板级适配、应用逻辑和服务端配合。理解它的总体架构,首先要弄清每部分接收什么、交付什么,以及出了问题该去哪里找。
本文以具备操作系统和网络协议栈的 Linux 类嵌入式设备为例,讨论原生 libwebrtc 接入。场景是一对一可视对讲:设备向对端发送视频,双方同时收发语音;对端可以是浏览器,也可以是原生应用。扩展到双向视频时,双方都要具备相应的视频发送和接收链路,并在协商中启用对应方向;板卡还要为解码、呈现和并发处理留出资源。
源码以 2026 年 10 月 17 日核验的官方主干快照为基线,固定提交为 [225793c2c02d80b988a9e43629127dfb1766eabd][src-root],位置为 main@{#48945}。下文的源码链接均固定到这一提交。图中表达的是职责、主要数据流和控制关系,具体实现会经过回调、任务队列和多个内部对象。
1. 先把完整系统摆出来
1.1 WebRTC 在这里指什么
谈 WebRTC 时,经常会交替使用两个含义。
一层是通信标准:W3C 定义浏览器的相关 API,IETF 定义会话协商、连通性、安全和媒体传输所用的协议。另一层是实现这些能力的开源代码。本文研究的 libwebrtc,就是官方仓库中的原生实现,核心代码以 C++ 为主。[W3C 的 WebRTC 规范][spec-pc]与[官方仓库说明][src-root]分别对应这两层。
嵌入式设备可以直接集成原生库,通过 C++ 接口接入音视频设备,不需要先在板卡上运行一个网页。与浏览器互通时,两端可以采用不同的内部实现,但协商结果、编码格式和线上协议必须兼容。
下面先看设备外部的关系。

1.2 信令服务器负责让双方谈妥这次通信
设备发起呼叫后,对端需要知道是谁发起、是否有权连接,以及是否接受这次呼叫。这些是产品自己的业务逻辑。WebRTC 会产生协商需要的信息,但应用必须把它们送到另一端。
其中两类信息最常见:
- SDP Offer / Answer:描述这次会话准备收发什么媒体、支持哪些编码格式、采用什么方向,以及建立安全传输需要的参数。Offer 是一方提出的会话描述,Answer 是另一方基于能力和策略给出的应答。
- ICE candidate,候选地址:描述可能用于通信的地址、端口及相关属性。候选可以随 SDP 交换,也可以在后续逐步补充。
SDP 是描述信息,本身不负责把消息发到对端。实际项目可以用 WebSocket、HTTP 或已有的设备消息通道传递它们。消息格式、设备鉴权、呼叫编号、超时重试和掉线处理,都需要应用定义。
官方 PeerConnectionInterface 头文件开头就给出了这种分工:库创建 Offer、报告候选,应用把这些信息发给远端;远端收到后,再调用相应接口交回库。头文件还明确说明,是否接听或拒接由应用决定。
1.3 ICE、STUN、TURN 各自解决哪一段问题
很多设备使用私有地址。路由器的 NAT 会改写部分地址和端口,防火墙又可能限制外部流量。因此,“知道对端的局域网 IP”通常不足以在公网建立通信。
STUN 提供发现映射地址等能力。设备向 STUN 服务器发请求,可以获知服务器观察到的来源地址和端口。这个信息有助于生成候选,但仍要验证对端能否沿该路径实际通信。
ICE 负责组织候选收集、交换、连通性检查和路径选择。它会把本地与远端候选组成候选对,通过检查判断哪些路径可用。连通性检查本身也使用 STUN 消息,检查对象是候选对对应的通信端点;不能把所有 STUN 流量都理解成“发往公网 STUN 服务器”。
TURN 提供中继地址。当直连受限,或策略要求使用中继时,媒体可以经过 TURN 服务器转发。TURN 需要承担实际转发流量,因此它的带宽、部署位置和稳定性会影响通信。其基本职责是转发报文,通常不需要解码画面、混音或转码。
这里也就能区分两个容易混在一起的概念:P2P 描述端点之间的通信组织方式,语音对讲描述业务形态。 对讲可以采用直连,也可以经过 TURN 中继;P2P 同样可以承载视频和数据。一个采用 P2P 思路的产品,依然可能需要信令、鉴权和 TURN 服务。
多人系统常见的 SFU 则承担媒体选择和转发,是另一种服务端角色。典型 SFU 会分别与终端建立 WebRTC 传输,处理 RTP 层的流与反馈;它和 TURN 的透明报文中继不能当成同一个模块。第一篇先采用端点之间的一对一结构,后续再讨论多人拓扑。
2. 回到板卡内部:五部分怎样分工
设备侧最容易出现的问题,是把“WebRTC 已经支持某种能力”和“这块板卡已经接好了这种能力”当成一回事。板卡上的摄像头、硬件编码器、声卡和显示设备,都有自己的驱动接口、格式限制和缓冲机制。libwebrtc 需要通过适配层使用这些能力。
在应用接口中,一条音频或视频被表示为 Track,媒体轨道;提供媒体数据的对象称为 source,媒体源。视频源可以从摄像头取得帧,也可以由应用提供帧。轨道表达连接准备使用哪路媒体,源负责提供内容,具体区别将在后面的对象关系中继续展开。

| 组成部分 | 主要职责 | 可视对讲中的具体工作 |
|---|---|---|
| 硬件与驱动 | 获得和呈现真实世界的声音、图像,提供计算与网络能力 | 图像传感器、ISP、麦克风、ADC / DAC、硬件编解码器、显示、网卡及其驱动 |
| 平台适配 | 把板级能力接到 libwebrtc 约定的接口 | 视频帧源、音频设备模块、硬件编解码器封装、显示输出、格式与缓冲区适配 |
| WebRTC 媒体处理 | 把设备数据组织成可实时收发的媒体 | 音频处理、编解码调度、视频适配、RTP 收发、丢包恢复、接收缓冲、同步 |
| WebRTC 连接与传输控制 | 建立安全路径,并根据网络状态调整发送 | ICE、DTLS-SRTP、拥塞控制、带宽估计、发送节奏和传输统计 |
| 应用与服务端 | 管理一次业务通信及其产品行为 | 登录鉴权、呼叫与接听、会话信令、对讲话权、设备控制、信令与中继服务 |
这张表是根据接口职责做出的工程划分。实际代码里,媒体和传输之间存在交叉协调;某些组件既处理数据,也输出控制信息。
2.1 视频接入要处理数据和控制两个方向
常规视频发送路径从原始帧进入 libwebrtc。平台从摄像头或 ISP 取得图像,把像素格式、宽高、时间戳和缓冲区包装成视频帧,再交给视频 source。AdaptedVideoTrackSource 是实现这类源时可以使用的基类,它提供帧适配和分发相关接口。它不会替应用打开某个厂商的摄像头,也不会自动完成所有格式转换。
如果采用 SoC 硬件编码器,还需要实现相应的编码器适配。接口的一端是“给你一帧图像,完成编码后回调”;另一端是“现在请调整目标码率、处理关键帧请求、重新初始化或释放资源”。VideoEncoder 中的 Encode、SetRates 和编码完成回调把这些关系写得很清楚。
假如适配层能够持续交出 H.264,却没有把新的码率要求传到硬件,网络控制器即使判断正确,编码输出仍可能超过链路承载能力。接入完成的判断应覆盖这两个方向。
已有编码流水线也有接入空间。本文固定的源码快照中,存在 EncodedVideoFrameInjectorInterface,并定义了关键帧请求和码率分配通知回调。不过,接口存在不等于任意 H.264 码流都能直接转发:编码格式、时序、关键帧响应和网络调节能力仍需适配。本文的主图采用原始帧接入,便于把完整控制关系讲清楚。
2.2 音频设备接口和视频帧源不是同一种接法
音频通常通过 ADM,Audio Device Module,音频设备模块接入。ADM 管理采集和播放设备,负责启动、停止,以及向上交付采集 PCM、向下取得播放 PCM。自定义板级音频后端时,除了格式,还要处理声道、块大小、回调节奏和设备延迟。它不负责把 PCM 压缩成 Opus,也不等同于音频 3A。
板级驱动一次完成的 DMA 数据量,可能与上层处理块不同。适配层可以拆分或积累数据来对接,但积累得越多,通常就引入越多等待。只核对“采样率是 48 kHz”还不够,还要确认每次交付了多少采样点、对应多长时间、是否按真实采集节奏交付。
2.3 适配层最需要守住的三个约定
第一是数据约定。 NV12、I420 和 RGB 的内存布局不同;PCM 的位宽、声道排列也不同。所谓“支持某种格式”,还要落实到 stride、平面地址、可访问性和缓冲区生命周期。缓冲区交出去后立刻被驱动复用,可能表现为偶发花屏,而不是清晰的接口错误。
第二是时间约定。 采集时间、编码完成时间、RTP 时间戳和系统日历时间有不同含义。摄像头采集时刻应尽可能准确地随帧传递;不能把出队时间随意当成曝光或采集时间。音频也需要真实的采集、播放节奏。错误时间戳会干扰同步和延迟分析。
第三是执行约定。 动态码率、关键帧、暂停和停止必须有明确的落地行为。零拷贝也需要逐段验证:摄像头可以输出 DMA buffer,只说明它提供了一种缓冲区,不代表图像处理、编码、解码和显示都能直接使用同一块内存。
这些属于嵌入式集成时需要验证的接口条件。Linux 板卡上的经验,不能直接换算成任意 MCU 或 RTOS 上运行完整 libwebrtc 的资源承诺。
3. 从发起呼叫到出现第一帧,中间发生什么
把一次连接拆开看,可以看到几个不同层次的状态。
- 应用决定发起或接受通信。 完成设备身份和权限检查,确定是否开启摄像头、麦克风,以及允许的收发方向。
- 创建连接与媒体对象。 配置编解码能力和 ICE 服务器,把需要发送的 Track 加入连接,设置接收能力。
- 交换 Offer、Answer 和候选。 双方得到可执行的会话描述,并逐步获知可能的通信路径。
- ICE 检查并选出可用候选对。 本端和远端之间具备承载后续协议数据的路径。
- 建立安全传输上下文。 DTLS-SRTP 完成相关握手和密钥建立,媒体用 SRTP / SRTCP 保护。
- 媒体实际到达并呈现。 发送端交付编码数据;接收端完成去包、缓冲、解码和输出,最终产生可听声音或可见画面。
这个列表表达依赖关系,不是所有操作必须严格串行。采用 Trickle ICE 时,候选可以边收集边交换,连通性检查也可以提前开展;不需要等全部候选收集结束才做下一件事。
同样,ICE 已连通,只说明连接建立中的一个阶段取得进展。后续仍可能因为安全协商、编码不兼容、关键帧缺失、解码失败或显示适配问题而没有画面。排查时,应分别观察会话状态、ICE 状态、安全传输状态、RTP 到达、解码输出和实际呈现。
3.1 媒体、反馈和数据通道怎样放在一起
视频和音频的压缩数据通常由 RTP 承载,统计与控制反馈使用 RTCP。在 WebRTC 的安全传输中,对应使用 SRTP 和 SRTCP。这里有一个必须画准的关系:DTLS-SRTP 利用 DTLS 建立 SRTP 所需的密钥;常规媒体包并不是先装进 SRTP,再整体装入 DTLS 应用数据记录。 它们可以复用同一个底层传输,但协议用途不同。
DataChannel 则提供通用数据通信,使用 SCTP over DTLS。连接建立后,可以用它承载云台控制、对讲状态等消息。初始 Offer、Answer 不能依赖一条尚未建立的 DataChannel 交换;有序、可靠的数据通道也不应被直接当成视频 RTP 的替代品。
在常见的 BUNDLE 和 RTCP 复用配置下,多条媒体流可以共享底层传输。于是,音频轨道和视频轨道各自存在,并不意味着它们必然各占一个独立 UDP Socket。协议负责标识和分用不同的数据。
信令还承担安全上的关联作用,例如传递证书指纹。DTLS 验证对端证书与协商信息相符,产品仍需确保“协商信息来自谁”可信。仅仅创建出连接对象,并没有同时完成业务身份认证。
4. 顺着一帧视频,走完整条链路

4.1 采集端交出的是图像,不是可直接上网的画面
传感器曝光后,图像通常要经过 ISP 处理,再由驱动或厂商接口交出像素数据。曝光、降噪、锐化和颜色处理已经影响了画质。后面的传输系统只能处理得到的图像,无法保证恢复传感器阶段已经丢失的细节。
以 1920 × 1080、30 fps、8 位 YUV 4:2:0 为例,不计 stride 对齐和附加元数据,每帧约占:
1920 × 1080 × 1.5 = 3,110,400 字节
原始数据速率约为 93.3 MB/s,即 746.5 Mb/s。这里使用十进制单位,MB 是字节,Mb 是比特。这只是原始图像数据量的计算示例,说明了压缩编码的必要性,不是推荐的传输码率。
视频 source 把采集帧送入媒体链路。根据网络和处理能力,系统可能提出降低帧率或分辨率等要求。实际如何裁剪、缩放和丢弃帧,需要 source 与适配路径协作;不会因为有一个 Track,就自动把所有硬件管线调整好。
4.2 编码器把图像变成压缩帧
编码器利用空间和时间冗余减少数据量。相同分辨率下,静止画面和快速运动画面所需的码量可能差别很大。设置为 2 Mb/s,表达的是一个速率目标,不能据此推导“每一帧大小相同”或“每一毫秒都恰好产生相同数据量”。
压缩视频还存在帧间依赖。某个画面可能需要参考先前已经解码的画面,因此网络上少了一部分数据,影响可能延续到后续帧。关键帧可以帮助重新建立解码起点,但关键帧本身往往也会带来码量上升。画面恢复策略需要兼顾解码状态和当前网络承载能力。
当前 VideoStreamEncoder 负责连接帧输入、编码器以及码率等控制逻辑;它与底层 VideoEncoder 接口的分工,是后续分析硬编接入和动态码率时的重要入口。
4.3 一帧视频怎样装进 RTP 包
网络链路对报文大小存在限制。较大的编码帧通常需要按对应编码格式的 RTP 负载规则拆分,附上序列号、时间戳、流标识等信息,再进入发送链路。
这里至少有四个不同的单位:
| 名称 | 表达的对象 | 不能直接推出什么 |
|---|---|---|
| 原始视频帧 | 某个时刻采集或处理后的像素图像 | 不能据此确定压缩后大小 |
| 编码视频帧 | 编码器交付的压缩表示 | 不能据此认为只需一个网络包 |
| RTP 包 | 带有 RTP 头和媒体负载的传输单元 | 收到一个包不代表收到完整画面 |
| Track | 应用接口中的音频或视频轨道 | 不等同于一个包、一个编码器或一个 Socket |
同一视频帧的 RTP 包可以乱序到达,也可以只到一部分。接收端需要根据包和帧的信息恢复结构,再判断能否解码。RTP 里的序列号用于包层排序等处理,时间戳反映媒体时序;它们都不是现成的“端到端延迟值”。
4.4 接收端要决定现在解码,还是再等一下
接收端先完成安全处理和 RTP 分用,再进行去包、组帧、依赖判断以及必要的丢包处理。帧进入缓冲后,还要结合解码和呈现时机决定何时释放。
这就是视频接收侧“抗抖动”的主要含义:网络到包时间不均匀,系统需要把它整理为适合解码和呈现的媒体节奏。这里的抖动是网络时延变化,与摄像头电子防抖补偿画面运动属于不同问题。
缓冲能够吸收一部分到达波动,但等待也会增加延迟。缺包时继续等待,可能使帧有机会补齐;如果等待超过实时需求,即使后来补齐了数据,也可能已经失去呈现价值。视频还受到参考帧依赖影响,不能简单地照搬一个按包序号排序的队列。
在当前源码中,视频接收缓冲分布在多个模块。VideoStreamBufferController 协调帧缓冲和解码释放时机;它的统计代码以 jitterBufferDelay 为例,把范围定义为:从 RtpVideoStreamReceiver2 收到一帧的首个包,到控制器释放可解码帧。这个范围跨越多个处理环节,并不等于某一个队列的停留时间。FrameDecodeTiming 等模块进一步参与解码时刻的判断。
解码器输出像素后,还要经过显示适配、合成或显示队列,最终才能看见画面。因而“解码输出了第一帧”和“屏幕出现第一帧”,仍然是两个事件。
5. 清晰度、流畅度和延迟,怎样形成一个控制问题
只画媒体的正向箭头,还不能解释 WebRTC 如何适应弱网。发送端需要持续观察已经发出去的数据发生了什么,再调整后续行为。图 3 中的反馈线就是这一过程。
5.1 带宽估计给判断,拥塞控制给动作
发送时间、接收反馈、往返时间、丢包和到达时间变化等信息,可以帮助系统判断当前路径的传输状态。带宽估计关注当前能够使用多大速率等判断;拥塞控制利用这些观测和估计,决定如何调整发送目标与节奏,避免持续把数据堆进拥塞队列。
源码快照中,默认网络控制器工厂的普通配置分支创建 GoogCcNetworkController。后续讨论 Google Congestion Control(GCC),会以这条路径为主。工厂也存在其他受配置约束的分支,网络控制器还可以通过接口注入,因此不能把“WebRTC 的协议标准”和“某一个拥塞控制算法实现”画等号。
用于拥塞控制的接收反馈会进入发送端的网络控制逻辑,再通过速率目标、分配逻辑和编码器接口影响后续媒体输出。RtpTransportControllerSend 中的反馈处理和控制状态更新,连接了这几部分。
5.2 码率调整与 Pacing 必须配合
编码器调整未来产生的数据量。 目标降低后,可以改变压缩程度,并根据适配策略影响帧率或分辨率。实际执行存在响应过程;硬件编码器如何实现码率控制,也会影响短时输出。
Pacer 调整已经产生的报文何时发送。 编码器可能在短时间内交出一整帧,Pacer 按允许的发送节奏安排这些包,减少突发发送对队列造成的冲击。它本身不会把已经编码好的大帧变成更小的帧。
用一个简化例子理解两者的关系:如果媒体持续产生 2 Mb/s 的数据,而实际只能以 1 Mb/s 消耗队列,在不丢弃数据的假设下,每秒就会净积压约 1 Mb。仅把包发送得更平缓,无法消除这个差额;还要让数据产出下降,或由明确策略舍弃部分数据。这是速率失配导致队列增长的示意,不是 WebRTC 固定采用的参数。
5.3 音频、视频和恢复数据都要占用链路
一个可视对讲连接,除了视频编码负载,还要发送音频、重传、冗余数据、协议头和控制信息,某些阶段还可能产生探测或填充流量。因此,下面三个数需要分开观察:
- 控制器或分配器给出的目标码率;
- 编码器真正输出的编码码率;
- 按明确统计口径计算的实际网络发送量。
把视频编码目标设置为估计可用带宽的全部,容易忽略其他流量的占用。同一次媒体连接中的音视频也会共享部分传输资源和带宽判断,资源紧张时需要共同协调.
丢包恢复同样有成本。ARQ(自动重传请求)利用反馈及超时等机制触发重传,尝试补回丢失的数据。在 RTP 传输中,NACK 用于反馈缺失的包;发送端据此进行选择性重传时,RTX 可以提供相应的 RTP 重传封装。FEC(前向纠错)则预先增加冗余,使接收端有机会在满足保护条件时恢复部分丢失数据。它们分别受到往返时延、可用带宽、保护范围和播放期限等条件限制。增加恢复能力,需要同时观察总带宽和延迟,不能只看丢包后的恢复比例。
音频接收还可以在缺失数据时用 PLC(丢包隐藏)做听感补偿。PLC 生成的是用于减轻中断感的替代信号,与成功找回原始数据应分别统计和理解。
5.4 QoS 涉及一组协调策略
在产品讨论中,QoS(服务质量)往往涵盖带宽分配、拥塞控制、发送优先级、恢复和缓冲等策略;严格到网络设备层,又可能特指分类、调度及 DSCP 等机制。文章讨论“优化 QoS”时,应先说明所指的范围。
画面清晰度、冻结时间、交互延迟和声音连续性属于最终体验。把某个包优先发出,不代表整个公网一定按照该优先级转发;WebRTC 的传输规范也明确不假定 DSCP 标记一定得到网络保留和执行。
后续研究 BBR 时,也需要先区分实现位置。这里的视频 RTP 发送以当前 libwebrtc 的应用层网络控制器为对象;改变 Linux TCP 的拥塞控制配置,不会直接把这条 RTP / UDP 控制路径替换成 BBR。TURN 若使用 TCP 承载其中一段,那一段的 TCP 行为又是另一个需要单独分析的层次。
6. 双向语音:为什么采集和播放必须一起看
可视对讲里的语音链路,不能只画“麦克风 → 编码 → 网络”。全双工意味着双方可以同时采集、发送、接收和播放,本机扬声器与麦克风之间还存在真实的声学耦合。

6.1 采集 PCM 先经过适配和音频处理
在本文对应的 Native 典型路径中,ADM 交付采集 PCM,AudioTransportImpl::RecordedDataIsAvailable 进行格式选择、声道与重采样适配,再调用 APM 处理,随后向音频发送流分发,进入音频编码与 RTP 发送。
APM,Audio Processing Module,音频处理模块,面向实时通信提供多种语音处理能力。其接口处理线性 PCM,典型处理块约为 10 ms。以 48 kHz 单声道为例,10 ms 对应 480 个采样点;如果采用 16 位 PCM,数据量就是 960 字节。这个块大小不等于硬件 DMA 中断周期,也不等于每个 Opus RTP 包必须只包含 10 ms 音频。
常说的音频 3A,通常包括以下三类处理,其中噪声抑制也常写作 ANS 或 NS。
| 模块 | 解决的问题 | 集成时要特别注意什么 |
|---|---|---|
| AEC,回声消除 | 减少本机播放声音经声学路径进入麦克风后形成的回声 | 播放参考、采播时序和设备延迟是否可信 |
| NS,噪声抑制 | 减轻环境噪声对近端语音的影响 | 噪声压得更低,可能同时损伤语音细节 |
| AGC,自动增益控制 | 让语音电平处在适合处理和收听的范围 | 数字增益与硬件增益是否协调,前端是否已经削波 |
这和摄像头 ISP 中常说的 AE、AWB、AF 那组“3A”不同。音频处理也不是单个“音质增强”开关:创建了 APM,与启用了 AEC、NS、AGC 是不同条件;设备自身若已做了处理,还需要评估多级处理叠加后的结果。
VAD,即语音活动检测,用于判断当前片段是否含有语音。业务中的“开始讲话”“一句话结束”“释放话权”,还需要在检测结果上增加时序和状态逻辑。DTX(不连续传输)关注低语音活动时的编码与发送行为;它与 VAD、对讲业务状态各有分工。
6.2 AEC 为什么需要另一条方向的声音
设设备 B 的人正在说话。这段声音经过网络到达 A,在 A 的扬声器播放。扬声器发出的声音又可能被 A 的麦克风采到,随后沿 A 的上行链路返回 B,于是 B 听见自己的回声。
从 A 的麦克风看,它采到的信号可以粗略理解为:近端人的声音、扬声器返回的回声以及环境噪声的叠加。AEC 希望保留近端声音,同时估计并减轻由本机播放引起的那部分成分。要做这件事,它需要知道本机正在播放什么。
因此,AEC 参考应来自准备送往本机扬声器的播放 PCM。收到的压缩包还没有经历解码、补偿和混音,不能直接充当这个参考;复制一份本机麦克风数据也不能提供同样的信息。
在当前 Native 的 NeedMorePlayData 路径里,代码先调用混音器,再把混音结果送到 APM 反向流处理,随后重采样到设备请求的输出格式。图 4 据此放置参考点。Chromium 的相关入口有自己的 APM 安排,不能把这张 Native 图直接当成浏览器所有内部路径的逐函数说明。
实际板卡还要核对:硬件混音是否在参考点之后加入提示音,扬声器音量和处理链是否发生改变,录放缓冲是否带来额外延迟。这些变化可能让参考与真实播放之间的关系变差。开启 AEC 之后仍有回声,应先确认这条关系,而不是立即把问题归结为算法强度不足。
6.3 NetEq 怎样把到包节奏变成播放节奏
网络不保证每隔固定时间交付一包音频,声卡播放却需要按设备时钟连续取得 PCM。接收端因此需要在两个节奏之间做协调。
NetEq 是音频接收侧的缓冲、解码调度与补偿系统。它接收压缩的音频负载和相关信息,结合缓冲状态决定正常解码、补偿或时间尺度调整,并按接口节奏输出 PCM。时间尺度调整,是在必要时对输出音频做小幅的时间伸缩,调节缓冲中的音频积累量,并尽量减轻听感变化。当前 NetEq 接口的 InsertPacket 与 GetAudio 分别体现输入和输出;GetAudio 每次提供 10 ms 音频。
所以图里把音频解码画在 NetEq 的处理范围内。不能先固定画成“独立 Opus 解码得到 PCM,再把 PCM 放进 NetEq”。进入混音器前,每条接收流还可能做格式适配,随后才形成送给本机播放链路的 PCM。
音频可以通过短时补偿和时间尺度调整减轻中断感,视频则要考虑帧依赖和解码、呈现期限。两者都要处理网络抖动,但接收缓冲结构和可用补偿手段不同。理解这个差别,后面分析 NetEq 与视频 jitter buffer 才不会混淆。
6.4 对讲业务还要管理谁能说、什么时候说
全双工对讲允许双方同时说话,半双工对讲通常需要管理话权。呼叫、响铃、接听、忙线、超时、静音、PTT(按键说话)和断线恢复,都应由业务状态机明确规定,再映射到媒体启停或收发控制。
APM 不决定谁拥有话权,ICE 不决定是否接听,源码里的 Call 也不等于产品的呼叫管理器。把业务状态与媒体状态分开记录,才能解释“通话已接听但没有声音”这类跨层问题。
7. 回到源码:几个核心对象为什么同时存在
从应用侧看,最先接触到的通常是工厂、连接和轨道。从媒体内部看,还会遇到引擎、通道、收发流及传输控制器。它们对应的是不同职责。
| 对象或接口 | 在架构中的职责 | 阅读时要分清的边界 |
|---|---|---|
PeerConnectionFactory | 装配依赖,创建连接和媒体对象 | 它是创建与配置入口,不是每帧媒体必经的处理节点 |
PeerConnection | 协调一次连接的会话描述、收发关系和传输状态 | 算法和媒体数据处理分布在其他模块 |
MediaStreamTrack | 表示一条音频或视频轨道,关联媒体源 | Track 与物理设备、编码器、RTP 流并非一一同义 |
RtpSender / RtpReceiver | 表达 RTP 媒体的发送与接收关系 | 对象存在不意味着媒体当前就在收发 |
RtpTransceiver | 组合相同媒体类型的 Sender 和 Receiver,参与方向协商 | 在 Unified Plan 下与相应媒体描述及 mid 建立关联 |
MediaEngine 及具体音视频引擎 | 提供媒体能力和通道创建,将媒体配置接入内部实现 | 属于内部组织与适配抽象,不负责响铃和鉴权 |
Call | 在常规媒体路径中汇聚音视频收发流及共享资源 | 这里的“Call”是媒体实现对象,不能按中文类名推断业务职责 |
AudioSendStream / VideoSendStream 等 | 管理具体媒体收发链路 | 继续连接编解码、RTP 和反馈等模块 |
这组关系可以沿着 [api/media_stream_interface.h][src-track]、[api/rtp_transceiver_interface.h][src-transceiver]、[media/base/media_engine.h][src-media-engine] 和 [call/call.h][src-call] 验证。它们不构成一条“Factory → PeerConnection → Track → Transceiver → Call”的逐帧函数调用链。
有两个细节也值得提前记住。
其一,MediaStream 主要用于组织 Track。把音轨和视频轨道放到同一个容器,不会凭空补齐正确的采集时序和同步关系。同步最终仍依赖媒体时间信息以及接收、播放调度。
其二,在启用媒体的常规 PeerConnection 路径中,可以沿着工厂找到 Call 的创建;但当前工厂代码在缺少媒体引擎或相应工厂时可以不创建它。纯数据通信和可裁剪构建需要按实际依赖理解,不能把普通音视频路径的对象关系扩展为所有构建的绝对规则。
线程也应按当前版本阅读。信令处理、网络处理和媒体任务有不同职责,但不应据此画出“永久固定的三个独立线程”。当前接口已弃用与 network thread 分开的 worker thread 配置,内部还使用 TaskQueue;具体任务归属和生命周期将在专门的线程篇分析。
8. 端到端延迟到底加在哪里
“网络延迟不高,但画面总慢半拍”,在嵌入式音视频链路里完全可能发生。网络只是其中一段,设备和媒体处理中的队列同样会让数据等待。

可以先把总延迟拆成五部分:
采集与前处理包括曝光、传感器读出、ISP 和采集缓冲。某些采集接口吐出的是已经在队列里放了一段时间的旧帧,即使调用返回很快,也不意味着图像很新。
编码与发送准备包括等待编码、编码执行和发送队列中的时间。硬件编码速度快,仍可能因为内部缓冲或异步任务积压而增加延迟。
网络传输包括链路传播、网络设备排队,以及选择中继路径带来的实际路程和处理。网络 RTT 测量的是某种往返过程,不能直接当成单向媒体延迟,更不能简单用 RTT / 2 替代整条视频链路的延迟。
接收恢复与调度包括等待乱序包、丢包恢复、帧依赖满足和播放时机。接收缓冲需要在抵抗抖动与交互时延之间取得平衡,它只占总延迟的一部分。
解码与呈现包括解码执行、渲染排队、合成和显示刷新。音频则还有播放缓冲和 DAC 输出等环节。
实际测量时,应给这些区间选择互不重叠的起止点,再跟踪同一帧或同一段音频。若恢复与缓冲发生交叠,不能把两个统计量机械相加。视频“玻璃到玻璃”、音频“口到耳”的测量,更接近业务看到和听到的结果;跨设备时间戳相减之前,需要建立可信的时钟对应关系。
音画同步也是时间关系问题。音视频采集与播放可能来自不同硬件时钟;RTP 时间戳通常属于各自的媒体时钟域。接收端需要借助相应的时间映射和同步关系安排播放,仅仅把两个独立时间戳按数值大小比较没有意义。
8.1 为什么降低延迟有时会降低画面稳定性
缩小接收等待可以让画面更快,但也减少迟到包和重传包赶上播放的机会。降低编码缓冲可能改善交互,却要接受不同的压缩效率。提高冗余可以抵抗一部分丢包,但在总带宽固定时会挤占原始媒体的预算。
因此,讨论优化时应同时写出条件:网络模型、目标延迟、发送总预算、分辨率、帧率以及测试画面内容。某个参数在静止场景里改善了清晰度,不能直接推导它在高运动、突发丢包和低带宽场景下同样有效。
9. 怎样确认这套架构真的接通了
第一阶段的实战可以用一块 Linux 板卡和一个浏览器对端,先完成单向视频加双向音频,再逐项验证跨网络和弱网行为。验证顺序最好与上面的职责边界对应。
先确认本地采集和播放:图像格式正确,音频采样点数量正确,时间戳单调且含义明确,长时间运行没有缓冲复用错误。再确认协商和传输:双方到底采用什么编码,媒体方向是什么,当前候选对是否走中继,安全握手是否完成。
媒体跑通之后,观察编码输出、RTP 发送、接收、解码和呈现之间的数量与时间关系。这样出现故障时,可以区分“没有采到帧”“编码器没输出”“包没有到”“帧不可解码”和“解码后没显示”。
最后再施加可控的带宽、时延、抖动和丢包,观察控制闭环:网络目标是否变化,硬件编码是否响应,发送队列是否持续增长,接收端用多大代价换取恢复。官方的网络仿真和测试目录,可以作为后续搭建实验的入口。
| 需要回答的问题 | 建议保留的观测 |
|---|---|
| 连接为什么没建立? | Offer / Answer 结果、候选类型、选中候选对、ICE 与 DTLS 状态 |
| 画面为什么变糊? | 输入画面、编码目标与实际码率、分辨率、帧率、编码器资源限制 |
| 为什么冻结或花屏? | RTP 接收、丢包与重传、关键帧、组帧与解码输出、呈现事件 |
| 为什么延迟不断增加? | 各阶段时间戳、队列中数据的年龄、编码耗时、接收等待、显示排队 |
| 为什么对讲有回声或断音? | 采播 PCM、参考信号、设备延迟、APM 配置、NetEq 输出和声卡欠载情况 |
Stats 很有用,但需要先读字段定义。累积计数应结合时间窗口的增量解释;缺少的字段不能当作零;编码码率也不能拿一个未核对口径的字节计数直接代替。RTC Event Log 可以补充传输事件,端到端画面和声音测量则用于验证最终体验。
10. 读源码时,沿着数据和控制各走一遍
第一遍可以先追踪数据:视频从 source 到编码器,再到 RTP 和接收呈现;音频从 ADM 到 APM、编码和网络,再从 NetEq 回到播放设备。第二遍追踪控制:反馈怎样进入网络控制器,速率怎样分配,编码器怎样响应,接收端怎样决定继续等待或释放数据。
| 要追踪的行为 | 这个快照中的入口 |
|---|---|
| 原生连接怎样装配 | [examples/peerconnection/client/conductor.cc][src-example]、[pc/peer_connection_factory.cc][src-factory] |
| 会话怎样协调 | [pc/peer_connection.h][src-pc]、[api/peer_connection_interface.h][src-pc-interface] |
| 音视频媒体能力怎样接入 | [media/base/media_engine.h][src-media-engine]、[api/audio/audio_device.h][src-adm]、[api/video_codecs/video_encoder.h][src-encoder] |
| 视频怎样编码和按反馈调整 | [src-video-stream-encoder]、[call/rtp_transport_controller_send.cc][src-send-controller] |
| 网络控制和发送节奏在哪里 | [api/transport/goog_cc_factory.cc][src-gcc]、[modules/pacing/][src-pacer] |
| 视频怎样缓冲、等待和解码 | [src-video-buffer]、[src-decode-timing] |
| 采集音频和播放参考怎样相遇 | [src-audio-transport]、[api/audio/audio_processing.h][src-apm] |
| 接收音频怎样变成连续 PCM | [src-audio-receive]、[api/neteq/neteq.h][src-neteq] |
| 连通性和安全传输怎样落地 | [p2p/base/][src-p2p]、[pc/dtls_srtp_transport.cc][src-dtls-srtp]、[pc/srtp_transport.cc][src-srtp] |
官方示例在这一快照中使用 PeerConnectionFactoryDependencies 装配依赖,调用 EnableMedia 启用媒体支持,再调用 CreateModularPeerConnectionFactory 创建模块化工厂。初读时更值得关注“依赖在何处装配、事件向谁回调”,而不是先记住一长串创建参数。
至此,一次可视对讲可以被放回清楚的位置:平台负责可靠地提供和呈现媒体;libwebrtc 组织媒体处理、连接、安全传输和网络适应;应用与服务端管理这次通信为何发生、是否允许以及怎样结束。画质和延迟则是这些部分共同作用的结果。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。