上面这张图是一个比较老的架构图,但是也基本能说明整体架构,早期webrtc版本带宽估计是放到接收端处理,目前最新版本带宽估计放到了发送端,但是接收端计算得到的带宽并没有废弃,而是通过rtcp remb反馈给发送端。
在发送端带宽估计由3个元素结合决定,基于丢包率估算的带宽(丢包率通过rtcp rr得到)、接收端的remb反馈的带宽、发送端带宽估计(方法类似于接收端的带宽估计,具体逻辑下面会介绍到),取三者带宽的最小值作为最终带宽估计值。
https://tools.ietf.org/html/rfc3550#section-6.4.2
接收端会通过rtcp rr将丢包率和丢包累计值反馈给发送端,发送端据此更新丢包率和rtt,具体逻辑后面会介绍
https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03
todo:字段具体计算方式待研究,可以参考源码remb.cc -->Remb::Parse
https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01
详细说明可以参见上面两篇文章,总结来看发送端会在session的层面统一对rtp包进行计算transport-cc sequence num,区别于之前的sequence num,原因是transport-cc目的是计算两点之间的带宽,两点通信可以传输多个视频流(通过ssrc来标识)。
接收端维护两个计数器,每收到一个RTP包都更新:
transmitted,接收到的RTP包的总数;
retransmitted,接收到重传RTP包的数量;
某时刻收到的有序包的数量Count = transmitted-retransmitte ,当前时刻为Count2,上一时刻为Count1;
接收端以一定的频率发送RTCP包(RR、REMB、NACK等)时,会统计两次发送间隔之间(fraction)的接收包信息
两次发送间隔之间理论上应该收到的包数量=当前接收到的最大包序号-上个时刻最大有序包序号 uint16_t exp_since_last = (received_seq_max_ - last_report_seq_max_);
两次发送间隔之间实际接收到有序包的数量=当前时刻收到的有序包的数量-上一个时刻收到的有序包的数量 uint32_t rec_since_last = Count2 - Count1
丢包数=理论上应收的包数-实际收到的包数 int32_t missing = exp_since_last - rec_since_last,missing即为两次发送间隔之间的丢包数量,会累加并通过RR包通知发送端
接收端发送的RR包中包含两个丢包,一个是fraction_lost,是两次统计间隔间的丢包率(以256为基数换算成8bit),一个是cumulative number of packets lost,是总的累积丢包。
基本数据结构
struct RtpPacketCounter {
uint32_t packets; // Number of packets.
}
struct StreamDataCounters {
RtpPacketCounter transmitted; // Number of transmitted packets/bytes.
RtpPacketCounter retransmitted; // Number of retransmitted packets/bytes.
}
class StreamStatisticianImpl : publicStreamStatistician {
StreamDataCounters receive_counters_;
}
更新transmitted和retransmitted
丢包率计算
todo:整体的流程和协议理解的基本一样,唯一没弄清楚的是实际收到的去重包的数量为啥要加上retransmitted_packets,加上不是就变成了所有实际收到的包吗?
基于丢包率进行带宽估计的必要性,先看下《draft-alvestrand-rmcat-congestion-03》文档的一段话。基于delay-based的带宽估计仅适用于中间路由器buffer队列足够大的情况,在小缓存场景rtt(包括绝对rtt和相对rtt)变化都不明显。
除了论文中提到的小缓存场景,在实际应用中,有一种情况是运营商限速,抽象来看,此种场景类似于无缓存或小缓存的路由器行为。在此种场景下,一旦带宽高于带宽限制就会有丢包,一旦带宽低于带宽限制丢包就停止。由于中间无缓存或缓存较小,所以无论原始包还是重传包RTT不会有明显变化(包括绝对值和相对值)因此不会触发GCC基于RTT变化的带宽调整,适合采用基于丢包率的带宽估计。同时GCC在不丢包时主动上探,丢包时立刻下探,导致带宽估计波动,照成视频卡顿。所以在RTT变化不明显(缓存小或无),应抑制带宽频繁的上探和下探,防止带宽估计波动较大。
As_hat(i) =As_hat(i-1),2%<=丢包率<=10%,保持不变
As_hat(i) = As_hat(i-1)(1-0.5p),丢包率>10%,下降,p是丢包率
As_hat(i) = 1.05*As_hat(i-1),丢包率<2%,上升
同时As_hat(i)是有上限和下限的,下限TFRC是一种TCP友好的速率控制公式,这个公式产生的带宽相对与 TCP 来说相当公平,且速率较为平稳。 上限A_hat(i)是基于delay-based估算的带宽值。
基于延时梯度的带宽估计是WebRTC GCC最为核心的拥塞控制算法,此算法在最新版的WebRTC有两种实现方式,一种是在接收端、另一种在发送端,两者唯一区别是到达时间滤波器不同,接收端是卡尔曼滤波器,发送端是trendline滤波器,其他完全相同
在 WebRTC 中,延迟梯度不是一个个包来计算的,而是通过将包分组,然后计算这些包组之间的延迟,这样做可以减少计算次数,同时减少误差。包组的划分原则为2个
在一个burst_time间隔内的一系列的包构成一个组。建议burst_time为5ms
任意到达时间间隔小于 burst_time 并且组间延迟差 d(i)< 0 的组都被考虑做为当前组的一部分。
d(i) = t(i) - t(i-1) - (T(i) - T(i-1))
其中T(i)是当前包组的最后一个离开的时间,t(i)是当前包组最后一个达到的时间,所有乱序的包都会被到达时间模型所忽略。
可以想象,当d(i)>0,到达时间差大于发送时间差,网络有拥塞,反之网络状况好转。GCC拥塞控制大体上也是基于这种思路估算当前网络拥塞情况。
todo,待补充
分析trendline最方便的方法是直接看WebRTC源码,见trendline_estimator.cc
滤波器参数
Trendline 滤波器的三个重要参数分别是:窗口大小、平滑系数、延迟梯度趋势的增益。
窗口大小决定收到多少包组之后开始计算延迟梯度趋势;平滑系数用于累计延迟梯度的一次指数平滑计算;对累计延迟梯度平滑值进行最小二乘法线性回归之后求得延迟梯度趋势,会乘以增益并和阈值作比较,以检测带宽使用状态。下面是 WebRTC 中这三个参数的默认值。
LinearFitSlope
该函数使用最小二乘法求解线性回归,输入 window_size_ 个样本点(arrival_time_ms, smoothed_delay),输出延迟梯度变化趋势的拟合直线斜率 trendline_slope。
update函数
voidTrendlineEstimator::Update(double recv_delta_ms,
double send_delta_ms,
int64_t arrival_time_ms)
此函数重点关注下面三件事
计算当前的延迟梯度累计值,如代码里的accumulated_delay_
计算延迟梯度累计值的一次指数平滑值,如代码里的smoothed_delay_,同时也是最小二乘法的y
最小二乘法线性回归求延迟梯度趋势斜率a,如代码里的trendline_
这个预测的斜率值 trendline_可以表征网络的拥塞程度(网络缓冲区,即路由器数据包排队的消涨情况)
trendline_过大,表征d(i)>0网络拥塞
trendline_适中,表征d(i)=0网络正常
trendline_过小,表征d(i)<0网络空闲
上面这张图其实是【Gcc-analysis】中对卡尔曼滤波器的分析示意图,不过卡尔曼滤波器和trendline滤波器最终解决的问题是一样的,卡尔曼算出m(ti),trendline算出斜率trendline_,然后和自适应阈值-r(ti)~r(ti)相比较得出当前网络状态(overuse、underuse、normal)。相比较的过程非常简,但是难点在于阈值的设定上。
为什么阈值不是静态的而是要变化自适应的
【Gcc-analysis】给出了两个理由,不过第一个理由貌似用自适应阈值也很难解决,应该采用丢包率来评估带宽,主要应该还是第二个理由。第二个问题应该是GCC和BBR拥塞控制算法一个核心要解决的问题。TCP的拥塞控制算法主要依据是丢包重传,而当网络出现丢包往往是中间路由器buffer被填满的时候,此时网络传输延时比较大。而像GCC和BBR这种基于延时来判断网络拥塞的算法,期望达到的效果是在延时最低而网络吞吐量达到最大,很难竞争过TCP的这种流氓算法,如果一味避让最终必然会被饿死,所以GCC在设计时,在和TCP竞争时会适当提高阈值。
自适应阈值带来的效果
网络带宽变化时
与tcp竞争时
【Gcc-analysis】文档中给出了详细测试分析,通过上面两张图可以看出在自适应阈值的情况下,吞吐量、延时、丢包率都会有明显改善。
detalT是包组到达的时间差
当m(ti)在阈值范围内时(-r~r),减小阈值,反之则增大阈值,kr决定了阈值增大和减小的速度,【Gcc-analysis】建议kd=0.00018,ku=0.01,【Gcc-analysis】有详细的推导过程。阈值上涨速度要大于阈值下降速度,保证和TCP竞争的公平性。
过载检测
过载触发条件需要同时满足4个条件
延时梯度>当前阈值
过载时间(发送的时间差)> 10ms
过载次数>1
当前斜率值大于之前计算的斜率值,延时不断恶化
阈值更新
阈值更新公式前文已经提及,唯一不同的点,kd默认为0.039,ku设置为0.0087。
前几节所介绍的内容总结来看既是通过延时梯度+trendline滤波器,估算延时梯度斜率值,并与自适应阈值比较得出当前网络状态(underuse、overuse、normal),本节则是以此作为输入事件,再结合当前网络控制状态(decr、incr、hold)+当前码率值,得出估算码率值
overuse |
decrease |
decrease |
decrease |
normal |
increase |
increase |
hold |
underuse |
hold |
hold |
hold |
一个典型的状态转换图
overuse转换为decrease,normal向上一个状态转换都还好理解,为什么underuse信号需要转换为hold?
当有under use信号产生时,表示当前网络正在排空中间路由器buffer,为了保证将当前网络的buffer排空,评估出当前网络的最大带宽+最小延时,所以under use信号下一个状态均是hold,维持当前码率不变直到normal信号产生。
如果按照buffer角度看这3个信号
overuse-->buffer再堆积
underuse-->buffer再减少
normal--->没有buffer的状态
速率控制三个状态和三个signal
三个状态:hold、increase、decrease
三个信号:underuse、overuse、normal
收敛区间
GCC的码率控制,有点类似于TCP的cubic算法,当接近临近值时,码率增速放缓,当远离临近值时,增速增大。
如果我们之前在 Decrease 状态,且当前的输入的 bitrate 的值 R_hat(i) 逼近 在 Decrease 状态下 输入的 bitrate 的平均值(在队列排空之后,这个 bitrate 如果和排空过程中的 bitrate 差不多,则这个 bitrate 近乎达到了通道的最大的 bitrate。),则我们离收敛不远了。这个不远,我们定义为3个标准差之内。我们建议使用0.95作为指数平滑系数来测量输入的 bitrate 的平均值和标准差,因为我们认为它可以覆盖在 Decrease 状态下的多个场景。
GCC草案中码率估算方法
R_hat(i),对应代码里的acked_bitrate_bps,发送的总包大小/duration得出的测量值,其中duration建议在0.5~1s之间
A_hat(i),估计值,我们必须确保我们的估计值不会远离发送端实际的发送码率。我们需要将可用带宽估计值限制到一个范围:A_hat(i) < 1.5 * R_hat(i)
increase:乘性增加+加性增加,在3个标准差内采用加,在3个标准差之外采用乘
乘性增加:
在乘性增加期间,这个估计值 A_hat(i) 大概每次增加 8%。
eta = 1.08^min(time_since_last_update_ms / 1000, 1.0)
A_hat(i) = eta * A_hat(i-1)
加性增加:
在线性增加期间,每过 response_time 的间隔时间,这个估计值 A_hat(i-1) 会增加大约半个数据包的大小。
response_time_ms = 100 + rtt_ms,其中100ms代表的是过载检测器的处理时间
beta = 0.5 * min(time_since_last_update_ms / response_time_ms, 1.0)
A_hat(i) = A_hat(i-1) + max(1000, beta * expected_packet_size_bits)
decrease
A_hat(i) = alpha * R_hat(i),alpha在范围[0.8 0.95]之间,建议为0.85
hold
hold状态是为了进入increase状态之前,排空已有的队列
当我们检测到 under-use 信号,我们检测到网络路径上的队列正在清空,这表明我们的可用带宽估计值 A_hat 低于实际的可用带宽。(但是此时输入的 bitrate 正处于带宽最大值)。此时,系统将要进入 Hold 状态,此时接收端的可用带宽估计值会维持不变,直到队列长度稳定。这是一种控制低延迟的方法。延迟的减小可能是由于这里的带宽估计值减小,也有可能由于一些交叉网络的连接减少了。 我们建议每个 response_time 间隔都要更新一个 A_hat(i)
收敛区间
上文已经介绍,GCC的码率控制有点类似于TCP的cubic,当码率增长到收敛区间附近,码率增长会缓慢,当远离收敛区间则码率增长会快速,那么在码率上涨期间,收敛区间的判断则变得尤为重要。
何为收敛区间,在长肥管的场景下,整个传输链路bottleneck带宽则是最大传输带宽, GCC草案建议最大带宽上下的3个标准差则认为是收敛区间。那么最终问题就归结为如何得到最大传输带宽,可以想象下在中间路由器buffer填满的情况下,当前的带宽则是最大传输带宽,回到GCC的场景下,正好对应overuse信号产生进入Decrease状态。所以GCC在decrease状态下计算最大码率和最大码率标准差。
在码率上涨期间,则根据最大码率均值和标准差来判断到底是加性增加还是乘性增加。
这个和GCC草案一致
加性增加
总结:每个response time(rtt+100ms)增加一个包大小,这个值如果换算为每秒小于4kbps,则取4kbps,也就是每秒最少增加4kbps
注意:
每秒增加4kbps,这个值是否适合vipkid?需要实际验证才行
需要注意的是GCC草案中是每个response time增加半个包的大小和实际代码不一致
乘性增加
每次增加为当前码率的8%和1kbps最大值,注意是每次(大约是rtt时间)不是每秒,如果按照40kbps和rtt=200ms估算,每秒大约增加5*40*0.08=16kbps
这个和GCC草案实现是一致的。
降低
这个和GCC草案基本一致
上面已经零星介绍过一些主要函数的实现方法,下面重点从整体代码结构的角度将GCC整体的代码流程加以介绍
代码分支M67
RTCPReceiver:rtcp包分发者,不同类型rtcp包分发给不同观察者(RtcpPacketTypeCounterObserver、RtcpBandwidthObserver、RtcpIntraFrameObserver、TransportFeedbackObserver、VideoBitrateAllocationObserver)
SendSideCongestionController:发送端的带宽估计模块,主要是基于transport cc的feedback进行基于延迟的带宽估计
BitrateControllerImpl和SendSideBandwidthEstimation:发送端的实际带宽控制模块,会结合rr、remb、transport cc feedback综合考虑,估算出理想的带宽值。
rr:更新丢包率和rtt---->SendSideBandwidthEstimation::UpdateReceiverBlock
remb:更新接收端估算的带宽--->SendSideBandwidthEstimation::UpdateReceiverEstimate
transport-cc:将接收端计算出来的rtt延时上报给发送端,然后发送端采用接收端几乎一样的算法估算带宽(滤波器发生了变更,kalman-->trendline)---->SendSideBandwidthEstimation::UpdateDelayBasedEstimate
开始发送的时候,在生成transport cc sequence number的时候将包添加到sendSideCongestionController中,实际发送的时候,将包发送时间记录到sendSideCongestionController中。
RTCPReceiver::TriggerCallbacksFromRtcpPacket--->
BitrateControllerImpl::OnReceivedRtcpReceiverReport-->
BitrateControllerImpl::OnReceivedRtcpReceiverReport-->
SendSideBandwidthEstimation::UpdateReceiverBlock
UpdatePacketsLost
UpdateRtt
RTCPReceiver::TriggerCallbacksFromRtcpPacket--->
BitrateControllerImpl::OnReceivedEstimatedBitrate-->
SendSideBandwidthEstimation::UpdateReceiverEstimate,设置接收端的带宽反馈结果
RTCPReceiver::TriggerCallbacksFromRtcpPacket--->
SendSideCongestionController::OnTransportFeedback--->
BitrateControllerImpl::OnDelayBasedBweResult-->
SendSideBandwidthEstimation::UpdateDelayBasedEstimate
此模块的输入是rr、remb、本地基于延迟的带宽估计模块结果,输出是估计带宽、丢包率、rtt
rr,根据report block更新丢包率和rtt,同时触发一次码率更新逻辑(UpdateEstimate)
rr-->UpdateReceiverBlock
UpdatePacketsLost
UpdateEstimate
基于丢包率,估算当前码率调整方式
CapBitrateToThresholds,远端计算的带宽估计值、发送端基于延迟计算的带宽估计值、基于丢包率估算的带宽值、配置的最大最小带宽值,选择一个最小值。
UpdateUmaStatsPacketsLost
UpdateRtt
remb,
remb-->UpdateReceiverEstimate
CapBitrateToThresholds,算法同上
transport cc feedback-->UpdateDelayBasedEstimate
CapBitrateToThresholds,算法同上
CapBitrateToThresholds
bitrate=min(bitrate_in, remb_bitrate, delay_based_bitrate_bps_)
同时bitrate要受到max_bitrate_configured_和min_bitrate_configured_的限制,计算最终的估计bitrate。
rr-->
process()-->UpdateEstimate
如果当前没有丢包,并且在2s以内,bitrate_in = max(remb, delay_based_bitrate_bps_),调用CapBitrateToThresholds
UpdateMinHistory,维护一个码率按照升序排队的队列
删除过期值
从后面开始删除比当前码率值大的值
将当前码率值pushback到队列中
当前码率也是即将变成旧值的码率,新码率即将被估算出来
如果未收到rr,CapBitrateToThresholds
判断和上一次rr反馈的结果还在时间间隔内
当前码率<码率阈值 || 丢包率<2%
设置码率为1.08*本轮间隔最小值
注意这块的逻辑,当丢包率<2%时,其实并没有判断remb和基于延时计算的带宽,直接增加带宽
当前码率>码率阈值
丢包率在2%和10%之间,do nothing,丢包率的最大最小阈值是在代码里写死的,这个区间在国内环境是是否适用?
丢包率>10%,如果之前没有因为丢包而降低带宽,同时上次降低带宽同时时间周期没有超过300ms+rtt,则将带宽设置为current_bitrate_bps_*(1-0.5*丢包率)
如果反馈消息超时到达,再超时的实验性开关开启的条件下,将带宽设置为原来的80%
CapBitrateToThresholds,注意这块,将基于丢包计算出的带宽作为输入,再结合当前remb、基于延时估算出的带宽、码率阈值的配置,得出最终码率值
这个模块主要是根据transport cc feedback进行带宽估计,主要是基于延时进行估计,大体功能相当于之前的接收方带宽估计模块,会将带宽估计的结果上报给SendSideBandwidthEstimation的更新函数UpdateDelayBasedEstimate更新估计结果
主要包含模块如下:
TransportFeedBackAdapter:记录每个包对应的创建、发送、接收时间和状态
AcknowledgedBitrateEstimator:根据反馈包处理,计算一个时间范围内的收包码率
delay_based_bwe:整个delay based bandwidth estimation模块,基于延时的带宽估计模块,从接收端移到发送端唯一区别是更改了滤波器,卡尔曼滤波器-->线性滤波器
主要模块功能如下:
interArrival:主要对到达时间进行小范围统计、采样,并根据一定的时间间隔计算出对应的延迟、传输大小变化
trendlineEstimator:线性滤波器,相当于卡尔曼滤波器+overuse detector
AimdRateControl:相当于TCP拥塞控制,慢启动、拥塞避免
transportFeedbackAdapter模块中包含了一个记录所有包收发的sendTimeHistory模块,这个模块记录如下内容:
包的序号,这里是transport cc的sequence number(调用点是RTPSender::prepareAndSendPacket)
transport cc包序号生成时间,addPacket的时候生成
实际包发送时间,onSentPacket
在transport cc feedback到来时,计算每个packet chunk中的每个包的到达时间
这个模块主要是统计单位时间内(500ms)接收方接收的码率,并输出给下游组件(delay based estimator),作为其初始带宽估计值和本次计算的输入值
当第一次进行估计的时候,初始窗口为500ms,后续每次时间窗口为150ms
每次根据feedback统计时间窗口内的平均码率,没超过时间窗口的时候会使用旧的估计值进行计算,实际计算并不是单纯的根据统计到的值进行计算,还会增加一个贝叶斯对统计的值进行估算,根据估计值和观测值重新计算估计值
RTCPReceiver::IncomingPacket
RTCPReceiver::TriggerCallbacksFromRtcpPacket
SendSideCongestionController::OnTransportFeedback
TransportFeedbackAdapter::OnTransportFeedback--->记录transport cc feedback vector
vector
AcknowledgedBitrateEstimator::IncomingPacketFeedbackVector-->估算初始码率
大体逻辑是在包发送之前,记录了每个包的bytes,同时根据包的arrive time时间差作为时间,用包总大小/时间差即可估算平均码率,当然这个统计是基于某个时间窗口期内的。代码中将本次估算结果作为输入+贝叶斯估算,将估算出来的最终结果作为本次码率的最终估算值。最终结果保存在bitrate_estimate_,可以通过BitrateEstimator::bitrate_bps获取到,这个结果是作为基于延时估算的初始输入和每次参考值
贝叶斯估计的作用?
BitrateEstimator::Update,更新估算的码率值
DelayBasedBwe::IncomingPacketFeedbackVector-->基于延时梯度+trendline滤波器+变化阈值-->网络状态,网络状态+前一次码率-->估算当前码率
遍历所有packet
IncomingPacketFeedback
InterArrival::ComputeDeltas
估计当前包的时间梯度值,包括发送时间和接收时间
根据kTimestampGroupLengthTicks将包进行分组,记下每个分组的最后一个发送时间和接收时间,这些时间都是按照升序排好序的
发送时间detal=用当前分组的最后一个包的发送时间-前一个分组最后一个包的发送时间,接收时间类似
TrendlineEstimator::Update,根据时间梯度,计算当前网络负载状态,状态信息通过TrendlineEstimator::State()来获取
delay_detector_->State(),根据当前滤波器估计prev状态(normal、under、over)
如果当前反馈包反馈的数据是非常久之前的OnLongFeedbackDelay
AimdRateControl::SetEstimate
更新target_bitrate_bps,并返回
DelayBasedBwe::MaybeUpdateEstimate
DelayBasedBwe::UpdateEstimate,aimd模块以当前滤波器估计的带宽状态和AcknowledgedBitrateEstimator模块估算的码率为输入更新当前估计带宽值
AimdRateControl::Update-->输入是滤波器得出的当前网络状态+acked_bitrate_bps,输出是估算码率
AimdRateControl::ChangeBitrate--->输入是当前码率值+滤波器得出的当前网络状态+acked_bitrate_bps,输出是新估算的码率
AimdRateControl::ChangeState:首先执行上述码率控制状态转换,得出当前码率控制状态,这个状态转换是rate_control_state_作为状态,网络使用状态作为事件执行状态变迁
rate_control_state_-->(kRcHold/kRcIncrease/kRcDecrease)
input.bw_state-->(kBwNormal/kBwOverusing/kBwUnderusing)
switch rate_control_state_ case,根据当前码率控制状态,在目前码率基础上得出新码率
kRcHold,维持码率不变
kRcIncrease,增长有点类似于tcp的cubic思路,分为加增和乘增,靠近拥塞避免阈值则加增,远离后则乘增
kRcDecrease
WebRTC的带宽估计会参考3个因素,丢包率、接收端带宽估计、发送端带宽估计,然后取三者最小值作为最终的估计码率。WebRTC的带宽估计是整个QoS优化的非常重要的一个环节,在弱网优化场景,会根据带宽估计作为输入,执行大小流、SVC、带宽分配、优先级等弱网降级策略。
GCC草案:https://tools.ietf.org/html/draft-alvestrand-rmcat-congestion-03
Gcc-analysis:https://c3lab.poliba.it/images/6/65/Gcc-analysis.pdf
RTP3550协议:https://tools.ietf.org/html/rfc3550#section-6.4.2
remb rtcp协议:https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03
transport-cc协议:https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01
RTCP feedback定义:https://tools.ietf.org/html/rfc4585