VideoAdapter: Interpret requested resolution as max restriction.

The `requested_resolution` API must not change aspect ratio, example:
- Frame is 60x30
- Requested is 30x30
- We expect 30x15 (not 30x30!) as to maintain aspect ratio.

This bug was previously fixed by making VideoAdapter unaware of the
requested resolution behind a flag: this seemed OK since the
VideoStreamEncoder ultimately decides the resolution, whether or not
the incoming frame is adapted.

But this is not desired for some non-Chrome use cases. This CL attempts
to make both Chrome and non-Chrome use cases happy by implementing the
aspect ratio preserving restriction inside VideoAdapter too.

This allows us to get rid of the "use_standard_requested_resolution"
flag and change the "VideoStreamEncoderResolutionTest" TEST_P to
TEST_F.

Bug: webrtc:366067962, webrtc:366284861
Change-Id: I1dfd10963274c5fdfd18d0f4443b2f209d2e9a4b
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/362720
Reviewed-by: Jonas Oreland <jonaso@webrtc.org>
Commit-Queue: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Reviewed-by: Henrik Andreassson <henrika@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#43037}
diff --git a/api/video/video_stream_encoder_settings.h b/api/video/video_stream_encoder_settings.h
index e49305b..c8cb368 100644
--- a/api/video/video_stream_encoder_settings.h
+++ b/api/video/video_stream_encoder_settings.h
@@ -57,25 +57,6 @@
   // Enables the frame instrumentation generator that is required for automatic
   // corruption detection.
   bool enable_frame_instrumentation_generator = false;
-
-  // According to spec, `requested_resolution` (called scaleResolutionDownTo in
-  // the web API) MUST NOT modify the aspect ratio of the frame, e.g. a 1280x720
-  // frame being restricted to maxWidth by maxHeight 720x720 should result in
-  // 720x405. In order for this to work, the video source must not adapt the
-  // input frame to the value of `requested_resolution`, as that would result in
-  // stretched 720x720.
-  //
-  // In order not to break backwards compatibility with C++ usage of this API,
-  // when `use_standard_requested_resolution` is false, the
-  // `requested_resolution` is signaled back to the video source. This works as
-  // long as the aspect ratio is the same, but breaks the web API use case.
-  //
-  // Spec:
-  // https://w3c.github.io/webrtc-extensions/#dom-rtcrtpencodingparameters-scaleresolutiondownto
-  //
-  // TODO(https://crbug.com/webrtc/366284861): Change the default to true,
-  // delete this flag and any code handling the legacy behavior.
-  bool use_standard_requested_resolution = false;
 };
 
 }  // namespace webrtc
diff --git a/media/BUILD.gn b/media/BUILD.gn
index 08e924a..c39f75c 100644
--- a/media/BUILD.gn
+++ b/media/BUILD.gn
@@ -92,6 +92,7 @@
     "../api/transport/rtp:rtp_source",
     "../api/units:time_delta",
     "../api/video:recordable_encoded_frame",
+    "../api/video:resolution",
     "../api/video:video_bitrate_allocation",
     "../api/video:video_bitrate_allocator_factory",
     "../api/video:video_frame",
@@ -159,6 +160,7 @@
   ]
   deps = [
     ":video_common",
+    "../api/video:resolution",
     "../api/video:video_frame",
     "../common_video",
     "../rtc_base:checks",
diff --git a/media/base/video_adapter.cc b/media/base/video_adapter.cc
index bb1515b..35bd722 100644
--- a/media/base/video_adapter.cc
+++ b/media/base/video_adapter.cc
@@ -211,6 +211,28 @@
     return false;
   }
 
+  // Adjust input width and height without modifying aspect ratio as to be less
+  // than or equal to the `requested_resolution_`.
+  if (requested_resolution_.has_value()) {
+    // Make frame and requested resolution have matching orientation.
+    webrtc::Resolution requested_resolution = requested_resolution_.value();
+    if ((in_width < in_height) !=
+        (requested_resolution_->width < requested_resolution_->height)) {
+      requested_resolution = {.width = requested_resolution_->height,
+                              .height = requested_resolution_->width};
+    }
+    // Downscale by smallest scaling factor, if necessary.
+    if (in_width > 0 && in_height > 0 &&
+        (requested_resolution.width < in_width ||
+         requested_resolution.height < in_height)) {
+      double scale_factor = std::min(
+          requested_resolution.width / static_cast<double>(in_width),
+          requested_resolution.height / static_cast<double>(in_height));
+      in_width = std::round(in_width * scale_factor);
+      in_height = std::round(in_height * scale_factor);
+    }
+  }
+
   // Calculate how the input should be cropped.
   if (!target_aspect_ratio || target_aspect_ratio->first <= 0 ||
       target_aspect_ratio->second <= 0) {
@@ -341,6 +363,13 @@
   max_framerate_request_ = sink_wants.max_framerate_fps;
   resolution_alignment_ = cricket::LeastCommonMultiple(
       source_resolution_alignment_, sink_wants.resolution_alignment);
+  // Convert from std::optional<rtc::VideoSinkWants::FrameSize> to
+  // std::optional<webrtc::Resolution>. Both are {int,int}.
+  requested_resolution_ = std::nullopt;
+  if (sink_wants.requested_resolution.has_value()) {
+    requested_resolution_ = {.width = sink_wants.requested_resolution->width,
+                             .height = sink_wants.requested_resolution->height};
+  }
 
   // If requested_resolution is used, and there are no active encoders
   // that are NOT using requested_resolution (aka newapi), then override
@@ -371,7 +400,7 @@
   }
 
   if (!stashed_output_format_request_) {
-    // The active output format request is about to be rewritten by
+    // The active output format request is about to be cleared due to
     // request_resolution. We need to save it for later use in case the encoder
     // which doesn't use request_resolution logic become active in the future.
     stashed_output_format_request_ = output_format_request_;
@@ -379,22 +408,9 @@
                      << stashed_output_format_request_->ToString();
   }
 
-  auto res = *sink_wants.requested_resolution;
-  if (res.width < res.height) {
-    // Adjust `res` to landscape mode.
-    res.width = sink_wants.requested_resolution->height;
-    res.height = sink_wants.requested_resolution->width;
-  }
-  auto pixel_count = res.width * res.height;
-  output_format_request_.target_landscape_aspect_ratio =
-      std::make_pair(res.width, res.height);
-  output_format_request_.max_landscape_pixel_count = pixel_count;
-  output_format_request_.target_portrait_aspect_ratio =
-      std::make_pair(res.height, res.width);
-  output_format_request_.max_portrait_pixel_count = pixel_count;
-  output_format_request_.max_fps = max_framerate_request_;
-  RTC_LOG(LS_INFO) << "Setting output_format_request_ based on sink_wants: "
-                   << output_format_request_.ToString();
+  // Clear the output format request. Requested resolution will be applied
+  // instead which happens inside AdaptFrameResolution().
+  output_format_request_ = {};
 }
 
 int VideoAdapter::GetTargetPixels() const {
diff --git a/media/base/video_adapter.h b/media/base/video_adapter.h
index 35ed672..4cc4a38 100644
--- a/media/base/video_adapter.h
+++ b/media/base/video_adapter.h
@@ -17,6 +17,7 @@
 #include <string>
 #include <utility>
 
+#include "api/video/resolution.h"
 #include "api/video/video_source_interface.h"
 #include "common_video/framerate_controller.h"
 #include "media/base/video_common.h"
@@ -148,6 +149,8 @@
   int resolution_request_target_pixel_count_ RTC_GUARDED_BY(mutex_);
   int resolution_request_max_pixel_count_ RTC_GUARDED_BY(mutex_);
   int max_framerate_request_ RTC_GUARDED_BY(mutex_);
+  std::optional<webrtc::Resolution> requested_resolution_
+      RTC_GUARDED_BY(mutex_);
 
   // Stashed OutputFormatRequest that is used to save value of
   // OnOutputFormatRequest in case all active encoders are using
diff --git a/media/base/video_adapter_unittest.cc b/media/base/video_adapter_unittest.cc
index 5011438..404d8e7 100644
--- a/media/base/video_adapter_unittest.cc
+++ b/media/base/video_adapter_unittest.cc
@@ -59,13 +59,8 @@
   wants.resolution_alignment = 1;
   wants.is_active = true;
   if (requested_resolution) {
-    wants.target_pixel_count = requested_resolution->PixelCount();
-    wants.max_pixel_count = requested_resolution->PixelCount();
     wants.requested_resolution.emplace(rtc::VideoSinkWants::FrameSize(
         requested_resolution->width, requested_resolution->height));
-  } else {
-    wants.target_pixel_count = kWidth * kHeight;
-    wants.max_pixel_count = kWidth * kHeight;
   }
   wants.aggregates.emplace(rtc::VideoSinkWants::Aggregates());
   wants.aggregates->any_active_without_requested_resolution =
@@ -1306,6 +1301,19 @@
   }
 }
 
+TEST_P(VideoAdapterTest, RequestedResolutionMaintainsAspectRatio) {
+  // Request 720x720.
+  adapter_.OnSinkWants(
+      BuildSinkWants(Resolution{.width = 720, .height = 720},
+                     /* any_active_without_requested_resolution= */ false));
+
+  // A 1280x720 frame restricted to 720x720 produces 720x405.
+  EXPECT_THAT(
+      AdaptFrameResolution(/* input frame */ {.width = 1280, .height = 720})
+          .first,
+      Eq(Resolution{.width = 720, .height = 405}));
+}
+
 class VideoAdapterWithSourceAlignmentTest : public VideoAdapterTest {
  protected:
   static constexpr int kSourceResolutionAlignment = 7;
diff --git a/video/video_stream_encoder.cc b/video/video_stream_encoder.cc
index 61b9483..da42b2a 100644
--- a/video/video_stream_encoder.cc
+++ b/video/video_stream_encoder.cc
@@ -891,18 +891,12 @@
       }
     }
   }
-  const bool signal_requested_resolution =
-      !settings_.use_standard_requested_resolution;
-  if ((signal_requested_resolution &&
-       requested_resolution !=
-           video_source_sink_controller_.requested_resolution()) ||
+  if (requested_resolution !=
+          video_source_sink_controller_.requested_resolution() ||
       active != video_source_sink_controller_.active() ||
       max_framerate !=
           video_source_sink_controller_.frame_rate_upper_limit().value_or(-1)) {
-    if (signal_requested_resolution) {
-      video_source_sink_controller_.SetRequestedResolution(
-          requested_resolution);
-    }
+    video_source_sink_controller_.SetRequestedResolution(requested_resolution);
     if (max_framerate >= 0) {
       video_source_sink_controller_.SetFrameRateUpperLimit(max_framerate);
     } else {
diff --git a/video/video_stream_encoder_unittest.cc b/video/video_stream_encoder_unittest.cc
index bf56316..9e0626c 100644
--- a/video/video_stream_encoder_unittest.cc
+++ b/video/video_stream_encoder_unittest.cc
@@ -2530,56 +2530,14 @@
   video_stream_encoder_->Stop();
 }
 
-// Tests both the standard and non-standard version of `requested_resolution`
-// (also known as scaleResolutionDownTo) which behave differently with regards
-// to signaling `requested_resolution` back to the source.
-enum class RequestedResolutionVersion {
-  kLegacyRequestedResolution,
-  kStandardRequestedResolution,
-};
-class VideoStreamEncoderResolutionTest
-    : public VideoStreamEncoderTest,
-      public ::testing::WithParamInterface<RequestedResolutionVersion> {
- public:
-  VideoStreamEncoderResolutionTest()
-      : VideoStreamEncoderTest(), requested_resolution_version_(GetParam()) {}
+TEST_F(VideoStreamEncoderTest, RequestInSinkWantsBeforeFirstFrame) {
+  // Use a real video stream factory or else `requested_resolution` is not
+  // applied correctly.
+  video_encoder_config_.video_stream_factory = nullptr;
+  ConfigureEncoder(video_encoder_config_.Copy());
+  video_stream_encoder_->OnBitrateUpdatedAndWaitForManagedResources(
+      kTargetBitrate, kTargetBitrate, kTargetBitrate, 0, 0, 0);
 
-  void SetUp() override {
-    VideoStreamEncoderTest::SetUp();
-    // Configure `use_standard_requested_resolution` based on test param.
-    switch (requested_resolution_version_) {
-      case RequestedResolutionVersion::kLegacyRequestedResolution:
-        video_send_config_.encoder_settings.use_standard_requested_resolution =
-            false;
-        break;
-      case RequestedResolutionVersion::kStandardRequestedResolution:
-        video_send_config_.encoder_settings.use_standard_requested_resolution =
-            true;
-        break;
-    }
-
-    // Use a real video stream factory or else `requested_resolution` is not
-    // applied correctly.
-    video_encoder_config_.video_stream_factory = nullptr;
-
-    ConfigureEncoder(video_encoder_config_.Copy());
-
-    video_stream_encoder_->OnBitrateUpdatedAndWaitForManagedResources(
-        kTargetBitrate, kTargetBitrate, kTargetBitrate, 0, 0, 0);
-  }
-
- protected:
-  const RequestedResolutionVersion requested_resolution_version_;
-};
-
-INSTANTIATE_TEST_SUITE_P(
-    VideoStreamEncoderResolutionTest,
-    VideoStreamEncoderResolutionTest,
-    ::testing::Values(
-        RequestedResolutionVersion::kLegacyRequestedResolution,
-        RequestedResolutionVersion::kStandardRequestedResolution));
-
-TEST_P(VideoStreamEncoderResolutionTest, RequestInSinkWantsBeforeFirstFrame) {
   ASSERT_THAT(video_encoder_config_.simulcast_layers, SizeIs(1));
   video_encoder_config_.simulcast_layers[0].requested_resolution.emplace(
       Resolution({.width = 320, .height = 160}));
@@ -2587,37 +2545,28 @@
   video_stream_encoder_->ConfigureEncoder(video_encoder_config_.Copy(),
                                           kMaxPayloadLength);
 
-  // The legacy path signals `requested_resolution` back to the source. The
-  // standard path does not.
-  switch (requested_resolution_version_) {
-    case RequestedResolutionVersion::kLegacyRequestedResolution:
-      EXPECT_EQ(video_source_.sink_wants().requested_resolution,
-                rtc::VideoSinkWants::FrameSize(320, 160));
-      break;
-    case RequestedResolutionVersion::kStandardRequestedResolution:
-      EXPECT_FALSE(video_source_.sink_wants().requested_resolution.has_value());
-      break;
-  }
+  EXPECT_EQ(video_source_.sink_wants().requested_resolution,
+            rtc::VideoSinkWants::FrameSize(320, 160));
 
   video_encoder_config_.simulcast_layers[0].requested_resolution->height = 320;
   video_encoder_config_.simulcast_layers[0].requested_resolution->width = 640;
   video_stream_encoder_->ConfigureEncoder(video_encoder_config_.Copy(),
                                           kMaxPayloadLength);
 
-  switch (requested_resolution_version_) {
-    case RequestedResolutionVersion::kLegacyRequestedResolution:
-      EXPECT_EQ(video_source_.sink_wants().requested_resolution,
-                rtc::VideoSinkWants::FrameSize(640, 320));
-      break;
-    case RequestedResolutionVersion::kStandardRequestedResolution:
-      EXPECT_FALSE(video_source_.sink_wants().requested_resolution.has_value());
-      break;
-  }
+  EXPECT_EQ(video_source_.sink_wants().requested_resolution,
+            rtc::VideoSinkWants::FrameSize(640, 320));
 
   video_stream_encoder_->Stop();
 }
 
-TEST_P(VideoStreamEncoderResolutionTest, RequestInWrongAspectRatioWithAdapter) {
+TEST_F(VideoStreamEncoderTest, RequestInWrongAspectRatioWithAdapter) {
+  // Use a real video stream factory or else `requested_resolution` is not
+  // applied correctly.
+  video_encoder_config_.video_stream_factory = nullptr;
+  ConfigureEncoder(video_encoder_config_.Copy());
+  video_stream_encoder_->OnBitrateUpdatedAndWaitForManagedResources(
+      kTargetBitrate, kTargetBitrate, kTargetBitrate, 0, 0, 0);
+
   // Use a source that adapts resolution based on OnSinkWants.
   AdaptingFrameForwarder source(&time_controller_);
   source.set_adaptation_enabled(true);
@@ -2633,16 +2582,8 @@
   // Capture a 60x30 frame.
   source.IncomingCapturedFrame(CreateFrame(1, 60, 30));
   // The 60x30 frame does not fit inside the 30x30 restrictions.
-  // - The non-standard path produces 30x30 due to OnSinkWants() signaling.
-  // - The standard path produces 30x15, maintaining aspect ratio.
-  switch (requested_resolution_version_) {
-    case RequestedResolutionVersion::kLegacyRequestedResolution:
-      WaitForEncodedFrame(30, 30);
-      break;
-    case RequestedResolutionVersion::kStandardRequestedResolution:
-      WaitForEncodedFrame(30, 15);
-      break;
-  }
+  // Expect 30x15, maintaining aspect ratio.
+  WaitForEncodedFrame(30, 15);
 
   video_stream_encoder_->Stop();
 }