在WebRTC传输层中有多种控制算法,可以被分为四个类别,拥塞控制;带宽估计;丢包恢复;JitterBuffer缓冲处理。这篇文章想要讨论的拥塞控制部分
对于拥塞控制(仅针对GCC V2& TransportCC,TransportCC是近些年推出用于代替GCC算法)主要作用在发送端,用于预测网络带宽瓶颈 ACK码率,控制编码器码率,和Pacing配合控制发包频率。首先应该清楚webrtc可以被分成两个协议栈,其一是SRTP协议栈,其二是SCTP协议栈。前者主要用于音视频数据传输没有ACK重传,后者主要用于控制信息等传输,将ACK报文封装在SCTP中传输,而gcc部分主要针对的是SCTP协议栈。但是两者都建立在UDP和DTLS加密传输之上。而选择ACK码率来作为带宽估计的核心指标,原因是发送端的发送码率并不代表环境中的真是码率,而解析接收端返回的ack,才能得到真实的数据发送情况。
而预测ACK码率,主要分成2种方式:Delay-based Controller; Loss-based Controller,
1 | Sender:给每个包打上 TransportSequenceNumber,记录发送时间 $T$。 |
gooc_cc模块
查看cosgestion_controller可以看到多个算法,其中goog_cc是核心的发送端拥塞控制方案,另外pcc、scream是实验算法,receive_side_congestion_controller是老旧版本的基于接收端的拥塞控制。
GoogCcNetworkController
实现了NetworkController接口,其中设置了一系列发包和收包的回调函数。分别引起不同的逻辑调用。例如OnNetworkRouteChange(NetworkRouteChange msg)回调处理网络路径变更,OnStreamsConfigUpdate(StreamsConfig msg)处理流配置更新。当音视频流的数量、最小/最大码率限制发生变化时,告知控制器调整边界约束。OnSentPacket(SentPacket msg)记录发送轨迹。每当一个 RTP 包离开发送端,Pacer 会通知控制器。控制器会将包的发送时间、序列号和大小存入 PacketInFlight 队列。没有这个函数,后续的反馈(Feedback)就无法计算出延迟差。
OnTransportPacketsFeedback(TransportPacketsFeedback msg)是最核心的一个结构,接收来自接收端的 RTCP 反馈包(包含每个包的接收时间戳)。将反馈数据与 OnSentPacket 记录的发送数据进行匹配。将整理好的“发送-接收”对传递给 DelayBasedBwe(触发 Trendline 预测)和 AcknowledgedBitrateEstimator。计算丢包情况并传递给 LossBasedBweV2。
TCC-Trendline
在AcknowledgedBitrateEstimator可以获得在过去一段时间窗口内,对方到底接受了多少比特的数据,得出最原始的吞吐量数据,但是这往往并不准确,在移动网络环境下,反馈包不是均匀到达,,因此简单估算器会认为瞬时带宽剧烈增长。而RobustThroughputEstimator通过复杂的过滤算法可以得到一个较为真实的过去一段时间带宽情况。
得到了多个时间段的带宽情况之后就可以预测下一个时刻的目标码率了,具体做法是通过两重验证逻辑来得到,分别是基于延迟估算(delay-based BWE)和基于丢包的估算(Loss-Based BWE),
Delay-Based
InterArrivalDelta 计算相邻包组之间的发送时间差和到达时间差。
TrendlineEstimator 利用最小二乘法对延迟偏离值进行线性回归。它不看单个包的延迟,而是看斜率。如果斜率为正且持续增长,说明路由器缓存快满了。
DelayBasedBwe 基于趋势线的输出(Overuse/Normal/Underuse),驱动一个状态机(AIMD:加性增、乘性减),计算出一个基于延迟的目标码率。
Loss-based
前面已经有两种方法用于拥塞评估,分别是bayes和TrendLine,都是基于延时的。
还有一种基于丢包的,实际上就是通过设置丢包门限来评定此时的网络传输质量。有两个版本,第一个版本类似TCP的加法增加乘法减少(AIMD)
- <2%,网络质量很好,可以加大码率
- 2% < x < 10%,说明网络与发送速率匹配
- 10%<,需要降低码率至(1-0.5*丢包率)*当前码率
第二个版本会结合当前吞吐量和延迟趋势,避免在网络稍微抖动时就大幅度断崖式降速。
辅助类
AlrDetector ALR (Application Limited Region) 非常关键。它判断:码率低是因为网不好,还是因为你(App 层)给的数据太少?如果是后者,算法会停止增加码率,防止误判。
ProbeController当连接刚建立或网速大幅好转时,它会命令 Pacer 发送特定大小的“探测包簇”,快速探测带宽天花板,而不是慢悠悠地通过 AIMD 增长。
LinkCapacityEstimator 它通过追踪长期的吞吐量表现,给出一个关于这条链路最大物理承载能力的估计值。
CongestionWindowPushbackController 基于发送队列积压情况直接压低码率。如果发送端本地积压了太多包没发出去,它会绕过复杂算法直接“按死”发送速率,防止本地内存溢出。
Pacer
在goog_c模块完成计算后会更新NetworkControlUpdate(位于api模块networktypes),并由RtpTransportControllerSend(位于call模块)发出到各个子模块,其结构如下
1 | std::optional<DataSize> congestion_window; |
std::optional
std::optional
std::vector
std::optional
pacing_controller
pacer_config $\rightarrow$ PacingController::SetPacingRates这是最直接的参数注入。它更新了 Pacer 内部的 pacing_rate_(发包速率)和 padding_rate_(填充速率)。
probe_cluster_configs $\rightarrow$ BitrateProber::CreateProbeCluster如果指令里包含了新的探测任务,bitrate_prober.cc 会立即根据给定的速度和包数量生成一个探测簇。
其中维护一个packet_sender类用于发包
1 | class PacketSender { |
interval_Budget
发包不是简单地把 pacing_rate 设给一个定时器。Pacer 内部使用了一个漏桶算法(Leaky Bucket)的变种,把速率转化成了字节额度。每隔一小段时间(比如 5ms),Pacer 会根据 pacing_rate 给预算桶里充值一定量的字节。每发出去一个包,就从桶里扣除相应的字节。如果桶空了,说明本周期的配额”完了,必须等待下一个周期的充值。
1 | void IntervalBudget::set_target_rate_kbps(int target_rate_kbps) { |
prioritized_packet_queue
一个非常优雅的优先级队列,我认为设计的很好。其内部维护了多个fifo子队列(五个优先级,依次为kAudio、kRetransmission、kVideo、kForwardErrorCorrection、 kPadding),Pop 的时候,永远先从优先级为 0 的子队列拿包。只有 0 号为空,才看 1 号。在同一个优先级内部,遵循“先来后到”原则。这样可以保证同一条流(SSRC)的包序列号顺序不被打乱,减少接收端丢包重组的压力。会记录每个包入队的 enqueue_time,这不仅是为了排序,更是为了监控延迟。
这个队列还会记录很多冗余信息来帮助其他模块,包括记录队列最老的一个包是何时进入的,来判断是否发生了队列延迟。记录当前队列总共有多少字节和多少包,告知ALRdetector是当前网不行还是压根没数据可发。记录ssrc(同步源),帮助pacer在多流并发时决策均衡哪个流。自动清理,会清理一些延时包。
1 | class QueuedPacket { |
其他
TaskQueuePacedSender封装了pacercontroller,并执行具体的发包逻辑。通过webrtc内部的taskqueue计算出下一额发包具体时间,并提交一个延迟任务,WebRTC 的网络反馈可能来自 NetworkThread,而媒体数据生成可能在 WorkerThread。TaskQueuePacedSender 将所有对 PacingController 的调用(如推包入队、更新速率)都封装并分发到同一个 TaskQueue 中执行,从而避免了复杂的加锁逻辑。
PacketRouter决定包应该走哪个socket发送,PacketRouter 维护了一个映射表,记录了所有当前活跃的发送模块(RtpRtcp 实例)。当 Pacer 弹出一个包时,PacketRouter 会根据包头中的 SSRC(同步源标识)找到对应的 RtpRtcp 模块,并调用其发送接口。当拥塞控制算法要求“占位”发一些 Padding(垃圾包)来探测带宽时,PacketRouter 负责协调哪个模块来产生这些包。通常它会找最近发过包的模块来生成,以确保网络路径的一致性。它还充当了反馈信息的传递者,将底层网络层收到的反馈信息(如包何时到达接收端)路由回对应的拥塞控制模块。