diff options
Diffstat (limited to 'webrtc/modules/video_coding/utility/quality_scaler_unittest.cc')
-rw-r--r-- | webrtc/modules/video_coding/utility/quality_scaler_unittest.cc | 419 |
1 files changed, 419 insertions, 0 deletions
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 0000000000..2ce1107472 --- /dev/null +++ b/webrtc/modules/video_coding/utility/quality_scaler_unittest.cc @@ -0,0 +1,419 @@ +/* + * 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/include/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 kHighQp = 40; +static const int kMaxQp = 56; +} // namespace + +class QualityScalerTest : public ::testing::Test { + public: + // Temporal and spatial resolution. + struct Resolution { + int framerate; + int width; + int height; + }; + protected: + enum ScaleDirection { + kKeepScaleAtHighQp, + kScaleDown, + kScaleDownAboveHighQp, + kScaleUp + }; + enum BadQualityMetric { kDropFrame, kReportLowQP }; + + QualityScalerTest() { + input_frame_.CreateEmptyFrame( + kWidth, kHeight, kWidth, kHalfWidth, kHalfWidth); + qs_.Init(kMaxQp / QualityScaler::kDefaultLowQpDenominator, kHighQp, false); + qs_.ReportFramerate(kFramerate); + qs_.OnEncodeFrame(input_frame_); + } + + bool TriggerScale(ScaleDirection scale_direction) { + qs_.OnEncodeFrame(input_frame_); + int initial_width = qs_.GetScaledResolution().width; + for (int i = 0; i < kFramerate * kNumSeconds; ++i) { + switch (scale_direction) { + case kScaleUp: + qs_.ReportQP(kLowQp); + break; + case kScaleDown: + qs_.ReportDroppedFrame(); + break; + case kKeepScaleAtHighQp: + qs_.ReportQP(kHighQp); + break; + case kScaleDownAboveHighQp: + qs_.ReportQP(kHighQp + 1); + break; + } + qs_.OnEncodeFrame(input_frame_); + if (qs_.GetScaledResolution().width != initial_width) + return true; + } + + return false; + } + + void ExpectOriginalFrame() { + EXPECT_EQ(&input_frame_, &qs_.GetScaledFrame(input_frame_)) + << "Using scaled frame instead of original input."; + } + + void ExpectScaleUsingReportedResolution() { + qs_.OnEncodeFrame(input_frame_); + QualityScaler::Resolution res = qs_.GetScaledResolution(); + const VideoFrame& 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); + + Resolution TriggerResolutionChange(BadQualityMetric dropframe_lowqp, + int num_second, + int initial_framerate); + + void VerifyQualityAdaptation(int initial_framerate, int seconds, + bool expect_spatial_resize, + bool expect_framerate_reduction); + + void DownscaleEndsAt(int input_width, + int input_height, + int end_width, + int end_height); + + QualityScaler qs_; + VideoFrame input_frame_; +}; + +TEST_F(QualityScalerTest, UsesOriginalFrameInitially) { + ExpectOriginalFrame(); +} + +TEST_F(QualityScalerTest, ReportsOriginalResolutionInitially) { + qs_.OnEncodeFrame(input_frame_); + QualityScaler::Resolution res = qs_.GetScaledResolution(); + EXPECT_EQ(input_frame_.width(), res.width); + EXPECT_EQ(input_frame_.height(), res.height); +} + +TEST_F(QualityScalerTest, DownscalesAfterContinuousFramedrop) { + EXPECT_TRUE(TriggerScale(kScaleDown)) << "No downscale within " << kNumSeconds + << " seconds."; + QualityScaler::Resolution res = qs_.GetScaledResolution(); + EXPECT_LT(res.width, input_frame_.width()); + EXPECT_LT(res.height, input_frame_.height()); +} + +TEST_F(QualityScalerTest, KeepsScaleAtHighQp) { + EXPECT_FALSE(TriggerScale(kKeepScaleAtHighQp)) + << "Downscale at high threshold which should keep scale."; + QualityScaler::Resolution res = qs_.GetScaledResolution(); + EXPECT_EQ(res.width, input_frame_.width()); + EXPECT_EQ(res.height, input_frame_.height()); +} + +TEST_F(QualityScalerTest, DownscalesAboveHighQp) { + EXPECT_TRUE(TriggerScale(kScaleDownAboveHighQp)) + << "No downscale within " << kNumSeconds << " seconds."; + QualityScaler::Resolution res = qs_.GetScaledResolution(); + 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_.ReportQP(kNormalQp); + qs_.ReportDroppedFrame(); + qs_.ReportDroppedFrame(); + qs_.OnEncodeFrame(input_frame_); + if (qs_.GetScaledResolution().width < input_frame_.width()) + return; + } + + FAIL() << "No downscale within " << kNumSeconds << " seconds."; +} + +TEST_F(QualityScalerTest, DoesNotDownscaleOnNormalQp) { + for (int i = 0; i < kFramerate * kNumSeconds; ++i) { + qs_.ReportQP(kNormalQp); + qs_.OnEncodeFrame(input_frame_); + ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution().width) + << "Unexpected scale on half framedrop."; + } +} + +TEST_F(QualityScalerTest, DoesNotDownscaleAfterHalfFramedrop) { + for (int i = 0; i < kFramerate * kNumSeconds / 2; ++i) { + qs_.ReportQP(kNormalQp); + qs_.OnEncodeFrame(input_frame_); + ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution().width) + << "Unexpected scale on half framedrop."; + + qs_.ReportDroppedFrame(); + qs_.OnEncodeFrame(input_frame_); + ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution().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 >= 2 * QualityScaler::kDefaultMinDownscaleDimension) { + EXPECT_TRUE(TriggerScale(kScaleDown)) << "No downscale within " + << kNumSeconds << " seconds."; + qs_.OnEncodeFrame(input_frame_); + QualityScaler::Resolution res = qs_.GetScaledResolution(); + 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) { + EXPECT_TRUE(TriggerScale(kScaleUp)) << "No upscale within " << kNumSeconds + << " seconds."; + qs_.OnEncodeFrame(input_frame_); + QualityScaler::Resolution res = qs_.GetScaledResolution(); + 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_.ReportQP(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(); + qs_.OnEncodeFrame(input_frame_); + ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution().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); +} + +QualityScalerTest::Resolution QualityScalerTest::TriggerResolutionChange( + BadQualityMetric dropframe_lowqp, int num_second, int initial_framerate) { + QualityScalerTest::Resolution res; + res.framerate = initial_framerate; + qs_.OnEncodeFrame(input_frame_); + res.width = qs_.GetScaledResolution().width; + res.height = qs_.GetScaledResolution().height; + for (int i = 0; i < kFramerate * num_second; ++i) { + switch (dropframe_lowqp) { + case kReportLowQP: + qs_.ReportQP(kLowQp); + break; + case kDropFrame: + qs_.ReportDroppedFrame(); + break; + } + qs_.OnEncodeFrame(input_frame_); + // Simulate the case when SetRates is called right after reducing + // framerate. + qs_.ReportFramerate(initial_framerate); + res.framerate = qs_.GetTargetFramerate(); + if (res.framerate != -1) + qs_.ReportFramerate(res.framerate); + res.width = qs_.GetScaledResolution().width; + res.height = qs_.GetScaledResolution().height; + } + return res; +} + +void QualityScalerTest::VerifyQualityAdaptation( + int initial_framerate, int seconds, bool expect_spatial_resize, + bool expect_framerate_reduction) { + const int kDisabledBadQpThreshold = kMaxQp + 1; + qs_.Init(kMaxQp / QualityScaler::kDefaultLowQpDenominator, + kDisabledBadQpThreshold, true); + qs_.OnEncodeFrame(input_frame_); + int init_width = qs_.GetScaledResolution().width; + int init_height = qs_.GetScaledResolution().height; + + // Test reducing framerate by dropping frame continuously. + QualityScalerTest::Resolution res = TriggerResolutionChange( + kDropFrame, seconds, initial_framerate); + + if (expect_framerate_reduction) { + EXPECT_LT(res.framerate, initial_framerate); + } else { + // No framerate reduction, video decimator should be disabled. + EXPECT_EQ(-1, res.framerate); + } + + if (expect_spatial_resize) { + EXPECT_LT(res.width, init_width); + EXPECT_LT(res.height, init_height); + } else { + EXPECT_EQ(init_width, res.width); + EXPECT_EQ(init_height, res.height); + } + + // The "seconds * 1.5" is to ensure spatial resolution to recover. + // For example, in 10 seconds test, framerate reduction happens in the first + // 5 seconds from 30fps to 15fps and causes the buffer size to be half of the + // original one. Then it will take only 75 samples to downscale (twice in 150 + // samples). So to recover the resolution changes, we need more than 10 + // seconds (i.e, seconds * 1.5). This is because the framerate increases + // before spatial size recovers, so it will take 150 samples to recover + // spatial size (300 for twice). + res = TriggerResolutionChange(kReportLowQP, seconds * 1.5, initial_framerate); + EXPECT_EQ(-1, res.framerate); + EXPECT_EQ(init_width, res.width); + EXPECT_EQ(init_height, res.height); +} + +// In 5 seconds test, only framerate adjusting should happen. +TEST_F(QualityScalerTest, ChangeFramerateOnly) { + VerifyQualityAdaptation(kFramerate, 5, false, true); +} + +// In 10 seconds test, framerate adjusting and scaling are both +// triggered, it shows that scaling would happen after framerate +// adjusting. +TEST_F(QualityScalerTest, ChangeFramerateAndSpatialSize) { + VerifyQualityAdaptation(kFramerate, 10, true, true); +} + +// When starting from a low framerate, only spatial size will be changed. +TEST_F(QualityScalerTest, ChangeSpatialSizeOnly) { + qs_.ReportFramerate(kFramerate >> 1); + VerifyQualityAdaptation(kFramerate >> 1, 10, true, false); +} + +TEST_F(QualityScalerTest, DoesNotDownscaleBelow2xDefaultMinDimensionsWidth) { + DoesNotDownscaleFrameDimensions( + 2 * QualityScaler::kDefaultMinDownscaleDimension - 1, 1000); +} + +TEST_F(QualityScalerTest, DoesNotDownscaleBelow2xDefaultMinDimensionsHeight) { + DoesNotDownscaleFrameDimensions( + 1000, 2 * QualityScaler::kDefaultMinDownscaleDimension - 1); +} + +void QualityScalerTest::DownscaleEndsAt(int input_width, + int input_height, + int end_width, + int end_height) { + // Create a frame with 2x expected end width/height to verify that we can + // scale down to expected end width/height. + input_frame_.CreateEmptyFrame(input_width, input_height, input_width, + (input_width + 1) / 2, (input_width + 1) / 2); + + int last_width = input_width; + int last_height = input_height; + // Drop all frames to force-trigger downscaling. + while (true) { + TriggerScale(kScaleDown); + QualityScaler::Resolution res = qs_.GetScaledResolution(); + if (last_width == res.width) { + EXPECT_EQ(last_height, res.height); + EXPECT_EQ(end_width, res.width); + EXPECT_EQ(end_height, res.height); + break; + } + last_width = res.width; + last_height = res.height; + } +} + +TEST_F(QualityScalerTest, DefaultDownscalesTo160x90) { + DownscaleEndsAt(320, 180, 160, 90); +} + +TEST_F(QualityScalerTest, DefaultDownscalesTo90x160) { + DownscaleEndsAt(180, 320, 90, 160); +} + +TEST_F(QualityScalerTest, DefaultDownscalesFrom1280x720To160x90) { + DownscaleEndsAt(1280, 720, 160, 90); +} + +TEST_F(QualityScalerTest, DefaultDoesntDownscaleBelow160x90) { + DownscaleEndsAt(320 - 1, 180 - 1, 320 - 1, 180 - 1); +} + +TEST_F(QualityScalerTest, DefaultDoesntDownscaleBelow90x160) { + DownscaleEndsAt(180 - 1, 320 - 1, 180 - 1, 320 - 1); +} + +TEST_F(QualityScalerTest, RespectsMinResolutionWidth) { + // Should end at 200x100, as width can't go lower. + qs_.SetMinResolution(200, 10); + DownscaleEndsAt(1600, 800, 200, 100); +} + +TEST_F(QualityScalerTest, RespectsMinResolutionHeight) { + // Should end at 100x200, as height can't go lower. + qs_.SetMinResolution(10, 200); + DownscaleEndsAt(800, 1600, 100, 200); +} + +} // namespace webrtc |