Add QualityConvergenceController to VideoStreamEncoder

QualityConvergenceController is a layer between VideoStreamEncoder
and QualityConvergenceMonitor that takes care of the simulcast
logic.

Bug: chromium:328598314
Change-Id: Iad8a9d9138e69a60fd508a7ef038220947888f0a
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/356420
Commit-Queue: Johannes Kron <kron@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#42642}
diff --git a/video/BUILD.gn b/video/BUILD.gn
index 7e85a55..a23d4bb 100644
--- a/video/BUILD.gn
+++ b/video/BUILD.gn
@@ -388,6 +388,8 @@
     "encoder_overshoot_detector.h",
     "frame_encode_metadata_writer.cc",
     "frame_encode_metadata_writer.h",
+    "quality_convergence_controller.cc",
+    "quality_convergence_controller.h",
     "quality_convergence_monitor.cc",
     "quality_convergence_monitor.h",
     "rate_utilization_tracker.cc",
@@ -778,6 +780,7 @@
       "frame_decode_timing_unittest.cc",
       "frame_encode_metadata_writer_unittest.cc",
       "picture_id_tests.cc",
+      "quality_convergence_controller_unittest.cc",
       "quality_convergence_monitor_unittest.cc",
       "quality_limitation_reason_tracker_unittest.cc",
       "quality_scaling_tests.cc",
diff --git a/video/quality_convergence_controller.cc b/video/quality_convergence_controller.cc
new file mode 100644
index 0000000..7a46e11
--- /dev/null
+++ b/video/quality_convergence_controller.cc
@@ -0,0 +1,73 @@
+/*
+ *  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 "video/quality_convergence_controller.h"
+
+#include "rtc_base/checks.h"
+
+namespace webrtc {
+namespace {
+// TODO(https://crbug.com/328598314): Remove default values once HW encoders
+// correctly report the minimum QP value. These thresholds correspond to the
+// default configurations used for the software encoders.
+constexpr int kVp8DefaultStaticQpThreshold = 15;
+constexpr int kVp9DefaultStaticQpThreshold = 32;
+constexpr int kAv1DefaultStaticQpThreshold = 40;
+
+int GetDefaultStaticQpThreshold(VideoCodecType codec) {
+  switch (codec) {
+    case kVideoCodecVP8:
+      return kVp8DefaultStaticQpThreshold;
+    case kVideoCodecVP9:
+      return kVp9DefaultStaticQpThreshold;
+    case kVideoCodecAV1:
+      return kAv1DefaultStaticQpThreshold;
+    case kVideoCodecGeneric:
+    case kVideoCodecH264:
+    case kVideoCodecH265:
+      // -1 will effectively disable the static QP threshold since QP values are
+      // always >= 0.
+      return -1;
+  }
+}
+}  // namespace
+
+void QualityConvergenceController::Initialize(
+    int number_of_layers,
+    absl::optional<int> static_qp_threshold,
+    VideoCodecType codec,
+    const FieldTrialsView& trials) {
+  RTC_CHECK(number_of_layers > 0);
+  number_of_layers_ = number_of_layers;
+  convergence_monitors_.clear();
+
+  int qp_threshold =
+      static_qp_threshold.value_or(GetDefaultStaticQpThreshold(codec));
+  for (int i = 0; i < number_of_layers_; ++i) {
+    convergence_monitors_.push_back(
+        QualityConvergenceMonitor::Create(qp_threshold, codec, trials));
+  }
+  initialized_ = true;
+}
+
+bool QualityConvergenceController::AddSampleAndCheckTargetQuality(
+    int layer_index,
+    int qp,
+    bool is_refresh_frame) {
+  RTC_CHECK(initialized_);
+  if (layer_index < 0 || layer_index >= number_of_layers_) {
+    return false;
+  }
+
+  convergence_monitors_[layer_index]->AddSample(qp, is_refresh_frame);
+  return convergence_monitors_[layer_index]->AtTargetQuality();
+}
+
+}  // namespace webrtc
diff --git a/video/quality_convergence_controller.h b/video/quality_convergence_controller.h
new file mode 100644
index 0000000..46f8419
--- /dev/null
+++ b/video/quality_convergence_controller.h
@@ -0,0 +1,48 @@
+/*
+ *  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 VIDEO_QUALITY_CONVERGENCE_CONTROLLER_H_
+#define VIDEO_QUALITY_CONVERGENCE_CONTROLLER_H_
+
+#include <memory>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/field_trials_view.h"
+#include "api/video/video_codec_type.h"
+#include "video/quality_convergence_monitor.h"
+
+namespace webrtc {
+
+class QualityConvergenceController {
+ public:
+  void Initialize(int number_of_layers,
+                  absl::optional<int> static_qp_threshold,
+                  VideoCodecType codec,
+                  const FieldTrialsView& trials);
+
+  // Add the supplied `qp` value to the detection window for specified layer.
+  // `is_refresh_frame` must only be `true` if the corresponding
+  // video frame is a refresh frame that is used to improve the visual quality.
+  // Returns `true` if the algorithm has determined that the supplied QP values
+  // have converged and reached the target quality for this layer.
+  bool AddSampleAndCheckTargetQuality(int layer_index,
+                                      int qp,
+                                      bool is_refresh_frame);
+
+ private:
+  bool initialized_ = false;
+  int number_of_layers_ = 0;
+  std::vector<std::unique_ptr<QualityConvergenceMonitor>> convergence_monitors_;
+};
+
+}  // namespace webrtc
+
+#endif  // VIDEO_QUALITY_CONVERGENCE_CONTROLLER_H_
diff --git a/video/quality_convergence_controller_unittest.cc b/video/quality_convergence_controller_unittest.cc
new file mode 100644
index 0000000..c1378e0
--- /dev/null
+++ b/video/quality_convergence_controller_unittest.cc
@@ -0,0 +1,66 @@
+
+/*
+ *  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 "video/quality_convergence_controller.h"
+
+#include "test/gtest.h"
+#include "test/scoped_key_value_config.h"
+
+namespace webrtc {
+namespace {
+constexpr int kStaticQpThreshold = 15;
+
+TEST(QualityConvergenceController, Singlecast) {
+  test::ScopedKeyValueConfig field_trials;
+  QualityConvergenceController controller;
+  controller.Initialize(1, kStaticQpThreshold, kVideoCodecVP8, field_trials);
+
+  EXPECT_FALSE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/0, kStaticQpThreshold + 1, /*is_refresh_frame=*/false));
+  EXPECT_TRUE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/0, kStaticQpThreshold, /*is_refresh_frame=*/false));
+}
+
+TEST(QualityConvergenceController, Simulcast) {
+  test::ScopedKeyValueConfig field_trials;
+  QualityConvergenceController controller;
+  controller.Initialize(2, kStaticQpThreshold, kVideoCodecVP8, field_trials);
+
+  EXPECT_FALSE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/0, kStaticQpThreshold + 1, /*is_refresh_frame=*/false));
+  EXPECT_FALSE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/1, kStaticQpThreshold + 1, /*is_refresh_frame=*/false));
+
+  // Layer 0 reaches target quality.
+  EXPECT_TRUE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/0, kStaticQpThreshold, /*is_refresh_frame=*/false));
+  EXPECT_FALSE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/1, kStaticQpThreshold + 1, /*is_refresh_frame=*/false));
+
+  // Frames are repeated for both layers. Layer 0 still at target quality.
+  EXPECT_TRUE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/0, kStaticQpThreshold, /*is_refresh_frame=*/true));
+  EXPECT_FALSE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/1, kStaticQpThreshold + 1, /*is_refresh_frame=*/true));
+}
+
+TEST(QualityConvergenceController, InvalidLayerIndex) {
+  test::ScopedKeyValueConfig field_trials;
+  QualityConvergenceController controller;
+  controller.Initialize(2, kStaticQpThreshold, kVideoCodecVP8, field_trials);
+
+  EXPECT_FALSE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/-1, kStaticQpThreshold, /*is_refresh_frame=*/false));
+  EXPECT_FALSE(controller.AddSampleAndCheckTargetQuality(
+      /*layer_index=*/3, kStaticQpThreshold, /*is_refresh_frame=*/false));
+}
+
+}  // namespace
+}  // namespace webrtc
diff --git a/video/video_stream_encoder.cc b/video/video_stream_encoder.cc
index 92f7308..af26db8 100644
--- a/video/video_stream_encoder.cc
+++ b/video/video_stream_encoder.cc
@@ -1254,6 +1254,10 @@
     codec.SetVideoEncoderComplexity(VideoCodecComplexity::kComplexityLow);
   }
 
+  quality_convergence_controller_.Initialize(
+      codec.numberOfSimulcastStreams, encoder_->GetEncoderInfo().min_qp,
+      codec.codecType, env_.field_trials());
+
   send_codec_ = codec;
 
   // Keep the same encoder, as long as the video_format is unchanged.
@@ -2085,14 +2089,23 @@
             .Parse(codec_type, stream_idx, image_copy.data(), image_copy.size())
             .value_or(-1);
   }
+
+  // Check if the encoded image has reached target quality.
+  const size_t simulcast_index = encoded_image.SimulcastIndex().value_or(0);
+  bool at_target_quality =
+      quality_convergence_controller_.AddSampleAndCheckTargetQuality(
+          simulcast_index, image_copy.qp_,
+          image_copy.IsSteadyStateRefreshFrame());
+  image_copy.SetAtTargetQuality(at_target_quality);
   TRACE_EVENT2("webrtc", "VideoStreamEncoder::AugmentEncodedImage",
                "stream_idx", stream_idx, "qp", image_copy.qp_);
+  TRACE_EVENT_INSTANT2("webrtc", "VideoStreamEncoder::AugmentEncodedImage",
+                       TRACE_EVENT_SCOPE_GLOBAL, "simulcast_idx",
+                       simulcast_index, "at_target_quality", at_target_quality);
   RTC_LOG(LS_VERBOSE) << __func__ << " ntp time " << encoded_image.NtpTimeMs()
                       << " stream_idx " << stream_idx << " qp "
-                      << image_copy.qp_;
-  image_copy.SetAtTargetQuality(codec_type == kVideoCodecVP8 &&
-                                image_copy.qp_ <= kVp8SteadyStateQpThreshold);
-
+                      << image_copy.qp_ << " at target quality "
+                      << at_target_quality;
   return image_copy;
 }
 
diff --git a/video/video_stream_encoder.h b/video/video_stream_encoder.h
index fe04fbf..579cd32 100644
--- a/video/video_stream_encoder.h
+++ b/video/video_stream_encoder.h
@@ -47,6 +47,7 @@
 #include "video/encoder_bitrate_adjuster.h"
 #include "video/frame_cadence_adapter.h"
 #include "video/frame_encode_metadata_writer.h"
+#include "video/quality_convergence_controller.h"
 #include "video/video_source_sink_controller.h"
 #include "video/video_stream_encoder_interface.h"
 #include "video/video_stream_encoder_observer.h"
@@ -418,6 +419,11 @@
   QpParser qp_parser_;
   const bool qp_parsing_allowed_;
 
+  // The quality convergence controller is used to determine if a codec has
+  // reached its target quality. This is used for screenshare to determine when
+  // there's no need to continue encoding the same repeated frame.
+  QualityConvergenceController quality_convergence_controller_;
+
   // Enables encoder switching on initialization failures.
   bool switch_encoder_on_init_failures_;
 
diff --git a/video/video_stream_encoder_unittest.cc b/video/video_stream_encoder_unittest.cc
index aab791f..ac4aa3e 100644
--- a/video/video_stream_encoder_unittest.cc
+++ b/video/video_stream_encoder_unittest.cc
@@ -701,6 +701,10 @@
                 (EncodedImage & encoded_image,
                  rtc::scoped_refptr<EncodedImageBuffer> buffer),
                 (override));
+    MOCK_METHOD(VideoEncoder::EncoderInfo,
+                GetEncoderInfo,
+                (),
+                (const, override));
   };
 
   SimpleVideoStreamEncoderFactory() {
@@ -9475,6 +9479,12 @@
   auto video_stream_encoder =
       factory.Create(std::move(adapter), &encoder_queue);
 
+  // Set minimum QP.
+  VideoEncoder::EncoderInfo info;
+  info.min_qp = kVp8SteadyStateQpThreshold;
+  EXPECT_CALL(factory.GetMockFakeEncoder(), GetEncoderInfo)
+      .WillRepeatedly(Return(info));
+
   // Configure 2 simulcast layers and setup 1 MBit/s to unpause the encoder.
   VideoEncoderConfig video_encoder_config;
   test::FillEncoderConfiguration(kVideoCodecVP8, 2, &video_encoder_config);