blob: 987f0ad4ae675504af9896e87c18d7a5b77ea9e8 [file] [log] [blame] [edit]
/*
* Copyright (c) 2025 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "modules/video_coding/utility/encoder_speed_controller_impl.h"
#include <cstddef>
#include <memory>
#include <optional>
#include "api/units/time_delta.h"
#include "api/video_codecs/encoder_speed_controller.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
namespace webrtc {
namespace {
// We want to increase the speed quickly in case we're overusing,
// but be slower to decrease speed and thus try using more resources.
// Constants governing how we adapt towards slower speed/higher quality.
constexpr double kSlowFilterAlpha = 0.1;
constexpr int kMinSamplesForDecreasedSpeed = 6;
constexpr double kReducedSpeedUtilizationFactorThreshold = 0.50;
// Constants governing how we adapt towards fasted speed/lower quality.
// Allows the utilization up to 95% for the fast reacting smaller window, but
// only up to 75% utilization for the slower and longer window.
constexpr double kFastFilterAlpha = 0.3;
constexpr int kMinSamplesForIncreasedSpeedFastFilter = 4;
constexpr int kMinSamplesForIncreasedSpeedSlowFilter = 10;
constexpr double kIncreasedSpeedUtilizationFactorThresholdSlowFilter = 0.75;
constexpr double kIncreasedSpeedUtilizationFactorThresholdFastFilter = 0.95;
// Exp filter constant for calculating the "current" QP.
constexpr double kQpFilterAlpha = 0.2;
// Keyframes usually take 4-5 times longer to encoder, but they are
// rare (relatively speaking) so divide the encode time by this
// factor in order to not over-react.
constexpr double kKeyframeEncodeTimeCompensator = 3.5;
} // namespace
EncoderSpeedControllerImpl::EncoderSpeedControllerImpl(
const Config& config,
TimeDelta start_frame_interval)
: config_(config),
frame_interval_(start_frame_interval),
current_speed_index_(config.start_speed_index),
num_samples_(0),
slow_filtered_encode_time_ms_(0),
fast_filtered_encode_time_ms_(0),
filtered_qp_(0) {}
std::unique_ptr<webrtc::EncoderSpeedController>
EncoderSpeedControllerImpl::Create(
const webrtc::EncoderSpeedController::Config& config,
TimeDelta start_frame_interval) {
if (config.speed_levels.empty()) {
RTC_LOG(LS_WARNING) << "EncoderSpeedController: No speed levels provided.";
return nullptr;
}
if (config.start_speed_index < 0 ||
static_cast<size_t>(config.start_speed_index) >=
config.speed_levels.size()) {
RTC_LOG(LS_WARNING) << "EncoderSpeedController: Invalid start_speed_index: "
<< config.start_speed_index;
return nullptr;
}
std::optional<int> last_seen_qp_limit = std::nullopt;
for (size_t i = 0; i < config.speed_levels.size(); ++i) {
const auto& speed_level = config.speed_levels[i];
if (speed_level.min_qp.has_value()) {
if (last_seen_qp_limit.has_value() &&
*speed_level.min_qp > *last_seen_qp_limit) {
RTC_LOG(LS_WARNING) << "EncoderSpeedController: Speed level " << i
<< " has min_qp value of " << *speed_level.min_qp
<< " which is higher than the previous limit of "
<< *last_seen_qp_limit;
return nullptr;
}
last_seen_qp_limit = speed_level.min_qp;
}
}
if (start_frame_interval.IsInfinite() || start_frame_interval.us() <= 0) {
RTC_LOG(LS_WARNING)
<< "EncoderSpeedController: Invalid start frame interval: "
<< start_frame_interval;
return nullptr;
}
return std::unique_ptr<EncoderSpeedControllerImpl>(
new EncoderSpeedControllerImpl(config, start_frame_interval));
}
void EncoderSpeedControllerImpl::ResetStats() {
num_samples_ = 0;
slow_filtered_encode_time_ms_ = 0;
fast_filtered_encode_time_ms_ = 0;
filtered_qp_ = 0;
}
void EncoderSpeedControllerImpl::IncreaseSpeed() {
if (static_cast<size_t>(current_speed_index_) <
config_.speed_levels.size() - 1) {
RTC_LOG(LS_VERBOSE) << "EncoderSpeedController: Increasing speed from "
<< current_speed_index_ << " to "
<< current_speed_index_ + 1;
++current_speed_index_;
ResetStats();
}
}
void EncoderSpeedControllerImpl::DecreaseSpeed() {
if (current_speed_index_ > 0) {
RTC_LOG(LS_VERBOSE) << "EncoderSpeedController: Decreasing speed from "
<< current_speed_index_ << " to "
<< current_speed_index_ - 1;
--current_speed_index_;
ResetStats();
}
}
void EncoderSpeedControllerImpl::SetFrameInterval(TimeDelta frame_interval) {
frame_interval_ = frame_interval;
}
EncoderSpeedController::EncodeSettings
EncoderSpeedControllerImpl::GetEncodeSettings(
EncoderSpeedController::FrameEncodingInfo frame_info) {
RTC_CHECK(frame_interval_.IsFinite());
EncodeSettings settings;
const Config::SpeedLevel& current_level =
config_.speed_levels[current_speed_index_];
settings.speed =
current_level.speeds[static_cast<int>(frame_info.reference_type)];
return settings;
}
void EncoderSpeedControllerImpl::OnEncodedFrame(
EncoderSpeedController::EncodeResults results) {
double encode_tims_ms = results.encode_time.us() / 1000.0;
if (results.frame_info.reference_type == ReferenceClass::kKey) {
encode_tims_ms /= kKeyframeEncodeTimeCompensator;
}
if (num_samples_ == 0) {
if (results.frame_info.is_repeat_frame) {
RTC_LOG(LS_WARNING) << "EncoderSpeedController: Try to start "
"measurements with a repeat frame.";
return;
}
slow_filtered_encode_time_ms_ = encode_tims_ms;
fast_filtered_encode_time_ms_ = encode_tims_ms;
filtered_qp_ = results.qp;
++num_samples_;
} else if (!results.frame_info.is_repeat_frame) {
// Add encode time measurement to filtered members. Don't count repeat
// frames as they have artificially low complexity due to zero movement.
++num_samples_;
slow_filtered_encode_time_ms_ =
(kSlowFilterAlpha * encode_tims_ms) +
((1 - kSlowFilterAlpha) * slow_filtered_encode_time_ms_);
fast_filtered_encode_time_ms_ =
(kFastFilterAlpha * encode_tims_ms) +
((1 - kFastFilterAlpha) * fast_filtered_encode_time_ms_);
filtered_qp_ =
(kQpFilterAlpha * results.qp) + ((1 - kQpFilterAlpha) * filtered_qp_);
}
if (ShouldIncreaseSpeed()) {
// Using too much resources or QP is good enough, try to increase the speed.
IncreaseSpeed();
} else if (ShouldDecreaseSpeed()) {
// Headroom exists to reduce speed.
DecreaseSpeed();
}
}
bool EncoderSpeedControllerImpl::ShouldIncreaseSpeed() const {
if (current_speed_index_ >=
static_cast<int>(config_.speed_levels.size()) - 1) {
// Already at max speed.
return false;
}
if (num_samples_ < kMinSamplesForIncreasedSpeedFastFilter) {
// Too few samples for fast filter.
return false;
}
if (fast_filtered_encode_time_ms_ >
(kIncreasedSpeedUtilizationFactorThresholdFastFilter *
frame_interval_.ms())) {
// Fast filter has detected overuse.
return true;
}
if (num_samples_ < kMinSamplesForIncreasedSpeedSlowFilter) {
// Too few samples for slow filter and filtered QP.
return false;
}
if (slow_filtered_encode_time_ms_ >
(kIncreasedSpeedUtilizationFactorThresholdSlowFilter *
frame_interval_.ms())) {
// Slow filter has detected overuse.
return true;
}
const Config::SpeedLevel& current_level =
config_.speed_levels[current_speed_index_];
if (current_level.min_qp.has_value() &&
filtered_qp_ < *current_level.min_qp) {
// Quality is already high, increase speed.
return true;
}
return false;
}
bool EncoderSpeedControllerImpl::ShouldDecreaseSpeed() const {
if (current_speed_index_ <= 0) {
// Already at slowest speed.
return false;
}
if (num_samples_ < kMinSamplesForDecreasedSpeed) {
// Not enough samples collected.
return false;
}
if (slow_filtered_encode_time_ms_ >
(kReducedSpeedUtilizationFactorThreshold * frame_interval_.ms())) {
// Not enough headroom exists to reduce speed.
return false;
}
// Headroom exists, check conditions of the next slower speed.
const Config::SpeedLevel& next_speed_level =
config_.speed_levels[current_speed_index_ - 1];
if (!next_speed_level.min_qp.has_value() ||
filtered_qp_ >= *next_speed_level.min_qp) {
// No QP limit, or current QP high enough - allow slower speed.
return true;
}
return false;
}
} // namespace webrtc