webrtc弱网自适应(自动升降速)要点


针对webrtc在弱网的码率自适应机制的学习。列举了几个影响因素。有时间的话,再来继续深入研究。

编码发送层优先级

  // RTP/RTCP initialization.

  // We add the highest spatial layer first to ensure it'll be prioritized
  // when sending padding, with the hope that the packet rate will be smaller,
  // and that it's more important to protect than the lower layers.

  // TODO(nisse): Consider moving registration with PacketRouter last, after the
  // modules are fully configured.
  for (const RtpStreamSender& stream : rtp_streams_) {
    constexpr bool remb_candidate = true;
    transport->packet_router()->AddSendRtpModule(stream.rtp_rtcp.get(),
                                                 remb_candidate);
  }

发送端利用带宽估计算法估算上行带宽,然后根据策略进行码率分配(rtcsdk上层会指定每一层的码率),到了webrtc后,webrtc会在此带宽配置下,按顺序优先分配码率,先小流,再中流,最后大流,当大流(或中流)分配到的码率小于大流(或中流)的最小码率时就会停发大流(或中流)。
比如: 估算出来的带宽是1 mbps,小流分配了256kbps,中流分配了512kbps,这个时候留给大流的只有256kbps,但是如果大流指定了最小带宽大小256kbps,则大流就会被停发。

DegradationPreference策略

// Based on the spec in
// https://w3c.github.io/webrtc-pc/#idl-def-rtcdegradationpreference.
// These options are enforced on a best-effort basis. For instance, all of
// these options may suffer some frame drops in order to avoid queuing.
// TODO(sprang): Look into possibility of more strictly enforcing the
// maintain-framerate option.
// TODO(deadbeef): Default to "balanced", as the spec indicates?
enum class DegradationPreference {
  // Don't take any actions based on over-utilization signals. Not part of the
  // web API.
  DISABLED,
  // On over-use, request lower resolution, possibly causing down-scaling.
  MAINTAIN_FRAMERATE,
  // On over-use, request lower frame rate, possibly causing frame drops.
  MAINTAIN_RESOLUTION,
  // Try to strike a "pleasing" balance between frame rate or resolution.
  BALANCED,
};

a)DISABLED:禁用,不进行任何调整;
b)MAINTAIN_FRAMERATE:保持帧率,降低分辨率(主流默认用这个策略);
c)MAINTAIN_RESOLUTION:保持分辨率,降低帧率(双流默认用这个策略);
d)BALANCED:平衡,降级时先尝试降低帧率,如果当前分辨率不适合再降低帧率,则降低分辨率,然后再尝试降低帧率,依次交替进行;升级时也是先尝试升高帧率,如果当前分辨率不适合升高帧率,则升高分辨率,依次交替进行。

field_trials_ += "WebRTC-Video-BalancedDegradation/Enabled/";  //启用Balanced降级策略
field_trials_ += "WebRTC-Video-BalancedDegradationSettings/pixels:76800|172800|307200|691200|1555200|2764800,fps:7|10|15|15|15|15/";

编码耗时统计

方法如下:
编码使用率=视频编码时间/采集间隔时间(对分子分母进行样本收集并做平滑处理)
视频编码时间 = 本帧编码结束时间 – 本帧编码开始时间
采集间隔时间 = 本帧编码开始时间 – 上帧编码开始时间

系统每5秒执行一次CheckForOveruse任务,

const int64_t kCheckForOveruseIntervalMs = 5000;
const int64_t kTimeToFirstCheckForOveruseMs = 100;

当编码使用率连续2次超过阈值上限,则判定为OverUse触发降级AdaptDown;反之当编码使用率低于阈值下限,则判定为UnderUse触发升级AdaptUp。为避免震荡,判定为UnderUse时会有迟滞处理。

void OveruseFrameDetector::CheckForOveruse(
    OveruseFrameDetectorObserverInterface* observer) {
  RTC_DCHECK_RUN_ON(&task_checker_);
  RTC_DCHECK(observer);
  ++num_process_times_;
  if (num_process_times_ <= options_.min_process_count ||
      !encode_usage_percent_)
    return;

  int64_t now_ms = rtc::TimeMillis();

  if (IsOverusing(*encode_usage_percent_)) {
    // If the last thing we did was going up, and now have to back down, we need
    // to check if this peak was short. If so we should back off to avoid going
    // back and forth between this load, the system doesn't seem to handle it.
    bool check_for_backoff = last_rampup_time_ms_ > last_overuse_time_ms_;
    if (check_for_backoff) {
      if (now_ms - last_rampup_time_ms_ < kStandardRampUpDelayMs ||
          num_overuse_detections_ > kMaxOverusesBeforeApplyRampupDelay) {
        // Going up was not ok for very long, back off.
        current_rampup_delay_ms_ *= kRampUpBackoffFactor;
        if (current_rampup_delay_ms_ > kMaxRampUpDelayMs)
          current_rampup_delay_ms_ = kMaxRampUpDelayMs;
      } else {
        // Not currently backing off, reset rampup delay.
        current_rampup_delay_ms_ = kStandardRampUpDelayMs;
      }
    }

    last_overuse_time_ms_ = now_ms;
    in_quick_rampup_ = false;
    checks_above_threshold_ = 0;
    ++num_overuse_detections_;

    observer->AdaptDown();
  } else if (IsUnderusing(*encode_usage_percent_, now_ms)) {
    last_rampup_time_ms_ = now_ms;
    in_quick_rampup_ = true;

    observer->AdaptUp();
  }

  int rampup_delay =
      in_quick_rampup_ ? kQuickRampUpDelayMs : current_rampup_delay_ms_;

  RTC_LOG(LS_VERBOSE) << " Frame stats: "
                         " encode usage "
                      << *encode_usage_percent_ << " overuse detections "
                      << num_overuse_detections_ << " rampup delay "
                      << rampup_delay;
}

不同运行环境的编码使用率上下限阈值默认设置如下(%):
硬编:200/150
软编:85/42
mac软编:单核20/10,双核40/20,其他核心数85/42。(目前没用到)

QP统计

与CPU使用度检测类似,初始化过程发生在编码器重新创建的时候(流初始化,或者编码格式变化)。
QualityScaler通过统计、计算编码后的每幅图像的量化参数(QP,Quantization Parameter,相当于图像的复杂度),当一系列图像的平均QP超过阈值时会调整分辨率(H264的合法范围是24~37),超过37要降分辨率,低于24要提高分辨率。
QP检测的主体代码在quality_scaler.cc的QualityScaler类中,后者作为observer注册到VideoStreamEncoder中,VideoStreamEncoder内完成了相关流程的控制。

  void StartDelayedTask() {
    RTC_DCHECK_EQ(state_, State::kNotStarted);
    state_ = State::kCheckingQp;
    TaskQueueBase::Current()->PostDelayedTask(
        ToQueuedTask([this_weak_ptr = weak_ptr_factory_.GetWeakPtr(), this] {
          if (!this_weak_ptr) {
            // The task has been cancelled through destruction.
            return;
          }
          RTC_DCHECK_EQ(state_, State::kCheckingQp);
          RTC_DCHECK_RUN_ON(&quality_scaler_->task_checker_);
          switch (quality_scaler_->CheckQp()) {
            case QualityScaler::CheckQpResult::kInsufficientSamples: {
              result_.observed_enough_frames = false;
              // After this line, |this| may be deleted.
              DoCompleteTask();
              return;
            }
            case QualityScaler::CheckQpResult::kNormalQp: {
              result_.observed_enough_frames = true;
              // After this line, |this| may be deleted.
              DoCompleteTask();
              return;
            }
            case QualityScaler::CheckQpResult::kHighQp: {
              result_.observed_enough_frames = true;
              result_.qp_usage_reported = true;
              state_ = State::kAwaitingQpUsageHandled;
              rtc::scoped_refptr<QualityScalerQpUsageHandlerCallbackInterface>
                  callback = ConstructCallback();
              quality_scaler_->fast_rampup_ = false;
              // After this line, |this| may be deleted.
              quality_scaler_->handler_->OnReportQpUsageHigh(callback);
              return;
            }
            case QualityScaler::CheckQpResult::kLowQp: {
              result_.observed_enough_frames = true;
              result_.qp_usage_reported = true;
              state_ = State::kAwaitingQpUsageHandled;
              rtc::scoped_refptr<QualityScalerQpUsageHandlerCallbackInterface>
                  callback = ConstructCallback();
              // After this line, |this| may be deleted.
              quality_scaler_->handler_->OnReportQpUsageLow(callback);
              return;
            }
          }
        }),
        GetCheckingQpDelayMs());
  }

每帧编码图像的QP值会被加入样本集中,然后周期性调用CheckQpTask(默认每2秒,但前一次执行结果会影响下一次执行的周期)。

  • 当平均QP值超过阈值上限,则判定为QpHigh触发AdaptDown(另外编码器超码率丢帧率>=60%也会被判定为QpHigh);
  • 当平均QP值低于阈值下限,则判定为QpLow触发AdaptUp。(QP升降级对保分辨率的双流无效)

qp的变化通过回调进行反馈。在函数ReportQPHigh中会修改fast_rampup_ = false; 目的是分辨率一旦降低后,CheckQP调用的周期将变长,再提高分辨率的话,需要等很长时间。

经在不同终端上的测试,不同运行平台的QP值上下限阈值,当前版本的默认QP值设置如下:
硬终端:22 ~ 45(插件中自己定义)
Windows:24 ~ 37(已经改成24 ~ 42)
Android:24 ~ 37(已经改成24 ~ 45)
ios/mac:28 ~ 39(已经改成28 ~ 45)
特殊环境下如果QP阈值默认设置不合适,可以通过WebRTC-Video-QualityScaling这个FieldTrial来设置修改,但一般情况下不建议修改。

降分辨率策略

交替乘 2/3 and 3/4以缩小分辨率到最接近目标分辨率,然后这个最接近的分辨率。
Alternately scale down by 3/4 and 2/3. This results in fractions which are effectively scalable.
For instance, starting at 1280×720 will result in the series
(3/4) => 960×540,
(1/2) => 640×360,
(3/8) => 480×270,
(1/4) => 320×180,
(3/16) => 240×125,
(1/8) => 160×90.

// Generates a scale factor that makes |input_pixels| close to |target_pixels|,
// but no higher than |max_pixels|.
Fraction FindScale(int input_width,
                   int input_height,
                   int target_pixels,
                   int max_pixels,
                   bool variable_start_scale_factor) {
  // This function only makes sense for a positive target.
  RTC_DCHECK_GT(target_pixels, 0);
  RTC_DCHECK_GT(max_pixels, 0);
  RTC_DCHECK_GE(max_pixels, target_pixels);

  const int input_pixels = input_width * input_height;

  // Don't scale up original.
  if (target_pixels >= input_pixels)
    return Fraction{1, 1};

  Fraction current_scale = Fraction{1, 1};
  Fraction best_scale = Fraction{1, 1};

  if (variable_start_scale_factor) {
    // Start scaling down by 2/3 depending on |input_width| and |input_height|.
    if (input_width % 3 == 0 && input_height % 3 == 0) {
      // 2/3 (then alternates 3/4, 2/3, 3/4,...).
      current_scale = Fraction{6, 6};
    }
    if (input_width % 9 == 0 && input_height % 9 == 0) {
      // 2/3, 2/3 (then alternates 3/4, 2/3, 3/4,...).
      current_scale = Fraction{36, 36};
    }
  }

  // The minimum (absolute) difference between the number of output pixels and
  // the target pixel count.
  int min_pixel_diff = std::numeric_limits<int>::max();
  if (input_pixels <= max_pixels) {
    // Start condition for 1/1 case, if it is less than max.
    min_pixel_diff = std::abs(input_pixels - target_pixels);
  }

  // Alternately scale down by 3/4 and 2/3. This results in fractions which are
  // effectively scalable. For instance, starting at 1280x720 will result in
  // the series (3/4) => 960x540, (1/2) => 640x360, (3/8) => 480x270,
  // (1/4) => 320x180, (3/16) => 240x125, (1/8) => 160x90.
  while (current_scale.scale_pixel_count(input_pixels) > target_pixels) {
    if (current_scale.numerator % 3 == 0 &&
        current_scale.denominator % 2 == 0) {
      // Multiply by 2/3.
      current_scale.numerator /= 3;
      current_scale.denominator /= 2;
    } else {
      // Multiply by 3/4.
      current_scale.numerator *= 3;
      current_scale.denominator *= 4;
    }

    int output_pixels = current_scale.scale_pixel_count(input_pixels);
    if (output_pixels <= max_pixels) {
      int diff = std::abs(target_pixels - output_pixels);
      if (diff < min_pixel_diff) {
        min_pixel_diff = diff;
        best_scale = current_scale;
      }
    }
  }
  best_scale.DivideByGcd();

  return best_scale;
}

Leave a comment

Your email address will not be published. Required fields are marked *