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

流媒体学习之路(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一般就是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的时间)。

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统计后的信息判断是否进入了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() 让它的历史差增大快速适应这个差值变化。

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


// 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中看看:

// 在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之后就会进入到探测状态,在每次定时器调用时会确认当前是否需要进行探测,这部分的逻辑为:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/612083.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

抛弃英特尔,采用自研M4芯片,苹果公司的iPad Pro有什么好?

【科技明说 &#xff5c; 科技热点关注】 看到​苹果公司最新发布的M4芯片iPad Pro&#xff0c;我的眼前一亮&#xff0c;“其是否又该更换新pad了&#xff1f;”在这样的扪心自问之中&#xff0c;我发现了M4芯片的iPad Pro几大好处。 一是性能更好&#xff0c;M4芯片采用第二…

系统Cpu利用率降低改造之路

系统Cpu利用率降低改造之路 一.背景 1.1 系统背景 该系统是一个专门爬取第三方数据的高并发系统&#xff0c;该系统单台机器以每分钟400万的频次查询第三方数据&#xff0c;并回推给内部第三方系统。从应用类型上看属于IO密集型应用,为了提高系统的吞吐量和并发&#xff0c;…

解决电脑睡眠后,主机ping不通VMware虚拟机

文章目录 问题解决方法方法一方法二注意 问题 原因&#xff1a;电脑休眠一段时间&#xff0c;再次打开电脑就ping不通VMware虚拟机。 解决方法 方法一 重启电脑即可&#xff0c;凡是遇到电脑有毛病&#xff0c;重启能解决90%问题。但是重启电脑比较慢&#xff0c;而且重启…

system函数和popen函数

system函数 #include <stdlib.h> *int system(const char command); system函数在linux中的源代码&#xff1a; int system(const char * cmdstring) {pid_t pid;int status;if(cmdstring NULL){return (1);}if((pid fork())<0){status -1;}else if(pid 0){ //子…

Spring MVC(三) 参数传递

1 Controller到View的参数传递 在Spring MVC中&#xff0c;把值从Controller传递到View共有5中操作方法&#xff0c;分别是。 使用HttpServletRequest或HttpSession。使用ModelAndView。使用Map集合使用Model使用ModelMap 使用HttpServletRequest或HttpSession传值 使用HttpSe…

Vue-Cli脚手架项目的搭建【新手快速入手】

目录 一、Vue CLI脚手架简介☺ 1.Node.js前置环境的安装 2.安装npm管理器 3.安装淘宝镜像(cnpm) 二、安装vue-cli 1. 版本号查看 2.旧版本卸载 3.新版本安装 4.检查 三、Vue项目的搭建 &#x1f4cc;进入Vue项目管理器 ★命令方式创建 若localhost拒绝访问怎么办&…

深度剖析:SSD能否全面取代HDD?-2

近日&#xff0c;希捷针对SSD即将全面取代HDD的市场预言也提出站在HDD厂商角度不同的观点。 这些观点出自希捷的一份演示文稿&#xff0c;实质上是对Pure Storage首席执行官Charlie Giancarlo所称“五年内不会再有新的磁盘系统出售”这一论断的回应&#xff0c;意味着到2028年底…

(十六)Servlet教程——Servlet文件下载

Servlet文件下载 文件下载是将服务器上的资源下载到本地&#xff0c;可以通过两种方式来下载服务器上的资源。第一种是使用超链接来下载&#xff0c;第二种是通过代码来下载。 超链接下载 在HTML或者JSP页面中使用超链接时&#xff0c;可以实现页面之间的跳转&#xff0c;但是…

深入理解卷积函数torch.nn.Conv2d的各个参数以及计算公式(看完写模型就很简单了)

代码解释帮助理解&#xff1a; torch.randn(10, 3, 32, 32)&#xff0c;初始数据&#xff1a;(10, 3, 32, 32)代表有10张图片&#xff0c;每张图片的像素点用三个数表示&#xff0c;每张图片大小为32x32。&#xff08;重点理解这个下面就好理解了&#xff09; nn.Conv2d(3, 64…

python自动打卡的代码

好的&#xff0c;以下是一个简单的Python自动打卡程序代码&#xff0c;用于在特定时间自动打卡&#xff1a; python import datetime import time # 设置打卡时间和打卡间隔 check_in_time datetime.datetime(2023, 3, 1, 9, 30) check_out_time datetime.datetime(2023, 3, …

苹果电脑免费第三方软件CleanMyMac X2025电脑版垃圾清理软件神器

Mac电脑用户在长时间使用电脑之后&#xff0c;时常会看到“暂存盘已满”的提示&#xff0c;这无疑会给后续的电脑使用带来烦恼&#xff0c;那么苹果电脑暂存盘已满怎么清理呢&#xff0c;下面将给大家带来一些干货帮你更好地解决这个问题。 CleanMyMac X2024全新版下载如下: h…

springboot之统一异常封装

一&#xff1a;统一返回实体对象 JsonInclude(Include.NON_NULL) public class ResponseObject implements Serializable {private static final long serialVersionUID 1L;private Integer code 0;private String message "success";private Long time System.…

新版文件同步工具(Python编写,其中同时加入了多进程计算MD5、多线程复制大文件、多协程复制小文件、彩色输出消息、日志功能)

两个月前&#xff0c;接到一个粉丝的要求&#xff0c;说希望在我之前编写的一个python编写的文件同步脚本(Python编写的简易文件同步工具(已解决大文件同步时内存溢出问题)https://blog.csdn.net/donglxd/article/details/131225175)上加入多线程复制文件的功能&#xff0c;前段…

flutter中固定底部按钮,防止键盘弹出时按钮跟随上移

当我们想要将底部按钮固定在底部&#xff0c;我们只需在Widget中的Scaffold里面加一句 resizeToAvoidBottomInset: false, // 设置为false&#xff0c;固定页面不会因为键盘弹出而移动 效果图如下

CSCWD 2024会议最后一天 女高音惊艳全场,相声笑破肚皮

会议之眼 快讯 今天是第27届国际计算机协同计算与设计大会&#xff08;CSCWD 2024&#xff09;举办的最后一天&#xff01;会议依然热络&#xff0c;紧张而充实&#xff01;各个技术分论坛持续展开&#xff0c;学者们的热情不减&#xff0c;对技术领域的热爱和探索精神令人赞叹…

国产开源物联网操作系统

软件介绍 RT-Thread是一个开源、中立、社区化发展的物联网操作系统&#xff0c;采用C语言编写&#xff0c;具有易移植的特性。该项目提供完整版和Nano版以满足不同设备的资源需求。 功能特点 1.内核层 RT-Thread内核包括多线程调度、信号量、邮箱、消息队列、内存管理、定时器…

VS配置三方依赖

1.配置include 1.1.打开属性 1.2.打开“配置属性”-"C/C"-"常规" 2.配置lib 2.1.配置lib目录 打开"配置属性"-“链接器”-“常规”。 2.2.配置具体的lib 打开"配置属性"-"链接器"-“输入”。 也可以通过代码方式加入&…

差速机器人模型LQR 控制仿真(c++ opencv显示)

1 差速机器人状态方程构建 1.1差速机器人运动学模型 1.2模型线性化 1.3模型离散化 2离散LQR迭代计算 注意1&#xff1a;P值的初值为Q。见链接中的&#xff1a; 注意2&#xff1a;Q, R参数调节 注意3&#xff1a;LQR一般只做横向控制&#xff0c;不做纵向控制。LQR输出的速度…

明火检测实时识别报警:视觉算法助力安全生产管理

背景与现状 在各种工作、生产环境下&#xff0c;明火的存在往往是潜在的安全隐患。无论是加油站、化工园区、仓储场所还是校园&#xff0c;明火一旦失控就会引发火灾&#xff0c;造成严重的人员伤亡和财产损失。传统的明火检查手段主要依赖于人工巡查和定期的消防检查&#xf…

拯救被勒索病毒加密的文件

无意间打开了勒索病毒的告知文件&#xff0c;几年前很多人很熟悉这个文件。 --- Welcome. Again. --- [] Whats Happen? [] Your files are encrypted, and currently unavailable. You can check it: all files on your computer has extension u347q678t1. By the way, e…