一,引言
音视频会议使用者的设备性能往往是参差不齐的,当我们一味的去追求视频的高清,高流畅,忽略设备性能时,就会出现用户抱怨设备发热,掉电快,视频卡顿,掉帧等问题,因此就需要一种策略根据当前设备性能情况来动态的调整视频码率/帧率,为用户提供更好音视频体验感。本文主要讲 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 代码量巨大,每次读完很快就忘记了,聊以此文以作备忘。
评论