diff --git a/modules/video_coding/BUILD.gn b/modules/video_coding/BUILD.gn index 2318bcdaa3..7e044d26c3 100644 --- a/modules/video_coding/BUILD.gn +++ b/modules/video_coding/BUILD.gn @@ -383,6 +383,8 @@ rtc_library("video_coding_utility") { sources = [ "utility/bandwidth_quality_scaler.cc", "utility/bandwidth_quality_scaler.h", + "utility/corruption_detection_settings_generator.cc", + "utility/corruption_detection_settings_generator.h", "utility/decoded_frames_history.cc", "utility/decoded_frames_history.h", "utility/frame_dropper.cc", @@ -419,6 +421,7 @@ rtc_library("video_coding_utility") { "../../api/environment", "../../api/units:data_rate", "../../api/units:time_delta", + "../../api/video:corruption_detection_filter_settings", "../../api/video:encoded_frame", "../../api/video:encoded_image", "../../api/video:video_adaptation", @@ -454,6 +457,7 @@ rtc_library("video_coding_utility") { "svc:scalability_mode_util", "//third_party/abseil-cpp/absl/numeric:bits", "//third_party/abseil-cpp/absl/strings:string_view", + "//third_party/abseil-cpp/absl/types:variant", ] } @@ -1133,6 +1137,7 @@ if (rtc_include_tests) { "rtp_vp8_ref_finder_unittest.cc", "rtp_vp9_ref_finder_unittest.cc", "utility/bandwidth_quality_scaler_unittest.cc", + "utility/corruption_detection_settings_generator_unittest.cc", "utility/decoded_frames_history_unittest.cc", "utility/frame_dropper_unittest.cc", "utility/framerate_controller_deprecated_unittest.cc", diff --git a/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc b/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc index 97d6125398..1289650a87 100644 --- a/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc +++ b/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc @@ -742,6 +742,31 @@ int LibvpxVp8Encoder::InitEncode(const VideoCodec* inst, UpdateVpxConfiguration(stream_idx); } + corruption_detection_settings_generator_ = + std::make_unique( + CorruptionDetectionSettingsGenerator::ExponentialFunctionParameters{ + .scale = 0.006, + .exponent_factor = 0.01857465, + .exponent_offset = -4.26470513}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{.luma = 5, + .chroma = 6}, + // On large changes, increase error threshold by one and std_dev + // by 2.0. Trigger on qp changes larger than 30, and fade down the + // adjusted value over 4 * num_temporal_layers to allow the base layer + // to converge somewhat. Set a minim filter size of 1.25 since some + // outlier pixels deviate a bit from truth even at very low QP, + // seeminly by bleeding into neighbours. + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{ + .max_qp = 127, + .keyframe_threshold_offset = 1, + .keyframe_stddev_offset = 2.0, + .keyframe_offset_duration_frames = + std::max(1, + SimulcastUtility::NumberOfTemporalLayers(*inst, 0)) * + 4, + .large_qp_change_threshold = 30, + .std_dev_lower_bound = 1.25}); + return InitAndSetControlSettings(); } @@ -1261,6 +1286,12 @@ int LibvpxVp8Encoder::GetEncodedPartitions(const VideoFrame& input_image, last_encoder_output_time_[stream_idx] = Timestamp::Micros(input_image.timestamp_us()); + encoded_images_[encoder_idx].set_corruption_detection_filter_settings( + corruption_detection_settings_generator_->OnFrame( + encoded_images_[encoder_idx].FrameType() == + VideoFrameType::kVideoFrameKey, + qp_128)); + encoded_complete_callback_->OnEncodedImage(encoded_images_[encoder_idx], &codec_specific); const size_t steady_state_size = SteadyStateSize( diff --git a/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.h b/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.h index 3f26efc395..733f746c1e 100644 --- a/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.h +++ b/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.h @@ -29,6 +29,7 @@ #include "modules/video_coding/codecs/interface/libvpx_interface.h" #include "modules/video_coding/codecs/vp8/include/vp8.h" #include "modules/video_coding/include/video_codec_interface.h" +#include "modules/video_coding/utility/corruption_detection_settings_generator.h" #include "modules/video_coding/utility/framerate_controller_deprecated.h" #include "modules/video_coding/utility/vp8_constants.h" #include "rtc_base/experiments/encoder_info_settings.h" @@ -148,6 +149,9 @@ class LibvpxVp8Encoder : public VideoEncoder { std::optional max_frame_drop_interval_; bool android_specific_threading_settings_; + + std::unique_ptr + corruption_detection_settings_generator_; }; } // namespace webrtc diff --git a/modules/video_coding/codecs/vp8/test/vp8_impl_unittest.cc b/modules/video_coding/codecs/vp8/test/vp8_impl_unittest.cc index 7b105413c6..a40f2f5f13 100644 --- a/modules/video_coding/codecs/vp8/test/vp8_impl_unittest.cc +++ b/modules/video_coding/codecs/vp8/test/vp8_impl_unittest.cc @@ -44,6 +44,8 @@ using ::testing::Field; using ::testing::Invoke; using ::testing::NiceMock; using ::testing::Return; +using ::testing::Values; +using ::testing::WithParamInterface; using EncoderInfo = webrtc::VideoEncoder::EncoderInfo; using FramerateFractions = absl::InlinedVector; @@ -633,6 +635,19 @@ TEST_F(TestVp8Impl, KeepsTimestampOnReencode) { encoder.Encode(NextInputFrame(), &delta_frame); } +TEST_F(TestVp8Impl, PopulatesFilterSettings) { + EXPECT_EQ(WEBRTC_VIDEO_CODEC_OK, encoder_->Release()); + EXPECT_EQ(WEBRTC_VIDEO_CODEC_OK, + encoder_->InitEncode(&codec_settings_, kSettings)); + + EncodedImage encoded_frame; + CodecSpecificInfo codec_specific_info; + EncodeAndWaitForFrame(NextInputFrame(), &encoded_frame, &codec_specific_info); + + ASSERT_TRUE(encoded_frame.corruption_detection_filter_settings().has_value()); + EXPECT_GT(encoded_frame.corruption_detection_filter_settings()->std_dev, 0.0); +} + TEST(LibvpxVp8EncoderTest, GetEncoderInfoReturnsStaticInformation) { auto* const vpx = new NiceMock(); LibvpxVp8Encoder encoder(CreateEnvironment(), {}, absl::WrapUnique(vpx)); @@ -678,7 +693,7 @@ TEST(LibvpxVp8EncoderTest, ResolutionBitrateLimitsFromFieldTrial) { EXPECT_THAT( encoder.GetEncoderInfo().resolution_bitrate_limits, - ::testing::ElementsAre( + ElementsAre( VideoEncoder::ResolutionBitrateLimits{123, 11000, 44000, 77000}, VideoEncoder::ResolutionBitrateLimits{456, 22000, 55000, 88000}, VideoEncoder::ResolutionBitrateLimits{789, 33000, 66000, 99000})); @@ -719,7 +734,7 @@ TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationNoLayers) { FramerateFractions(1, EncoderInfo::kMaxFramerateFraction)}; EXPECT_THAT(encoder_->GetEncoderInfo().fps_allocation, - ::testing::ElementsAreArray(expected_fps_allocation)); + ElementsAreArray(expected_fps_allocation)); } TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationTwoTemporalLayers) { @@ -737,7 +752,7 @@ TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationTwoTemporalLayers) { expected_fps_allocation[0].push_back(EncoderInfo::kMaxFramerateFraction); EXPECT_THAT(encoder_->GetEncoderInfo().fps_allocation, - ::testing::ElementsAreArray(expected_fps_allocation)); + ElementsAreArray(expected_fps_allocation)); } TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationThreeTemporalLayers) { @@ -756,7 +771,7 @@ TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationThreeTemporalLayers) { expected_fps_allocation[0].push_back(EncoderInfo::kMaxFramerateFraction); EXPECT_THAT(encoder_->GetEncoderInfo().fps_allocation, - ::testing::ElementsAreArray(expected_fps_allocation)); + ElementsAreArray(expected_fps_allocation)); } TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationScreenshareLayers) { @@ -777,7 +792,7 @@ TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationScreenshareLayers) { // Expect empty vector, since this mode doesn't have a fixed framerate. FramerateFractions expected_fps_allocation[kMaxSpatialLayers]; EXPECT_THAT(encoder_->GetEncoderInfo().fps_allocation, - ::testing::ElementsAreArray(expected_fps_allocation)); + ElementsAreArray(expected_fps_allocation)); } TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationSimulcastVideo) { @@ -809,7 +824,7 @@ TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationSimulcastVideo) { expected_fps_allocation[1] = expected_fps_allocation[0]; expected_fps_allocation[2] = expected_fps_allocation[0]; EXPECT_THAT(encoder_->GetEncoderInfo().fps_allocation, - ::testing::ElementsAreArray(expected_fps_allocation)); + ElementsAreArray(expected_fps_allocation)); // Release encoder and re-init without temporal layers. EXPECT_EQ(WEBRTC_VIDEO_CODEC_OK, encoder_->Release()); @@ -818,7 +833,7 @@ TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationSimulcastVideo) { FramerateFractions default_fps_fraction[kMaxSpatialLayers]; default_fps_fraction[0].push_back(EncoderInfo::kMaxFramerateFraction); EXPECT_THAT(encoder_->GetEncoderInfo().fps_allocation, - ::testing::ElementsAreArray(default_fps_fraction)); + ElementsAreArray(default_fps_fraction)); for (int i = 0; i < codec_settings_.numberOfSimulcastStreams; ++i) { codec_settings_.simulcastStream[i].numberOfTemporalLayers = 1; @@ -831,13 +846,12 @@ TEST_F(TestVp8Impl, GetEncoderInfoFpsAllocationSimulcastVideo) { expected_fps_allocation[i].push_back(EncoderInfo::kMaxFramerateFraction); } EXPECT_THAT(encoder_->GetEncoderInfo().fps_allocation, - ::testing::ElementsAreArray(expected_fps_allocation)); + ElementsAreArray(expected_fps_allocation)); } class TestVp8ImplWithMaxFrameDropTrial : public TestVp8Impl, - public ::testing::WithParamInterface< - std::tuple> { + public WithParamInterface> { public: TestVp8ImplWithMaxFrameDropTrial() : TestVp8Impl(), trials_(std::get<0>(GetParam())) {} @@ -960,7 +974,7 @@ TEST_P(TestVp8ImplWithMaxFrameDropTrial, EnforcesMaxFrameDropInterval) { INSTANTIATE_TEST_SUITE_P( All, TestVp8ImplWithMaxFrameDropTrial, - ::testing::Values( + Values( // Tuple of { // trial string, // configured max frame interval, @@ -976,7 +990,7 @@ INSTANTIATE_TEST_SUITE_P( class TestVp8ImplForPixelFormat : public TestVp8Impl, - public ::testing::WithParamInterface { + public WithParamInterface { public: TestVp8ImplForPixelFormat() : TestVp8Impl(), mappable_type_(GetParam()) {} @@ -1049,7 +1063,7 @@ TEST_P(TestVp8ImplForPixelFormat, EncodeNativeFrameSimulcast) { INSTANTIATE_TEST_SUITE_P(All, TestVp8ImplForPixelFormat, - ::testing::Values(VideoFrameBuffer::Type::kI420, - VideoFrameBuffer::Type::kNV12)); + Values(VideoFrameBuffer::Type::kI420, + VideoFrameBuffer::Type::kNV12)); } // namespace webrtc diff --git a/modules/video_coding/utility/corruption_detection_settings_generator.cc b/modules/video_coding/utility/corruption_detection_settings_generator.cc new file mode 100644 index 0000000000..037a631535 --- /dev/null +++ b/modules/video_coding/utility/corruption_detection_settings_generator.cc @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024 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/corruption_detection_settings_generator.h" + +#include +#include + +#include "rtc_base/checks.h" + +namespace webrtc { +namespace { +void ValidateParameters( + const CorruptionDetectionSettingsGenerator::ErrorThresholds& + default_error_thresholds, + const CorruptionDetectionSettingsGenerator::TransientParameters& + transient_params) { + int offset = transient_params.keyframe_threshold_offset; + RTC_DCHECK_GE(offset, 0); + RTC_DCHECK_LE(offset, 15); + RTC_DCHECK_GE(default_error_thresholds.chroma, 0); + RTC_DCHECK_LE(default_error_thresholds.chroma + offset, 15); + RTC_DCHECK_GE(default_error_thresholds.luma, 0); + RTC_DCHECK_LE(default_error_thresholds.luma + offset, 15); + + RTC_DCHECK_GE(transient_params.max_qp, 0); + RTC_DCHECK_GE(transient_params.keyframe_stddev_offset, 0.0); + RTC_DCHECK_GE(transient_params.keyframe_offset_duration_frames, 0); + RTC_DCHECK_GE(transient_params.large_qp_change_threshold, 0); + RTC_DCHECK_LE(transient_params.large_qp_change_threshold, + transient_params.max_qp); + RTC_DCHECK_GE(transient_params.std_dev_lower_bound, 0.0); + RTC_DCHECK_LE(transient_params.std_dev_lower_bound, 40.0); +} +} // namespace + +CorruptionDetectionSettingsGenerator::CorruptionDetectionSettingsGenerator( + const RationalFunctionParameters& function_params, + const ErrorThresholds& default_error_thresholds, + const TransientParameters& transient_params) + : function_params_(function_params), + error_thresholds_(default_error_thresholds), + transient_params_(transient_params), + frames_since_keyframe_(0) { + ValidateParameters(default_error_thresholds, transient_params); +} + +CorruptionDetectionSettingsGenerator::CorruptionDetectionSettingsGenerator( + const ExponentialFunctionParameters& function_params, + const ErrorThresholds& default_error_thresholds, + const TransientParameters& transient_params) + : function_params_(function_params), + error_thresholds_(default_error_thresholds), + transient_params_(transient_params), + frames_since_keyframe_(0) { + ValidateParameters(default_error_thresholds, transient_params); +} + +CorruptionDetectionFilterSettings CorruptionDetectionSettingsGenerator::OnFrame( + bool is_keyframe, + int qp) { + double std_dev = CalculateStdDev(qp); + int y_err = error_thresholds_.luma; + int uv_err = error_thresholds_.chroma; + + if (is_keyframe || (transient_params_.large_qp_change_threshold > 0 && + std::abs(previous_qp_.value_or(qp) - qp) >= + transient_params_.large_qp_change_threshold)) { + frames_since_keyframe_ = 0; + } + previous_qp_ = qp; + + if (frames_since_keyframe_ <= + transient_params_.keyframe_offset_duration_frames) { + // The progress, from the start at the keyframe at 0.0 to completely back to + // normal at 1.0. + double progress = transient_params_.keyframe_offset_duration_frames == 0 + ? 1.0 + : (static_cast(frames_since_keyframe_) / + transient_params_.keyframe_offset_duration_frames); + double adjusted_std_dev = + std::min(std_dev + transient_params_.keyframe_stddev_offset, 40.0); + double adjusted_y_err = + std::min(y_err + transient_params_.keyframe_threshold_offset, 15); + double adjusted_uv_err = + std::min(uv_err + transient_params_.keyframe_threshold_offset, 15); + + std_dev = ((1.0 - progress) * adjusted_std_dev) + (progress * std_dev); + y_err = static_cast(((1.0 - progress) * adjusted_y_err) + + (progress * y_err) + 0.5); + uv_err = static_cast(((1.0 - progress) * adjusted_uv_err) + + (progress * uv_err) + 0.5); + } + + ++frames_since_keyframe_; + + std_dev = std::max(std_dev, transient_params_.std_dev_lower_bound); + std_dev = std::min(std_dev, 40.0); + + return CorruptionDetectionFilterSettings{.std_dev = std_dev, + .luma_error_threshold = y_err, + .chroma_error_threshold = uv_err}; +} + +double CorruptionDetectionSettingsGenerator::CalculateStdDev(int qp) const { + if (absl::holds_alternative(function_params_)) { + const auto& params = + absl::get(function_params_); + return (qp * params.numerator_factor) / (qp + params.denumerator_term) + + params.offset; + } + RTC_DCHECK( + absl::holds_alternative(function_params_)); + + const auto& params = + absl::get(function_params_); + return params.scale * + std::exp(params.exponent_factor * qp - params.exponent_offset); +} + +} // namespace webrtc diff --git a/modules/video_coding/utility/corruption_detection_settings_generator.h b/modules/video_coding/utility/corruption_detection_settings_generator.h new file mode 100644 index 0000000000..ec788af596 --- /dev/null +++ b/modules/video_coding/utility/corruption_detection_settings_generator.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 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. + */ + +#ifndef MODULES_VIDEO_CODING_UTILITY_CORRUPTION_DETECTION_SETTINGS_GENERATOR_H_ +#define MODULES_VIDEO_CODING_UTILITY_CORRUPTION_DETECTION_SETTINGS_GENERATOR_H_ + +#include + +#include "absl/types/variant.h" +#include "api/video/corruption_detection_filter_settings.h" + +namespace webrtc { + +class CorruptionDetectionSettingsGenerator { + public: + // A struct with the parameters for a ration function used to determine the + // standard deviation as function of the qp. It has the form f(qp) = + // (-numerator_factor * qp) / (denumerator_term + qp) + offset. + struct RationalFunctionParameters { + double numerator_factor = 0.0; + double denumerator_term = 0.0; + double offset = 0.0; + }; + + // A struct with the parameters for an exponential function used to determine + // the standard deviation as a function of the qp. It has the form f(qp) = + // scale * std::exp(exponent_factor * qp - exponent_offset). + struct ExponentialFunctionParameters { + double scale = 0.0; + double exponent_factor = 0.0; + double exponent_offset = 0.0; + }; + + // Allowed error thresholds for luma (Y) and chroma (UV) channels. + struct ErrorThresholds { + int luma = 0; + int chroma = 0; + }; + + // Settings relating to transient events like key-frames. + struct TransientParameters { + // The max qp for the codec in use (e.g. 255 for AV1). + int max_qp = 0; + + // Temporary increase to error thresholds on keyframes. + int keyframe_threshold_offset = 0; + // Temporary increase to std dev on keyframes. + double keyframe_stddev_offset = 0.0; + // Fade-out time (in frames) for temporary keyframe offsets. + int keyframe_offset_duration_frames = 0; + + // How many QP points count as a "large change", or 0 to disable. + // A large change will trigger the same compensation as a keyframe. + int large_qp_change_threshold = 0; + + // Don't use a filter kernel smaller than this. + double std_dev_lower_bound = 0.0; + }; + + CorruptionDetectionSettingsGenerator( + const RationalFunctionParameters& function_params, + const ErrorThresholds& default_error_thresholds, + const TransientParameters& transient_params); + CorruptionDetectionSettingsGenerator( + const ExponentialFunctionParameters& function_params, + const ErrorThresholds& default_error_thresholds, + const TransientParameters& transient_params); + + CorruptionDetectionFilterSettings OnFrame(bool is_keyframe, int qp); + + private: + double CalculateStdDev(int qp) const; + + const absl::variant + function_params_; + const ErrorThresholds error_thresholds_; + const TransientParameters transient_params_; + + int frames_since_keyframe_; + std::optional previous_qp_; +}; + +} // namespace webrtc + +#endif // MODULES_VIDEO_CODING_UTILITY_CORRUPTION_DETECTION_SETTINGS_GENERATOR_H_ diff --git a/modules/video_coding/utility/corruption_detection_settings_generator_unittest.cc b/modules/video_coding/utility/corruption_detection_settings_generator_unittest.cc new file mode 100644 index 0000000000..6d86a8332b --- /dev/null +++ b/modules/video_coding/utility/corruption_detection_settings_generator_unittest.cc @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2024 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/corruption_detection_settings_generator.h" + +#include "test/gmock.h" +#include "test/gtest.h" + +using ::testing::AllOf; +using ::testing::DoubleEq; +using ::testing::DoubleNear; +using ::testing::Eq; +using ::testing::Field; + +namespace webrtc { + +TEST(CorruptionDetectionSettingsGenerator, ExponentialFunctionStdDev) { + CorruptionDetectionSettingsGenerator settings_generator( + CorruptionDetectionSettingsGenerator::ExponentialFunctionParameters{ + .scale = 0.006, + .exponent_factor = 0.01857465, + .exponent_offset = -4.26470513}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{}, + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{}); + + // 0.006 * e^(0.01857465 * 20 + 4.26470513) ~= 0.612 + CorruptionDetectionFilterSettings settings = + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/20); + EXPECT_THAT(settings.std_dev, DoubleNear(0.612, 0.01)); + + // 0.006 * e^(0.01857465 * 20 + 4.26470513) ~= 1.886 + settings = settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/80); + EXPECT_THAT(settings.std_dev, DoubleNear(1.886, 0.01)); +} + +TEST(CorruptionDetectionSettingsGenerator, ExponentialFunctionThresholds) { + CorruptionDetectionSettingsGenerator settings_generator( + CorruptionDetectionSettingsGenerator::ExponentialFunctionParameters{ + .scale = 0.006, + .exponent_factor = 0.01857465, + .exponent_offset = -4.26470513}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{.luma = 5, + .chroma = 6}, + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{}); + + CorruptionDetectionFilterSettings settings = + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/20); + EXPECT_EQ(settings.chroma_error_threshold, 6); + EXPECT_EQ(settings.luma_error_threshold, 5); +} + +TEST(CorruptionDetectionSettingsGenerator, RationalFunctionStdDev) { + CorruptionDetectionSettingsGenerator settings_generator( + CorruptionDetectionSettingsGenerator::RationalFunctionParameters{ + .numerator_factor = -5.5, .denumerator_term = -97, .offset = -1}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{}, + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{}); + + // (20 * -5.5) / (20 - 97) - 1 ~= 0.429 + CorruptionDetectionFilterSettings settings = + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/20); + EXPECT_THAT(settings.std_dev, DoubleNear(0.429, 0.01)); + + // (40 * -5.5) / (40 - 97) - 1 ~= 2.860 + settings = settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/40); + EXPECT_THAT(settings.std_dev, DoubleNear(2.860, 0.01)); +} + +TEST(CorruptionDetectionSettingsGenerator, RationalFunctionThresholds) { + CorruptionDetectionSettingsGenerator settings_generator( + CorruptionDetectionSettingsGenerator::RationalFunctionParameters{ + .numerator_factor = -5.5, .denumerator_term = -97, .offset = -1}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{.luma = 5, + .chroma = 6}, + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{}); + + CorruptionDetectionFilterSettings settings = + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/20); + EXPECT_EQ(settings.chroma_error_threshold, 6); + EXPECT_EQ(settings.luma_error_threshold, 5); +} + +TEST(CorruptionDetectionSettingsGenerator, TransientStdDevOffset) { + CorruptionDetectionSettingsGenerator settings_generator( + // (1 * qp) / (qp - 0) + 1 = 2, for all values of qp. + CorruptionDetectionSettingsGenerator::RationalFunctionParameters{ + .numerator_factor = 1, .denumerator_term = 0, .offset = 1}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{}, + // Two frames with adjusted settings, including the keyframe. + // Adjust the keyframe std_dev by 2. + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{ + .keyframe_stddev_offset = 2.0, + .keyframe_offset_duration_frames = 2, + }); + + EXPECT_THAT(settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/1), + Field(&CorruptionDetectionFilterSettings::std_dev, + DoubleNear(4.0, 0.001))); + + // Second frame has std_dev ofset interpolated halfway between keyframe + // (2.0 + 2.0) and default (2.0) => 3.0 + EXPECT_THAT(settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/1), + Field(&CorruptionDetectionFilterSettings::std_dev, + DoubleNear(3.0, 0.001))); + + EXPECT_THAT(settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/1), + Field(&CorruptionDetectionFilterSettings::std_dev, + DoubleNear(2.0, 0.001))); + + EXPECT_THAT(settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/1), + Field(&CorruptionDetectionFilterSettings::std_dev, + DoubleNear(2.0, 0.001))); +} + +TEST(CorruptionDetectionSettingsGenerator, TransientThresholdOffsets) { + CorruptionDetectionSettingsGenerator settings_generator( + CorruptionDetectionSettingsGenerator::RationalFunctionParameters{ + .numerator_factor = 1, .denumerator_term = 0, .offset = 1}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{.luma = 2, + .chroma = 3}, + // Two frames with adjusted settings, including the keyframe. + // Adjust the error thresholds by 2. + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{ + .keyframe_threshold_offset = 2, + .keyframe_offset_duration_frames = 2, + }); + + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/1), + AllOf(Field(&CorruptionDetectionFilterSettings::chroma_error_threshold, + Eq(5)), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, + Eq(4)))); + + // Second frame has offset interpolated halfway between keyframe and default. + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/1), + AllOf(Field(&CorruptionDetectionFilterSettings::chroma_error_threshold, + Eq(4)), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, + Eq(3)))); + + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/1), + AllOf(Field(&CorruptionDetectionFilterSettings::chroma_error_threshold, + Eq(3)), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, + Eq(2)))); + + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/1), + AllOf(Field(&CorruptionDetectionFilterSettings::chroma_error_threshold, + Eq(3)), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, + Eq(2)))); +} + +TEST(CorruptionDetectionSettingsGenerator, StdDevUpperBound) { + CorruptionDetectionSettingsGenerator settings_generator( + // (1 * qp) / (qp - 0) + 41 = 42, for all values of qp. + CorruptionDetectionSettingsGenerator::RationalFunctionParameters{ + .numerator_factor = 1, .denumerator_term = 0, .offset = 41}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{}, + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{}); + + // `std_dev` capped at max 40.0, which is the limit for the protocol. + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/1), + Field(&CorruptionDetectionFilterSettings::std_dev, DoubleEq(40.0))); +} + +TEST(CorruptionDetectionSettingsGenerator, StdDevLowerBound) { + CorruptionDetectionSettingsGenerator settings_generator( + // (1 * qp) / (qp - 0) + 1 = 2, for all values of qp. + CorruptionDetectionSettingsGenerator::RationalFunctionParameters{ + .numerator_factor = 1, .denumerator_term = 0, .offset = 1}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{}, + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{ + .std_dev_lower_bound = 5.0}); + + // `std_dev` capped at lower bound of 5.0. + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/1), + Field(&CorruptionDetectionFilterSettings::std_dev, DoubleEq(5.0))); +} + +TEST(CorruptionDetectionSettingsGenerator, TreatsLargeQpChangeAsKeyFrame) { + CorruptionDetectionSettingsGenerator settings_generator( + CorruptionDetectionSettingsGenerator::RationalFunctionParameters{ + .numerator_factor = 1, .denumerator_term = 0, .offset = 1}, + CorruptionDetectionSettingsGenerator::ErrorThresholds{.luma = 2, + .chroma = 3}, + // Two frames with adjusted settings, including the keyframe. + // Adjust the error thresholds by 2. + webrtc::CorruptionDetectionSettingsGenerator::TransientParameters{ + .max_qp = 100, + .keyframe_threshold_offset = 2, + .keyframe_offset_duration_frames = 1, + .large_qp_change_threshold = 20}); + + // +2 offset due to keyframe. + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/true, /*qp=*/10), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, Eq(4))); + + // Back to normal. + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/10), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, Eq(2))); + + // Large change in qp, treat as keyframe => add +2 offset. + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/30), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, Eq(4))); + + // Back to normal. + EXPECT_THAT( + settings_generator.OnFrame(/*is_keyframe=*/false, /*qp=*/30), + Field(&CorruptionDetectionFilterSettings::luma_error_threshold, Eq(2))); +} + +} // namespace webrtc