Zero-hertz encoding mode: avoid encoder bitrate overshooting.

The encoders wrapped in VideoStreamEncoder grossly over-estimates
available bitrate when capture FPS falls close to zero, and frames
re-commence highly frequent delivery. Avoid this by moving the input
RateStatistics inside VSE into the frame cadence adapter, and changing
the reported framerate under zero-hertz encoding mode to always return
the configured max FPS.

Bug: chromium:1255737
Change-Id: Iaa71ef51c0755b12e24e435d86d9562122ed494e
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/239126
Commit-Queue: Markus Handell <handellm@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Reviewed-by: Stefan Holmer <stefan@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35431}
diff --git a/video/BUILD.gn b/video/BUILD.gn
index 84b2d98..99e82c1 100644
--- a/video/BUILD.gn
+++ b/video/BUILD.gn
@@ -273,6 +273,7 @@
     "../rtc_base:macromagic",
     "../rtc_base:rtc_base_approved",
     "../rtc_base/synchronization:mutex",
+    "../rtc_base/system:no_unique_address",
     "../rtc_base/task_utils:pending_task_safety_flag",
     "../rtc_base/task_utils:to_queued_task",
     "../system_wrappers",
diff --git a/video/frame_cadence_adapter.cc b/video/frame_cadence_adapter.cc
index c82ab5a..c467909 100644
--- a/video/frame_cadence_adapter.cc
+++ b/video/frame_cadence_adapter.cc
@@ -18,7 +18,9 @@
 #include "api/task_queue/task_queue_base.h"
 #include "rtc_base/logging.h"
 #include "rtc_base/race_checker.h"
+#include "rtc_base/rate_statistics.h"
 #include "rtc_base/synchronization/mutex.h"
+#include "rtc_base/system/no_unique_address.h"
 #include "rtc_base/task_utils/pending_task_safety_flag.h"
 #include "rtc_base/task_utils/to_queued_task.h"
 #include "system_wrappers/include/clock.h"
@@ -28,6 +30,80 @@
 namespace webrtc {
 namespace {
 
+// Abstracts concrete modes of the cadence adapter.
+class AdapterMode {
+ public:
+  virtual ~AdapterMode() = default;
+
+  // Called on the worker thread for every frame that enters.
+  virtual void OnFrame(Timestamp post_time,
+                       int frames_scheduled_for_processing,
+                       const VideoFrame& frame) = 0;
+
+  // Returns the currently estimated input framerate.
+  virtual absl::optional<uint32_t> GetInputFrameRateFps() = 0;
+
+  // Updates the frame rate.
+  virtual void UpdateFrameRate() = 0;
+};
+
+// Implements a pass-through adapter. Single-threaded.
+class PassthroughAdapterMode : public AdapterMode {
+ public:
+  PassthroughAdapterMode(Clock* clock,
+                         FrameCadenceAdapterInterface::Callback* callback)
+      : clock_(clock), callback_(callback) {
+    sequence_checker_.Detach();
+  }
+
+  // Adapter overrides.
+  void OnFrame(Timestamp post_time,
+               int frames_scheduled_for_processing,
+               const VideoFrame& frame) override {
+    RTC_DCHECK_RUN_ON(&sequence_checker_);
+    callback_->OnFrame(post_time, frames_scheduled_for_processing, frame);
+  }
+
+  absl::optional<uint32_t> GetInputFrameRateFps() override {
+    RTC_DCHECK_RUN_ON(&sequence_checker_);
+    return input_framerate_.Rate(clock_->TimeInMilliseconds());
+  }
+
+  void UpdateFrameRate() override {
+    RTC_DCHECK_RUN_ON(&sequence_checker_);
+    input_framerate_.Update(1, clock_->TimeInMilliseconds());
+  }
+
+ private:
+  Clock* const clock_;
+  FrameCadenceAdapterInterface::Callback* const callback_;
+  RTC_NO_UNIQUE_ADDRESS SequenceChecker sequence_checker_;
+  // Input frame rate statistics for use when not in zero-hertz mode.
+  RateStatistics input_framerate_ RTC_GUARDED_BY(sequence_checker_){
+      FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000};
+};
+
+// Implements a frame cadence adapter supporting zero-hertz input.
+class ZeroHertzAdapterMode : public AdapterMode {
+ public:
+  ZeroHertzAdapterMode(FrameCadenceAdapterInterface::Callback* callback,
+                       double max_fps);
+
+  // Adapter overrides.
+  void OnFrame(Timestamp post_time,
+               int frames_scheduled_for_processing,
+               const VideoFrame& frame) override;
+  absl::optional<uint32_t> GetInputFrameRateFps() override;
+  void UpdateFrameRate() override {}
+
+ private:
+  FrameCadenceAdapterInterface::Callback* const callback_;
+  // The configured max_fps.
+  // TODO(crbug.com/1255737): support max_fps updates.
+  const double max_fps_;
+  RTC_NO_UNIQUE_ADDRESS SequenceChecker sequence_checker_;
+};
+
 class FrameCadenceAdapterImpl : public FrameCadenceAdapterInterface {
  public:
   FrameCadenceAdapterImpl(Clock* clock, TaskQueueBase* queue);
@@ -35,6 +111,8 @@
   // FrameCadenceAdapterInterface overrides.
   void Initialize(Callback* callback) override;
   void SetZeroHertzModeEnabled(bool enabled) override;
+  absl::optional<uint32_t> GetInputFrameRateFps() override;
+  void UpdateFrameRate() override;
 
   // VideoFrameSink overrides.
   void OnFrame(const VideoFrame& frame) override;
@@ -48,8 +126,18 @@
                           int frames_scheduled_for_processing,
                           const VideoFrame& frame) RTC_RUN_ON(queue_);
 
+  // Returns true under all of the following conditions:
+  // - constraints min fps set to 0
+  // - constraints max fps set and greater than 0,
+  // - field trial enabled
+  // - zero-hertz mode enabled
+  bool IsZeroHertzScreenshareEnabled() const RTC_RUN_ON(queue_);
+
+  // Handles adapter creation on configuration changes.
+  void MaybeReconfigureAdapters(bool was_zero_hertz_enabled) RTC_RUN_ON(queue_);
+
   // Called to report on constraint UMAs.
-  void MaybeReportFrameRateConstraintUmas() RTC_RUN_ON(&queue_);
+  void MaybeReportFrameRateConstraintUmas() RTC_RUN_ON(queue_);
 
   Clock* const clock_;
   TaskQueueBase* const queue_;
@@ -58,6 +146,12 @@
   // 0 Hz.
   const bool zero_hertz_screenshare_enabled_;
 
+  // The two possible modes we're under.
+  absl::optional<PassthroughAdapterMode> passthrough_adapter_;
+  absl::optional<ZeroHertzAdapterMode> zero_hertz_adapter_;
+  // Cache for the current adapter mode.
+  AdapterMode* current_adapter_mode_ = nullptr;
+
   // Set up during Initialize.
   Callback* callback_ = nullptr;
 
@@ -80,6 +174,26 @@
   ScopedTaskSafetyDetached safety_;
 };
 
+ZeroHertzAdapterMode::ZeroHertzAdapterMode(
+    FrameCadenceAdapterInterface::Callback* callback,
+    double max_fps)
+    : callback_(callback), max_fps_(max_fps) {
+  sequence_checker_.Detach();
+}
+
+void ZeroHertzAdapterMode::OnFrame(Timestamp post_time,
+                                   int frames_scheduled_for_processing,
+                                   const VideoFrame& frame) {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  // TODO(crbug.com/1255737): fill with meaningful implementation.
+  callback_->OnFrame(post_time, frames_scheduled_for_processing, frame);
+}
+
+absl::optional<uint32_t> ZeroHertzAdapterMode::GetInputFrameRateFps() {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  return max_fps_;
+}
+
 FrameCadenceAdapterImpl::FrameCadenceAdapterImpl(Clock* clock,
                                                  TaskQueueBase* queue)
     : clock_(clock),
@@ -89,13 +203,30 @@
 
 void FrameCadenceAdapterImpl::Initialize(Callback* callback) {
   callback_ = callback;
+  passthrough_adapter_.emplace(clock_, callback);
+  current_adapter_mode_ = &passthrough_adapter_.value();
 }
 
 void FrameCadenceAdapterImpl::SetZeroHertzModeEnabled(bool enabled) {
   RTC_DCHECK_RUN_ON(queue_);
+  bool was_zero_hertz_enabled = zero_hertz_and_uma_reporting_enabled_;
   if (enabled && !zero_hertz_and_uma_reporting_enabled_)
     has_reported_screenshare_frame_rate_umas_ = false;
   zero_hertz_and_uma_reporting_enabled_ = enabled;
+  MaybeReconfigureAdapters(was_zero_hertz_enabled);
+}
+
+absl::optional<uint32_t> FrameCadenceAdapterImpl::GetInputFrameRateFps() {
+  RTC_DCHECK_RUN_ON(queue_);
+  return current_adapter_mode_->GetInputFrameRateFps();
+}
+
+void FrameCadenceAdapterImpl::UpdateFrameRate() {
+  RTC_DCHECK_RUN_ON(queue_);
+  // The frame rate need not be updated for the zero-hertz adapter. The
+  // passthrough adapter however uses it. Always pass frames into the
+  // passthrough to keep the estimation alive should there be an adapter switch.
+  passthrough_adapter_->UpdateFrameRate();
 }
 
 void FrameCadenceAdapterImpl::OnFrame(const VideoFrame& frame) {
@@ -124,7 +255,9 @@
                    << constraints.max_fps.value_or(-1);
   queue_->PostTask(ToQueuedTask(safety_.flag(), [this, constraints] {
     RTC_DCHECK_RUN_ON(queue_);
+    bool was_zero_hertz_enabled = IsZeroHertzScreenshareEnabled();
     source_constraints_ = constraints;
+    MaybeReconfigureAdapters(was_zero_hertz_enabled);
   }));
 }
 
@@ -133,7 +266,33 @@
     Timestamp post_time,
     int frames_scheduled_for_processing,
     const VideoFrame& frame) {
-  callback_->OnFrame(post_time, frames_scheduled_for_processing, frame);
+  current_adapter_mode_->OnFrame(post_time, frames_scheduled_for_processing,
+                                 frame);
+}
+
+// RTC_RUN_ON(queue_)
+bool FrameCadenceAdapterImpl::IsZeroHertzScreenshareEnabled() const {
+  return zero_hertz_screenshare_enabled_ && source_constraints_.has_value() &&
+         source_constraints_->max_fps.value_or(-1) > 0 &&
+         source_constraints_->min_fps.value_or(-1) == 0 &&
+         zero_hertz_and_uma_reporting_enabled_;
+}
+
+// RTC_RUN_ON(queue_)
+void FrameCadenceAdapterImpl::MaybeReconfigureAdapters(
+    bool was_zero_hertz_enabled) {
+  bool is_zero_hertz_enabled = IsZeroHertzScreenshareEnabled();
+  if (is_zero_hertz_enabled) {
+    if (!was_zero_hertz_enabled) {
+      zero_hertz_adapter_.emplace(callback_,
+                                  source_constraints_->max_fps.value());
+    }
+    current_adapter_mode_ = &zero_hertz_adapter_.value();
+  } else {
+    if (was_zero_hertz_enabled)
+      zero_hertz_adapter_ = absl::nullopt;
+    current_adapter_mode_ = &passthrough_adapter_.value();
+  }
 }
 
 // RTC_RUN_ON(queue_)
diff --git a/video/frame_cadence_adapter.h b/video/frame_cadence_adapter.h
index beb7396..8685f37 100644
--- a/video/frame_cadence_adapter.h
+++ b/video/frame_cadence_adapter.h
@@ -29,6 +29,10 @@
 class FrameCadenceAdapterInterface
     : public rtc::VideoSinkInterface<VideoFrame> {
  public:
+  // Averaging window spanning 90 frames at default 30fps, matching old media
+  // optimization module defaults.
+  static constexpr int64_t kFrameRateAveragingWindowSizeMs = (1000 / 30) * 90;
+
   // Callback interface used to inform instance owners.
   class Callback {
    public:
@@ -66,6 +70,14 @@
 
   // Pass true in |enabled| as a prerequisite to enable zero-hertz operation.
   virtual void SetZeroHertzModeEnabled(bool enabled) = 0;
+
+  // Returns the input framerate. This is measured by RateStatistics when
+  // zero-hertz mode is off, and returns the max framerate in zero-hertz mode.
+  virtual absl::optional<uint32_t> GetInputFrameRateFps() = 0;
+
+  // Updates frame rate. This is done unconditionally irrespective of adapter
+  // mode.
+  virtual void UpdateFrameRate() = 0;
 };
 
 }  // namespace webrtc
diff --git a/video/frame_cadence_adapter_unittest.cc b/video/frame_cadence_adapter_unittest.cc
index a6b6a87..dae0b86 100644
--- a/video/frame_cadence_adapter_unittest.cc
+++ b/video/frame_cadence_adapter_unittest.cc
@@ -16,6 +16,7 @@
 #include "api/task_queue/task_queue_base.h"
 #include "api/video/nv12_buffer.h"
 #include "api/video/video_frame.h"
+#include "rtc_base/rate_statistics.h"
 #include "rtc_base/ref_counted_object.h"
 #include "system_wrappers/include/metrics.h"
 #include "test/field_trial.h"
@@ -56,6 +57,12 @@
       : test::ScopedFieldTrials("WebRTC-ZeroHertzScreenshare/Disabled/") {}
 };
 
+class ZeroHertzFieldTrialEnabler : public test::ScopedFieldTrials {
+ public:
+  ZeroHertzFieldTrialEnabler()
+      : test::ScopedFieldTrials("WebRTC-ZeroHertzScreenshare/Enabled/") {}
+};
+
 TEST(FrameCadenceAdapterTest,
      ForwardsFramesOnConstructionAndUnderDisabledFieldTrial) {
   GlobalSimulatedTimeController time_controller(Timestamp::Millis(1));
@@ -93,6 +100,92 @@
   time_controller.AdvanceTime(TimeDelta::Zero());
 }
 
+TEST(FrameCadenceAdapterTest, FrameRateFollowsRateStatisticsByDefault) {
+  GlobalSimulatedTimeController time_controller(Timestamp::Millis(0));
+  auto adapter = CreateAdapter(time_controller.GetClock());
+  adapter->Initialize(nullptr);
+
+  // Create an "oracle" rate statistics which should be followed on a sequence
+  // of frames.
+  RateStatistics rate(
+      FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000);
+
+  for (int frame = 0; frame != 10; ++frame) {
+    time_controller.AdvanceTime(TimeDelta::Millis(10));
+    rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+    adapter->UpdateFrameRate();
+    EXPECT_EQ(rate.Rate(time_controller.GetClock()->TimeInMilliseconds()),
+              adapter->GetInputFrameRateFps())
+        << " failed for frame " << frame;
+  }
+}
+
+TEST(FrameCadenceAdapterTest,
+     FrameRateFollowsRateStatisticsWhenFeatureDisabled) {
+  ZeroHertzFieldTrialDisabler feature_disabler;
+  GlobalSimulatedTimeController time_controller(Timestamp::Millis(0));
+  auto adapter = CreateAdapter(time_controller.GetClock());
+  adapter->Initialize(nullptr);
+
+  // Create an "oracle" rate statistics which should be followed on a sequence
+  // of frames.
+  RateStatistics rate(
+      FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000);
+
+  for (int frame = 0; frame != 10; ++frame) {
+    time_controller.AdvanceTime(TimeDelta::Millis(10));
+    rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+    adapter->UpdateFrameRate();
+    EXPECT_EQ(rate.Rate(time_controller.GetClock()->TimeInMilliseconds()),
+              adapter->GetInputFrameRateFps())
+        << " failed for frame " << frame;
+  }
+}
+
+TEST(FrameCadenceAdapterTest, FrameRateFollowsMaxFpsWhenZeroHertzActivated) {
+  ZeroHertzFieldTrialEnabler enabler;
+  MockCallback callback;
+  GlobalSimulatedTimeController time_controller(Timestamp::Millis(0));
+  auto adapter = CreateAdapter(time_controller.GetClock());
+  adapter->Initialize(nullptr);
+  adapter->SetZeroHertzModeEnabled(true);
+  adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+  for (int frame = 0; frame != 10; ++frame) {
+    time_controller.AdvanceTime(TimeDelta::Millis(10));
+    adapter->UpdateFrameRate();
+    EXPECT_EQ(adapter->GetInputFrameRateFps(), 1u);
+  }
+}
+
+TEST(FrameCadenceAdapterTest,
+     FrameRateFollowsRateStatisticsAfterZeroHertzDeactivated) {
+  ZeroHertzFieldTrialEnabler enabler;
+  MockCallback callback;
+  GlobalSimulatedTimeController time_controller(Timestamp::Millis(0));
+  auto adapter = CreateAdapter(time_controller.GetClock());
+  adapter->Initialize(nullptr);
+  adapter->SetZeroHertzModeEnabled(true);
+  adapter->OnConstraintsChanged(VideoTrackSourceConstraints{0, 1});
+  RateStatistics rate(
+      FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000);
+  constexpr int MAX = 10;
+  for (int frame = 0; frame != MAX; ++frame) {
+    time_controller.AdvanceTime(TimeDelta::Millis(10));
+    rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+    adapter->UpdateFrameRate();
+  }
+  // Turn off zero hertz on the next-last frame; after the last frame we
+  // should see a value that tracks the rate oracle.
+  adapter->SetZeroHertzModeEnabled(false);
+  // Last frame.
+  time_controller.AdvanceTime(TimeDelta::Millis(10));
+  rate.Update(1, time_controller.GetClock()->TimeInMilliseconds());
+  adapter->UpdateFrameRate();
+
+  EXPECT_EQ(rate.Rate(time_controller.GetClock()->TimeInMilliseconds()),
+            adapter->GetInputFrameRateFps());
+}
+
 class FrameCadenceAdapterMetricsTest : public ::testing::Test {
  public:
   FrameCadenceAdapterMetricsTest() : time_controller_(Timestamp::Millis(1)) {
diff --git a/video/video_stream_encoder.cc b/video/video_stream_encoder.cc
index 9fd8e69..640c230 100644
--- a/video/video_stream_encoder.cc
+++ b/video/video_stream_encoder.cc
@@ -64,10 +64,6 @@
 
 constexpr char kFrameDropperFieldTrial[] = "WebRTC-FrameDropper";
 
-// Averaging window spanning 90 frames at default 30fps, matching old media
-// optimization module defaults.
-const int64_t kFrameRateAvergingWindowSizeMs = (1000 / 30) * 90;
-
 const size_t kDefaultPayloadSize = 1440;
 
 const int64_t kParameterUpdateIntervalMs = 1000;
@@ -633,7 +629,6 @@
       expect_resize_state_(ExpectResizeState::kNoResize),
       fec_controller_override_(nullptr),
       force_disable_frame_dropper_(false),
-      input_framerate_(kFrameRateAvergingWindowSizeMs, 1000),
       pending_frame_drops_(0),
       cwnd_frame_counter_(0),
       next_frame_types_(1, VideoFrameType::kVideoFrameDelta),
@@ -1422,8 +1417,13 @@
 
 uint32_t VideoStreamEncoder::GetInputFramerateFps() {
   const uint32_t default_fps = max_framerate_ != -1 ? max_framerate_ : 30;
+
+  // This method may be called after we cleared out the frame_cadence_adapter_
+  // reference in Stop(). In such a situation it's probably not important with a
+  // decent estimate.
   absl::optional<uint32_t> input_fps =
-      input_framerate_.Rate(clock_->TimeInMilliseconds());
+      frame_cadence_adapter_ ? frame_cadence_adapter_->GetInputFrameRateFps()
+                             : absl::nullopt;
   if (!input_fps || *input_fps == 0) {
     return default_fps;
   }
@@ -1525,7 +1525,7 @@
   // Poll the rate before updating, otherwise we risk the rate being estimated
   // a little too high at the start of the call when then window is small.
   uint32_t framerate_fps = GetInputFramerateFps();
-  input_framerate_.Update(1u, clock_->TimeInMilliseconds());
+  frame_cadence_adapter_->UpdateFrameRate();
 
   int64_t now_ms = clock_->TimeInMilliseconds();
   if (pending_encoder_reconfiguration_) {
diff --git a/video/video_stream_encoder.h b/video/video_stream_encoder.h
index 5194ff3..cd181fc 100644
--- a/video/video_stream_encoder.h
+++ b/video/video_stream_encoder.h
@@ -339,7 +339,6 @@
   // trusted rate controller. This is determined on a per-frame basis, as the
   // encoder behavior might dynamically change.
   bool force_disable_frame_dropper_ RTC_GUARDED_BY(&encoder_queue_);
-  RateStatistics input_framerate_ RTC_GUARDED_BY(&encoder_queue_);
   // Incremented on worker thread whenever `frame_dropper_` determines that a
   // frame should be dropped. Decremented on whichever thread runs
   // OnEncodedImage(), which is only called by one thread but not necessarily
diff --git a/video/video_stream_encoder_unittest.cc b/video/video_stream_encoder_unittest.cc
index 32d4f94..c5ff292 100644
--- a/video/video_stream_encoder_unittest.cc
+++ b/video/video_stream_encoder_unittest.cc
@@ -76,6 +76,7 @@
 using ::testing::Field;
 using ::testing::Ge;
 using ::testing::Gt;
+using ::testing::Invoke;
 using ::testing::Le;
 using ::testing::Lt;
 using ::testing::Matcher;
@@ -640,30 +641,26 @@
     ~AdaptedVideoStreamEncoder() { Stop(); }
   };
 
-  SimpleVideoStreamEncoderFactory()
-      : time_controller_(Timestamp::Millis(0)),
-        task_queue_factory_(time_controller_.CreateTaskQueueFactory()),
-        stats_proxy_(std::make_unique<MockableSendStatisticsProxy>(
-            time_controller_.GetClock(),
-            VideoSendStream::Config(nullptr),
-            webrtc::VideoEncoderConfig::ContentType::kRealtimeVideo)),
-        encoder_settings_(
-            VideoEncoder::Capabilities(/*loss_notification=*/false)),
-        fake_encoder_(time_controller_.GetClock()),
-        encoder_factory_(&fake_encoder_) {
+  SimpleVideoStreamEncoderFactory() {
     encoder_settings_.encoder_factory = &encoder_factory_;
+    encoder_settings_.bitrate_allocator_factory =
+        bitrate_allocator_factory_.get();
   }
 
   std::unique_ptr<AdaptedVideoStreamEncoder> Create(
-      std::unique_ptr<FrameCadenceAdapterInterface> zero_hertz_adapter) {
+      std::unique_ptr<FrameCadenceAdapterInterface> zero_hertz_adapter,
+      TaskQueueBase** encoder_queue_ptr = nullptr) {
+    auto encoder_queue =
+        time_controller_.GetTaskQueueFactory()->CreateTaskQueue(
+            "EncoderQueue", TaskQueueFactory::Priority::NORMAL);
+    if (encoder_queue_ptr)
+      *encoder_queue_ptr = encoder_queue.get();
     auto result = std::make_unique<AdaptedVideoStreamEncoder>(
         time_controller_.GetClock(),
         /*number_of_cores=*/1,
         /*stats_proxy=*/stats_proxy_.get(), encoder_settings_,
         std::make_unique<CpuOveruseDetectorProxy>(/*stats_proxy=*/nullptr),
-        std::move(zero_hertz_adapter),
-        time_controller_.GetTaskQueueFactory()->CreateTaskQueue(
-            "EncoderQueue", TaskQueueFactory::Priority::NORMAL),
+        std::move(zero_hertz_adapter), std::move(encoder_queue),
         VideoStreamEncoder::BitrateAllocationCallbackType::
             kVideoBitrateAllocation);
     result->SetSink(&sink_, /*rotation_applied=*/false);
@@ -692,12 +689,20 @@
     }
   };
 
-  GlobalSimulatedTimeController time_controller_;
-  std::unique_ptr<TaskQueueFactory> task_queue_factory_;
-  std::unique_ptr<MockableSendStatisticsProxy> stats_proxy_;
-  VideoStreamEncoderSettings encoder_settings_;
-  test::FakeEncoder fake_encoder_;
-  test::VideoEncoderProxyFactory encoder_factory_;
+  GlobalSimulatedTimeController time_controller_{Timestamp::Millis(0)};
+  std::unique_ptr<TaskQueueFactory> task_queue_factory_{
+      time_controller_.CreateTaskQueueFactory()};
+  std::unique_ptr<MockableSendStatisticsProxy> stats_proxy_ =
+      std::make_unique<MockableSendStatisticsProxy>(
+          time_controller_.GetClock(),
+          VideoSendStream::Config(nullptr),
+          webrtc::VideoEncoderConfig::ContentType::kRealtimeVideo);
+  std::unique_ptr<VideoBitrateAllocatorFactory> bitrate_allocator_factory_ =
+      CreateBuiltinVideoBitrateAllocatorFactory();
+  VideoStreamEncoderSettings encoder_settings_{
+      VideoEncoder::Capabilities(/*loss_notification=*/false)};
+  test::FakeEncoder fake_encoder_{time_controller_.GetClock()};
+  test::VideoEncoderProxyFactory encoder_factory_{&fake_encoder_};
   NullEncoderSink sink_;
 };
 
@@ -706,6 +711,8 @@
   MOCK_METHOD(void, Initialize, (Callback * callback), (override));
   MOCK_METHOD(void, SetZeroHertzModeEnabled, (bool), (override));
   MOCK_METHOD(void, OnFrame, (const VideoFrame&), (override));
+  MOCK_METHOD(absl::optional<uint32_t>, GetInputFrameRateFps, (), (override));
+  MOCK_METHOD(void, UpdateFrameRate, (), (override));
 };
 
 class MockEncoderSelector
@@ -8746,4 +8753,46 @@
           .build());
 }
 
+TEST(VideoStreamEncoderFrameCadenceTest, UsesFrameCadenceAdapterForFrameRate) {
+  auto adapter = std::make_unique<MockFrameCadenceAdapter>();
+  auto* adapter_ptr = adapter.get();
+  test::FrameForwarder video_source;
+  SimpleVideoStreamEncoderFactory factory;
+  FrameCadenceAdapterInterface::Callback* video_stream_encoder_callback =
+      nullptr;
+  EXPECT_CALL(*adapter_ptr, Initialize)
+      .WillOnce(Invoke([&video_stream_encoder_callback](
+                           FrameCadenceAdapterInterface::Callback* callback) {
+        video_stream_encoder_callback = callback;
+      }));
+  TaskQueueBase* encoder_queue = nullptr;
+  auto video_stream_encoder =
+      factory.Create(std::move(adapter), &encoder_queue);
+
+  // This is just to make the VSE operational. We'll feed a frame directly by
+  // the callback interface.
+  video_stream_encoder->SetSource(
+      &video_source, webrtc::DegradationPreference::MAINTAIN_FRAMERATE);
+
+  VideoEncoderConfig video_encoder_config;
+  test::FillEncoderConfiguration(kVideoCodecGeneric, 1, &video_encoder_config);
+  video_stream_encoder->ConfigureEncoder(std::move(video_encoder_config),
+                                         /*max_data_payload_length=*/1000);
+
+  EXPECT_CALL(*adapter_ptr, GetInputFrameRateFps);
+  EXPECT_CALL(*adapter_ptr, UpdateFrameRate);
+  encoder_queue->PostTask(ToQueuedTask([video_stream_encoder_callback] {
+    video_stream_encoder_callback->OnFrame(
+        Timestamp::Millis(1), 1,
+        VideoFrame::Builder()
+            .set_video_frame_buffer(
+                rtc::make_ref_counted<NV12Buffer>(/*width=*/16, /*height=*/16))
+            .set_ntp_time_ms(0)
+            .set_timestamp_ms(0)
+            .set_rotation(kVideoRotation_0)
+            .build());
+  }));
+  factory.DepleteTaskQueues();
+}
+
 }  // namespace webrtc