BalancedDegradationSettings: add option to configure a min framerate diff.

If a framerate reduction (input fps - restricted fps) is less than the
configured diff, shorten interval to next qp check.

Bug: none
Change-Id: Ia0b9e0638e5ba75cdc20a1bb45bfcb7d858c5f89
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/149040
Commit-Queue: Åsa Persson <asapersson@webrtc.org>
Reviewed-by: Magnus Flodman <mflodman@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#28880}
diff --git a/modules/video_coding/utility/quality_scaler.cc b/modules/video_coding/utility/quality_scaler.cc
index 42c40c5..d31b2cd 100644
--- a/modules/video_coding/utility/quality_scaler.cc
+++ b/modules/video_coding/utility/quality_scaler.cc
@@ -96,7 +96,8 @@
                                 .value_or(kSamplePeriodScaleFactor)),
       scale_factor_(
           QualityScalerSettings::ParseFromFieldTrials().ScaleFactor()),
-      last_adapted_(false) {
+      adapt_called_(false),
+      adapt_failed_(false) {
   RTC_DCHECK_RUN_ON(&task_checker_);
   if (experiment_enabled_) {
     config_ = QualityScalingExperiment::GetConfig();
@@ -127,8 +128,12 @@
     // Use half the interval while waiting for enough frames.
     return sampling_period_ms_ / 2;
   }
-  if (scale_factor_ && !last_adapted_) {
-    // Last check did not result in a AdaptDown/Up, possibly reduce interval.
+  if (adapt_failed_) {
+    // Check shortly again.
+    return sampling_period_ms_ / 8;
+  }
+  if (scale_factor_ && !adapt_called_) {
+    // Last CheckQp did not call AdaptDown/Up, possibly reduce interval.
     return sampling_period_ms_ * scale_factor_.value();
   }
   return sampling_period_ms_ * initial_scale_factor_;
@@ -165,7 +170,8 @@
   RTC_DCHECK_RUN_ON(&task_checker_);
   // Should be set through InitEncode -> Should be set by now.
   RTC_DCHECK_GE(thresholds_.low, 0);
-  last_adapted_ = false;
+  adapt_failed_ = false;
+  adapt_called_ = false;
 
   // If we have not observed at least this many frames we can't make a good
   // scaling decision.
@@ -215,18 +221,24 @@
   RTC_DCHECK_RUN_ON(&task_checker_);
   ClearSamples();
   observer_->AdaptUp(AdaptationObserverInterface::AdaptReason::kQuality);
-  last_adapted_ = true;
+  adapt_called_ = true;
 }
 
 void QualityScaler::ReportQpHigh() {
   RTC_DCHECK_RUN_ON(&task_checker_);
-  ClearSamples();
-  observer_->AdaptDown(AdaptationObserverInterface::AdaptReason::kQuality);
+
+  if (observer_->AdaptDown(
+          AdaptationObserverInterface::AdaptReason::kQuality)) {
+    ClearSamples();
+  } else {
+    adapt_failed_ = true;
+  }
+
   // If we've scaled down, wait longer before scaling up again.
   if (fast_rampup_) {
     fast_rampup_ = false;
   }
-  last_adapted_ = true;
+  adapt_called_ = true;
 }
 
 void QualityScaler::ClearSamples() {
diff --git a/modules/video_coding/utility/quality_scaler.h b/modules/video_coding/utility/quality_scaler.h
index 32e522b..9a1b384 100644
--- a/modules/video_coding/utility/quality_scaler.h
+++ b/modules/video_coding/utility/quality_scaler.h
@@ -37,7 +37,9 @@
   // Called to signal that we can handle larger or more frequent frames.
   virtual void AdaptUp(AdaptReason reason) = 0;
   // Called to signal that the source should reduce the resolution or framerate.
-  virtual void AdaptDown(AdaptReason reason) = 0;
+  // Returns false if a downgrade was requested but the request did not result
+  // in a new limiting resolution or fps.
+  virtual bool AdaptDown(AdaptReason reason) = 0;
 
  protected:
   virtual ~AdaptationObserverInterface() {}
@@ -101,7 +103,8 @@
   const size_t min_frames_needed_;
   const double initial_scale_factor_;
   const absl::optional<double> scale_factor_;
-  bool last_adapted_ RTC_GUARDED_BY(&task_checker_);
+  bool adapt_called_ RTC_GUARDED_BY(&task_checker_);
+  bool adapt_failed_ RTC_GUARDED_BY(&task_checker_);
 };
 }  // namespace webrtc
 
diff --git a/modules/video_coding/utility/quality_scaler_unittest.cc b/modules/video_coding/utility/quality_scaler_unittest.cc
index 99eaed1..012ef72 100644
--- a/modules/video_coding/utility/quality_scaler_unittest.cc
+++ b/modules/video_coding/utility/quality_scaler_unittest.cc
@@ -36,9 +36,10 @@
     adapt_up_events_++;
     event.Set();
   }
-  void AdaptDown(AdaptReason r) override {
+  bool AdaptDown(AdaptReason r) override {
     adapt_down_events_++;
     event.Set();
+    return true;
   }
 
   rtc::Event event;
diff --git a/rtc_base/experiments/balanced_degradation_settings.cc b/rtc_base/experiments/balanced_degradation_settings.cc
index 3dda134..8306027 100644
--- a/rtc_base/experiments/balanced_degradation_settings.cc
+++ b/rtc_base/experiments/balanced_degradation_settings.cc
@@ -24,9 +24,30 @@
 constexpr int kMaxFps = 100;  // 100 means unlimited fps.
 
 std::vector<BalancedDegradationSettings::Config> DefaultConfigs() {
-  return {{320 * 240, 7, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
-          {480 * 270, 10, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
-          {640 * 480, 15, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}};
+  return {{320 * 240,
+           7,
+           0,
+           BalancedDegradationSettings::kNoFpsDiff,
+           {0, 0, 0},
+           {0, 0, 0},
+           {0, 0, 0},
+           {0, 0, 0}},
+          {480 * 270,
+           10,
+           0,
+           BalancedDegradationSettings::kNoFpsDiff,
+           {0, 0, 0},
+           {0, 0, 0},
+           {0, 0, 0},
+           {0, 0, 0}},
+          {640 * 480,
+           15,
+           0,
+           BalancedDegradationSettings::kNoFpsDiff,
+           {0, 0, 0},
+           {0, 0, 0},
+           {0, 0, 0},
+           {0, 0, 0}}};
 }
 
 bool IsValidConfig(
@@ -200,6 +221,7 @@
 BalancedDegradationSettings::Config::Config(int pixels,
                                             int fps,
                                             int kbps,
+                                            int fps_diff,
                                             CodecTypeSpecific vp8,
                                             CodecTypeSpecific vp9,
                                             CodecTypeSpecific h264,
@@ -207,6 +229,7 @@
     : pixels(pixels),
       fps(fps),
       kbps(kbps),
+      fps_diff(fps_diff),
       vp8(vp8),
       vp9(vp9),
       h264(h264),
@@ -217,6 +240,8 @@
       {FieldTrialStructMember("pixels", [](Config* c) { return &c->pixels; }),
        FieldTrialStructMember("fps", [](Config* c) { return &c->fps; }),
        FieldTrialStructMember("kbps", [](Config* c) { return &c->kbps; }),
+       FieldTrialStructMember("fps_diff",
+                              [](Config* c) { return &c->fps_diff; }),
        FieldTrialStructMember("vp8_qp_low",
                               [](Config* c) { return &c->vp8.qp_low; }),
        FieldTrialStructMember("vp8_qp_high",
@@ -292,6 +317,17 @@
   return absl::nullopt;
 }
 
+absl::optional<int> BalancedDegradationSettings::MinFpsDiff(int pixels) const {
+  for (const auto& config : configs_) {
+    if (pixels <= config.pixels) {
+      return (config.fps_diff > kNoFpsDiff)
+                 ? absl::optional<int>(config.fps_diff)
+                 : absl::nullopt;
+    }
+  }
+  return absl::nullopt;
+}
+
 absl::optional<VideoEncoder::QpThresholds>
 BalancedDegradationSettings::GetQpThresholds(VideoCodecType type,
                                              int pixels) const {
diff --git a/rtc_base/experiments/balanced_degradation_settings.h b/rtc_base/experiments/balanced_degradation_settings.h
index e29b7d5..494102e 100644
--- a/rtc_base/experiments/balanced_degradation_settings.h
+++ b/rtc_base/experiments/balanced_degradation_settings.h
@@ -20,6 +20,8 @@
 
 class BalancedDegradationSettings {
  public:
+  static constexpr int kNoFpsDiff = -100;
+
   BalancedDegradationSettings();
   ~BalancedDegradationSettings();
 
@@ -45,6 +47,7 @@
     Config(int pixels,
            int fps,
            int kbps,
+           int fps_diff,
            CodecTypeSpecific vp8,
            CodecTypeSpecific vp9,
            CodecTypeSpecific h264,
@@ -52,14 +55,17 @@
 
     bool operator==(const Config& o) const {
       return pixels == o.pixels && fps == o.fps && kbps == o.kbps &&
-             vp8 == o.vp8 && vp9 == o.vp9 && h264 == o.h264 &&
-             generic == o.generic;
+             fps_diff == o.fps_diff && vp8 == o.vp8 && vp9 == o.vp9 &&
+             h264 == o.h264 && generic == o.generic;
     }
 
     int pixels = 0;  // Video frame size.
     // If the frame size is less than or equal to |pixels|:
     int fps = 0;   // Min framerate to be used.
     int kbps = 0;  // Min bitrate needed to adapt up to this resolution.
+    int fps_diff = kNoFpsDiff;  // Min fps reduction needed (input fps - |fps|)
+                                // w/o triggering a new subsequent downgrade
+                                // check.
     CodecTypeSpecific vp8;
     CodecTypeSpecific vp9;
     CodecTypeSpecific h264;
@@ -76,6 +82,9 @@
   // Gets the bitrate for the first resolution above |pixels|.
   absl::optional<int> NextHigherBitrateKbps(int pixels) const;
 
+  // Gets the min framerate diff from |configs_| based on |pixels|.
+  absl::optional<int> MinFpsDiff(int pixels) const;
+
   // Gets QpThresholds for the codec |type| based on |pixels|.
   absl::optional<VideoEncoder::QpThresholds> GetQpThresholds(
       VideoCodecType type,
diff --git a/rtc_base/experiments/balanced_degradation_settings_unittest.cc b/rtc_base/experiments/balanced_degradation_settings_unittest.cc
index 9aaf55a..4c719d7 100644
--- a/rtc_base/experiments/balanced_degradation_settings_unittest.cc
+++ b/rtc_base/experiments/balanced_degradation_settings_unittest.cc
@@ -21,15 +21,34 @@
 
 void VerifyIsDefault(
     const std::vector<BalancedDegradationSettings::Config>& config) {
-  EXPECT_THAT(
-      config,
-      ::testing::ElementsAre(
-          BalancedDegradationSettings::Config{
-              320 * 240, 7, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
-          BalancedDegradationSettings::Config{
-              480 * 270, 10, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
-          BalancedDegradationSettings::Config{
-              640 * 480, 15, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}));
+  EXPECT_THAT(config, ::testing::ElementsAre(
+                          BalancedDegradationSettings::Config{
+                              320 * 240,
+                              7,
+                              0,
+                              BalancedDegradationSettings::kNoFpsDiff,
+                              {0, 0, 0},
+                              {0, 0, 0},
+                              {0, 0, 0},
+                              {0, 0, 0}},
+                          BalancedDegradationSettings::Config{
+                              480 * 270,
+                              10,
+                              0,
+                              BalancedDegradationSettings::kNoFpsDiff,
+                              {0, 0, 0},
+                              {0, 0, 0},
+                              {0, 0, 0},
+                              {0, 0, 0}},
+                          BalancedDegradationSettings::Config{
+                              640 * 480,
+                              15,
+                              0,
+                              BalancedDegradationSettings::kNoFpsDiff,
+                              {0, 0, 0},
+                              {0, 0, 0},
+                              {0, 0, 0},
+                              {0, 0, 0}}));
 }
 }  // namespace
 
@@ -38,6 +57,7 @@
   BalancedDegradationSettings settings;
   VerifyIsDefault(settings.GetConfigs());
   EXPECT_FALSE(settings.NextHigherBitrateKbps(1));
+  EXPECT_FALSE(settings.MinFpsDiff(1));
   EXPECT_FALSE(settings.GetQpThresholds(kVideoCodecVP8, 1));
   EXPECT_FALSE(settings.GetQpThresholds(kVideoCodecVP9, 1));
   EXPECT_FALSE(settings.GetQpThresholds(kVideoCodecH264, 1));
@@ -53,11 +73,32 @@
   EXPECT_THAT(settings.GetConfigs(),
               ::testing::ElementsAre(
                   BalancedDegradationSettings::Config{
-                      11, 5, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
+                      11,
+                      5,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0}},
                   BalancedDegradationSettings::Config{
-                      22, 15, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
+                      22,
+                      15,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0}},
                   BalancedDegradationSettings::Config{
-                      33, 25, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}));
+                      33,
+                      25,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0}}));
 }
 
 TEST(BalancedDegradationSettings, GetsDefaultConfigForZeroFpsValue) {
@@ -90,15 +131,35 @@
       "pixels:1000|2000|3000,fps:5|15|25,vp8_fps:7|8|9,vp9_fps:9|10|11,"
       "h264_fps:11|12|13,generic_fps:13|14|15/");
   BalancedDegradationSettings settings;
-  EXPECT_THAT(
-      settings.GetConfigs(),
-      ::testing::ElementsAre(
-          BalancedDegradationSettings::Config{
-              1000, 5, 0, {0, 0, 7}, {0, 0, 9}, {0, 0, 11}, {0, 0, 13}},
-          BalancedDegradationSettings::Config{
-              2000, 15, 0, {0, 0, 8}, {0, 0, 10}, {0, 0, 12}, {0, 0, 14}},
-          BalancedDegradationSettings::Config{
-              3000, 25, 0, {0, 0, 9}, {0, 0, 11}, {0, 0, 13}, {0, 0, 15}}));
+  EXPECT_THAT(settings.GetConfigs(),
+              ::testing::ElementsAre(
+                  BalancedDegradationSettings::Config{
+                      1000,
+                      5,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 7},
+                      {0, 0, 9},
+                      {0, 0, 11},
+                      {0, 0, 13}},
+                  BalancedDegradationSettings::Config{
+                      2000,
+                      15,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 8},
+                      {0, 0, 10},
+                      {0, 0, 12},
+                      {0, 0, 14}},
+                  BalancedDegradationSettings::Config{
+                      3000,
+                      25,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 9},
+                      {0, 0, 11},
+                      {0, 0, 13},
+                      {0, 0, 15}}));
 }
 
 TEST(BalancedDegradationSettings, GetsDefaultConfigForZeroVp8FpsValue) {
@@ -229,11 +290,32 @@
   EXPECT_THAT(settings.GetConfigs(),
               ::testing::ElementsAre(
                   BalancedDegradationSettings::Config{
-                      11, 5, 44, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
+                      11,
+                      5,
+                      44,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0}},
                   BalancedDegradationSettings::Config{
-                      22, 15, 88, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
+                      22,
+                      15,
+                      88,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0}},
                   BalancedDegradationSettings::Config{
-                      33, 25, 99, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}));
+                      33,
+                      25,
+                      99,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0},
+                      {0, 0, 0}}));
 }
 
 TEST(BalancedDegradationSettings, GetsDefaultConfigIfBitrateDecreases) {
@@ -277,6 +359,31 @@
   EXPECT_FALSE(settings.NextHigherBitrateKbps(2001));
 }
 
+TEST(BalancedDegradationSettings, GetsFpsDiff) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:1000|2000|3000,fps:5|15|25,fps_diff:0|-2|3/");
+  BalancedDegradationSettings settings;
+  EXPECT_EQ(0, settings.MinFpsDiff(1));
+  EXPECT_EQ(0, settings.MinFpsDiff(1000));
+  EXPECT_EQ(-2, settings.MinFpsDiff(1001));
+  EXPECT_EQ(-2, settings.MinFpsDiff(2000));
+  EXPECT_EQ(3, settings.MinFpsDiff(2001));
+  EXPECT_EQ(3, settings.MinFpsDiff(3000));
+  EXPECT_FALSE(settings.MinFpsDiff(3001));
+}
+
+TEST(BalancedDegradationSettings, GetsNoFpsDiffIfValueBelowMinSetting) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:1000|2000|3000,fps:5|15|25,fps_diff:-100|-99|-101/");
+  // Min valid fps_diff setting: -99.
+  BalancedDegradationSettings settings;
+  EXPECT_FALSE(settings.MinFpsDiff(1000));
+  EXPECT_EQ(-99, settings.MinFpsDiff(2000));
+  EXPECT_FALSE(settings.MinFpsDiff(3000));
+}
+
 TEST(BalancedDegradationSettings, QpThresholdsNotSetByDefault) {
   webrtc::test::ScopedFieldTrials field_trials(
       "WebRTC-Video-BalancedDegradationSettings/"
@@ -292,19 +399,39 @@
   webrtc::test::ScopedFieldTrials field_trials(
       "WebRTC-Video-BalancedDegradationSettings/"
       "pixels:1000|2000|3000,fps:5|15|25,vp8_qp_low:89|90|88,"
-      "vp8_qp_high:90|91|92,vp9_qp_low:27|28|29,vp9_qp_high:82|83|84,"
+      "vp8_qp_high:90|91|92,vp9_qp_low:27|28|29,vp9_qp_high:120|130|140,"
       "h264_qp_low:12|13|14,h264_qp_high:20|30|40,generic_qp_low:7|6|5,"
       "generic_qp_high:22|23|24/");
   BalancedDegradationSettings settings;
-  EXPECT_THAT(
-      settings.GetConfigs(),
-      ::testing::ElementsAre(
-          BalancedDegradationSettings::Config{
-              1000, 5, 0, {89, 90, 0}, {27, 82, 0}, {12, 20, 0}, {7, 22, 0}},
-          BalancedDegradationSettings::Config{
-              2000, 15, 0, {90, 91, 0}, {28, 83, 0}, {13, 30, 0}, {6, 23, 0}},
-          BalancedDegradationSettings::Config{
-              3000, 25, 0, {88, 92, 0}, {29, 84, 0}, {14, 40, 0}, {5, 24, 0}}));
+  EXPECT_THAT(settings.GetConfigs(),
+              ::testing::ElementsAre(
+                  BalancedDegradationSettings::Config{
+                      1000,
+                      5,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {89, 90, 0},
+                      {27, 120, 0},
+                      {12, 20, 0},
+                      {7, 22, 0}},
+                  BalancedDegradationSettings::Config{
+                      2000,
+                      15,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {90, 91, 0},
+                      {28, 130, 0},
+                      {13, 30, 0},
+                      {6, 23, 0}},
+                  BalancedDegradationSettings::Config{
+                      3000,
+                      25,
+                      0,
+                      BalancedDegradationSettings::kNoFpsDiff,
+                      {88, 92, 0},
+                      {29, 140, 0},
+                      {14, 40, 0},
+                      {5, 24, 0}}));
 }
 
 TEST(BalancedDegradationSettings, GetsDefaultConfigIfOnlyHasLowThreshold) {
diff --git a/video/overuse_frame_detector_unittest.cc b/video/overuse_frame_detector_unittest.cc
index b66d119..3a1c97c 100644
--- a/video/overuse_frame_detector_unittest.cc
+++ b/video/overuse_frame_detector_unittest.cc
@@ -42,7 +42,7 @@
   virtual ~MockCpuOveruseObserver() {}
 
   MOCK_METHOD1(AdaptUp, void(AdaptReason));
-  MOCK_METHOD1(AdaptDown, void(AdaptReason));
+  MOCK_METHOD1(AdaptDown, bool(AdaptReason));
 };
 
 class CpuOveruseObserverImpl : public AdaptationObserverInterface {
@@ -50,7 +50,10 @@
   CpuOveruseObserverImpl() : overuse_(0), normaluse_(0) {}
   virtual ~CpuOveruseObserverImpl() {}
 
-  void AdaptDown(AdaptReason) { ++overuse_; }
+  bool AdaptDown(AdaptReason) {
+    ++overuse_;
+    return true;
+  }
   void AdaptUp(AdaptReason) { ++normaluse_; }
 
   int overuse_;
diff --git a/video/video_stream_encoder.cc b/video/video_stream_encoder.cc
index 8e22930..de807db 100644
--- a/video/video_stream_encoder.cc
+++ b/video/video_stream_encoder.cc
@@ -1781,7 +1781,7 @@
   return false;
 }
 
-void VideoStreamEncoder::AdaptDown(AdaptReason reason) {
+bool VideoStreamEncoder::AdaptDown(AdaptReason reason) {
   RTC_DCHECK_RUN_ON(&encoder_queue_);
   AdaptationRequest adaptation_request = {
       last_frame_info_->pixel_count(),
@@ -1792,6 +1792,8 @@
       last_adaptation_request_ &&
       last_adaptation_request_->mode_ == AdaptationRequest::Mode::kAdaptDown;
 
+  bool did_adapt = true;
+
   switch (degradation_preference_) {
     case DegradationPreference::BALANCED:
       break;
@@ -1801,7 +1803,7 @@
               last_adaptation_request_->input_pixel_count_) {
         // Don't request lower resolution if the current resolution is not
         // lower than the last time we asked for the resolution to be lowered.
-        return;
+        return true;
       }
       break;
     case DegradationPreference::MAINTAIN_RESOLUTION:
@@ -1814,11 +1816,11 @@
         // we have to estimate, and can fluctuate naturally over time, don't
         // make the same kind of limitations as for resolution, but trust the
         // overuse detector to not trigger too often.
-        return;
+        return true;
       }
       break;
     case DegradationPreference::DISABLED:
-      return;
+      return true;
   }
 
   switch (degradation_preference_) {
@@ -1828,6 +1830,15 @@
                                           last_frame_info_->pixel_count());
       if (source_proxy_->RestrictFramerate(fps)) {
         GetAdaptCounter().IncrementFramerate(reason);
+        // Check if requested fps is higher (or close to) input fps.
+        absl::optional<int> min_diff =
+            balanced_settings_.MinFpsDiff(last_frame_info_->pixel_count());
+        if (min_diff && adaptation_request.framerate_fps_ > 0) {
+          int fps_diff = adaptation_request.framerate_fps_ - fps;
+          if (fps_diff < min_diff.value()) {
+            did_adapt = false;
+          }
+        }
         break;
       }
       // Scale down resolution.
@@ -1842,7 +1853,7 @@
               &min_pixels_reached)) {
         if (min_pixels_reached)
           encoder_stats_observer_->OnMinPixelLimitReached();
-        return;
+        return true;
       }
       GetAdaptCounter().IncrementResolution(reason);
       break;
@@ -1852,7 +1863,7 @@
       const int requested_framerate = source_proxy_->RequestFramerateLowerThan(
           adaptation_request.framerate_fps_);
       if (requested_framerate == -1)
-        return;
+        return true;
       RTC_DCHECK_NE(max_framerate_, -1);
       overuse_detector_->OnTargetFramerateUpdated(
           std::min(max_framerate_, requested_framerate));
@@ -1868,6 +1879,7 @@
   UpdateAdaptationStats(reason);
 
   RTC_LOG(LS_INFO) << GetConstAdaptCounter().ToString();
+  return did_adapt;
 }
 
 void VideoStreamEncoder::AdaptUp(AdaptReason reason) {
diff --git a/video/video_stream_encoder.h b/video/video_stream_encoder.h
index 3b589bf..b7070f6 100644
--- a/video/video_stream_encoder.h
+++ b/video/video_stream_encoder.h
@@ -103,7 +103,7 @@
   // AdaptationObserverInterface implementation.
   // These methods are protected for easier testing.
   void AdaptUp(AdaptReason reason) override;
-  void AdaptDown(AdaptReason reason) override;
+  bool AdaptDown(AdaptReason reason) override;
 
  private:
   class VideoSourceProxy;
diff --git a/video/video_stream_encoder_unittest.cc b/video/video_stream_encoder_unittest.cc
index 35439d2..1ad4cbf 100644
--- a/video/video_stream_encoder_unittest.cc
+++ b/video/video_stream_encoder_unittest.cc
@@ -148,7 +148,10 @@
   void PostTaskAndWait(bool down, AdaptReason reason) {
     rtc::Event event;
     encoder_queue()->PostTask([this, &event, reason, down] {
-      down ? AdaptDown(reason) : AdaptUp(reason);
+      if (down)
+        AdaptDown(reason);
+      else
+        AdaptUp(reason);
       event.Set();
     });
     ASSERT_TRUE(event.Wait(5000));