Add ability to downscale content to improve quality.

BUG=3712
R=marpan@google.com, stefan@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/18169004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@7164 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/webrtc/modules/modules.gyp b/webrtc/modules/modules.gyp
index a1a1bf0..f517568 100644
--- a/webrtc/modules/modules.gyp
+++ b/webrtc/modules/modules.gyp
@@ -246,6 +246,7 @@
             'video_coding/main/source/qm_select_unittest.cc',
             'video_coding/main/source/test/stream_generator.cc',
             'video_coding/main/source/test/stream_generator.h',
+            'video_coding/utility/quality_scaler_unittest.cc',
             'video_processing/main/test/unit_test/brightness_detection_test.cc',
             'video_processing/main/test/unit_test/color_enhancement_test.cc',
             'video_processing/main/test/unit_test/content_metrics_test.cc',
diff --git a/webrtc/modules/video_coding/codecs/test/videoprocessor_integrationtest.cc b/webrtc/modules/video_coding/codecs/test/videoprocessor_integrationtest.cc
index e7b934d..bd4a563 100644
--- a/webrtc/modules/video_coding/codecs/test/videoprocessor_integrationtest.cc
+++ b/webrtc/modules/video_coding/codecs/test/videoprocessor_integrationtest.cc
@@ -703,34 +703,26 @@
                          rc_metrics);
 }
 
-// Run with no packet loss, at low bitrate, then increase rate somewhat.
-// Key frame is thrown in every 120 frames. Can expect some frame drops after
-// key frame, even at high rate. The internal spatial resizer is on, so expect
-// spatial resize down at first key frame, and back up at second key frame.
-// Error_concealment is off in this test since there is a memory leak with
-// resizing and error concealment.
+// Run with no packet loss, at low bitrate. During this time we should've
+// resized once.
 TEST_F(VideoProcessorIntegrationTest,
        DISABLED_ON_ANDROID(ProcessNoLossSpatialResizeFrameDrop)) {
   config_.networking_config.packet_loss_probability = 0;
   // Bitrate and frame rate profile.
   RateProfile rate_profile;
-  SetRateProfilePars(&rate_profile, 0, 100, 30, 0);
-  SetRateProfilePars(&rate_profile, 1, 200, 30, 120);
-  SetRateProfilePars(&rate_profile, 2, 200, 30, 240);
-  rate_profile.frame_index_rate_update[3] = kNbrFramesLong + 1;
+  SetRateProfilePars(&rate_profile, 0, 50, 30, 0);
+  rate_profile.frame_index_rate_update[1] = kNbrFramesLong + 1;
   rate_profile.num_frames = kNbrFramesLong;
   // Codec/network settings.
   CodecConfigPars process_settings;
-  SetCodecParameters(&process_settings, 0.0f, 120, 1, false, true, true, true);
-  // Metrics for expected quality.: lower quality on average from up-sampling
-  // the down-sampled portion of the run, in case resizer is on.
+  SetCodecParameters(
+      &process_settings, 0.0f, kNbrFramesLong, 1, false, true, true, true);
+  // Metrics for expected quality.
   QualityMetrics quality_metrics;
-  SetQualityMetrics(&quality_metrics, 29.0, 20.0, 0.75, 0.60);
+  SetQualityMetrics(&quality_metrics, 25.0, 15.0, 0.70, 0.40);
   // Metrics for rate control.
-  RateControlMetrics rc_metrics[3];
-  SetRateControlMetrics(rc_metrics, 0, 45, 30, 75, 20, 70, 0);
-  SetRateControlMetrics(rc_metrics, 1, 20, 35, 30, 20, 15, 1);
-  SetRateControlMetrics(rc_metrics, 2, 0, 30, 30, 15, 25, 1);
+  RateControlMetrics rc_metrics[1];
+  SetRateControlMetrics(rc_metrics, 0, 160, 60, 120, 20, 70, 1);
   ProcessFramesAndVerify(quality_metrics,
                          rate_profile,
                          process_settings,
diff --git a/webrtc/modules/video_coding/codecs/vp8/vp8_impl.cc b/webrtc/modules/video_coding/codecs/vp8/vp8_impl.cc
index 4901edf..16d105d 100644
--- a/webrtc/modules/video_coding/codecs/vp8/vp8_impl.cc
+++ b/webrtc/modules/video_coding/codecs/vp8/vp8_impl.cc
@@ -105,6 +105,7 @@
   temporal_layers_->ConfigureBitrates(new_bitrate_kbit, codec_.maxBitrate,
                                       new_framerate, config_);
   codec_.maxFramerate = new_framerate;
+  quality_scaler_.ReportFramerate(new_framerate);
 
   // update encoder context
   if (vpx_codec_enc_config_set(encoder_, config_)) {
@@ -230,8 +231,8 @@
       30 : 0;
   config_->rc_end_usage = VPX_CBR;
   config_->g_pass = VPX_RC_ONE_PASS;
-  config_->rc_resize_allowed = inst->codecSpecific.VP8.automaticResizeOn ?
-      1 : 0;
+  // Handle resizing outside of libvpx.
+  config_->rc_resize_allowed = 0;
   config_->rc_min_quantizer = 2;
   config_->rc_max_quantizer = inst->qpMax;
   config_->rc_undershoot_pct = 100;
@@ -272,6 +273,8 @@
   cpu_speed_ = -12;
 #endif
   rps_->Init();
+  quality_scaler_.Init(codec_.qpMax);
+  quality_scaler_.ReportFramerate(codec_.maxFramerate);
   return InitAndSetControlSettings(inst);
 }
 
@@ -296,6 +299,7 @@
   vpx_codec_control(encoder_, VP8E_SET_MAX_INTRA_BITRATE_PCT,
                     rc_max_intra_target_);
   inited_ = true;
+
   return WEBRTC_VIDEO_CODEC_OK;
 }
 
@@ -315,15 +319,15 @@
   return (targetPct < minIntraTh) ? minIntraTh: targetPct;
 }
 
-int VP8EncoderImpl::Encode(const I420VideoFrame& input_image,
+int VP8EncoderImpl::Encode(const I420VideoFrame& input_frame,
                            const CodecSpecificInfo* codec_specific_info,
                            const std::vector<VideoFrameType>* frame_types) {
-  TRACE_EVENT1("webrtc", "VP8::Encode", "timestamp", input_image.timestamp());
+  TRACE_EVENT1("webrtc", "VP8::Encode", "timestamp", input_frame.timestamp());
 
   if (!inited_) {
     return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
   }
-  if (input_image.IsZeroSize()) {
+  if (input_frame.IsZeroSize()) {
     return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
   }
   if (encoded_complete_callback_ == NULL) {
@@ -336,25 +340,31 @@
     frame_type = (*frame_types)[0];
   }
 
+  const I420VideoFrame& frame =
+      config_->rc_dropframe_thresh > 0 &&
+              codec_.codecSpecific.VP8.automaticResizeOn
+          ? quality_scaler_.GetScaledFrame(input_frame)
+          : input_frame;
+
   // Check for change in frame size.
-  if (input_image.width() != codec_.width ||
-      input_image.height() != codec_.height) {
-    int ret = UpdateCodecFrameSize(input_image);
+  if (frame.width() != codec_.width ||
+      frame.height() != codec_.height) {
+    int ret = UpdateCodecFrameSize(frame);
     if (ret < 0) {
       return ret;
     }
   }
   // Image in vpx_image_t format.
-  // Input image is const. VP8's raw image is not defined as const.
-  raw_->planes[PLANE_Y] = const_cast<uint8_t*>(input_image.buffer(kYPlane));
-  raw_->planes[PLANE_U] = const_cast<uint8_t*>(input_image.buffer(kUPlane));
-  raw_->planes[PLANE_V] = const_cast<uint8_t*>(input_image.buffer(kVPlane));
+  // Input frame is const. VP8's raw frame is not defined as const.
+  raw_->planes[PLANE_Y] = const_cast<uint8_t*>(frame.buffer(kYPlane));
+  raw_->planes[PLANE_U] = const_cast<uint8_t*>(frame.buffer(kUPlane));
+  raw_->planes[PLANE_V] = const_cast<uint8_t*>(frame.buffer(kVPlane));
   // TODO(mikhal): Stride should be set in initialization.
-  raw_->stride[VPX_PLANE_Y] = input_image.stride(kYPlane);
-  raw_->stride[VPX_PLANE_U] = input_image.stride(kUPlane);
-  raw_->stride[VPX_PLANE_V] = input_image.stride(kVPlane);
+  raw_->stride[VPX_PLANE_Y] = frame.stride(kYPlane);
+  raw_->stride[VPX_PLANE_U] = frame.stride(kUPlane);
+  raw_->stride[VPX_PLANE_V] = frame.stride(kVPlane);
 
-  int flags = temporal_layers_->EncodeFlags(input_image.timestamp());
+  int flags = temporal_layers_->EncodeFlags(frame.timestamp());
 
   bool send_keyframe = (frame_type == kKeyFrame);
   if (send_keyframe) {
@@ -370,11 +380,11 @@
             codec_specific_info->codecSpecific.VP8.pictureIdRPSI);
       }
       if (codec_specific_info->codecSpecific.VP8.hasReceivedSLI) {
-        sendRefresh = rps_->ReceivedSLI(input_image.timestamp());
+        sendRefresh = rps_->ReceivedSLI(frame.timestamp());
       }
     }
     flags = rps_->EncodeFlags(picture_id_, sendRefresh,
-                              input_image.timestamp());
+                              frame.timestamp());
   }
 
   // TODO(holmer): Ideally the duration should be the timestamp diff of this
@@ -390,7 +400,7 @@
   }
   timestamp_ += duration;
 
-  return GetEncodedPartitions(input_image);
+  return GetEncodedPartitions(frame);
 }
 
 int VP8EncoderImpl::UpdateCodecFrameSize(const I420VideoFrame& input_image) {
@@ -480,6 +490,11 @@
     encoded_image_._encodedWidth = codec_.width;
     encoded_complete_callback_->Encoded(encoded_image_, &codec_specific,
                                       &frag_info);
+    int qp;
+    vpx_codec_control(encoder_, VP8E_GET_LAST_QUANTIZER_64, &qp);
+    quality_scaler_.ReportEncodedFrame(qp);
+  } else {
+    quality_scaler_.ReportDroppedFrame();
   }
   return WEBRTC_VIDEO_CODEC_OK;
 }
diff --git a/webrtc/modules/video_coding/codecs/vp8/vp8_impl.h b/webrtc/modules/video_coding/codecs/vp8/vp8_impl.h
index 56f7219..08ce3c9 100644
--- a/webrtc/modules/video_coding/codecs/vp8/vp8_impl.h
+++ b/webrtc/modules/video_coding/codecs/vp8/vp8_impl.h
@@ -14,6 +14,7 @@
 #define WEBRTC_MODULES_VIDEO_CODING_CODECS_VP8_IMPL_H_
 
 #include "webrtc/modules/video_coding/codecs/vp8/include/vp8.h"
+#include "webrtc/modules/video_coding/utility/quality_scaler.h"
 
 // VPX forward declaration
 typedef struct vpx_codec_ctx vpx_codec_ctx_t;
@@ -139,6 +140,7 @@
   vpx_codec_ctx_t* encoder_;
   vpx_codec_enc_cfg_t* config_;
   vpx_image_t* raw_;
+  QualityScaler quality_scaler_;
 };  // end of VP8Encoder class
 
 
diff --git a/webrtc/modules/video_coding/main/source/video_coding_test.gypi b/webrtc/modules/video_coding/main/source/video_coding_test.gypi
index 576524d..64cb602 100644
--- a/webrtc/modules/video_coding/main/source/video_coding_test.gypi
+++ b/webrtc/modules/video_coding/main/source/video_coding_test.gypi
@@ -44,8 +44,8 @@
         '../test/codec_database_test.cc',
         '../test/generic_codec_test.cc',
         '../test/media_opt_test.cc',
-        '../test/mt_test_common.cc',
         '../test/mt_rx_tx_test.cc',
+        '../test/mt_test_common.cc',
         '../test/normal_test.cc',
         '../test/quality_modes_test.cc',
         '../test/rtp_player.cc',
@@ -53,8 +53,8 @@
         '../test/test_util.cc',
         '../test/tester_main.cc',
         '../test/vcm_payload_sink_factory.cc',
-        '../test/video_rtp_play_mt.cc',
         '../test/video_rtp_play.cc',
+        '../test/video_rtp_play_mt.cc',
         '../test/video_source.cc',
       ], # sources
     },
diff --git a/webrtc/modules/video_coding/utility/quality_scaler.cc b/webrtc/modules/video_coding/utility/quality_scaler.cc
new file mode 100644
index 0000000..327748d
--- /dev/null
+++ b/webrtc/modules/video_coding/utility/quality_scaler.cc
@@ -0,0 +1,137 @@
+/*
+ *  Copyright (c) 2014 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 "webrtc/modules/video_coding/utility/quality_scaler.h"
+
+namespace webrtc {
+
+static const int kMinFps = 10;
+static const int kMeasureSeconds = 5;
+static const int kFramedropPercentThreshold = 60;
+static const int kLowQpThresholdDenominator = 3;
+
+QualityScaler::QualityScaler()
+    : num_samples_(0), low_qp_threshold_(-1), downscale_shift_(0) {
+}
+
+void QualityScaler::Init(int max_qp) {
+  ClearSamples();
+  downscale_shift_ = 0;
+  low_qp_threshold_ = max_qp / kLowQpThresholdDenominator ;
+}
+
+void QualityScaler::ReportFramerate(int framerate) {
+  num_samples_ = static_cast<size_t>(
+      kMeasureSeconds * (framerate < kMinFps ? kMinFps : framerate));
+}
+
+void QualityScaler::ReportEncodedFrame(int qp) {
+  average_qp_.AddSample(qp);
+  framedrop_percent_.AddSample(0);
+}
+
+void QualityScaler::ReportDroppedFrame() {
+  framedrop_percent_.AddSample(100);
+}
+
+QualityScaler::Resolution QualityScaler::GetScaledResolution(
+    const I420VideoFrame& frame) {
+  // Both of these should be set through InitEncode -> Should be set by now.
+  assert(low_qp_threshold_ >= 0);
+  assert(num_samples_ > 0);
+  // Update scale factor.
+  int avg;
+  if (framedrop_percent_.GetAverage(num_samples_, &avg) &&
+      avg >= kFramedropPercentThreshold) {
+    AdjustScale(false);
+  } else if (average_qp_.GetAverage(num_samples_, &avg) &&
+             avg <= low_qp_threshold_) {
+    AdjustScale(true);
+  }
+
+  Resolution res;
+  res.width = frame.width();
+  res.height = frame.height();
+
+  assert(downscale_shift_ >= 0);
+  for (int shift = downscale_shift_;
+       shift > 0 && res.width > 1 && res.height > 1;
+       --shift) {
+    res.width >>= 1;
+    res.height >>= 1;
+  }
+
+  return res;
+}
+
+const I420VideoFrame& QualityScaler::GetScaledFrame(
+    const I420VideoFrame& frame) {
+  Resolution res = GetScaledResolution(frame);
+  if (res.width == frame.width())
+    return frame;
+
+  scaler_.Set(frame.width(),
+              frame.height(),
+              res.width,
+              res.height,
+              kI420,
+              kI420,
+              kScaleBox);
+  if (scaler_.Scale(frame, &scaled_frame_) != 0)
+    return frame;
+
+  scaled_frame_.set_ntp_time_ms(frame.ntp_time_ms());
+  scaled_frame_.set_timestamp(frame.timestamp());
+  scaled_frame_.set_render_time_ms(frame.render_time_ms());
+
+  return scaled_frame_;
+}
+
+QualityScaler::MovingAverage::MovingAverage() : sum_(0) {
+}
+
+void QualityScaler::MovingAverage::AddSample(int sample) {
+  samples_.push_back(sample);
+  sum_ += sample;
+}
+
+bool QualityScaler::MovingAverage::GetAverage(size_t num_samples, int* avg) {
+  assert(num_samples > 0);
+  if (num_samples > samples_.size())
+    return false;
+
+  // Remove old samples.
+  while (num_samples < samples_.size()) {
+    sum_ -= samples_.front();
+    samples_.pop_front();
+  }
+
+  *avg = sum_ / static_cast<int>(num_samples);
+  return true;
+}
+
+void QualityScaler::MovingAverage::Reset() {
+  sum_ = 0;
+  samples_.clear();
+}
+
+void QualityScaler::ClearSamples() {
+  average_qp_.Reset();
+  framedrop_percent_.Reset();
+}
+
+void QualityScaler::AdjustScale(bool up) {
+  downscale_shift_ += up ? -1 : 1;
+  if (downscale_shift_ < 0)
+    downscale_shift_ = 0;
+  ClearSamples();
+}
+
+}  // namespace webrtc
diff --git a/webrtc/modules/video_coding/utility/quality_scaler.h b/webrtc/modules/video_coding/utility/quality_scaler.h
new file mode 100644
index 0000000..47d6cb1
--- /dev/null
+++ b/webrtc/modules/video_coding/utility/quality_scaler.h
@@ -0,0 +1,65 @@
+/*
+ *  Copyright (c) 2014 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 WEBRTC_MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
+#define WEBRTC_MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
+
+#include <list>
+
+#include "webrtc/common_video/libyuv/include/scaler.h"
+
+namespace webrtc {
+class QualityScaler {
+ public:
+  struct Resolution {
+    int width;
+    int height;
+  };
+
+  QualityScaler();
+  void Init(int max_qp);
+
+  void ReportFramerate(int framerate);
+  void ReportEncodedFrame(int qp);
+  void ReportDroppedFrame();
+
+  Resolution GetScaledResolution(const I420VideoFrame& frame);
+  const I420VideoFrame& GetScaledFrame(const I420VideoFrame& frame);
+
+ private:
+  class MovingAverage {
+   public:
+    MovingAverage();
+    void AddSample(int sample);
+    bool GetAverage(size_t num_samples, int* average);
+    void Reset();
+
+   private:
+    int sum_;
+    std::list<int> samples_;
+  };
+
+  void AdjustScale(bool up);
+  void ClearSamples();
+
+  Scaler scaler_;
+  I420VideoFrame scaled_frame_;
+
+  size_t num_samples_;
+  int low_qp_threshold_;
+  MovingAverage average_qp_;
+  MovingAverage framedrop_percent_;
+
+  int downscale_shift_;
+};
+
+}  // namespace webrtc
+
+#endif  // WEBRTC_MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
diff --git a/webrtc/modules/video_coding/utility/quality_scaler_unittest.cc b/webrtc/modules/video_coding/utility/quality_scaler_unittest.cc
new file mode 100644
index 0000000..381b959
--- /dev/null
+++ b/webrtc/modules/video_coding/utility/quality_scaler_unittest.cc
@@ -0,0 +1,198 @@
+/*
+ *  Copyright (c) 2014 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 "webrtc/modules/video_coding/utility/quality_scaler.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace webrtc {
+namespace {
+static const int kNumSeconds = 10;
+static const int kWidth = 1920;
+static const int kHalfWidth = kWidth / 2;
+static const int kHeight = 1080;
+static const int kFramerate = 30;
+static const int kLowQp = 15;
+static const int kNormalQp = 30;
+static const int kMaxQp = 56;
+}  // namespace
+
+class QualityScalerTest : public ::testing::Test {
+ protected:
+  enum ScaleDirection { kScaleDown, kScaleUp };
+
+  QualityScalerTest() {
+    input_frame_.CreateEmptyFrame(
+        kWidth, kHeight, kWidth, kHalfWidth, kHalfWidth);
+    qs_.Init(kMaxQp);
+    qs_.ReportFramerate(kFramerate);
+  }
+
+  void TriggerScale(ScaleDirection scale_direction) {
+    int initial_width = qs_.GetScaledResolution(input_frame_).width;
+    for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
+      switch (scale_direction) {
+        case kScaleUp:
+          qs_.ReportEncodedFrame(kLowQp);
+          break;
+        case kScaleDown:
+          qs_.ReportDroppedFrame();
+          break;
+      }
+
+      if (qs_.GetScaledResolution(input_frame_).width != initial_width)
+        return;
+    }
+
+    FAIL() << "No downscale within " << kNumSeconds << " seconds.";
+  }
+
+  void ExpectOriginalFrame() {
+    EXPECT_EQ(&input_frame_, &qs_.GetScaledFrame(input_frame_))
+        << "Using scaled frame instead of original input.";
+  }
+
+  void ExpectScaleUsingReportedResolution() {
+    QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
+    const I420VideoFrame& scaled_frame = qs_.GetScaledFrame(input_frame_);
+    EXPECT_EQ(res.width, scaled_frame.width());
+    EXPECT_EQ(res.height, scaled_frame.height());
+  }
+
+  void ContinuouslyDownscalesByHalfDimensionsAndBackUp();
+
+  void DoesNotDownscaleFrameDimensions(int width, int height);
+
+  QualityScaler qs_;
+  I420VideoFrame input_frame_;
+};
+
+TEST_F(QualityScalerTest, UsesOriginalFrameInitially) {
+  ExpectOriginalFrame();
+}
+
+TEST_F(QualityScalerTest, ReportsOriginalResolutionInitially) {
+  QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
+  EXPECT_EQ(input_frame_.width(), res.width);
+  EXPECT_EQ(input_frame_.height(), res.height);
+}
+
+TEST_F(QualityScalerTest, DownscalesAfterContinuousFramedrop) {
+  TriggerScale(kScaleDown);
+  QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
+  EXPECT_LT(res.width, input_frame_.width());
+  EXPECT_LT(res.height, input_frame_.height());
+}
+
+TEST_F(QualityScalerTest, DownscalesAfterTwoThirdsFramedrop) {
+  for (int i = 0; i < kFramerate * kNumSeconds / 3; ++i) {
+    qs_.ReportEncodedFrame(kNormalQp);
+    qs_.ReportDroppedFrame();
+    qs_.ReportDroppedFrame();
+    if (qs_.GetScaledResolution(input_frame_).width < input_frame_.width())
+      return;
+  }
+
+  FAIL() << "No downscale within " << kNumSeconds << " seconds.";
+}
+
+TEST_F(QualityScalerTest, DoesNotDownscaleOnNormalQp) {
+  for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
+    qs_.ReportEncodedFrame(kNormalQp);
+    ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
+        << "Unexpected scale on half framedrop.";
+  }
+}
+
+TEST_F(QualityScalerTest, DoesNotDownscaleAfterHalfFramedrop) {
+  for (int i = 0; i < kFramerate * kNumSeconds / 2; ++i) {
+    qs_.ReportEncodedFrame(kNormalQp);
+    ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
+        << "Unexpected scale on half framedrop.";
+
+    qs_.ReportDroppedFrame();
+    ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
+        << "Unexpected scale on half framedrop.";
+  }
+}
+
+void QualityScalerTest::ContinuouslyDownscalesByHalfDimensionsAndBackUp() {
+  const int initial_min_dimension = input_frame_.width() < input_frame_.height()
+                                  ? input_frame_.width()
+                                  : input_frame_.height();
+  int min_dimension = initial_min_dimension;
+  int current_shift = 0;
+  // Drop all frames to force-trigger downscaling.
+  while (min_dimension > 16) {
+    TriggerScale(kScaleDown);
+    QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
+    min_dimension = res.width < res.height ? res.width : res.height;
+    ++current_shift;
+    ASSERT_EQ(input_frame_.width() >> current_shift, res.width);
+    ASSERT_EQ(input_frame_.height() >> current_shift, res.height);
+    ExpectScaleUsingReportedResolution();
+  }
+
+  // Make sure we can scale back with good-quality frames.
+  while (min_dimension < initial_min_dimension) {
+    TriggerScale(kScaleUp);
+    QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
+    min_dimension = res.width < res.height ? res.width : res.height;
+    --current_shift;
+    ASSERT_EQ(input_frame_.width() >> current_shift, res.width);
+    ASSERT_EQ(input_frame_.height() >> current_shift, res.height);
+    ExpectScaleUsingReportedResolution();
+  }
+
+  // Verify we don't start upscaling after further low use.
+  for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
+    qs_.ReportEncodedFrame(kLowQp);
+    ExpectOriginalFrame();
+  }
+}
+
+TEST_F(QualityScalerTest, ContinuouslyDownscalesByHalfDimensionsAndBackUp) {
+  ContinuouslyDownscalesByHalfDimensionsAndBackUp();
+}
+
+TEST_F(QualityScalerTest,
+       ContinuouslyDownscalesOddResolutionsByHalfDimensionsAndBackUp) {
+  const int kOddWidth = 517;
+  const int kHalfOddWidth = (kOddWidth + 1) / 2;
+  const int kOddHeight = 1239;
+  input_frame_.CreateEmptyFrame(
+      kOddWidth, kOddHeight, kOddWidth, kHalfOddWidth, kHalfOddWidth);
+  ContinuouslyDownscalesByHalfDimensionsAndBackUp();
+}
+
+void QualityScalerTest::DoesNotDownscaleFrameDimensions(int width, int height) {
+  input_frame_.CreateEmptyFrame(
+      width, height, width, (width + 1) / 2, (width + 1) / 2);
+
+  for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
+    qs_.ReportDroppedFrame();
+    ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
+        << "Unexpected scale of minimal-size frame.";
+  }
+}
+
+TEST_F(QualityScalerTest, DoesNotDownscaleFrom1PxWidth) {
+  DoesNotDownscaleFrameDimensions(1, kHeight);
+}
+
+TEST_F(QualityScalerTest, DoesNotDownscaleFrom1PxHeight) {
+  DoesNotDownscaleFrameDimensions(kWidth, 1);
+}
+
+TEST_F(QualityScalerTest, DoesNotDownscaleFrom1Px) {
+  DoesNotDownscaleFrameDimensions(1, 1);
+}
+
+}  // namespace webrtc
diff --git a/webrtc/modules/video_coding/utility/video_coding_utility.gyp b/webrtc/modules/video_coding/utility/video_coding_utility.gyp
index 2f0202b..dc2b33f 100644
--- a/webrtc/modules/video_coding/utility/video_coding_utility.gyp
+++ b/webrtc/modules/video_coding/utility/video_coding_utility.gyp
@@ -18,8 +18,10 @@
         '<(webrtc_root)/system_wrappers/source/system_wrappers.gyp:system_wrappers',
       ],
       'sources': [
-        'include/frame_dropper.h',
         'frame_dropper.cc',
+        'include/frame_dropper.h',
+        'quality_scaler.cc',
+        'quality_scaler.h',
       ],
     },
   ], # targets