/* * 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 #include #include #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 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(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 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( 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(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(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(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