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