一,引言
音视频会议使用者的设备性能往往是参差不齐的,当我们一味的去追求视频的高清,高流畅,忽略设备性能时,就会出现用户抱怨设备发热,掉电快,视频卡顿,掉帧等问题,因此就需要一种策略根据当前设备性能情况来动态的调整视频码率/帧率,为用户提供更好音视频体验感。本文主要讲 webrtc 如何实现这一策略的。
二,Video 自适应策略
用户开启网络视频会议一般会有文档模式和视频模式两种使用场景,文档模式要求较高的清晰度,而视频模式则对视频流畅度要求较高。当资源过载时,针对不同的场景需要不同的调整策略,Webrtc 实现 3 种调整策略。
 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,};
       复制代码
 
三,如何设置自适应策略
webrtc api 层提供了自适应策略的设置接口,通过设置 videotrack 的 ContentHint 属性就可以了。
   enum class ContentHint { kNone, kFluid, kDetailed, kText };
   
       复制代码
 ContentHint 为 kDetailed/kText 暗示了编码器保持分辨率,降帧率,设置为 kFluid,暗示编码器保持帧率,示例:
  void StartVideo(){   			.....        auto track = mSharedFactory->createVideoTrack(tag, source);   			track->set_content_hint(webrtc::VideoTrackInterface::ContentHint::kFluid); }
       复制代码
 
* 在 WebRtcVideoSendStream::GetDegradationPreference 函数中将 ContentHint  转换成 DegradationPreference ,webrtc branch89 只能通过追加"WebRTC-Video-BalancedDegradation"到field_trial 才能开启 BALANCED 模式。
四,自适应流程
下图是 webrtc 视频自适应的大致流程图,典型的封闭反馈系统。
五,“过载检测器”
webrtc gcc 里面有一个过载检测器,用于判断网络的拥塞状况。在视频自适应这一块 webrtc 也是用过载检测器,分别对 cpu,qp,分辨率进行状态检测,通过与设定阈值比较,高于就认为过载,低于就认为欠载。
1,  EncodeUsageResource
“cpu 检测器”,通过编码器占用率与设定的阀值进行比较,编码器占用率计算公式:
编码器占用率 = 编码时长/采集间隔,具体的实现在 SendProcessingUsage1 类中,编码时长与采集间隔
都用了指数加权移动平均法(EWMA)。
 void FrameCaptured(const VideoFrame& frame,                     int64_t time_when_first_seen_us,                     int64_t last_capture_time_us) override {    if (last_capture_time_us != -1)      AddCaptureSample(1e-3 * (time_when_first_seen_us - last_capture_time_us));
    frame_timing_.push_back(FrameTiming(frame.timestamp_us(), frame.timestamp(),                                        time_when_first_seen_us));  }
       复制代码
  absl::optional<int> FrameSent(      uint32_t timestamp,      int64_t time_sent_in_us,      int64_t /* capture_time_us */,      absl::optional<int> /* encode_duration_us */) override {    absl::optional<int> encode_duration_us;        while (!frame_timing_.empty()) {          if (timing.last_send_us != -1) {        encode_duration_us.emplace(            static_cast<int>(timing.last_send_us - timing.capture_us));
        if (last_processed_capture_time_us_ != -1) {          int64_t diff_us = timing.capture_us - last_processed_capture_time_us_;          AddSample(1e-3 * (*encode_duration_us), 1e-3 * diff_us);        }    }
       复制代码
 
 int Value() override {    if (count_ < static_cast<uint32_t>(options_.min_frame_samples)) {      return static_cast<int>(InitialUsageInPercent() + 0.5f);    }    float frame_diff_ms = std::max(filtered_frame_diff_ms_->filtered(), 1.0f);    frame_diff_ms = std::min(frame_diff_ms, max_sample_diff_ms_);    float encode_usage_percent =        100.0f * filtered_processing_ms_->filtered() / frame_diff_ms;    return static_cast<int>(encode_usage_percent + 0.5);  }
       复制代码
 Overuse 与 Underuse 状态的判断
经过上面几步,得到了编码器占用率,然后与设定的 CpuOveruseOptions 做对比就可以判断是否过载了,看下 CpuOveruseOptions 定义,
 struct CpuOveruseOptions {  int low_encode_usage_threshold_percent;  // Threshold for triggering underuse.  int high_encode_usage_threshold_percent;  // Threshold for triggering overuse.  // General settings.  int frame_timeout_interval_ms;  // The maximum allowed interval between two                                  // frames before resetting estimations.  int min_frame_samples;          // The minimum number of frames required.  int min_process_count;  // The number of initial process times required before                          // triggering an overuse/underuse.  int high_threshold_consecutive_count;  // The number of consecutive checks                                         // above the high threshold before                                         // triggering an overuse.  // New estimator enabled if this is set non-zero.  int filter_time_ms;  // Time constant for averaging};
       复制代码
 high_encode_usage_threshold_percent:过载阀值,默认为 85.
low_encode_usage_threshold_percent: 欠载阀值,默认值为(high_encode_usage_threshold_percent - 1) / 2 也就是 42.
high_threshold_consecutive_count:过载的次数,默认为 2。
过载需满足的两个条件
 usage_percent 值超过 85
 usage_percent 连续超过两次,当被认定过载 checks_above_threshold_会被重置为 0.
 bool OveruseFrameDetector::IsOverusing(int usage_percent) {  if (usage_percent >= options_.high_encode_usage_threshold_percent) {    ++checks_above_threshold_;  } else {    checks_above_threshold_ = 0;  }  return checks_above_threshold_ >= options_.high_threshold_consecutive_count;}
       复制代码
 
欠载处理的 case 稍微就有点复杂了,目的是为了避免 Underuse 与 Overuse 频繁切换。
 bool OveruseFrameDetector::IsUnderusing(int usage_percent, int64_t time_now) {  RTC_DCHECK_RUN_ON(&task_checker_);  int delay = in_quick_rampup_ ? kQuickRampUpDelayMs : current_rampup_delay_ms_;  if (time_now < last_rampup_time_ms_ + delay)    return false;
  return usage_percent < options_.low_encode_usage_threshold_percent;}
       复制代码
 kQuickRampUpDelayMs: 默认为 10s。
in_quick_rampup_: 标识是否需要快速上升,当上次是 Underuse 时,in_quick_rampup_才为 true。
current_rampup_delay_ms_:假设上次是 Underuse ,当前是 Overuse 状态,为了避免下次很快切换成 Underuse 状态,引入了这个变量。 
T1: 上次 Underuse  --> Overuse 的间隔
T2: 下次 Overuse --> Underuse  的间隔
当 T1 >= 40, T2 也就是 current_rampup_delayms 直接取 40,
当 T1 < 40 或者 Overuse 的总次数大于 4, T2 的范围在[80,240].
delay:用于计算当前时间与上次状态为 Underuse 时间间隔,防止状态切换过于频繁(不仅是 Underuse 与 Overuse,还要考虑 Underuse 与 Underuse)。
为了方便介绍 Underuse 处理的两种 case,对 IsUnderusing 函数引入几个变量进行了注解。
case1:  假设上次是 Underuse,此时 in_quick_rampup_为 true,需要快速上升,仅需当前时间与上次切换时间间隔大于 10s 就行了。
case2:假设上次是 Overuse 或者其它,这时候 delay 就是 current_rampup_delay_ms_,current_rampup_delay_ms_的计算原理见上面的注解.
上面两种处理 case 完成,还要必须满足 usage_percent  必须小于 42,才算为 Underuse。
2,  QualityScalerResource
"QP 检测器" QP 反应了视频的质量,过大,图像失真质量下降。过小,导致码率上升。QP 变化跟目标码率,编码内容有关,当 qp 过大或者过小,可以通过设置帧率和分辨率来影响到目标码率从而对 qp 进行调整。
kHighQp 与 kLowQp 判断
为了防止输入 QP 值波动过大影响结果,这里使用了移动平均法(Moving Average)进行了简单处理,将结果与我们设定的值进行比较就可以得出是 kHighQp /kLowQp ,为了方便后期处理 kHighQp 会转化成 kOveruse,kLowQp  转化成 kUnderuse,
 QualityScaler::CheckQpResult QualityScaler::CheckQp() const {
  // If we have not observed at least this many frames we can't make a good  // scaling decision.  const size_t frames = config_.use_all_drop_reasons                            ? framedrop_percent_all_.Size()                            : framedrop_percent_media_opt_.Size();    //1, 采样点小于60, 认为采样点不足,直接返回  if (frames < min_frames_needed_) {    return CheckQpResult::kInsufficientSamples;  }
  // Check if we should scale down due to high frame drop.  const absl::optional<int> drop_rate =      config_.use_all_drop_reasons          ? framedrop_percent_all_.GetAverageRoundedDown()          : framedrop_percent_media_opt_.GetAverageRoundedDown();  //2,当drop_rate大于 60%,认为视频质量不佳  if (drop_rate && *drop_rate >= kFramedropPercentThreshold) {    RTC_LOG(LS_INFO) << "Reporting high QP, framedrop percent " << *drop_rate;    return CheckQpResult::kHighQp;  }
  // Check if we should scale up or down based on QP.  const absl::optional<int> avg_qp_high =      qp_smoother_high_ ? qp_smoother_high_->GetAvg()                        : average_qp_.GetAverageRoundedDown();  const absl::optional<int> avg_qp_low =      qp_smoother_low_ ? qp_smoother_low_->GetAvg()                       : average_qp_.GetAverageRoundedDown();  if (avg_qp_high && avg_qp_low) {        // 3,当*avg_qp_high > 37,qp 过高    if (*avg_qp_high > thresholds_.high) {      return CheckQpResult::kHighQp;    }    // 4,当*avg_qp_low <= 24,qp 过低    if (*avg_qp_low <= thresholds_.low) {      return CheckQpResult::kLowQp;    }  }  return CheckQpResult::kNormalQp;}
       复制代码
 
3,  PixelLimitResource
“分辨率检测器” 目前 webrtc 这一块未启用,需要设置在 field_trial 才能开启。结合代码介绍
 void PixelLimitResource::SetResourceListener(ResourceListener* listener) {
      // 1, 启用一个5s 的定时器    repeating_task_ = RepeatingTaskHandle::Start(task_queue_, [&] {          //2,获取最后一帧编码前视频帧的frame_size(宽*高)      absl::optional<int> frame_size_pixels =          input_state_provider_->InputState().frame_size_pixels();      if (!frame_size_pixels.has_value()) {        // We haven't observed a frame yet so we don't know if it's going to be        // too big or too small, try again later.        return kResourceUsageCheckIntervalMs;      }      int current_pixels = frame_size_pixels.value();            //3,max_pixels_ 是从field_trial 读取而来      int target_pixel_upper_bounds = max_pixels_.value();      int target_pixels_lower_bounds =          GetLowerResolutionThan(target_pixel_upper_bounds);      // 4,与max_pixels_ 进行比较      if (current_pixels > target_pixel_upper_bounds) {        listener_->OnResourceUsageStateMeasured(this,                                                ResourceUsageState::kOveruse);      } else if (current_pixels < target_pixels_lower_bounds) {        listener_->OnResourceUsageStateMeasured(this,                                                ResourceUsageState::kUnderuse);      }      return kResourceUsageCheckIntervalMs;    });  } }
       复制代码
 
native 端开发,一般会使用 VideoAdapter 对视频流进行预处理,那么对分辨率的过载检测是否还有意义?
六,“自适应处理器”
经过上面检测器的处理生成了 kOveruse 和 kUnderuse 两种信号,再结合 DegradationPreference 就可以对调整的力度进行控制了。
1,DegradationPreference 为 MAINTAIN_RESOLUTION ,调整分辨率。
 int GetHigherResolutionThan(int pixel_count) {  return pixel_count != std::numeric_limits<int>::max()             ? (pixel_count * 5) / 3             : std::numeric_limits<int>::max();}
       复制代码
  int GetLowerResolutionThan(int pixel_count) {  return (pixel_count * 3) / 5;}
       复制代码
 
2,DegradationPreference 为 MAINTAIN_FRAMERATE,调整帧率。
 int GetHigherFrameRateThan(int fps) {  return fps != std::numeric_limits<int>::max()             ? (fps * 3) / 2             : std::numeric_limits<int>::max();}
       复制代码
 
 int GetLowerFrameRateThan(int fps) {  RTC_DCHECK(fps != std::numeric_limits<int>::max());  return (fps * 2) / 3;}
       复制代码
 3,DegradationPreference 为 BALANCED,平衡帧率和分辨率,功能是否实现?
七,结尾
以上便是 webrtc 视频自适应的主要内容,在 webrtc 开发工作中,经常遇到帧率骤降,网络良好分辨率很低等问题,但是由于 webrtc 代码量巨大,每次读完很快就忘记了,聊以此文以作备忘。
评论