diff --git a/modules/video_coding/BUILD.gn b/modules/video_coding/BUILD.gn index 619f15e499..b2fd91d467 100644 --- a/modules/video_coding/BUILD.gn +++ b/modules/video_coding/BUILD.gn @@ -243,6 +243,7 @@ rtc_source_set("video_coding_utility") { "../../rtc_base:rtc_numerics", "../../rtc_base:rtc_task_queue", "../../rtc_base:sequenced_task_checker", + "../../rtc_base/experiments:quality_scaling_experiment", "../../system_wrappers", "../rtp_rtcp:rtp_rtcp_format", ] diff --git a/modules/video_coding/utility/quality_scaler.cc b/modules/video_coding/utility/quality_scaler.cc index e839f9e168..273d7a61ec 100644 --- a/modules/video_coding/utility/quality_scaler.cc +++ b/modules/video_coding/utility/quality_scaler.cc @@ -17,7 +17,9 @@ #include "rtc_base/checks.h" #include "rtc_base/logging.h" +#include "rtc_base/numerics/exp_filter.h" #include "rtc_base/task_queue.h" +#include "rtc_base/timeutils.h" // TODO(kthelgason): Some versions of Android have issues with log2. // See https://code.google.com/p/android/issues/detail?id=212634 for details @@ -37,6 +39,33 @@ static const int kMinFramesNeededToScale = 2 * 30; } // namespace +class QualityScaler::QpSmoother { + public: + explicit QpSmoother(float alpha) + : alpha_(alpha), last_sample_ms_(rtc::TimeMillis()), smoother_(alpha) {} + + rtc::Optional GetAvg() const { + float value = smoother_.filtered(); + if (value == rtc::ExpFilter::kValueUndefined) { + return rtc::nullopt; + } + return static_cast(value); + } + + void Add(float sample) { + int64_t now_ms = rtc::TimeMillis(); + smoother_.Apply(static_cast(now_ms - last_sample_ms_), sample); + last_sample_ms_ = now_ms; + } + + void Reset() { smoother_.Reset(alpha_); } + + private: + const float alpha_; + int64_t last_sample_ms_; + rtc::ExpFilter smoother_; +}; + class QualityScaler::CheckQpTask : public rtc::QueuedTask { public: explicit CheckQpTask(QualityScaler* scaler) : scaler_(scaler) { @@ -81,8 +110,16 @@ QualityScaler::QualityScaler(AdaptationObserverInterface* observer, fast_rampup_(true), // Arbitrarily choose size based on 30 fps for 5 seconds. average_qp_(5 * 30), - framedrop_percent_(5 * 30) { + framedrop_percent_media_opt_(5 * 30), + framedrop_percent_all_(5 * 30), + experiment_enabled_(QualityScalingExperiment::Enabled()), + observed_enough_frames_(false) { RTC_DCHECK_CALLED_SEQUENTIALLY(&task_checker_); + if (experiment_enabled_) { + config_ = QualityScalingExperiment::GetConfig(); + qp_smoother_high_.reset(new QpSmoother(config_.alpha_high)); + qp_smoother_low_.reset(new QpSmoother(config_.alpha_low)); + } RTC_DCHECK(observer_ != nullptr); check_qp_task_ = new CheckQpTask(this); RTC_LOG(LS_INFO) << "QP thresholds: low: " << thresholds_.low @@ -96,19 +133,36 @@ QualityScaler::~QualityScaler() { int64_t QualityScaler::GetSamplingPeriodMs() const { RTC_DCHECK_CALLED_SEQUENTIALLY(&task_checker_); - return fast_rampup_ ? sampling_period_ms_ - : (sampling_period_ms_ * kSamplePeriodScaleFactor); + if (fast_rampup_) { + return sampling_period_ms_; + } + if (experiment_enabled_ && !observed_enough_frames_) { + // Use half the interval while waiting for enough frames. + return sampling_period_ms_ / 2; + } + return sampling_period_ms_ * kSamplePeriodScaleFactor; } -void QualityScaler::ReportDroppedFrame() { +void QualityScaler::ReportDroppedFrameByMediaOpt() { RTC_DCHECK_CALLED_SEQUENTIALLY(&task_checker_); - framedrop_percent_.AddSample(100); + framedrop_percent_media_opt_.AddSample(100); + framedrop_percent_all_.AddSample(100); +} + +void QualityScaler::ReportDroppedFrameByEncoder() { + RTC_DCHECK_CALLED_SEQUENTIALLY(&task_checker_); + framedrop_percent_all_.AddSample(100); } void QualityScaler::ReportQp(int qp) { RTC_DCHECK_CALLED_SEQUENTIALLY(&task_checker_); - framedrop_percent_.AddSample(0); + framedrop_percent_media_opt_.AddSample(0); + framedrop_percent_all_.AddSample(0); average_qp_.AddSample(qp); + if (qp_smoother_high_) + qp_smoother_high_->Add(qp); + if (qp_smoother_low_) + qp_smoother_low_->Add(qp); } void QualityScaler::CheckQp() { @@ -118,11 +172,19 @@ void QualityScaler::CheckQp() { // If we have not observed at least this many frames we can't make a good // scaling decision. - if (framedrop_percent_.size() < kMinFramesNeededToScale) + const size_t frames = config_.use_all_drop_reasons + ? framedrop_percent_all_.size() + : framedrop_percent_media_opt_.size(); + if (frames < kMinFramesNeededToScale) { + observed_enough_frames_ = false; return; + } + observed_enough_frames_ = true; // Check if we should scale down due to high frame drop. - const rtc::Optional drop_rate = framedrop_percent_.GetAverage(); + const rtc::Optional drop_rate = + config_.use_all_drop_reasons ? framedrop_percent_all_.GetAverage() + : framedrop_percent_media_opt_.GetAverage(); if (drop_rate && *drop_rate >= kFramedropPercentThreshold) { RTC_LOG(LS_INFO) << "Reporting high QP, framedrop percent " << *drop_rate; ReportQpHigh(); @@ -130,14 +192,19 @@ void QualityScaler::CheckQp() { } // Check if we should scale up or down based on QP. - const rtc::Optional avg_qp = average_qp_.GetAverage(); - if (avg_qp) { - RTC_LOG(LS_INFO) << "Checking average QP " << *avg_qp; - if (*avg_qp > thresholds_.high) { + const rtc::Optional avg_qp_high = qp_smoother_high_ + ? qp_smoother_high_->GetAvg() + : average_qp_.GetAverage(); + const rtc::Optional avg_qp_low = + qp_smoother_low_ ? qp_smoother_low_->GetAvg() : average_qp_.GetAverage(); + if (avg_qp_high && avg_qp_low) { + RTC_LOG(LS_INFO) << "Checking average QP " << *avg_qp_high << " (" + << *avg_qp_low << ")."; + if (*avg_qp_high > thresholds_.high) { ReportQpHigh(); return; } - if (*avg_qp <= thresholds_.low) { + if (*avg_qp_low <= thresholds_.low) { // QP has been low. We want to try a higher resolution. ReportQpLow(); return; @@ -163,7 +230,12 @@ void QualityScaler::ReportQpHigh() { void QualityScaler::ClearSamples() { RTC_DCHECK_CALLED_SEQUENTIALLY(&task_checker_); - framedrop_percent_.Reset(); + framedrop_percent_media_opt_.Reset(); + framedrop_percent_all_.Reset(); average_qp_.Reset(); + if (qp_smoother_high_) + qp_smoother_high_->Reset(); + if (qp_smoother_low_) + qp_smoother_low_->Reset(); } } // namespace webrtc diff --git a/modules/video_coding/utility/quality_scaler.h b/modules/video_coding/utility/quality_scaler.h index e8234611c3..996b9f2528 100644 --- a/modules/video_coding/utility/quality_scaler.h +++ b/modules/video_coding/utility/quality_scaler.h @@ -11,12 +11,14 @@ #ifndef MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_ #define MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_ +#include #include #include "api/optional.h" #include "api/video_codecs/video_encoder.h" #include "common_types.h" // NOLINT(build/include) #include "modules/video_coding/utility/moving_average.h" +#include "rtc_base/experiments/quality_scaling_experiment.h" #include "rtc_base/sequenced_task_checker.h" namespace webrtc { @@ -49,8 +51,9 @@ class QualityScaler { QualityScaler(AdaptationObserverInterface* observer, VideoEncoder::QpThresholds thresholds); virtual ~QualityScaler(); - // Should be called each time the encoder drops a frame. - void ReportDroppedFrame(); + // Should be called each time a frame is dropped at encoding. + void ReportDroppedFrameByMediaOpt(); + void ReportDroppedFrameByEncoder(); // Inform the QualityScaler of the last seen QP. void ReportQp(int qp); @@ -62,6 +65,7 @@ class QualityScaler { private: class CheckQpTask; + class QpSmoother; void CheckQp(); void ClearSamples(); void ReportQpLow(); @@ -76,7 +80,15 @@ class QualityScaler { const int64_t sampling_period_ms_; bool fast_rampup_ RTC_GUARDED_BY(&task_checker_); MovingAverage average_qp_ RTC_GUARDED_BY(&task_checker_); - MovingAverage framedrop_percent_ RTC_GUARDED_BY(&task_checker_); + MovingAverage framedrop_percent_media_opt_ RTC_GUARDED_BY(&task_checker_); + MovingAverage framedrop_percent_all_ RTC_GUARDED_BY(&task_checker_); + + // Used by QualityScalingExperiment. + const bool experiment_enabled_; + QualityScalingExperiment::Config config_ RTC_GUARDED_BY(&task_checker_); + std::unique_ptr qp_smoother_high_ RTC_GUARDED_BY(&task_checker_); + std::unique_ptr qp_smoother_low_ RTC_GUARDED_BY(&task_checker_); + bool observed_enough_frames_ 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 824826d7c5..58a381c5ac 100644 --- a/modules/video_coding/utility/quality_scaler_unittest.cc +++ b/modules/video_coding/utility/quality_scaler_unittest.cc @@ -11,9 +11,11 @@ #include "modules/video_coding/utility/quality_scaler.h" #include +#include #include "rtc_base/event.h" #include "rtc_base/task_queue.h" +#include "test/field_trial.h" #include "test/gmock.h" #include "test/gtest.h" @@ -63,7 +65,8 @@ class QualityScalerUnderTest : public QualityScaler { : QualityScaler(observer, thresholds, 5) {} }; -class QualityScalerTest : public ::testing::Test { +class QualityScalerTest : public ::testing::Test, + public ::testing::WithParamInterface { protected: enum ScaleDirection { kKeepScaleAboveLowQp, @@ -74,7 +77,8 @@ class QualityScalerTest : public ::testing::Test { }; QualityScalerTest() - : q_(new rtc::TaskQueue("QualityScalerTestQueue")), + : scoped_field_trial_(GetParam()), + q_(new rtc::TaskQueue("QualityScalerTestQueue")), observer_(new MockAdaptationObserver()) { DO_SYNC(q_, { qs_ = std::unique_ptr(new QualityScalerUnderTest( @@ -96,7 +100,7 @@ class QualityScalerTest : public ::testing::Test { qs_->ReportQp(kLowQp); break; case kScaleDown: - qs_->ReportDroppedFrame(); + qs_->ReportDroppedFrameByMediaOpt(); break; case kKeepScaleAtHighQp: qs_->ReportQp(kHighQp); @@ -108,37 +112,45 @@ class QualityScalerTest : public ::testing::Test { } } + test::ScopedFieldTrials scoped_field_trial_; std::unique_ptr q_; std::unique_ptr qs_; std::unique_ptr observer_; }; -TEST_F(QualityScalerTest, DownscalesAfterContinuousFramedrop) { +INSTANTIATE_TEST_CASE_P( + FieldTrials, + QualityScalerTest, + ::testing::Values( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,7,8,0.9,0.99,1/", + "")); + +TEST_P(QualityScalerTest, DownscalesAfterContinuousFramedrop) { DO_SYNC(q_, { TriggerScale(kScaleDown); }); EXPECT_TRUE(observer_->event.Wait(kDefaultTimeoutMs)); EXPECT_EQ(1, observer_->adapt_down_events_); EXPECT_EQ(0, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, KeepsScaleAtHighQp) { +TEST_P(QualityScalerTest, KeepsScaleAtHighQp) { DO_SYNC(q_, { TriggerScale(kKeepScaleAtHighQp); }); EXPECT_FALSE(observer_->event.Wait(kDefaultTimeoutMs)); EXPECT_EQ(0, observer_->adapt_down_events_); EXPECT_EQ(0, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, DownscalesAboveHighQp) { +TEST_P(QualityScalerTest, DownscalesAboveHighQp) { DO_SYNC(q_, { TriggerScale(kScaleDownAboveHighQp); }); EXPECT_TRUE(observer_->event.Wait(kDefaultTimeoutMs)); EXPECT_EQ(1, observer_->adapt_down_events_); EXPECT_EQ(0, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, DownscalesAfterTwoThirdsFramedrop) { +TEST_P(QualityScalerTest, DownscalesAfterTwoThirdsFramedrop) { DO_SYNC(q_, { for (int i = 0; i < kFramerate * 5; ++i) { - qs_->ReportDroppedFrame(); - qs_->ReportDroppedFrame(); + qs_->ReportDroppedFrameByMediaOpt(); + qs_->ReportDroppedFrameByMediaOpt(); qs_->ReportQp(kHighQp); } }); @@ -147,10 +159,10 @@ TEST_F(QualityScalerTest, DownscalesAfterTwoThirdsFramedrop) { EXPECT_EQ(0, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, DoesNotDownscaleAfterHalfFramedrop) { +TEST_P(QualityScalerTest, DoesNotDownscaleAfterHalfFramedrop) { DO_SYNC(q_, { for (int i = 0; i < kFramerate * 5; ++i) { - qs_->ReportDroppedFrame(); + qs_->ReportDroppedFrameByMediaOpt(); qs_->ReportQp(kHighQp); } }); @@ -159,21 +171,35 @@ TEST_F(QualityScalerTest, DoesNotDownscaleAfterHalfFramedrop) { EXPECT_EQ(0, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, KeepsScaleOnNormalQp) { +TEST_P(QualityScalerTest, DownscalesAfterTwoThirdsIfFieldTrialEnabled) { + const bool kDownScaleExpected = !GetParam().empty(); + DO_SYNC(q_, { + for (int i = 0; i < kFramerate * 5; ++i) { + qs_->ReportDroppedFrameByMediaOpt(); + qs_->ReportDroppedFrameByEncoder(); + qs_->ReportQp(kHighQp); + } + }); + EXPECT_EQ(kDownScaleExpected, observer_->event.Wait(kDefaultTimeoutMs)); + EXPECT_EQ(kDownScaleExpected ? 1 : 0, observer_->adapt_down_events_); + EXPECT_EQ(0, observer_->adapt_up_events_); +} + +TEST_P(QualityScalerTest, KeepsScaleOnNormalQp) { DO_SYNC(q_, { TriggerScale(kKeepScaleAboveLowQp); }); EXPECT_FALSE(observer_->event.Wait(kDefaultTimeoutMs)); EXPECT_EQ(0, observer_->adapt_down_events_); EXPECT_EQ(0, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, UpscalesAfterLowQp) { +TEST_P(QualityScalerTest, UpscalesAfterLowQp) { DO_SYNC(q_, { TriggerScale(kScaleUp); }); EXPECT_TRUE(observer_->event.Wait(kDefaultTimeoutMs)); EXPECT_EQ(0, observer_->adapt_down_events_); EXPECT_EQ(1, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, ScalesDownAndBackUp) { +TEST_P(QualityScalerTest, ScalesDownAndBackUp) { DO_SYNC(q_, { TriggerScale(kScaleDown); }); EXPECT_TRUE(observer_->event.Wait(kDefaultTimeoutMs)); EXPECT_EQ(1, observer_->adapt_down_events_); @@ -184,7 +210,7 @@ TEST_F(QualityScalerTest, ScalesDownAndBackUp) { EXPECT_EQ(1, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, DoesNotScaleUntilEnoughFramesObserved) { +TEST_P(QualityScalerTest, DoesNotScaleUntilEnoughFramesObserved) { DO_SYNC(q_, { // Not enough frames to make a decision. for (int i = 0; i < kMinFramesNeededToScale - 1; ++i) { @@ -210,7 +236,7 @@ TEST_F(QualityScalerTest, DoesNotScaleUntilEnoughFramesObserved) { EXPECT_EQ(1, observer_->adapt_up_events_); } -TEST_F(QualityScalerTest, ScalesDownAndBackUpWithMinFramesNeeded) { +TEST_P(QualityScalerTest, ScalesDownAndBackUpWithMinFramesNeeded) { DO_SYNC(q_, { for (int i = 0; i < kMinFramesNeededToScale; ++i) { qs_->ReportQp(kHighQp + 1); diff --git a/rtc_base/experiments/BUILD.gn b/rtc_base/experiments/BUILD.gn index 0ee402be05..1ba8827b6b 100644 --- a/rtc_base/experiments/BUILD.gn +++ b/rtc_base/experiments/BUILD.gn @@ -32,15 +32,31 @@ rtc_static_library("congestion_controller_experiment") { ] } +rtc_static_library("quality_scaling_experiment") { + sources = [ + "quality_scaling_experiment.cc", + "quality_scaling_experiment.h", + ] + deps = [ + "../:rtc_base_approved", + "../..:webrtc_common", + "../../api:optional", + "../../api/video_codecs:video_codecs_api", + "../../system_wrappers:field_trial_api", + ] +} + if (rtc_include_tests) { rtc_source_set("experiments_unittests") { testonly = true sources = [ "congestion_controller_experiment_unittest.cc", + "quality_scaling_experiment_unittest.cc", ] deps = [ ":congestion_controller_experiment", + ":quality_scaling_experiment", "../:rtc_base_tests_main", "../:rtc_base_tests_utils", "../../test:field_trial", diff --git a/rtc_base/experiments/quality_scaling_experiment.cc b/rtc_base/experiments/quality_scaling_experiment.cc new file mode 100644 index 0000000000..8c999543c2 --- /dev/null +++ b/rtc_base/experiments/quality_scaling_experiment.cc @@ -0,0 +1,97 @@ +/* + * Copyright 2018 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 "rtc_base/experiments/quality_scaling_experiment.h" + +#include + +#include "rtc_base/logging.h" +#include "system_wrappers/include/field_trial.h" + +namespace webrtc { +namespace { +constexpr char kFieldTrial[] = "WebRTC-Video-QualityScaling"; +constexpr int kMinQp = 1; +constexpr int kMaxVp8Qp = 127; +constexpr int kMaxVp9Qp = 255; +constexpr int kMaxH264Qp = 51; +constexpr int kMaxGenericQp = 255; + +rtc::Optional GetThresholds(int low, + int high, + int max) { + if (low < kMinQp || high > max || high < low) + return rtc::nullopt; + + RTC_LOG(LS_INFO) << "QP thresholds: low: " << low << ", high: " << high; + return rtc::Optional( + VideoEncoder::QpThresholds(low, high)); +} +} // namespace + +bool QualityScalingExperiment::Enabled() { + return webrtc::field_trial::IsEnabled(kFieldTrial); +} + +rtc::Optional +QualityScalingExperiment::ParseSettings() { + const std::string group = webrtc::field_trial::FindFullName(kFieldTrial); + if (group.empty()) + return rtc::nullopt; + + Settings s; + if (sscanf(group.c_str(), "Enabled-%d,%d,%d,%d,%d,%d,%d,%d,%f,%f,%d", + &s.vp8_low, &s.vp8_high, &s.vp9_low, &s.vp9_high, &s.h264_low, + &s.h264_high, &s.generic_low, &s.generic_high, &s.alpha_high, + &s.alpha_low, &s.drop) != 11) { + RTC_LOG(LS_WARNING) << "Invalid number of parameters provided."; + return rtc::nullopt; + } + return s; +} + +rtc::Optional +QualityScalingExperiment::GetQpThresholds(VideoCodecType codec_type) { + const auto settings = ParseSettings(); + if (!settings) + return rtc::nullopt; + + switch (codec_type) { + case kVideoCodecVP8: + return GetThresholds(settings->vp8_low, settings->vp8_high, kMaxVp8Qp); + case kVideoCodecVP9: + return GetThresholds(settings->vp9_low, settings->vp9_high, kMaxVp9Qp); + case kVideoCodecH264: + return GetThresholds(settings->h264_low, settings->h264_high, kMaxH264Qp); + case kVideoCodecGeneric: + return GetThresholds(settings->generic_low, settings->generic_high, + kMaxGenericQp); + default: + return rtc::nullopt; + } +} + +QualityScalingExperiment::Config QualityScalingExperiment::GetConfig() { + const auto settings = ParseSettings(); + if (!settings) + return Config(); + + Config config; + config.use_all_drop_reasons = settings->drop > 0; + + if (settings->alpha_high < 0 || settings->alpha_low < settings->alpha_high) { + RTC_LOG(LS_WARNING) << "Invalid alpha value provided, using default."; + return config; + } + config.alpha_high = settings->alpha_high; + config.alpha_low = settings->alpha_low; + return config; +} + +} // namespace webrtc diff --git a/rtc_base/experiments/quality_scaling_experiment.h b/rtc_base/experiments/quality_scaling_experiment.h new file mode 100644 index 0000000000..6f24d6bfd7 --- /dev/null +++ b/rtc_base/experiments/quality_scaling_experiment.h @@ -0,0 +1,59 @@ +/* + * Copyright 2018 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 RTC_BASE_EXPERIMENTS_QUALITY_SCALING_EXPERIMENT_H_ +#define RTC_BASE_EXPERIMENTS_QUALITY_SCALING_EXPERIMENT_H_ + +#include "api/optional.h" +#include "api/video_codecs/video_encoder.h" +#include "common_types.h" // NOLINT(build/include) + +namespace webrtc { +class QualityScalingExperiment { + public: + struct Settings { + int vp8_low; // VP8: low QP threshold. + int vp8_high; // VP8: high QP threshold. + int vp9_low; // VP9: low QP threshold. + int vp9_high; // VP9: high QP threshold. + int h264_low; // H264: low QP threshold. + int h264_high; // H264: high QP threshold. + int generic_low; // Generic: low QP threshold. + int generic_high; // Generic: high QP threshold. + float alpha_high; // |alpha_| for ExpFilter used when checking high QP. + float alpha_low; // |alpha_| for ExpFilter used when checking low QP. + int drop; // >0 sets |use_all_drop_reasons| to true. + }; + + // Used by QualityScaler. + struct Config { + float alpha_high = 0.9995f; + float alpha_low = 0.9999f; + // If set, all type of dropped frames are used. + // Otherwise only dropped frames by MediaOptimization are used. + bool use_all_drop_reasons = false; + }; + + // Returns true if the experiment is enabled. + static bool Enabled(); + + // Returns settings from field trial. + static rtc::Optional ParseSettings(); + + // Returns QpThresholds for the |codec_type|. + static rtc::Optional GetQpThresholds( + VideoCodecType codec_type); + + // Returns parsed values. If the parsing fails, default values are returned. + static Config GetConfig(); +}; + +} // namespace webrtc + +#endif // RTC_BASE_EXPERIMENTS_QUALITY_SCALING_EXPERIMENT_H_ diff --git a/rtc_base/experiments/quality_scaling_experiment_unittest.cc b/rtc_base/experiments/quality_scaling_experiment_unittest.cc new file mode 100644 index 0000000000..15aadd6bb1 --- /dev/null +++ b/rtc_base/experiments/quality_scaling_experiment_unittest.cc @@ -0,0 +1,169 @@ +/* + * Copyright 2018 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 "rtc_base/experiments/quality_scaling_experiment.h" + +#include "rtc_base/gunit.h" +#include "test/field_trial.h" + +namespace webrtc { +namespace { +void ExpectEqualSettings(QualityScalingExperiment::Settings a, + QualityScalingExperiment::Settings b) { + EXPECT_EQ(a.vp8_low, b.vp8_low); + EXPECT_EQ(a.vp8_high, b.vp8_high); + EXPECT_EQ(a.vp9_low, b.vp9_low); + EXPECT_EQ(a.vp9_high, b.vp9_high); + EXPECT_EQ(a.h264_low, b.h264_low); + EXPECT_EQ(a.h264_high, b.h264_high); + EXPECT_EQ(a.generic_low, b.generic_low); + EXPECT_EQ(a.generic_high, b.generic_high); + EXPECT_EQ(a.alpha_high, b.alpha_high); + EXPECT_EQ(a.alpha_low, b.alpha_low); + EXPECT_EQ(a.drop, b.drop); +} + +void ExpectEqualConfig(QualityScalingExperiment::Config a, + QualityScalingExperiment::Config b) { + EXPECT_EQ(a.alpha_high, b.alpha_high); + EXPECT_EQ(a.alpha_low, b.alpha_low); + EXPECT_EQ(a.use_all_drop_reasons, b.use_all_drop_reasons); +} +} // namespace + +TEST(QualityScalingExperimentTest, DisabledWithoutFieldTrial) { + webrtc::test::ScopedFieldTrials field_trials(""); + EXPECT_FALSE(QualityScalingExperiment::Enabled()); +} + +TEST(QualityScalingExperimentTest, EnabledWithFieldTrial) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled/"); + EXPECT_TRUE(QualityScalingExperiment::Enabled()); +} + +TEST(QualityScalingExperimentTest, ParseSettings) { + const QualityScalingExperiment::Settings kExpected = {1, 2, 3, 4, 5, 6, + 7, 8, 0.9f, 0.99f, 1}; + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,7,8,0.9,0.99,1/"); + const auto settings = QualityScalingExperiment::ParseSettings(); + EXPECT_TRUE(settings); + ExpectEqualSettings(kExpected, *settings); +} + +TEST(QualityScalingExperimentTest, ParseSettingsFailsWithoutFieldTrial) { + webrtc::test::ScopedFieldTrials field_trials(""); + EXPECT_FALSE(QualityScalingExperiment::ParseSettings()); +} + +TEST(QualityScalingExperimentTest, ParseSettingsFailsWithInvalidFieldTrial) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-invalid/"); + EXPECT_FALSE(QualityScalingExperiment::ParseSettings()); +} + +TEST(QualityScalingExperimentTest, GetConfig) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,7,8,0.9,0.99,0/"); + const auto config = QualityScalingExperiment::GetConfig(); + EXPECT_EQ(0.9f, config.alpha_high); + EXPECT_EQ(0.99f, config.alpha_low); + EXPECT_FALSE(config.use_all_drop_reasons); +} + +TEST(QualityScalingExperimentTest, GetsDefaultConfigForInvalidFieldTrial) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-invalid/"); + const auto config = QualityScalingExperiment::GetConfig(); + ExpectEqualConfig(config, QualityScalingExperiment::Config()); +} + +TEST(QualityScalingExperimentTest, GetsDefaultAlphaForInvalidValue) { + QualityScalingExperiment::Config expected_config; + expected_config.use_all_drop_reasons = true; + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,7,8,0.99,0.9,1/"); + const auto config = QualityScalingExperiment::GetConfig(); + ExpectEqualConfig(config, expected_config); +} + +TEST(QualityScalingExperimentTest, GetVp8Thresholds) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,0,0,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecVP8); + EXPECT_TRUE(thresholds); + EXPECT_EQ(1, thresholds->low); + EXPECT_EQ(2, thresholds->high); +} + +TEST(QualityScalingExperimentTest, GetThresholdsFailsForInvalidVp8Value) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-0,0,3,4,5,6,7,8,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecVP8); + EXPECT_FALSE(thresholds); +} + +TEST(QualityScalingExperimentTest, GetVp9Thresholds) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,0,0,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecVP9); + EXPECT_TRUE(thresholds); + EXPECT_EQ(3, thresholds->low); + EXPECT_EQ(4, thresholds->high); +} + +TEST(QualityScalingExperimentTest, GetThresholdsFailsForInvalidVp9Value) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,0,0,5,6,7,8,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecVP9); + EXPECT_FALSE(thresholds); +} + +TEST(QualityScalingExperimentTest, GetH264Thresholds) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,0,0,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecH264); + EXPECT_TRUE(thresholds); + EXPECT_EQ(5, thresholds->low); + EXPECT_EQ(6, thresholds->high); +} + +TEST(QualityScalingExperimentTest, GetThresholdsFailsForInvalidH264Value) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,0,0,7,8,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecH264); + EXPECT_FALSE(thresholds); +} + +TEST(QualityScalingExperimentTest, GetGenericThresholds) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,0,0,7,8,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecGeneric); + EXPECT_TRUE(thresholds); + EXPECT_EQ(7, thresholds->low); + EXPECT_EQ(8, thresholds->high); +} + +TEST(QualityScalingExperimentTest, GetThresholdsFailsForInvalidGenericValue) { + webrtc::test::ScopedFieldTrials field_trials( + "WebRTC-Video-QualityScaling/Enabled-1,2,3,4,5,6,0,0,0.9,0.99,1/"); + const auto thresholds = + QualityScalingExperiment::GetQpThresholds(kVideoCodecGeneric); + EXPECT_FALSE(thresholds); +} +} // namespace webrtc diff --git a/video/BUILD.gn b/video/BUILD.gn index a31b366f2e..1030439bc4 100644 --- a/video/BUILD.gn +++ b/video/BUILD.gn @@ -75,6 +75,7 @@ rtc_static_library("video") { "../rtc_base:checks", "../rtc_base:stringutils", "../rtc_base/experiments:alr_experiment", + "../rtc_base/experiments:quality_scaling_experiment", "../rtc_base/system:fallthrough", "../system_wrappers:field_trial_api", "../system_wrappers:metrics_api", diff --git a/video/video_stream_encoder.cc b/video/video_stream_encoder.cc index 9b5dc1600f..42d1c7e417 100644 --- a/video/video_stream_encoder.cc +++ b/video/video_stream_encoder.cc @@ -23,6 +23,7 @@ #include "modules/video_coding/include/video_coding_defines.h" #include "rtc_base/arraysize.h" #include "rtc_base/checks.h" +#include "rtc_base/experiments/quality_scaling_experiment.h" #include "rtc_base/location.h" #include "rtc_base/logging.h" #include "rtc_base/system/fallthrough.h" @@ -330,6 +331,7 @@ VideoStreamEncoder::VideoStreamEncoder( : shutdown_event_(true /* manual_reset */, false), number_of_cores_(number_of_cores), initial_rampup_(0), + quality_scaling_experiment_enabled_(QualityScalingExperiment::Enabled()), source_proxy_(new VideoSourceProxy(this)), sink_(nullptr), settings_(settings), @@ -606,10 +608,17 @@ void VideoStreamEncoder::ConfigureQualityScaler() { // Quality scaler has not already been configured. // Drop frames and scale down until desired quality is achieved. + // Use experimental thresholds if available. + rtc::Optional experimental_thresholds; + if (quality_scaling_experiment_enabled_) { + experimental_thresholds = QualityScalingExperiment::GetQpThresholds( + encoder_config_.codec_type); + } // Since the interface is non-public, MakeUnique can't do this upcast. AdaptationObserverInterface* observer = this; quality_scaler_ = rtc::MakeUnique( - observer, *(scaling_settings.thresholds)); + observer, experimental_thresholds ? *experimental_thresholds + : *(scaling_settings.thresholds)); } } else { quality_scaler_.reset(nullptr); @@ -904,11 +913,16 @@ void VideoStreamEncoder::OnDroppedFrame(DropReason reason) { encoder_queue_.PostTask([this] { RTC_DCHECK_RUN_ON(&encoder_queue_); if (quality_scaler_) - quality_scaler_->ReportDroppedFrame(); + quality_scaler_->ReportDroppedFrameByMediaOpt(); }); break; case DropReason::kDroppedByEncoder: stats_proxy_->OnFrameDroppedByEncoder(); + encoder_queue_.PostTask([this] { + RTC_DCHECK_RUN_ON(&encoder_queue_); + if (quality_scaler_) + quality_scaler_->ReportDroppedFrameByEncoder(); + }); break; } } diff --git a/video/video_stream_encoder.h b/video/video_stream_encoder.h index 59974dc4de..e2c7d5da44 100644 --- a/video/video_stream_encoder.h +++ b/video/video_stream_encoder.h @@ -235,6 +235,8 @@ class VideoStreamEncoder : public VideoStreamEncoderInterface, // Counts how many frames we've dropped in the initial rampup phase. int initial_rampup_; + const bool quality_scaling_experiment_enabled_; + const std::unique_ptr source_proxy_; EncoderSink* sink_; const VideoSendStream::Config::EncoderSettings settings_;