| /* | |
| * 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 |