本文使用 Zhihu On VSCode 创作并发布
在WebRTC音视频通话中,难免会遇到设备性能比较差,在开启多个应用的时候CPU占用比较高的问题。因此我们需要根据当前性能情况对当前视频能力做调整,优先调整的对象是对CPU消耗最多的编码。
在WebRTC中评估性能是通过对编码时长的估计,而不是系统的CPU统计,这是为什么呢?一方面是因为在不同平台甚至是统一平台不同系统版本上,CPU统计存在较大差异,另一方面CPU占用和目标能力并无法直接关联上。所以,这里选择了比较直接的指标,即编码时长,一旦编码时长超过了采集间隔,那说明当前编码存在非常大的性能瓶颈。选择合适的编码能力能够确保得到一个合理的编码时长。
1. 基本流程
WebRTC中提供了一个根据CPU占用动态调整编码能力的策略,其中CPU占用率没有从系统读取,而是使用编码时长相对采集间隔的占比来估计。主要流程如下:
流程
上述的流程运行于编码线程,由VideoStreamEncoder每隔5s触发一次检查(前3次不做任何处理)。OveruseFrameDetector是一个根据编码时长和采集间隔估计当前性能的管理类,ProcessingUsage是性能估计类,输出编码时长与采集间隔的比值。OveruseFrameDetector检测到overuse或者underuse会回调到VideoStreamEncoder,做AdaptDown或者AdaptUp。
2. 编码占用率计算
编码占用率计算接口为OveruseFrameDetector::ProcessingUsage,实现类SendProcessingUsage1。大概原理:占用率=编码时长/采集间隔,通过编码时长估计当期性能,如果编码时长超过采集间隔,那么当前性能肯定存在瓶颈。这里的编码时长和采集间隔使用指数滤波平滑。 详细实现可以见源码,这里没有必要过多介绍了。
3. Overuse和Underuse的检测
如何获取性能的指标usage_percent我们先按下不表,先看下如何得到overuse和uderuse这两个输出信号。OveruseFrameDetector根据当前编码占用率判断是否为overuse或者underuse,再根据这两个信号AdaptDown或者AdaptUp。
判断为Overuse的条件:
bool OveruseFrameDetector::IsOverusing(int usage_percent) {
// 使用率超过overuse的阈值,一般为90
if (usage_percent >= options_.high_encode_usage_threshold_percent) {
++checks_above_threshold_;
} else {
checks_above_threshold_ = 0;
}
// 连续2次超过阈值才认为是overuse
return checks_above_threshold_ >= options_.high_threshold_consecutive_count;
}
判断为Underuse的条件:
bool OveruseFrameDetector::IsUnderusing(int usage_percent, int64_t time_now) {
// 当前performance上升(ramp up),需要超过一定时长,否则不认为已经是underuse
int delay = in_quick_rampup_ ? kQuickRampUpDelayMs : current_rampup_delay_ms_;
if (time_now < last_rampup_time_ms_ + delay)
return false;
// 低于阈值(一般overuse阈值的一半)就是underuse
return usage_percent < options_.low_encode_usage_threshold_percent;
}
上面uderuse中,ramp up有一个delay,这个delay不是固定不变的,需要根据实际情况调整。这称作overuse退避,为了避免频繁在overuse和underuse之间切换,所以需要对上升做限制,需要满足一定的时长。
- Quick ramp up时的阈值为kQuickRampUpDelayMs(10s)
- 非quick ramp up时,需要做退避。
- overuse->underuse->overuse时,如果距离最后一次ramp up时间小于40s或者连续4次检测overuse,为了避免频繁变换或者避免容易进入overuse,需要对ramp up的时长做x2退避,限制最大ramp up delay为240s。
- 检测到overuse时,后续ramp up需要做退避;检测到underuse时,后续ramp up做quick ramp up。
退避相关算法如下:
bool check_for_backoff = last_rampup_time_ms_ > last_overuse_time_ms_;
if (check_for_backoff) {
if (now_ms - last_rampup_time_ms_ < kStandardRampUpDelayMs ||
num_overuse_detections_ > kMaxOverusesBeforeApplyRampupDelay) {
// Going up was not ok for very long, back off.
current_rampup_delay_ms_ *= kRampUpBackoffFactor;
if (current_rampup_delay_ms_ > kMaxRampUpDelayMs)
current_rampup_delay_ms_ = kMaxRampUpDelayMs;
} else {
// Not currently backing off, reset rampup delay.
current_rampup_delay_ms_ = kStandardRampUpDelayMs;
}
}
另外,overuse、underuser检测阈值可以配置,主要使用CpuOveruseOptions来配置。比如针对单核、双核系统overuse阈值可以降低到20、40,阈值调整到100以上可以disable自适应功能。
4. AdaptDown
当性能不足时需要做视频降级,视频降级有几种策略:
- BALANCED,帧率和分辨率之间平衡
- MAINTAIN_FRAMERATE,保帧率,调整分辨率
- MAINTAIN_RESOLUTION,保分辨率,调整帧率
4.1 MAINTAIN_FRAMERATE
这次adapt down的分辨率需要比上次请求的小(否则就是ramp up了),否则不调整。
分辨率选择通过VideoStreamEncoder::VideoSourceProxy::RequestResolutionLowerThan实现:
bool RequestResolutionLowerThan(int pixel_count,
int min_pixels_per_frame,
bool* min_pixels_reached) {
...
// MAINTAIN_FRAMERATE或者BALANCED时才能调整分辨率
if (!source_ || !IsResolutionScalingEnabled(degradation_preference_)) {
return false;
}
// 根据像素个数来做分辨率选择,像素个数变为原来的3/5
const int pixels_wanted = (pixel_count * 3) / 5;
if (pixels_wanted >= sink_wants_.max_pixel_count) {
return false;
}
// 分辨率降低有限制
if (pixels_wanted < min_pixels_per_frame) {
*min_pixels_reached = true;
return false;
}
// 更新sink wants到source
sink_wants_.max_pixel_count = pixels_wanted;
sink_wants_.target_pixel_count = absl::nullopt;
source_->AddOrUpdateSink(video_stream_encoder_,
GetActiveSinkWantsInternal());
return true;
}
4.2 MAINTAIN_RESOLUTION
帧率不能低于降低到2帧以下。
分辨率选择通过VideoStreamEncoder::VideoSourceProxy::RequestFramerateLowerThan实现:
webrtc:
int RequestFramerateLowerThan(int fps) {
// 帧率降低到2/3
int framerate_wanted = (fps * 2) / 3;
return RestrictFramerate(framerate_wanted) ? framerate_wanted : -1;
}
bool RestrictFramerate(int fps) {
...
// MAINTAIN_RESOLUTION或BALANCED时才能降低帧率
if (!source_ || !IsFramerateScalingEnabled(degradation_preference_))
return false;
// 帧率不能降低到2fps
const int fps_wanted = std::max(kMinFramerateFps, fps);
if (fps_wanted >= sink_wants_.max_framerate_fps)
return false;
// 更新sink wants到source
sink_wants_.max_framerate_fps = fps_wanted;
source_->AddOrUpdateSink(video_stream_encoder_,
GetActiveSinkWantsInternal());
return true;
}
4.3 BALANCED
有时候需要在帧率和分辨率之间找一个平衡点(最新的WebRTC代码中使用BalancedDegradationSettings来实现,这里以老代码为例)。balace采取先降帧率,再降分辨率的策略,即先RestrictFramerate,再走MAINTAIN_FRAMERATE逻辑。
先限制帧率:
int MinFps(int pixels) {
if (pixels <= 320 * 240) {
return 7;
} else if (pixels <= 480 * 270) {
return 10;
} else if (pixels <= 640 * 480) {
return 15;
} else {
return std::numeric_limits<int>::max();
}
}
再走再走MAINTAIN_FRAMERATE逻辑调整分辨率。
5. AdaptUp
当检测性能处于underuse的时候,视频能力需要升级。和AdaptDown一样,也有三种方式,BALANCED、MAINTAIN_FRAMERATE、MAINTAIN_RESOLUTION。
这里对向上调整做了限制,只有向下调整过才能向上调整。
5.1 MAINTAIN_FRAMERATE
bool RequestHigherResolutionThan(int pixel_count) {
...
// MAINTAIN_FRAMERATE或者BALANCED时才能调整分辨率
if (!source_ || !IsResolutionScalingEnabled(degradation_preference_)) {
return false;
}
// 像素x4上调
int max_pixels_wanted = pixel_count;
if (max_pixels_wanted != std::numeric_limits<int>::max())
max_pixels_wanted = pixel_count * 4;
if (max_pixels_wanted <= sink_wants_.max_pixel_count)
return false;
sink_wants_.max_pixel_count = max_pixels_wanted;
if (max_pixels_wanted == std::numeric_limits<int>::max()) {
// Remove any constraints.
sink_wants_.target_pixel_count.reset();
} else {
sink_wants_.target_pixel_count = GetHigherResolutionThan(pixel_count);
}
// 更新sink wants到source
source_->AddOrUpdateSink(video_stream_encoder_,
GetActiveSinkWantsInternal());
return true;
}
5.2 MAINTAIN_RESOLUTION
和AdaptUp类似,也是按照固定倍数上调。
int RequestHigherFramerateThan(int fps) {
// 调整速率,down是2/3,up是3/2
int framerate_wanted = fps;
if (fps != std::numeric_limits<int>::max())
framerate_wanted = (fps * 3) / 2;
return IncreaseFramerate(framerate_wanted) ? framerate_wanted : -1;
}
5.3 BALANCED
先升分辨率,再升帧率(走MAINTAIN_FRAMERATE
逻辑)。
int MaxFps(int pixels) {
if (pixels <= 320 * 240) {
return 10;
} else if (pixels <= 480 * 270) {
return 15;
} else {
return std::numeric_limits<int>::max();
}
}
6. 总结
总结一下,性能自适应代码逻辑也不是很负载,主要的思想也就几点:
- 采取编码时长占采集间隔的比例得到性能的估计
- 根据性能估计得到overuse、underuse信号做adapt down和adapt up
- 从overuse到underuse需要谨慎,避免频繁切换
- 向上调整和向下调整策略主要有三个:保帧率、保分辨率、帧率和分辨率平衡。
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓