流媒体学习之路(WebRTC)——GCC中ProbeBitrateEstimator和AcknowledgedBitrateEstimator的大作用(7)

news/2024/5/21 3:10:44

流媒体学习之路(WebRTC)——GCC中ProbeBitrateEstimator和AcknowledgedBitrateEstimator的大作用(7)

——
我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。欢迎大家使用
——

文章目录

  • 流媒体学习之路(WebRTC)——GCC中ProbeBitrateEstimator和AcknowledgedBitrateEstimator的大作用(7)
  • 一、探测估计与确认估计的意义
    • 1.1 BitrateEstimator
    • 1.2 再会AcknowledgedBitrateEstimator
    • 1.3 ProbeBitrateEstimator
    • 1.4 小结
  • 二、探测怎么用于码率控制
  • 三、总结


  在讲具体内容之前插一句嘴,从GCC分析(3)开始,我们将针对GCC的实现细节去分析它设计的原理,让我们理解这些类存在的意义,不再带大家去串具体的流程了。

一、探测估计与确认估计的意义

  在GCC探测的过程中,拥塞检测和码率计算是多个模块组成的,在上涨的过程中,涨多少?在下降的过程中,降多少?这是个需要好好思考的问题。例如:我们在传输过程中,传统的有线网络突然发生了严重的拥塞(就假设有个下载任务突然进行竞争),那么大部分的网络被占用了。对端接收的状态如下:
在这里插入图片描述
  那么对端所接收的数据就只剩一半了,返馈到发送方时会变成 1/2 * send_bitrate。因此统计出该阶段的确认数据为ack_bitrate。
  聪明的小伙伴就会发现,我们依赖于这个确认值可以很有效的获得当前网络的吞吐量——但是——这个值是一个滞后的值,滞后的点在于它是上一个feedback周期内的确认值,假设当时发生了拥塞,那么这个值的延迟可能打到1~2s,非常的不靠谱。由此,我们引出我们需要基于这样的采样值,合理的做出估计,用于我们下一步的探测。

1.1 BitrateEstimator

  BitrateEstimator这个类是ProbeBitrateEstimator和AcknowledgedBitrateEstimator都会用到的统计类,它最重要的作用就是通过贝叶斯估计,使得码率的统计值更加合理、准确。
  当我们在传输过程中,ack的码率不是恒定的,通过贝叶斯估计后将会得到当前ack码率的估计值,我们在脑子里模拟一下这个计算过程或许能得到一些启发:
在这里插入图片描述

  首先我们假设RTT小于每次feedback定时触发的时长。那么数据包最长的确认延迟为:RTT + FeedbackInterval = MaxAckDelay。而我们可确认的数据则是在Feedback发送前可接收到的所有数据(排除定时发送周期和Feedback发送周期的差,假设它俩严丝合缝),由此可知,统计的ack数据则是:前MaxAckDelay时间前RTT时间 的数据,数据的窗口大小很明显就是一个FeedbackInterval。而当前发送端就需要使用该数据去决策我当前的吞吐量,于是贝叶斯估计的逻辑就起作用了。

  下面展示的是窗口更新的逻辑:

float BitrateEstimator::UpdateWindow(int64_t now_ms, int bytes,int rate_window_ms) {// Reset if time moves backwards.// 异常返回if (now_ms < prev_time_ms_) {prev_time_ms_ = -1;sum_ = 0;current_window_ms_ = 0;}// 上一次进行窗口更新之后,进行确认计算,目标就是算出整个计算窗口。// 这个值rate_window_ms一般就是150if (prev_time_ms_ >= 0) {current_window_ms_ += now_ms - prev_time_ms_;// Reset if nothing has been received for more than a full window.if (now_ms - prev_time_ms_ > rate_window_ms) {sum_ = 0;// 大于默认窗口大小则前面的计算窗口就被干掉了// 这是因为有可能feedback在传输中丢了,前面的那个窗口啥也没确认current_window_ms_ %= rate_window_ms;}}prev_time_ms_ = now_ms;float bitrate_sample = -1.0f;// 窗口满了就进行计算if (current_window_ms_ >= rate_window_ms) {// 大B除以窗口时间,换算成小b进行统计每毫秒的码率值 bit/msbitrate_sample = 8.0f * sum_ / static_cast<float>(rate_window_ms);current_window_ms_ -= rate_window_ms;sum_ = 0;}sum_ += bytes;return bitrate_sample;
}

  从代码中可以看出来,webrtc使用的窗口值是每次feedback接收确认后记录的值来进行ack计算的,而webrtc中默认的feedback发送间隔为50ms一次,那么很快咱们就能想明白,这个150ms是3个feedback之后计算一次ack的采样值(根据上面解释的内容,这个值始终滞后了一个rtt的时间)。

void BitrateEstimator::Update(Timestamp at_time, DataSize amount, bool in_alr) {// 赋值窗口值int rate_window_ms = noninitial_window_ms_;// We use a larger window at the beginning to get a more stable sample that// we can use to initialize the estimate.// 初始状态if (bitrate_estimate_kbps_ < 0.f) rate_window_ms = initial_window_ms_;// 每次feedback都更新一次,但是满足窗口大小就会输出一个不为-1.0的值float bitrate_sample_kbps =UpdateWindow(at_time.ms(), amount.bytes(), rate_window_ms);// 不满足直接返回if (bitrate_sample_kbps < 0.0f) return;// 第一次更新码率if (bitrate_estimate_kbps_ < 0.0f) {// This is the very first sample we get. Use it to initialize the estimate.bitrate_estimate_kbps_ = bitrate_sample_kbps;return;}// Define the sample uncertainty as a function of how far away it is from the// current estimate. With low values of uncertainty_symmetry_cap_ we add more// uncertainty to increases than to decreases. For higher values we approach// symmetry.// 初始化不确定度,不确定度会被用于计算采样值与估计值之间的偏差float scale = uncertainty_scale_;// alr状态下可以调高不确定度,但目前初始化时一样的值,都是 10.if (in_alr && bitrate_sample_kbps < bitrate_estimate_kbps_) {// Optionally use higher uncertainty for samples obtained during ALR.scale = uncertainty_scale_in_alr_;}// 计算码率样本的不确定度// (|历史值 - 样本值| * 不确定度) / 历史值 = 样本不确定度// 这里的对称性让我百思不得其解,但是后来想了一下,所谓的对称性就是在 bitrate_sample_kbps 同为负值时,就使用负值。// 那么就会变成 bitrate_estimate_kbps_ - bitrate_sample_kbps 与 bitrate_estimate_kbps_ + bitrate_sample_kbps// (|历史值 - 样本值| * 不确定度) / 历史值 = 样本不确定度 这个公式得到的是——相对偏差// (|历史值 - 样本值| * 不确定度) / (历史值 + 样本值) = 这个两个样本的相似度:越接近1,相似度越低;越接近0,相似度越高float sample_uncertainty =scale * std::abs(bitrate_estimate_kbps_ - bitrate_sample_kbps) /(bitrate_estimate_kbps_ +std::min(bitrate_sample_kbps,uncertainty_symmetry_cap_.Get().kbps<float>()));// 求采样不确定度的平方float sample_var = sample_uncertainty * sample_uncertainty;// Update a bayesian estimate of the rate, weighting it lower if the sample// uncertainty is large.// The bitrate estimate uncertainty is increased with each update to model// that the bitrate changes over time.// float pred_bitrate_estimate_var = bitrate_estimate_var_ + 5.f;// 根据不确定度进行加权平均:// 采样的不确定度 * 历史估计值 = 根据历史值推算的当前值 ——> 当前偏差很越小,历史数据可信度低,反之高// 先验不确定度 * 当前采样值 = 根据历史不确定性推算的当前值 ——> 先验偏差越小,采样数据可信度低,反之高// 以上两者比值类似于 谁不确定度大,那么码率的占比越低,最终输出一个平均值。bitrate_estimate_kbps_ = (sample_var * bitrate_estimate_kbps_ +pred_bitrate_estimate_var * bitrate_sample_kbps) /(sample_var + pred_bitrate_estimate_var);// 当前值必须要大于等于0,否则后面没法算了bitrate_estimate_kbps_ =std::max(bitrate_estimate_kbps_, estimate_floor_.Get().kbps<float>());// 贝叶斯公式中:当前采样的不确定度 * 先验的码率不确定度 / (采样不确定度 + 先验码率不确定度) = 当前的估计不确定度bitrate_estimate_var_ = sample_var * pred_bitrate_estimate_var /(sample_var + pred_bitrate_estimate_var);
}

  上面的注释内容详细讲解了估计的运算原理,在码率计算中,根据我们采样的偏差值与历史偏差值进行运算,等到一个加权的平均码率值,可以很直观的发现:
  1.当我们历史的不确定度越大,那么当前采样的可信度就高,那么我们给他的权重就相对高;
  2.当我们采样的不确定度越大,那么历史值的可信度就高,那么我们给历史值的权重就高。

1.2 再会AcknowledgedBitrateEstimator

  AcknowledgedBitrateEstimator类在前面一篇文章中流媒体学习之路(WebRTC)——GCC分析(3)简单提及,今天我们不执着于这个类的代码解析,而是从它延伸出去看我们整个码率估计系统中,吞吐量计算的逻辑,让我们理解webrtc吞吐量计算中的优点。与它相关的调用关系我列在下方:

// 网络出现变化,重置相关计算类
GoogCcNetworkController::OnNetworkRouteChange() {...if (safe_reset_on_route_change_) {absl::optional<DataRate> estimated_bitrate;...// 获取估计码率estimated_bitrate = acknowledged_bitrate_estimator_->bitrate();if (!estimated_bitrate)// 没有估计码率峰值也行estimated_bitrate = acknowledged_bitrate_estimator_->PeekRate();}...// 重置acknowledged_bitrate_estimator_.reset(new AcknowledgedBitrateEstimator(key_value_config_));}NetworkControlUpdate GoogCcNetworkController::OnSentPacket(SentPacket sent_packet) {...// 编码器进入alr状态acknowledged_bitrate_estimator_->SetAlr(alr_detector_->GetApplicationLimitedRegionStartTime().has_value());...}NetworkControlUpdate GoogCcNetworkController::OnTransportPacketsFeedback(TransportPacketsFeedback report) {...if (previously_in_alr_ && !alr_start_time.has_value()) {int64_t now_ms = report.feedback_time.ms();// 根据feedback adapt统计后的信息判断是否进入了AlrEndTimeacknowledged_bitrate_estimator_->SetAlrEndedTime(report.feedback_time);probe_controller_->SetAlrEndedTimeMs(now_ms);}...// 把feedback放入计算acknowledged_bitrate_estimator_->IncomingPacketFeedbackVector(report.SortedByReceiveTime());auto acknowledged_bitrate = acknowledged_bitrate_estimator_->bitrate();...}

  上面的东西没啥特别的,主要是Alr状态的判断。当Alr状态的时候,我们的码率是无法输出到达我们的预期的,但是当它离开时很可能码率立马上涨,此时的bitrate estimate中我们提到的采样差值可能会剧烈上涨,但是算法加权之后权重下降了,因此我们要在SetAlrEndedTime 的时候调用一下 BitrateEstimator::ExpectFastRateChange() 让它的历史差增大快速适应这个差值变化。

void BitrateEstimator::ExpectFastRateChange() {// By setting the bitrate-estimate variance to a higher value we allow the// bitrate to change fast for the next few samples.bitrate_estimate_var_ += 200;
}

1.3 ProbeBitrateEstimator

  ProbeBitrateEstimator的逻辑相对AcknowledgedBitrateEstimator就复杂一些,WebRTC把每一次探测归类为群组(cluster),然后通过统计每次发送的时间和内容进行计算探测。

  struct AggregatedCluster {// 探测包数int num_probes = 0;// 第一个发送包时间Timestamp first_send = Timestamp::PlusInfinity();// 最后一个发送包时间Timestamp last_send = Timestamp::MinusInfinity();// 第一个包到达时间Timestamp first_receive = Timestamp::PlusInfinity();// 最后一个包到达时间Timestamp last_receive = Timestamp::MinusInfinity();// 最后发送包大小DataSize size_last_send = DataSize::Zero();// 第一个接收包大小DataSize size_first_receive = DataSize::Zero();// 总大小DataSize size_total = DataSize::Zero();};

  处理探测的逻辑如下:

absl::optional<DataRate> ProbeBitrateEstimator::HandleProbeAndEstimateBitrate(const PacketResult& packet_feedback) {int cluster_id = packet_feedback.sent_packet.pacing_info.probe_cluster_id;// RTC_DCHECK_NE(cluster_id, PacedPacketInfo::kNotAProbe);EraseOldClusters(packet_feedback.receive_time);AggregatedCluster* cluster = &clusters_[cluster_id];// 取出所有数据if (packet_feedback.sent_packet.send_time < cluster->first_send) {cluster->first_send = packet_feedback.sent_packet.send_time;}if (packet_feedback.sent_packet.send_time > cluster->last_send) {cluster->last_send = packet_feedback.sent_packet.send_time;cluster->size_last_send = packet_feedback.sent_packet.size;}if (packet_feedback.receive_time < cluster->first_receive) {cluster->first_receive = packet_feedback.receive_time;cluster->size_first_receive = packet_feedback.sent_packet.size;}if (packet_feedback.receive_time > cluster->last_receive) {cluster->last_receive = packet_feedback.receive_time;}cluster->size_total += packet_feedback.sent_packet.size;cluster->num_probes += 1;// RTC_DCHECK_GT(// packet_feedback.sent_packet.pacing_info.probe_cluster_min_probes, 0);// RTC_DCHECK_GT(packet_feedback.sent_packet.pacing_info.probe_cluster_min_bytes,// 0);// 最小探测包接收数:kMinReceivedProbesRatio 为 0.8 * 最小发送包数int min_probes =packet_feedback.sent_packet.pacing_info.probe_cluster_min_probes *kMinReceivedProbesRatio;// 最小接收包大小DataSize min_size =DataSize::bytes(packet_feedback.sent_packet.pacing_info.probe_cluster_min_bytes) *kMinReceivedBytesRatio;// 探测包数太少、不符合运算要求直接返回if (cluster->num_probes < min_probes || cluster->size_total < min_size)return absl::nullopt;// 发送间隔TimeDelta send_interval = cluster->last_send - cluster->first_send;// 接收间隔TimeDelta receive_interval = cluster->last_receive - cluster->first_receive;// // TODO: TMP// MS_WARN_DEV(//   "-------------- probing cluster result"//   " [cluster id:%d]"//   " [send interval:%s]"//   " [receive interval:%s]",//   cluster_id,//   ToString(send_interval).c_str(),//   ToString(receive_interval).c_str());// TODO: TMP WIP cerdo to avoid that send_interval or receive_interval is// zero.//// if (send_interval <= TimeDelta::Zero())//   send_interval = TimeDelta::ms(1u);// if (receive_interval <= TimeDelta::Zero())//   receive_interval = TimeDelta::ms(1u);// 发送间隔异常返回if (send_interval <= TimeDelta::Zero() || send_interval > kMaxProbeInterval ||receive_interval <= TimeDelta::Zero() ||receive_interval > kMaxProbeInterval) {return absl::nullopt;}// Since the |send_interval| does not include the time it takes to actually// send the last packet the size of the last sent packet should not be// included when calculating the send bitrate.// RTC_DCHECK_GT(cluster->size_total, cluster->size_last_send);// 发送时间内的大小计算DataSize send_size = cluster->size_total - cluster->size_last_send;// 计算发送率DataRate send_rate = send_size / send_interval;// Since the |receive_interval| does not include the time it takes to// actually receive the first packet the size of the first received packet// should not be included when calculating the receive bitrate.// RTC_DCHECK_GT(cluster->size_total, cluster->size_first_receive);// 接收间隔内的大小计算DataSize receive_size = cluster->size_total - cluster->size_first_receive;// 计算接收率DataRate receive_rate = receive_size / receive_interval;// 接收率/发送率 过大(大于2)则异常,直接返回double ratio = receive_rate / send_rate;if (ratio > kMaxValidRatio) {return absl::nullopt;}// 去发送、接收的小值作为探测到的结果DataRate res = std::min(send_rate, receive_rate);// If we're receiving at significantly lower bitrate than we were sending at,// it suggests that we've found the true capacity of the link. In this case,// set the target bitrate slightly lower to not immediately overuse.// 当接收码率小于90%的发送码率,则认为网络出现了异常,将会返回更低的探测值(当前探测值 * 95%)防止它下一步进入overuse状态if (receive_rate < kMinRatioForUnsaturatedLink * send_rate) {// RTC_DCHECK_GT(send_rate, receive_rate);res = kTargetUtilizationFraction * receive_rate;}last_estimate_ = res;estimated_data_rate_ = res;return res;
}

  探测的逻辑中我们需要避免造成网络异常拥塞,因此对各类异常情况进行类确认。在最后的逻辑中,当接收码率小于发送码率的90%,可以确定当前发生拥塞的概率很大,因此需要降低我们的发送码率为当前的95%(目前还不知道这个95%是怎么定的)。计算的逻辑只是探测很小的一部分,探测的逻辑涉及了很多位置——pacer中也有很多实现,我们展开看看其他部分是怎么决定开始探测,又怎么保证它影响最小的?

  先确定什么时候会进入探测状态?
  1.网络初始化阶段,需要探测到最新的网络状态;
  2.在UnderUse状态切换到Normal时,并处于无码率增长状态且降码率不足5s时,认定为需要快恢复状态,则立刻探测。

  上面的初始化就不用过多介绍了,但第二个逻辑是在 probe_controller.cc 中进行判断的,而判断 RequestProbe 这个函数的调用在 GoogCcNetworkController::OnTransportPacketsFeedback 函数的最下面的位置:


// modules/congestion_controller/goog_cc/goog_cc_network_control.ccNetworkControlUpdate GoogCcNetworkController::OnTransportPacketsFeedback(TransportPacketsFeedback report) {...// 这个结果在detecter里做的recovered_from_overuse = result.recovered_from_overuse;...if (recovered_from_overuse) {probe_controller_->SetAlrStartTimeMs(alr_start_time);auto probes = probe_controller_->RequestProbe(report.feedback_time.ms());update.probe_cluster_configs.insert(update.probe_cluster_configs.end(),probes.begin(), probes.end());} else if (backoff_in_alr) {// If we just backed off during ALR, request a new probe.auto probes = probe_controller_->RequestProbe(report.feedback_time.ms());update.probe_cluster_configs.insert(update.probe_cluster_configs.end(),probes.begin(), probes.end());}
}// modules/congestion_controller/goog_cc/probe_controller.ccstd::vector<ProbeClusterConfig> ProbeController::RequestProbe(int64_t at_time_ms) {// Called once we have returned to normal state after a large drop in// estimated bandwidth. The current response is to initiate a single probe// session (if not already probing) at the previous bitrate.//// If the probe session fails, the assumption is that this drop was a// real one from a competing flow or a network change.bool in_alr = alr_start_time_ms_.has_value();bool alr_ended_recently =(alr_end_time_ms_.has_value() &&at_time_ms - alr_end_time_ms_.value() < kAlrEndedTimeoutMs);if (in_alr || alr_ended_recently || in_rapid_recovery_experiment_) {if (state_ == State::kProbingComplete) {uint32_t suggested_probe_bps =kProbeFractionAfterDrop * bitrate_before_last_large_drop_bps_;uint32_t min_expected_probe_result_bps =(1 - kProbeUncertainty) * suggested_probe_bps;int64_t time_since_drop_ms = at_time_ms - time_of_last_large_drop_ms_;int64_t time_since_probe_ms = at_time_ms - last_bwe_drop_probing_time_ms_;if (min_expected_probe_result_bps > estimated_bitrate_bps_ &&time_since_drop_ms < kBitrateDropTimeoutMs &&time_since_probe_ms > kMinTimeBetweenAlrProbesMs) {// Track how often we probe in response to bandwidth drop in ALR.// RTC_HISTOGRAM_COUNTS_10000(//     "WebRTC.BWE.BweDropProbingIntervalInS",//     (at_time_ms - last_bwe_drop_probing_time_ms_) / 1000);last_bwe_drop_probing_time_ms_ = at_time_ms;return InitiateProbing(at_time_ms, {suggested_probe_bps}, false);}}}return std::vector<ProbeClusterConfig>();
}

  可以看出,ProbeBitrateEstimator是为了支持在码率输出不足的情况下,去做补充和填充的。

1.4 小结

  上面我们介绍了两个类存在的意义,首先他们都是利用较短的时间、较少的包数量去估算可能达到的带宽上限。要注意的是——它们代表的不是这一秒钟或者这一段时间完整的带宽情况,而是根据当前估算周期内,计算出来的瞬时速率,是个估计值并不是它们真的在某一秒钟发送了巨量的数据做的探测,因此它们对带宽的消耗是较小的、同时也损失了一定的准确度。

二、探测怎么用于码率控制

  本章我们单独把探测的逻辑拿出来好好说一下,因为webrtc的探测逻辑让我感受到是个非常灵活、收放自如的助手工具,怎么做到这点的呢?本章会好好解释。下图展示涉及到探测的模块关系图:
在这里插入图片描述

  在pacer中,prober的概念是做状态的控制,在决定做探测时它根据cluster的数据进行精细的控制(也可以直接调用创建cluser进行探测)。在起始阶段,它直接创建了cluster进行探测:


// modules/congestion_controller/goog_cc/goog_cc_network_control.cc// 在每次网络可用的时候进行网络带宽探测
NetworkControlUpdate GoogCcNetworkController::OnNetworkAvailability(NetworkAvailability msg) {NetworkControlUpdate update;// 创建探测configsupdate.probe_cluster_configs = probe_controller_->OnNetworkAvailability(msg);return update;
}// modules/congestion_controller/goog_cc/probe_controller.ccstd::vector<ProbeClusterConfig> ProbeController::OnNetworkAvailability(NetworkAvailability msg) {network_available_ = msg.network_available;// kWaitingForProbingResult 的意义是等待探测结果的状态if (!network_available_ && state_ == State::kWaitingForProbingResult) {state_ = State::kProbingComplete;min_bitrate_to_probe_further_bps_ = kExponentialProbingDisabled;}// 网络处于初始状态时,初始化探测指数if (network_available_ && state_ == State::kInit && start_bitrate_bps_ > 0)return InitiateExponentialProbing(msg.at_time.ms());return std::vector<ProbeClusterConfig>();
}std::vector<ProbeClusterConfig> ProbeController::InitiateExponentialProbing(int64_t at_time_ms) {// RTC_DCHECK(network_available_);// RTC_DCHECK(state_ == State::kInit);// RTC_DCHECK_GT(start_bitrate_bps_, 0);// When probing at 1.8 Mbps ( 6x 300), this represents a threshold of// 1.2 Mbps to continue probing.// first_exponential_probe_scale 数值为3.0,探测目标为3倍的初始码率std::vector<int64_t> probes = {static_cast<int64_t>(config_.first_exponential_probe_scale * start_bitrate_bps_)};// second_exponential_probe_scale 二次探测指数为6.0,探测目标更大if (config_.second_exponential_probe_scale) {probes.push_back(config_.second_exponential_probe_scale.Value() *start_bitrate_bps_);}return InitiateProbing(at_time_ms, probes, true);
}std::vector<ProbeClusterConfig> ProbeController::InitiateProbing(int64_t now_ms, std::vector<int64_t> bitrates_to_probe,bool probe_further) {// 默认最大探测码率限制int64_t max_probe_bitrate_bps =max_bitrate_bps_ > 0 ? max_bitrate_bps_ : kDefaultMaxProbingBitrateBps;if (limit_probes_with_allocateable_rate_ &&max_total_allocated_bitrate_ > 0) {// If a max allocated bitrate has been configured, allow probing up to 2x// that rate. This allows some overhead to account for bursty streams,// which otherwise would have to ramp up when the overshoot is already in// progress.// It also avoids minor quality reduction caused by probes often being// received at slightly less than the target probe bitrate.max_probe_bitrate_bps =std::min(max_probe_bitrate_bps, max_total_allocated_bitrate_ * 2);}// 创建 pending 探测,创建的内容根据探测的码率数组创建clusterstd::vector<ProbeClusterConfig> pending_probes;for (int64_t bitrate : bitrates_to_probe) {// RTC_DCHECK_GT(bitrate, 0);// 最大码率限制,到达最大码率限制之后只能等进一步的码率探测if (bitrate > max_probe_bitrate_bps) {bitrate = max_probe_bitrate_bps;probe_further = false;}// 探测配置ProbeClusterConfig config;config.at_time = Timestamp::ms(now_ms);// dchecked_cast 就是个static_castconfig.target_data_rate = DataRate::bps(rtc::dchecked_cast<int>(bitrate));// 最小探测间隔 kMinProbeDurationMs 15msconfig.target_duration = TimeDelta::ms(kMinProbeDurationMs);// 探测目标包数,最小为 kMinProbePacketsSent 5个config.target_probe_count = kMinProbePacketsSent;config.id = next_probe_cluster_id_;next_probe_cluster_id_++;// 日志打印MaybeLogProbeClusterCreated(config);pending_probes.push_back(config);}time_last_probing_initiated_ms_ = now_ms;// 需要进行进一步码率探测则更新码率if (probe_further) {state_ = State::kWaitingForProbingResult;// 获取进一步的最小探测码率min_bitrate_to_probe_further_bps_ =(*(bitrates_to_probe.end() - 1)) * config_.further_probe_threshold;} else {// 否则探测码率为0,立即进行探测state_ = State::kProbingComplete;min_bitrate_to_probe_further_bps_ = kExponentialProbingDisabled;}return pending_probes;
}

  这里的逻辑是运算我们每次需要探测的码率大小,默认就是5个包。但是有个疑问,为什么即规定了5个包又设置了目标的估计码率呢?具体的逻辑我们需要从pacer中看看:

// 在RtpTransportControllerSend::PostUpdates函数,pacer和gcc两个类关联了起来
// pacer根据gcc计算出来的configs,创建clustervoid PacedSender::CreateProbeCluster(int bitrate_bps, int cluster_id) {// TODO: REMOVE// MS_DEBUG_DEV("---- bitrate_bps:%d, cluster_id:%d", bitrate_bps,// cluster_id);prober_.CreateProbeCluster(bitrate_bps, loop_->get_time_ms_int64(),cluster_id);
}void BitrateProber::CreateProbeCluster(int bitrate_bps, int64_t now_ms,int cluster_id) {// RTC_DCHECK(probing_state_ != ProbingState::kDisabled);// RTC_DCHECK_GT(bitrate_bps, 0);// 探测次数记录total_probe_count_++;// 移除超时clusterwhile (!clusters_.empty() &&now_ms - clusters_.front().time_created_ms > kProbeClusterTimeoutMs) {clusters_.pop();total_failed_probe_count_++;}// 根据config创建clusterProbeCluster cluster;cluster.time_created_ms = now_ms;cluster.pace_info.probe_cluster_min_probes = config_.min_probe_packets_sent;cluster.pace_info.probe_cluster_min_bytes =static_cast<int32_t>(static_cast<int64_t>(bitrate_bps) *config_.min_probe_duration->ms() / 8000);// RTC_DCHECK_GE(cluster.pace_info.probe_cluster_min_bytes, 0);cluster.pace_info.send_bitrate_bps = bitrate_bps;cluster.pace_info.probe_cluster_id = cluster_id;clusters_.push(cluster);// If we are already probing, continue to do so. Otherwise set it to// kInactive and wait for OnIncomingPacket to start the probing.if (probing_state_ != ProbingState::kActive)probing_state_ = ProbingState::kInactive;// TODO (ibc): We need to send probation even if there is no real packets, so// add this code (taken from `OnIncomingPacket()` above) also here.if (probing_state_ == ProbingState::kInactive && !clusters_.empty()) {// Send next probe right away.next_probe_time_ms_ = -1;// 开启探测状态probing_state_ = ProbingState::kActive;}// TODO: jejeTODO_PRINT_PROBING_STATE();
}

  当我们创建完cluster之后就会进入到探测状态,在每次定时器调用时会确认当前是否需要进行探测,这部分的逻辑为:

void PacedSender::Process() {int64_t now_us = loop_->get_time_ms_int64();int64_t elapsed_time_ms = UpdateTimeAndGetElapsedMs(now_us);if (paused_) return;if (elapsed_time_ms > 0) {int target_bitrate_kbps = pacing_bitrate_kbps_;media_budget_.set_target_rate_kbps(target_bitrate_kbps);UpdateBudgetWithElapsedTime(elapsed_time_ms);}// 需要开启探测if (!prober_.IsProbing()) return;PacedPacketInfo pacing_info;absl::optional<size_t> recommended_probe_size;// 获取当前的clusterpacing_info = prober_.CurrentCluster();recommended_probe_size = prober_.RecommendedMinProbeSize();size_t bytes_sent = 0;// MS_NOTE: Let's not use a useless vector.std::shared_ptr<bifrost::RtpPacket> padding_packet{nullptr};// Check if we should send padding.while (true) {// 获取需要padding的码率size_t padding_bytes_to_add =PaddingBytesToAdd(recommended_probe_size, bytes_sent);if (padding_bytes_to_add == 0) break;// TODO: REMOVE// MS_DEBUG_DEV(//   "[recommended_probe_size:%zu, padding_bytes_to_add:%zu]",//   *recommended_probe_size, padding_bytes_to_add);// 根据需要产生的padding码率获取padding包padding_packet = packet_router_->GeneratePadding(padding_bytes_to_add);// TODO: REMOVE.// MS_DEBUG_DEV("sending padding packet [size:%zu]",// padding_packet->GetSize());// 发送padding包packet_router_->SendPacket(padding_packet.get(), pacing_info);bytes_sent += padding_packet->GetSize();// 发送的码率超过探测码率则退出if (recommended_probe_size && bytes_sent > *recommended_probe_size) break;}// 剩余padding不足也退出if (bytes_sent != 0) {auto now = loop_->get_time_ms_int64();// 更新padding记录OnPaddingSent(now, bytes_sent);prober_.ProbeSent((now + 500) / 1000, bytes_sent);}
}size_t PacedSender::PaddingBytesToAdd(absl::optional<size_t> recommended_probe_size, size_t bytes_sent) {// Don't add padding if congested, even if requested for probing.// 正在拥塞直接返回if (Congested()) {return 0;}// MS_NOTE: This does not apply to mediaproxy.// We can not send padding unless a normal packet has first been sent. If we// do, timestamps get messed up.// if (packet_counter_ == 0) {//   return 0;// }// 计算需要的码率if (recommended_probe_size) {if (*recommended_probe_size > bytes_sent) {return *recommended_probe_size - bytes_sent;}return 0;}return padding_budget_.bytes_remaining();
}

三、总结

  ProbeBitrateEstimator和AcknowledgedBitrateEstimator两个类是gcc做码率控制的基础,webrtc对AcknowledgedBitrateEstimator的修改较少,但是对Probe相关的类一直在做调整。上面展示的m77代码和我最近看的m105代码差距就已经发生明显的变化。在Pacer中,m105增加了线程控制而且产生padding包的逻辑也做了调整。同时在触发探测的逻辑上也进行多处修改。但是我们根据上述的代码走读,也理解了webrtc在设计中的思想,它们把观测值通过数学的方式转化成较为可靠的估计值,并且在不断的优化数学方法,我们可以考虑用到当前的一些统计上面。


http://www.mrgr.cn/p/32418234

相关文章

移动端定位打卡

签到按钮脚本 Mobile_NS.getCurrPosition(function(result){var lngdangq = result["lng"];var lathoum = result["lat"];var minDistance = null;//alert("addr"+addr);var dkzt = $f("dkzt").val();//alert(dkzt);if(dkzt==0){//$f(…

steam错误代码118?报错118?手把手教你应对Steam错误代码攻略

steam是由美国游戏开发公司Valve开发的一款数字发行、数字版权管理、多人游戏和社交平台。它最初是为Valve公司所开发的游戏而设计的&#xff0c;但现在已经发展成为游戏行业最大的数字发行平台之一。Steam平台提供了丰富的游戏资源&#xff0c;包括最新的独立游戏、大型多人在…

3秒修复老照片,一键智能变高清!

你肯定有一些年代久远的老照片,以及网络下载的图片或视频,不够高清还非常模糊,如果能一键修复成高清就好了!现在推荐一款神奇的Real-ESRGAN镜像,可以将模糊老照片和视频修复成高清晰,动动手分分钟帮你一键焕新!操作指南这就马上附上!你肯定有一些年代久远的老照片,以及…

EC11的中断实验——NVICEXTI

本文隶属于《GD32 示波器项目软件部分重难点及相关疑问解决》 4-EC11的中断实验——NVIC&EXTI 4-1 实验目标以及原理图 GD32E230外部中断EXTI(中断/事件控制器)包括21个相互独立的边沿检测电路并且能够向处理器内核产生中断请求或唤醒事件。EXTI有三种触发类型:上升沿触…

使用stable diffusion设计logo的提示词

使用stable diffusion设计logo的提示词 Stable Diffusion是一种基于图像处理和机器学习的算法,可以用于生成各种类型的图像,包括Logo设计。本文将介绍如何使用Stable Diffusion来设计Logo,并提供一些提示词以帮助读者更好地理解和应用这种技术。 1.了解Stable Diffusion的基…

方正字体 3.0 和 5.0 的比较

默认字体就是宋体(方正书宋)主要区别在于英文字体。5.0 中的斜体、宋体、黑体和楷体的英文字体都变化较大。 个人认为 5.0 更为合理。因为斜体和楷体本身就是较为接近手写字体的字体。而 5.0 中的英文斜体和楷体改的更像手写了。

实验四——代码审查

一、实验题目 :代码审查 二、实验目的 1、熟悉编码风格,利用开发环境所提供的平台工具对代码进行自动格式审查; 2、根据代码规范制定代码走查表,并按所制定的审查规范互审代码。 三、实验内容 1、IDEA环境和PyCharm环境二选一; IDEA环境 (1)预先准备在IDEA环境下实现对输…

vue开发网站—①调用$notify弹窗、②$notify弹窗层级问题、③js判断两个数组是否相同等。

一、vue中如何使用vant的 $notify&#xff08;展示通知&#xff09; 在Vue中使用Vant组件库的$notify方法来展示通知&#xff0c;首先确保正确安装了Vant并在项目中引入了Notify组件。 1.安装vant npm install vant --save# 或者使用yarn yarn add vant2.引入&#xff1a;在ma…

nginx--压缩https证书favicon.iconginx隐藏版本号 去掉nginxopenSSL

压缩功能 简介 Nginx⽀持对指定类型的⽂件进行压缩然后再传输给客户端&#xff0c;而且压缩还可以设置压缩比例&#xff0c;压缩后的文件大小将比源文件显著变小&#xff0c;这样有助于降低出口带宽的利用率&#xff0c;降低企业的IT支出&#xff0c;不过会占用相应的CPU资源…

VBA_NZ系列工具NZ06:VBA创建PDF文件说明

我的教程一共九套及VBA汉英手册一部&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到数据库&#xff0c;到字典&#xff0c;到高级的网抓及类的应用。大家在学习的过程中可能会存在困惑&#xff0c;这么多知识点该如何组织…

ssrf漏洞学习——基础知识

一、SSRF是什么&#xff1f; SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。 一般情况下&#xff0c;SSRF攻击的目标是从外网无法访问的内部系统。&#xff08;正是因为它是由服务端发起的&#xff0c;所以它能…

《架构风清扬-Java面试系列第29讲》聊聊DelayQueue的使用场景

DelayQueue是BlockingQueue接口的一个实现类之一 这个属于基础性问题&#xff0c;老规矩&#xff0c;我们将从使用场景和代码示例来进行讲解 来&#xff0c;思考片刻&#xff0c;给出你的答案 1&#xff0c;使用场景 实现&#xff1a;延迟队列&#xff0c;其中元素只有在其预定…

layui的treeTable组件,多层级上传按钮失效的问题解决

现象描述: layui的treeTable 的上传按钮在一层能用&#xff0c;展开后其他按钮正常点击&#xff0c;上传按钮无效。 具体原因没有深究&#xff0c;大概率是展开的子菜单没有被渲染treeTable的done管理到&#xff0c;导致没有重绘上传按钮。 解决方案: 不使用layu的上传组件方法…

springboot+vue快速部署前后台项目,无需服务器

问题 前言 我们都知道,现在的主流开发大多数为,前后端分离,目前流行的框架,大多数是spring boot+element ui 这些框架,这无疑是给开发部署项目带来了便利,我们后台开发无需关心前端如何部署的,前端同样也无需关系后台如何部署,只需要确认能够访问即可。 存在有如下问题…

DDR5和LPDDR4/5 命令解析

关键名称介绍 DDR5 SDRAM和LPDDR4/5都采用了高级的命令集来支持更高效的内存管理和操作,其中“Multi-purpose command (MPC)”、“Mode Register Read (MRR)”、“Mode Register Write (MRW)”,以及“Write Pattern Command”是几种关键的命令类型,它们在内存初始化、配置和…

大型语言模型的新挑战:AMR语义表示的神秘力量

DeepVisionary 每日深度学习前沿科技推送&顶会论文&数学建模与科技信息前沿资讯分享&#xff0c;与你一起了解前沿科技知识&#xff01; 引言&#xff1a;AMR在大型语言模型中的作用 在自然语言处理&#xff08;NLP&#xff09;的领域中&#xff0c;抽象意义表示&…

HBM供不应求,SK海力士称2025年订单都几乎售罄

【科技明说 &#xff5c; 科技热点关注】 据外媒报道&#xff0c;SK海力士透露公司今年的HBM产能已经全部售罄&#xff0c;明年订单也基本售罄。此外&#xff0c;SK海力士预计在2024年5月提供世界最高性能的12层堆叠HBM3E产品的样品&#xff0c;并准备在第三季度开始量产。 ​…

项目打包与上线

目录1.修改好上线环境中的请求地址2.打包项目3.连接服务器4.配置nginx代理5.上线成功 1.修改好上线环境中的请求地址2.打包项目进入项目根目录,输入npm run build解决报错问题 当我们无法解决多而烦的ts检查报错时,可以在项目中的package.json文件中把下图中原本的红色框内容…

西门子数控网络IP设定配置

总结&#xff1a;menuselect-诊断-屏幕下方右翻页找到tcp/ip&#xff0c;进去选择tcp/ip诊断&#xff0c;进去选择x130网口&#xff0c;点击更改&#xff0c; 如果没有更改&#xff0c;menuselect-调试-口令&#xff0c;输入口令 sunrise 然后重新配置tcp/ip&#xff0c;配置完…