BalancedDegradationSettings: add option to configure min bitrate.

Add possibility to configure min bitrate based on resolution.
Only adapt up if bw estimate is above the min bitrate for next higher resolution.

Bug: none
Change-Id: Ie38faae07d23336675ec33697ace6f6fed322efa
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/148598
Reviewed-by: Magnus Flodman <mflodman@webrtc.org>
Commit-Queue: Åsa Persson <asapersson@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#28863}
diff --git a/rtc_base/experiments/balanced_degradation_settings.cc b/rtc_base/experiments/balanced_degradation_settings.cc
index e1ed1bc..3dda134 100644
--- a/rtc_base/experiments/balanced_degradation_settings.cc
+++ b/rtc_base/experiments/balanced_degradation_settings.cc
@@ -24,9 +24,9 @@
 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}},
-          {480 * 270, 10, {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}}};
+  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}}};
 }
 
 bool IsValidConfig(
@@ -75,6 +75,16 @@
       return false;
     }
   }
+  int last_kbps = configs[0].kbps;
+  for (size_t i = 1; i < configs.size(); ++i) {
+    if (configs[i].kbps > 0) {
+      if (configs[i].kbps < last_kbps) {
+        RTC_LOG(LS_WARNING) << "Invalid bitrate value provided.";
+        return false;
+      }
+      last_kbps = configs[i].kbps;
+    }
+  }
   for (size_t i = 1; i < configs.size(); ++i) {
     if (configs[i].pixels < configs[i - 1].pixels ||
         configs[i].fps < configs[i - 1].fps) {
@@ -189,12 +199,14 @@
 
 BalancedDegradationSettings::Config::Config(int pixels,
                                             int fps,
+                                            int kbps,
                                             CodecTypeSpecific vp8,
                                             CodecTypeSpecific vp9,
                                             CodecTypeSpecific h264,
                                             CodecTypeSpecific generic)
     : pixels(pixels),
       fps(fps),
+      kbps(kbps),
       vp8(vp8),
       vp9(vp9),
       h264(h264),
@@ -204,6 +216,7 @@
   FieldTrialStructList<Config> configs(
       {FieldTrialStructMember("pixels", [](Config* c) { return &c->pixels; }),
        FieldTrialStructMember("fps", [](Config* c) { return &c->fps; }),
+       FieldTrialStructMember("kbps", [](Config* c) { return &c->kbps; }),
        FieldTrialStructMember("vp8_qp_low",
                               [](Config* c) { return &c->vp8.qp_low; }),
        FieldTrialStructMember("vp8_qp_high",
@@ -267,6 +280,18 @@
   return absl::nullopt;
 }
 
+absl::optional<int> BalancedDegradationSettings::NextHigherBitrateKbps(
+    int pixels) const {
+  for (size_t i = 0; i < configs_.size() - 1; ++i) {
+    if (pixels <= configs_[i].pixels) {
+      return (configs_[i + 1].kbps > 0)
+                 ? absl::optional<int>(configs_[i + 1].kbps)
+                 : 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 448dea6..e29b7d5 100644
--- a/rtc_base/experiments/balanced_degradation_settings.h
+++ b/rtc_base/experiments/balanced_degradation_settings.h
@@ -44,19 +44,23 @@
     Config();
     Config(int pixels,
            int fps,
+           int kbps,
            CodecTypeSpecific vp8,
            CodecTypeSpecific vp9,
            CodecTypeSpecific h264,
            CodecTypeSpecific generic);
 
     bool operator==(const Config& o) const {
-      return pixels == o.pixels && fps == o.fps && vp8 == o.vp8 &&
-             vp9 == o.vp9 && h264 == o.h264 && generic == o.generic;
+      return pixels == o.pixels && fps == o.fps && kbps == o.kbps &&
+             vp8 == o.vp8 && vp9 == o.vp9 && h264 == o.h264 &&
+             generic == o.generic;
     }
 
-    int pixels = 0;         // The video frame size.
-    int fps = 0;            // The framerate and thresholds to be used if the
-    CodecTypeSpecific vp8;  // frame size is less than or equal to |pixels|.
+    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.
+    CodecTypeSpecific vp8;
     CodecTypeSpecific vp9;
     CodecTypeSpecific h264;
     CodecTypeSpecific generic;
@@ -69,6 +73,9 @@
   int MinFps(VideoCodecType type, int pixels) const;
   int MaxFps(VideoCodecType type, int pixels) const;
 
+  // Gets the bitrate for the first resolution above |pixels|.
+  absl::optional<int> NextHigherBitrateKbps(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 76a7624..9aaf55a 100644
--- a/rtc_base/experiments/balanced_degradation_settings_unittest.cc
+++ b/rtc_base/experiments/balanced_degradation_settings_unittest.cc
@@ -25,11 +25,11 @@
       config,
       ::testing::ElementsAre(
           BalancedDegradationSettings::Config{
-              320 * 240, 7, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
+              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}},
+              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}}));
+              640 * 480, 15, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}));
 }
 }  // namespace
 
@@ -37,6 +37,7 @@
   webrtc::test::ScopedFieldTrials field_trials("");
   BalancedDegradationSettings settings;
   VerifyIsDefault(settings.GetConfigs());
+  EXPECT_FALSE(settings.NextHigherBitrateKbps(1));
   EXPECT_FALSE(settings.GetQpThresholds(kVideoCodecVP8, 1));
   EXPECT_FALSE(settings.GetQpThresholds(kVideoCodecVP9, 1));
   EXPECT_FALSE(settings.GetQpThresholds(kVideoCodecH264, 1));
@@ -52,11 +53,11 @@
   EXPECT_THAT(settings.GetConfigs(),
               ::testing::ElementsAre(
                   BalancedDegradationSettings::Config{
-                      11, 5, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}},
+                      11, 5, 0, {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}},
+                      22, 15, 0, {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}}));
+                      33, 25, 0, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}));
 }
 
 TEST(BalancedDegradationSettings, GetsDefaultConfigForZeroFpsValue) {
@@ -93,11 +94,11 @@
       settings.GetConfigs(),
       ::testing::ElementsAre(
           BalancedDegradationSettings::Config{
-              1000, 5, {0, 0, 7}, {0, 0, 9}, {0, 0, 11}, {0, 0, 13}},
+              1000, 5, 0, {0, 0, 7}, {0, 0, 9}, {0, 0, 11}, {0, 0, 13}},
           BalancedDegradationSettings::Config{
-              2000, 15, {0, 0, 8}, {0, 0, 10}, {0, 0, 12}, {0, 0, 14}},
+              2000, 15, 0, {0, 0, 8}, {0, 0, 10}, {0, 0, 12}, {0, 0, 14}},
           BalancedDegradationSettings::Config{
-              3000, 25, {0, 0, 9}, {0, 0, 11}, {0, 0, 13}, {0, 0, 15}}));
+              3000, 25, 0, {0, 0, 9}, {0, 0, 11}, {0, 0, 13}, {0, 0, 15}}));
 }
 
 TEST(BalancedDegradationSettings, GetsDefaultConfigForZeroVp8FpsValue) {
@@ -220,6 +221,62 @@
   EXPECT_EQ(kUnlimitedFps, settings.MinFps(kVideoCodecVP8, 1001));
 }
 
+TEST(BalancedDegradationSettings, GetsConfigWithBitrate) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:11|22|33,fps:5|15|25,kbps:44|88|99/");
+  BalancedDegradationSettings settings;
+  EXPECT_THAT(settings.GetConfigs(),
+              ::testing::ElementsAre(
+                  BalancedDegradationSettings::Config{
+                      11, 5, 44, {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}},
+                  BalancedDegradationSettings::Config{
+                      33, 25, 99, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}));
+}
+
+TEST(BalancedDegradationSettings, GetsDefaultConfigIfBitrateDecreases) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:11|22|33,fps:5|15|25,kbps:44|43|99/");
+  BalancedDegradationSettings settings;
+  VerifyIsDefault(settings.GetConfigs());
+}
+
+TEST(BalancedDegradationSettings,
+     GetsDefaultConfigIfBitrateDecreasesWithUnsetValue) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:11|22|33,fps:5|15|25,kbps:44|0|43/");
+  BalancedDegradationSettings settings;
+  VerifyIsDefault(settings.GetConfigs());
+}
+
+TEST(BalancedDegradationSettings, GetsNextHigherBitrate) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:1000|2000|3000,fps:5|15|25,kbps:44|88|99/");
+  BalancedDegradationSettings settings;
+  EXPECT_EQ(88, settings.NextHigherBitrateKbps(1));
+  EXPECT_EQ(88, settings.NextHigherBitrateKbps(1000));
+  EXPECT_EQ(99, settings.NextHigherBitrateKbps(1001));
+  EXPECT_EQ(99, settings.NextHigherBitrateKbps(2000));
+  EXPECT_FALSE(settings.NextHigherBitrateKbps(2001));
+}
+
+TEST(BalancedDegradationSettings, GetsNextHigherBitrateWithUnsetValue) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:1000|2000|3000,fps:5|15|25,kbps:10|0|20/");
+  BalancedDegradationSettings settings;
+  EXPECT_FALSE(settings.NextHigherBitrateKbps(1));
+  EXPECT_FALSE(settings.NextHigherBitrateKbps(1000));
+  EXPECT_EQ(20, settings.NextHigherBitrateKbps(1001));
+  EXPECT_EQ(20, settings.NextHigherBitrateKbps(2000));
+  EXPECT_FALSE(settings.NextHigherBitrateKbps(2001));
+}
+
 TEST(BalancedDegradationSettings, QpThresholdsNotSetByDefault) {
   webrtc::test::ScopedFieldTrials field_trials(
       "WebRTC-Video-BalancedDegradationSettings/"
@@ -235,7 +292,7 @@
   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:120|130|140,"
+      "vp8_qp_high:90|91|92,vp9_qp_low:27|28|29,vp9_qp_high:82|83|84,"
       "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;
@@ -243,11 +300,11 @@
       settings.GetConfigs(),
       ::testing::ElementsAre(
           BalancedDegradationSettings::Config{
-              1000, 5, {89, 90, 0}, {27, 120, 0}, {12, 20, 0}, {7, 22, 0}},
+              1000, 5, 0, {89, 90, 0}, {27, 82, 0}, {12, 20, 0}, {7, 22, 0}},
           BalancedDegradationSettings::Config{
-              2000, 15, {90, 91, 0}, {28, 130, 0}, {13, 30, 0}, {6, 23, 0}},
+              2000, 15, 0, {90, 91, 0}, {28, 83, 0}, {13, 30, 0}, {6, 23, 0}},
           BalancedDegradationSettings::Config{
-              3000, 25, {88, 92, 0}, {29, 140, 0}, {14, 40, 0}, {5, 24, 0}}));
+              3000, 25, 0, {88, 92, 0}, {29, 84, 0}, {14, 40, 0}, {5, 24, 0}}));
 }
 
 TEST(BalancedDegradationSettings, GetsDefaultConfigIfOnlyHasLowThreshold) {
diff --git a/video/video_stream_encoder.cc b/video/video_stream_encoder.cc
index 5d82434..8e22930 100644
--- a/video/video_stream_encoder.cc
+++ b/video/video_stream_encoder.cc
@@ -1900,6 +1900,16 @@
 
   switch (degradation_preference_) {
     case DegradationPreference::BALANCED: {
+      // Do not adapt up if bwe is less than min bitrate for next resolution.
+      absl::optional<int> next_layer_min_kbps =
+          balanced_settings_.NextHigherBitrateKbps(
+              last_frame_info_->pixel_count());
+      if (next_layer_min_kbps && encoder_start_bitrate_bps_ > 0 &&
+          reason == kQuality &&
+          encoder_start_bitrate_bps_ <
+              static_cast<uint32_t>(next_layer_min_kbps.value() * 1000)) {
+        return;
+      }
       // Try scale up framerate, if higher.
       int fps = balanced_settings_.MaxFps(encoder_config_.codec_type,
                                           last_frame_info_->pixel_count());
diff --git a/video/video_stream_encoder_unittest.cc b/video/video_stream_encoder_unittest.cc
index 04a8802..35439d2 100644
--- a/video/video_stream_encoder_unittest.cc
+++ b/video/video_stream_encoder_unittest.cc
@@ -2599,6 +2599,80 @@
   video_stream_encoder_->Stop();
 }
 
+TEST_F(VideoStreamEncoderTest, NoAdaptUpIfBwEstimateIsLessThanMinBitrate) {
+  webrtc::test::ScopedFieldTrials field_trials(
+      "WebRTC-Video-BalancedDegradationSettings/"
+      "pixels:57600|129600|230400,fps:7|10|14,kbps:0|0|425/");
+  // Reset encoder for field trials to take effect.
+  ConfigureEncoder(video_encoder_config_.Copy());
+
+  const int kWidth = 640;  // pixels:640x360=230400
+  const int kHeight = 360;
+  const int64_t kFrameIntervalMs = 150;
+  const int kMinBitrateBps = 425000;
+  const int kTooLowMinBitrateBps = 424000;
+  video_stream_encoder_->OnBitrateUpdated(DataRate::bps(kTooLowMinBitrateBps),
+                                          DataRate::bps(kTooLowMinBitrateBps),
+                                          0, 0);
+
+  // Enable BALANCED preference, no initial limitation.
+  AdaptingFrameForwarder source;
+  source.set_adaptation_enabled(true);
+  video_stream_encoder_->SetSource(&source,
+                                   webrtc::DegradationPreference::BALANCED);
+
+  int64_t timestamp_ms = kFrameIntervalMs;
+  source.IncomingCapturedFrame(CreateFrame(timestamp_ms, kWidth, kHeight));
+  sink_.WaitForEncodedFrame(kWidth, kHeight);
+  VerifyFpsMaxResolutionMax(source.sink_wants());
+  EXPECT_FALSE(stats_proxy_->GetStats().bw_limited_framerate);
+  EXPECT_FALSE(stats_proxy_->GetStats().bw_limited_resolution);
+  EXPECT_EQ(0, stats_proxy_->GetStats().number_of_quality_adapt_changes);
+
+  // Trigger adapt down, expect scaled down framerate (640x360@14fps).
+  video_stream_encoder_->TriggerQualityLow();
+  timestamp_ms += kFrameIntervalMs;
+  source.IncomingCapturedFrame(CreateFrame(timestamp_ms, kWidth, kHeight));
+  sink_.WaitForEncodedFrame(timestamp_ms);
+  VerifyFpsEqResolutionMax(source.sink_wants(), 14);
+  EXPECT_TRUE(stats_proxy_->GetStats().bw_limited_framerate);
+  EXPECT_FALSE(stats_proxy_->GetStats().bw_limited_resolution);
+  EXPECT_EQ(1, stats_proxy_->GetStats().number_of_quality_adapt_changes);
+
+  // Trigger adapt down, expect scaled down resolution (480x270@14fps).
+  video_stream_encoder_->TriggerQualityLow();
+  timestamp_ms += kFrameIntervalMs;
+  source.IncomingCapturedFrame(CreateFrame(timestamp_ms, kWidth, kHeight));
+  sink_.WaitForEncodedFrame(timestamp_ms);
+  VerifyFpsEqResolutionLt(source.sink_wants(), source.last_wants());
+  EXPECT_TRUE(stats_proxy_->GetStats().bw_limited_framerate);
+  EXPECT_TRUE(stats_proxy_->GetStats().bw_limited_resolution);
+  EXPECT_EQ(2, stats_proxy_->GetStats().number_of_quality_adapt_changes);
+
+  // Trigger adapt up, expect no upscale (target bitrate < min bitrate).
+  video_stream_encoder_->TriggerQualityHigh();
+  timestamp_ms += kFrameIntervalMs;
+  source.IncomingCapturedFrame(CreateFrame(timestamp_ms, kWidth, kHeight));
+  sink_.WaitForEncodedFrame(timestamp_ms);
+  EXPECT_TRUE(stats_proxy_->GetStats().bw_limited_framerate);
+  EXPECT_TRUE(stats_proxy_->GetStats().bw_limited_resolution);
+  EXPECT_EQ(2, stats_proxy_->GetStats().number_of_quality_adapt_changes);
+
+  // Trigger adapt up, expect upscaled resolution (target bitrate == min
+  // bitrate).
+  video_stream_encoder_->OnBitrateUpdated(DataRate::bps(kMinBitrateBps),
+                                          DataRate::bps(kMinBitrateBps), 0, 0);
+  video_stream_encoder_->TriggerQualityHigh();
+  timestamp_ms += kFrameIntervalMs;
+  source.IncomingCapturedFrame(CreateFrame(timestamp_ms, kWidth, kHeight));
+  sink_.WaitForEncodedFrame(timestamp_ms);
+  EXPECT_TRUE(stats_proxy_->GetStats().bw_limited_framerate);
+  EXPECT_FALSE(stats_proxy_->GetStats().bw_limited_resolution);
+  EXPECT_EQ(3, stats_proxy_->GetStats().number_of_quality_adapt_changes);
+
+  video_stream_encoder_->Stop();
+}
+
 TEST_F(VideoStreamEncoderTest,
        AdaptsResolutionOnOveruseAndLowQuality_MaintainFramerateMode) {
   const int kWidth = 1280;