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

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

复制代码
------
我正在的github给大家开发一个用于做实验的项目 ------ github.com/qw225967/Bifrost

目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。

欢迎大家使用
------

文章目录


在讲具体内容之前插一句嘴,从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。而当前发送端就需要使用该数据去决策我当前的吞吐量,于是贝叶斯估计的逻辑就起作用了。

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

cpp 复制代码
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一般就是150
  if (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/ms
    bitrate_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的时间)。

cpp 复制代码
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吞吐量计算中的优点。与它相关的调用关系我列在下方:

cpp 复制代码
// 网络出现变化,重置相关计算类
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统计后的信息判断是否进入了AlrEndTime
      acknowledged_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() 让它的历史差增大快速适应这个差值变化。

cpp 复制代码
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),然后通过统计每次发送的时间和内容进行计算探测。

cpp 复制代码
  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();
  };

处理探测的逻辑如下:

cpp 复制代码
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 函数的最下面的位置:

cpp 复制代码
// modules/congestion_controller/goog_cc/goog_cc_network_control.cc

NetworkControlUpdate 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.cc

std::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进行探测:

cpp 复制代码
// modules/congestion_controller/goog_cc/goog_cc_network_control.cc

// 在每次网络可用的时候进行网络带宽探测
NetworkControlUpdate GoogCcNetworkController::OnNetworkAvailability(
    NetworkAvailability msg) {
  NetworkControlUpdate update;

  // 创建探测configs
  update.probe_cluster_configs = probe_controller_->OnNetworkAvailability(msg);
  return update;
}

// modules/congestion_controller/goog_cc/probe_controller.cc

std::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 探测,创建的内容根据探测的码率数组创建cluster
  std::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_cast
    config.target_data_rate = DataRate::bps(rtc::dchecked_cast<int>(bitrate));

	// 最小探测间隔 kMinProbeDurationMs 15ms
    config.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中看看:

cpp 复制代码
// 在RtpTransportControllerSend::PostUpdates函数,pacer和gcc两个类关联了起来
// pacer根据gcc计算出来的configs,创建cluster

void 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_++;

  // 移除超时cluster
  while (!clusters_.empty() &&
         now_ms - clusters_.front().time_created_ms > kProbeClusterTimeoutMs) {
    clusters_.pop();
    total_failed_probe_count_++;
  }

  // 根据config创建cluster
  ProbeCluster 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: jeje
  TODO_PRINT_PROBING_STATE();
}

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

cpp 复制代码
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;

  // 获取当前的cluster
  pacing_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在设计中的思想,它们把观测值通过数学的方式转化成较为可靠的估计值,并且在不断的优化数学方法,我们可以考虑用到当前的一些统计上面。

相关推荐
简离4 天前
前端调试实战:基于 chrome://webrtc-internals/ 高效排查WebRTC问题
前端·chrome·webrtc
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode