写点什么

Webrtc video framerate/resolution 自适应

用户头像
糖米唐爹
关注
发布于: 刚刚
Webrtc video framerate/resolution自适应

一,引言

音视频会议使用者的设备性能往往是参差不齐的,当我们一味的去追求视频的高清,高流畅,忽略设备性能时,就会出现用户抱怨设备发热,掉电快,视频卡顿,掉帧等问题,因此就需要一种策略根据当前设备性能情况来动态的调整视频码率/帧率,为用户提供更好音视频体验感。本文主要讲 webrtc 如何实现这一策略的。

二,Video 自适应策略

用户开启网络视频会议一般会有文档模式和视频模式两种使用场景,文档模式要求较高的清晰度,而视频模式则对视频流畅度要求较高。当资源过载时,针对不同的场景需要不同的调整策略,Webrtc 实现 3 种调整策略。

  • MAINTAIN_FRAMERATE:保帧率,降分辨率,该模式的使用场景为视频模式。

  • MAINTAIN_RESOLUTION: 保分辨率降帧率,使用场景为屏幕共享或者文档模式,对清晰度要求较高的场景。

  • BALANCED: 平衡帧率与分辨率。

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。


  • Overuse

过载需满足的两个条件

  1. usage_percent 值超过 85

  2. 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;}
复制代码


  • Underuse

欠载处理的 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 代码量巨大,每次读完很快就忘记了,聊以此文以作备忘。


用户头像

糖米唐爹

关注

还未添加个人签名 2020.08.12 加入

还未添加个人简介

评论

发布
暂无评论
Webrtc video framerate/resolution自适应