mirror of
https://github.com/mollyim/webrtc.git
synced 2025-05-13 05:40:42 +01:00

Bug: webrtc:13485 Change-Id: I352b15a65867f0d56fc8e9a9e03081bd3258108e Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/316283 Reviewed-by: Sergey Silkin <ssilkin@webrtc.org> Reviewed-by: Harald Alvestrand <hta@webrtc.org> Commit-Queue: Sergey Silkin <ssilkin@webrtc.org> Cr-Commit-Position: refs/heads/main@{#40773}
284 lines
12 KiB
C++
284 lines
12 KiB
C++
/*
|
|
* Copyright (c) 2019 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 "video/encoder_overshoot_detector.h"
|
|
|
|
#include <string>
|
|
|
|
#include "api/units/data_rate.h"
|
|
#include "rtc_base/fake_clock.h"
|
|
#include "rtc_base/time_utils.h"
|
|
#include "system_wrappers/include/metrics.h"
|
|
#include "test/gtest.h"
|
|
|
|
namespace webrtc {
|
|
|
|
namespace {
|
|
|
|
using ::testing::TestWithParam;
|
|
using ::testing::ValuesIn;
|
|
|
|
static std::string CodecTypeToHistogramSuffix(VideoCodecType codec) {
|
|
switch (codec) {
|
|
case kVideoCodecVP8:
|
|
return "Vp8";
|
|
case kVideoCodecVP9:
|
|
return "Vp9";
|
|
case kVideoCodecAV1:
|
|
return "Av1";
|
|
case kVideoCodecH264:
|
|
return "H264";
|
|
case kVideoCodecH265:
|
|
return "H265";
|
|
case kVideoCodecGeneric:
|
|
return "Generic";
|
|
case kVideoCodecMultiplex:
|
|
return "Multiplex";
|
|
}
|
|
}
|
|
|
|
struct TestParams {
|
|
VideoCodecType codec_type;
|
|
bool is_screenshare;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
class EncoderOvershootDetectorTest : public TestWithParam<TestParams> {
|
|
public:
|
|
static constexpr int kDefaultBitrateBps = 300000;
|
|
static constexpr double kDefaultFrameRateFps = 15;
|
|
EncoderOvershootDetectorTest()
|
|
: detector_(kWindowSizeMs,
|
|
GetParam().codec_type,
|
|
GetParam().is_screenshare),
|
|
target_bitrate_(DataRate::BitsPerSec(kDefaultBitrateBps)),
|
|
target_framerate_fps_(kDefaultFrameRateFps) {}
|
|
|
|
protected:
|
|
void SetUp() override { metrics::Reset(); }
|
|
void RunConstantUtilizationTest(double actual_utilization_factor,
|
|
double expected_utilization_factor,
|
|
double allowed_error,
|
|
int64_t test_duration_ms) {
|
|
const int frame_size_bytes =
|
|
static_cast<int>(actual_utilization_factor *
|
|
(target_bitrate_.bps() / target_framerate_fps_) / 8);
|
|
detector_.SetTargetRate(target_bitrate_, target_framerate_fps_,
|
|
rtc::TimeMillis());
|
|
|
|
if (rtc::TimeMillis() == 0) {
|
|
// Encode a first frame which by definition has no overuse factor.
|
|
detector_.OnEncodedFrame(frame_size_bytes, rtc::TimeMillis());
|
|
clock_.AdvanceTime(TimeDelta::Seconds(1) / target_framerate_fps_);
|
|
}
|
|
|
|
int64_t runtime_us = 0;
|
|
while (runtime_us < test_duration_ms * 1000) {
|
|
detector_.OnEncodedFrame(frame_size_bytes, rtc::TimeMillis());
|
|
runtime_us += rtc::kNumMicrosecsPerSec / target_framerate_fps_;
|
|
clock_.AdvanceTime(TimeDelta::Seconds(1) / target_framerate_fps_);
|
|
}
|
|
|
|
// At constant utilization, both network and media utilization should be
|
|
// close to expected.
|
|
const absl::optional<double> network_utilization_factor =
|
|
detector_.GetNetworkRateUtilizationFactor(rtc::TimeMillis());
|
|
EXPECT_NEAR(network_utilization_factor.value_or(-1),
|
|
expected_utilization_factor, allowed_error);
|
|
|
|
const absl::optional<double> media_utilization_factor =
|
|
detector_.GetMediaRateUtilizationFactor(rtc::TimeMillis());
|
|
EXPECT_NEAR(media_utilization_factor.value_or(-1),
|
|
expected_utilization_factor, allowed_error);
|
|
}
|
|
|
|
static constexpr int64_t kWindowSizeMs = 3000;
|
|
EncoderOvershootDetector detector_;
|
|
rtc::ScopedFakeClock clock_;
|
|
DataRate target_bitrate_;
|
|
double target_framerate_fps_;
|
|
};
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, NoUtilizationIfNoRate) {
|
|
const int frame_size_bytes = 1000;
|
|
const int64_t time_interval_ms = 33;
|
|
detector_.SetTargetRate(target_bitrate_, target_framerate_fps_,
|
|
rtc::TimeMillis());
|
|
|
|
// No data points, can't determine overshoot rate.
|
|
EXPECT_FALSE(
|
|
detector_.GetNetworkRateUtilizationFactor(rtc::TimeMillis()).has_value());
|
|
|
|
detector_.OnEncodedFrame(frame_size_bytes, rtc::TimeMillis());
|
|
clock_.AdvanceTime(TimeDelta::Millis(time_interval_ms));
|
|
EXPECT_TRUE(
|
|
detector_.GetNetworkRateUtilizationFactor(rtc::TimeMillis()).has_value());
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, OptimalSize) {
|
|
// Optimally behaved encoder.
|
|
// Allow some error margin due to rounding errors, eg due to frame
|
|
// interval not being an integer.
|
|
RunConstantUtilizationTest(1.0, 1.0, 0.01, kWindowSizeMs);
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, Undershoot) {
|
|
// Undershoot, reported utilization factor should be capped to 1.0 so
|
|
// that we don't incorrectly boost encoder bitrate during movement.
|
|
RunConstantUtilizationTest(0.5, 1.0, 0.00, kWindowSizeMs);
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, Overshoot) {
|
|
// Overshoot by 20%.
|
|
// Allow some error margin due to rounding errors.
|
|
RunConstantUtilizationTest(1.2, 1.2, 0.01, kWindowSizeMs);
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, ConstantOvershootVaryingRates) {
|
|
// Overshoot by 20%, but vary framerate and bitrate.
|
|
// Allow some error margin due to rounding errors.
|
|
RunConstantUtilizationTest(1.2, 1.2, 0.01, kWindowSizeMs);
|
|
target_framerate_fps_ /= 2;
|
|
RunConstantUtilizationTest(1.2, 1.2, 0.01, kWindowSizeMs / 2);
|
|
target_bitrate_ = DataRate::BitsPerSec(target_bitrate_.bps() / 2);
|
|
RunConstantUtilizationTest(1.2, 1.2, 0.01, kWindowSizeMs / 2);
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, ConstantRateVaryingOvershoot) {
|
|
// Overshoot by 10%, keep framerate and bitrate constant.
|
|
// Allow some error margin due to rounding errors.
|
|
RunConstantUtilizationTest(1.1, 1.1, 0.01, kWindowSizeMs);
|
|
// Change overshoot to 20%, run for half window and expect overshoot
|
|
// to be 15%.
|
|
RunConstantUtilizationTest(1.2, 1.15, 0.01, kWindowSizeMs / 2);
|
|
// Keep running at 20% overshoot, after window is full that should now
|
|
// be the reported overshoot.
|
|
RunConstantUtilizationTest(1.2, 1.2, 0.01, kWindowSizeMs / 2);
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, PartialOvershoot) {
|
|
const int ideal_frame_size_bytes =
|
|
(target_bitrate_.bps() / target_framerate_fps_) / 8;
|
|
detector_.SetTargetRate(target_bitrate_, target_framerate_fps_,
|
|
rtc::TimeMillis());
|
|
|
|
// Test scenario with average bitrate matching the target bitrate, but
|
|
// with some utilization factor penalty as the frames can't be paced out
|
|
// on the network at the target rate.
|
|
// Insert a series of four frames:
|
|
// 1) 20% overshoot, not penalized as buffer if empty.
|
|
// 2) 20% overshoot, the 20% overshoot from the first frame is penalized.
|
|
// 3) 20% undershoot, negating the overshoot from the last frame.
|
|
// 4) 20% undershoot, no penalty.
|
|
// On average then utilization penalty is thus 5%.
|
|
|
|
int64_t runtime_us = 0;
|
|
int i = 0;
|
|
while (runtime_us < kWindowSizeMs * rtc::kNumMicrosecsPerMillisec) {
|
|
runtime_us += rtc::kNumMicrosecsPerSec / target_framerate_fps_;
|
|
clock_.AdvanceTime(TimeDelta::Seconds(1) / target_framerate_fps_);
|
|
int frame_size_bytes = (i++ % 4 < 2) ? (ideal_frame_size_bytes * 120) / 100
|
|
: (ideal_frame_size_bytes * 80) / 100;
|
|
detector_.OnEncodedFrame(frame_size_bytes, rtc::TimeMillis());
|
|
}
|
|
|
|
// Expect 5% overshoot for network rate, see above.
|
|
const absl::optional<double> network_utilization_factor =
|
|
detector_.GetNetworkRateUtilizationFactor(rtc::TimeMillis());
|
|
EXPECT_NEAR(network_utilization_factor.value_or(-1), 1.05, 0.01);
|
|
|
|
// Expect media rate to be on average correct.
|
|
const absl::optional<double> media_utilization_factor =
|
|
detector_.GetMediaRateUtilizationFactor(rtc::TimeMillis());
|
|
EXPECT_NEAR(media_utilization_factor.value_or(-1), 1.00, 0.01);
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest, RecordsZeroErrorMetricWithNoOvershoot) {
|
|
DataSize ideal_frame_size =
|
|
target_bitrate_ / Frequency::Hertz(target_framerate_fps_);
|
|
detector_.SetTargetRate(target_bitrate_, target_framerate_fps_,
|
|
rtc::TimeMillis());
|
|
detector_.OnEncodedFrame(ideal_frame_size.bytes(), rtc::TimeMillis());
|
|
detector_.Reset();
|
|
|
|
const VideoCodecType codec = GetParam().codec_type;
|
|
const bool is_screenshare = GetParam().is_screenshare;
|
|
const std::string rmse_histogram_prefix =
|
|
is_screenshare ? "WebRTC.Video.Screenshare.RMSEOfEncodingBitrateInKbps."
|
|
: "WebRTC.Video.RMSEOfEncodingBitrateInKbps.";
|
|
const std::string overshoot_histogram_prefix =
|
|
is_screenshare ? "WebRTC.Video.Screenshare.EncodingBitrateOvershoot."
|
|
: "WebRTC.Video.EncodingBitrateOvershoot.";
|
|
// RMSE and overshoot percent = 0, since we used ideal frame size.
|
|
EXPECT_METRIC_EQ(1, metrics::NumSamples(rmse_histogram_prefix +
|
|
CodecTypeToHistogramSuffix(codec)));
|
|
EXPECT_METRIC_EQ(
|
|
1, metrics::NumEvents(
|
|
rmse_histogram_prefix + CodecTypeToHistogramSuffix(codec), 0));
|
|
|
|
EXPECT_METRIC_EQ(1, metrics::NumSamples(overshoot_histogram_prefix +
|
|
CodecTypeToHistogramSuffix(codec)));
|
|
EXPECT_METRIC_EQ(1, metrics::NumEvents(overshoot_histogram_prefix +
|
|
CodecTypeToHistogramSuffix(codec),
|
|
0));
|
|
}
|
|
|
|
TEST_P(EncoderOvershootDetectorTest,
|
|
RecordScreenshareZeroMetricWithNoOvershoot) {
|
|
DataSize ideal_frame_size =
|
|
target_bitrate_ / Frequency::Hertz(target_framerate_fps_);
|
|
// Use target frame size with 50% overshoot.
|
|
DataSize target_frame_size = ideal_frame_size * 3 / 2;
|
|
detector_.SetTargetRate(target_bitrate_, target_framerate_fps_,
|
|
rtc::TimeMillis());
|
|
detector_.OnEncodedFrame(target_frame_size.bytes(), rtc::TimeMillis());
|
|
detector_.Reset();
|
|
|
|
const VideoCodecType codec = GetParam().codec_type;
|
|
const bool is_screenshare = GetParam().is_screenshare;
|
|
const std::string rmse_histogram_prefix =
|
|
is_screenshare ? "WebRTC.Video.Screenshare.RMSEOfEncodingBitrateInKbps."
|
|
: "WebRTC.Video.RMSEOfEncodingBitrateInKbps.";
|
|
const std::string overshoot_histogram_prefix =
|
|
is_screenshare ? "WebRTC.Video.Screenshare.EncodingBitrateOvershoot."
|
|
: "WebRTC.Video.EncodingBitrateOvershoot.";
|
|
// Use ideal_frame_size_kbits to represnt ideal_frame_size.bytes()*8/1000,
|
|
// then rmse_in_kbps = ideal_frame_size_kbits/2
|
|
// since we use target frame size with 50% overshoot.
|
|
int64_t rmse_in_kbps = ideal_frame_size.bytes() * 8 / 1000 / 2;
|
|
EXPECT_METRIC_EQ(1, metrics::NumSamples(rmse_histogram_prefix +
|
|
CodecTypeToHistogramSuffix(codec)));
|
|
EXPECT_METRIC_EQ(1, metrics::NumEvents(rmse_histogram_prefix +
|
|
CodecTypeToHistogramSuffix(codec),
|
|
rmse_in_kbps));
|
|
// overshoot percent = 50, since we used ideal_frame_size * 3 / 2;
|
|
EXPECT_METRIC_EQ(1, metrics::NumSamples(overshoot_histogram_prefix +
|
|
CodecTypeToHistogramSuffix(codec)));
|
|
EXPECT_METRIC_EQ(1, metrics::NumEvents(overshoot_histogram_prefix +
|
|
CodecTypeToHistogramSuffix(codec),
|
|
50));
|
|
}
|
|
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
PerCodecType,
|
|
EncoderOvershootDetectorTest,
|
|
ValuesIn<TestParams>({{VideoCodecType::kVideoCodecVP8, false},
|
|
{VideoCodecType::kVideoCodecVP8, true},
|
|
{VideoCodecType::kVideoCodecVP9, false},
|
|
{VideoCodecType::kVideoCodecVP9, true},
|
|
{VideoCodecType::kVideoCodecAV1, false},
|
|
{VideoCodecType::kVideoCodecAV1, true},
|
|
{VideoCodecType::kVideoCodecH264, false},
|
|
{VideoCodecType::kVideoCodecH264, true},
|
|
{VideoCodecType::kVideoCodecH265, false},
|
|
{VideoCodecType::kVideoCodecH265, true}}));
|
|
|
|
} // namespace webrtc
|