转:Webrtc video framerate/resolution 自适应

转:https://xie.infoq.cn/article/50b7931b8a023f8ca7f25d4e9
一,引言
音视频会议使用者的设备性能往往是参差不齐的,当我们一味的去追求视频的高清,高流畅,忽略设备性能时,就会出现用户抱怨设备发热,掉电快,视频卡顿,掉帧等问题,因此就需要一种策略根据当前设备性能情况来动态的调整视频码率/帧率,为用户提供更好音视频体验感。本文主要讲 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

    过载需满足的两个条件
    usage_percent 值超过 85

    1. usage_percent 连续超过两次,当被认定过载
    2. 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 代码量巨大,每次读完很快就忘记了,聊以此文以作备忘。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值