blob: 46f662bcd2c049b2c5d2eb10f1a301d55ce704df [file] [log] [blame]
/*
* Copyright (c) 2020 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/adaptation/video_stream_adapter.h"
#include <string>
#include <utility>
#include "absl/types/optional.h"
#include "api/video_codecs/video_codec.h"
#include "api/video_codecs/video_encoder.h"
#include "api/video_codecs/video_encoder_config.h"
#include "call/adaptation/encoder_settings.h"
#include "call/adaptation/video_source_restrictions.h"
#include "rtc_base/string_encode.h"
#include "test/field_trial.h"
#include "test/gmock.h"
#include "test/gtest.h"
#include "test/testsupport/rtc_expect_death.h"
namespace webrtc {
namespace {
// GetAdaptationUp() requires an AdaptReason. This is only used in edge cases,
// so most tests don't care what reason is used.
const auto kReasonDontCare = AdaptationObserverInterface::AdaptReason::kQuality;
const int kBalancedHighResolutionPixels = 1280 * 720;
const int kBalancedHighFrameRateFps = 30;
const int kBalancedMediumResolutionPixels = 640 * 480;
const int kBalancedMediumFrameRateFps = 20;
const int kBalancedLowResolutionPixels = 320 * 240;
const int kBalancedLowFrameRateFps = 10;
std::string BalancedFieldTrialConfig() {
return "WebRTC-Video-BalancedDegradationSettings/pixels:" +
rtc::ToString(kBalancedLowResolutionPixels) + "|" +
rtc::ToString(kBalancedMediumResolutionPixels) + "|" +
rtc::ToString(kBalancedHighResolutionPixels) +
",fps:" + rtc::ToString(kBalancedLowFrameRateFps) + "|" +
rtc::ToString(kBalancedMediumFrameRateFps) + "|" +
rtc::ToString(kBalancedHighFrameRateFps) + "/";
}
// Responsible for adjusting the inputs to VideoStreamAdapter (SetInput), such
// as pixels and frame rate, according to the most recent source restrictions.
// This helps tests that apply adaptations multiple times: if the input is not
// adjusted between adaptations, the subsequent adaptations fail with
// kAwaitingPreviousAdaptation.
class FakeVideoStream {
public:
FakeVideoStream(VideoStreamAdapter* adapter,
VideoStreamAdapter::VideoInputMode input_mode,
int input_pixels,
int input_fps,
absl::optional<EncoderSettings> encoder_settings,
absl::optional<uint32_t> encoder_target_bitrate_bps)
: adapter_(adapter),
input_mode_(std::move(input_mode)),
input_pixels_(input_pixels),
input_fps_(input_fps),
encoder_settings_(std::move(encoder_settings)),
encoder_target_bitrate_bps_(std::move(encoder_target_bitrate_bps)) {
adapter_->SetInput(input_mode_, input_pixels_, input_fps_,
encoder_settings_, encoder_target_bitrate_bps_);
}
int input_pixels() const { return input_pixels_; }
int input_fps() const { return input_fps_; }
// Performs ApplyAdaptation() followed by SetInput() with input pixels and
// frame rate adjusted according to the resulting restrictions.
void ApplyAdaptation(Adaptation adaptation) {
adapter_->ApplyAdaptation(adaptation);
// Update input pixels and fps according to the resulting restrictions.
auto restrictions = adapter_->source_restrictions();
if (restrictions.target_pixels_per_frame().has_value()) {
RTC_DCHECK(!restrictions.max_pixels_per_frame().has_value() ||
restrictions.max_pixels_per_frame().value() >=
restrictions.target_pixels_per_frame().value());
input_pixels_ = restrictions.target_pixels_per_frame().value();
} else if (restrictions.max_pixels_per_frame().has_value()) {
input_pixels_ = restrictions.max_pixels_per_frame().value();
}
if (restrictions.max_frame_rate().has_value()) {
input_fps_ = restrictions.max_frame_rate().value();
}
adapter_->SetInput(input_mode_, input_pixels_, input_fps_,
encoder_settings_, encoder_target_bitrate_bps_);
}
private:
VideoStreamAdapter* adapter_;
VideoStreamAdapter::VideoInputMode input_mode_;
int input_pixels_;
int input_fps_;
absl::optional<EncoderSettings> encoder_settings_;
absl::optional<uint32_t> encoder_target_bitrate_bps_;
};
EncoderSettings EncoderSettingsWithMinPixelsPerFrame(int min_pixels_per_frame) {
VideoEncoder::EncoderInfo encoder_info;
encoder_info.scaling_settings.min_pixels_per_frame = min_pixels_per_frame;
return EncoderSettings(std::move(encoder_info), VideoEncoderConfig(),
VideoCodec());
}
EncoderSettings EncoderSettingsWithBitrateLimits(int resolution_pixels,
int min_start_bitrate_bps) {
VideoEncoder::EncoderInfo encoder_info;
// For bitrate limits, we only care about the next resolution up's
// min_start_bitrate_bps. (...Why do we look at start bitrate and not min
// bitrate?)
encoder_info.resolution_bitrate_limits.emplace_back(
resolution_pixels,
/* min_start_bitrate_bps */ min_start_bitrate_bps,
/* min_bitrate_bps */ 0,
/* max_bitrate_bps */ 0);
return EncoderSettings(std::move(encoder_info), VideoEncoderConfig(),
VideoCodec());
}
} // namespace
TEST(VideoStreamAdapterTest, NoRestrictionsByDefault) {
VideoStreamAdapter adapter;
EXPECT_EQ(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_EQ(0, adapter.adaptation_counters().Total());
}
TEST(VideoStreamAdapterTest, MaintainFramerate_DecreasesPixelsToThreeFifths) {
const int kInputPixels = 1280 * 720;
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo,
kInputPixels, 30, absl::nullopt, absl::nullopt);
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
EXPECT_FALSE(adaptation.min_pixel_limit_reached());
adapter.ApplyAdaptation(adaptation);
EXPECT_EQ(static_cast<size_t>((kInputPixels * 3) / 5),
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(absl::nullopt, adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
}
TEST(VideoStreamAdapterTest, MaintainFramerate_DecreasesPixelsToLimitReached) {
const int kMinPixelsPerFrame = 640 * 480;
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo,
kMinPixelsPerFrame + 1, 30,
EncoderSettingsWithMinPixelsPerFrame(kMinPixelsPerFrame),
absl::nullopt);
// Even though we are above kMinPixelsPerFrame, because adapting down would
// have exceeded the limit, we are said to have reached the limit already.
// This differs from the frame rate adaptation logic, which would have clamped
// to the limit in the first step and reported kLimitReached in the second
// step.
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kLimitReached, adaptation.status());
EXPECT_TRUE(adaptation.min_pixel_limit_reached());
}
TEST(VideoStreamAdapterTest, MaintainFramerate_IncreasePixelsToFiveThirds) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
FakeVideoStream fake_stream(&adapter,
VideoStreamAdapter::VideoInputMode::kNormalVideo,
1280 * 720, 30, absl::nullopt, absl::nullopt);
// Go down twice, ensuring going back up is still a restricted resolution.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(2, adapter.adaptation_counters().resolution_adaptations);
int input_pixels = fake_stream.input_pixels();
// Go up once. The target is 5/3 and the max is 12/5 of the target.
const int target = (input_pixels * 5) / 3;
fake_stream.ApplyAdaptation(adapter.GetAdaptationUp(kReasonDontCare));
EXPECT_EQ(static_cast<size_t>((target * 12) / 5),
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(static_cast<size_t>(target),
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(absl::nullopt, adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
}
TEST(VideoStreamAdapterTest, MaintainFramerate_IncreasePixelsToUnrestricted) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
FakeVideoStream fake_stream(&adapter,
VideoStreamAdapter::VideoInputMode::kNormalVideo,
1280 * 720, 30, absl::nullopt, absl::nullopt);
// We are unrestricted by default and should not be able to adapt up.
EXPECT_EQ(Adaptation::Status::kLimitReached,
adapter.GetAdaptationUp(kReasonDontCare).status());
// If we go down once and then back up we should not have any restrictions.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
fake_stream.ApplyAdaptation(adapter.GetAdaptationUp(kReasonDontCare));
EXPECT_EQ(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_EQ(0, adapter.adaptation_counters().Total());
}
TEST(VideoStreamAdapterTest, MaintainResolution_DecreasesFpsToTwoThirds) {
const int kInputFps = 30;
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_RESOLUTION);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
kInputFps, absl::nullopt, absl::nullopt);
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
adapter.ApplyAdaptation(adaptation);
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(static_cast<double>((kInputFps * 2) / 3),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
}
TEST(VideoStreamAdapterTest, MaintainResolution_DecreasesFpsToLimitReached) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_RESOLUTION);
FakeVideoStream fake_stream(
&adapter, VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
kMinFrameRateFps + 1, absl::nullopt, absl::nullopt);
// If we are not yet at the limit and the next step would exceed it, the step
// is clamped such that we end up exactly on the limit.
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(static_cast<double>(kMinFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
// Having reached the limit, the next adaptation down is not valid.
EXPECT_EQ(Adaptation::Status::kLimitReached,
adapter.GetAdaptationDown().status());
}
TEST(VideoStreamAdapterTest, MaintainResolution_IncreaseFpsToThreeHalves) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_RESOLUTION);
FakeVideoStream fake_stream(&adapter,
VideoStreamAdapter::VideoInputMode::kNormalVideo,
1280 * 720, 30, absl::nullopt, absl::nullopt);
// Go down twice, ensuring going back up is still a restricted frame rate.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(2, adapter.adaptation_counters().fps_adaptations);
int input_fps = fake_stream.input_fps();
// Go up once. The target is 3/2 of the input.
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(static_cast<double>((input_fps * 3) / 2),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
}
TEST(VideoStreamAdapterTest, MaintainResolution_IncreaseFpsToUnrestricted) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_RESOLUTION);
FakeVideoStream fake_stream(&adapter,
VideoStreamAdapter::VideoInputMode::kNormalVideo,
1280 * 720, 30, absl::nullopt, absl::nullopt);
// We are unrestricted by default and should not be able to adapt up.
EXPECT_EQ(Adaptation::Status::kLimitReached,
adapter.GetAdaptationUp(kReasonDontCare).status());
// If we go down once and then back up we should not have any restrictions.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
fake_stream.ApplyAdaptation(adapter.GetAdaptationUp(kReasonDontCare));
EXPECT_EQ(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_EQ(0, adapter.adaptation_counters().Total());
}
TEST(VideoStreamAdapterTest, Balanced_DecreaseFrameRate) {
webrtc::test::ScopedFieldTrials balanced_field_trials(
BalancedFieldTrialConfig());
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::BALANCED);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo,
kBalancedMediumResolutionPixels, kBalancedHighFrameRateFps,
absl::nullopt, absl::nullopt);
// If our frame rate is higher than the frame rate associated with our
// resolution we should try to adapt to the frame rate associated with our
// resolution: kBalancedMediumFrameRateFps.
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
adapter.ApplyAdaptation(adaptation);
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(static_cast<double>(kBalancedMediumFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(0, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
}
TEST(VideoStreamAdapterTest, Balanced_DecreaseResolution) {
webrtc::test::ScopedFieldTrials balanced_field_trials(
BalancedFieldTrialConfig());
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::BALANCED);
FakeVideoStream fake_stream(
&adapter, VideoStreamAdapter::VideoInputMode::kNormalVideo,
kBalancedHighResolutionPixels, kBalancedHighFrameRateFps, absl::nullopt,
absl::nullopt);
// If we are not below the current resolution's frame rate limit, we should
// adapt resolution according to "maintain-framerate" logic (three fifths).
//
// However, since we are unlimited at the start and input frame rate is not
// below kBalancedHighFrameRateFps, we first restrict the frame rate to
// kBalancedHighFrameRateFps even though that is our current frame rate. This
// does prevent the source from going higher, though, so it's technically not
// a NO-OP.
{
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
}
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(static_cast<double>(kBalancedHighFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(0, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
// Verify "maintain-framerate" logic the second time we adapt: Frame rate
// restrictions remains the same and resolution goes down.
{
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
}
constexpr size_t kReducedPixelsFirstStep =
static_cast<size_t>((kBalancedHighResolutionPixels * 3) / 5);
EXPECT_EQ(kReducedPixelsFirstStep,
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(static_cast<double>(kBalancedHighFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
// If we adapt again, because the balanced settings' proposed frame rate is
// still kBalancedHighFrameRateFps, "maintain-framerate" will trigger again.
static_assert(kReducedPixelsFirstStep > kBalancedMediumResolutionPixels,
"The reduced resolution is still greater than the next lower "
"balanced setting resolution");
constexpr size_t kReducedPixelsSecondStep = (kReducedPixelsFirstStep * 3) / 5;
{
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
}
EXPECT_EQ(kReducedPixelsSecondStep,
adapter.source_restrictions().max_pixels_per_frame());
EXPECT_EQ(absl::nullopt,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(static_cast<double>(kBalancedHighFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(2, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
}
// Testing when to adapt frame rate and when to adapt resolution is quite
// entangled, so this test covers both cases.
//
// There is an asymmetry: When we adapt down we do it in one order, but when we
// adapt up we don't do it in the reverse order. Instead we always try to adapt
// frame rate first according to balanced settings' configs and only when the
// frame rate is already achieved do we adjust the resolution.
TEST(VideoStreamAdapterTest, Balanced_IncreaseFrameRateAndResolution) {
webrtc::test::ScopedFieldTrials balanced_field_trials(
BalancedFieldTrialConfig());
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::BALANCED);
FakeVideoStream fake_stream(
&adapter, VideoStreamAdapter::VideoInputMode::kNormalVideo,
kBalancedHighResolutionPixels, kBalancedHighFrameRateFps, absl::nullopt,
absl::nullopt);
// The desired starting point of this test is having adapted frame rate twice.
// This requires performing a number of adaptations.
constexpr size_t kReducedPixelsFirstStep =
static_cast<size_t>((kBalancedHighResolutionPixels * 3) / 5);
constexpr size_t kReducedPixelsSecondStep = (kReducedPixelsFirstStep * 3) / 5;
constexpr size_t kReducedPixelsThirdStep = (kReducedPixelsSecondStep * 3) / 5;
static_assert(kReducedPixelsFirstStep > kBalancedMediumResolutionPixels,
"The first pixel reduction is greater than the balanced "
"settings' medium pixel configuration");
static_assert(kReducedPixelsSecondStep > kBalancedMediumResolutionPixels,
"The second pixel reduction is greater than the balanced "
"settings' medium pixel configuration");
static_assert(kReducedPixelsThirdStep <= kBalancedMediumResolutionPixels,
"The third pixel reduction is NOT greater than the balanced "
"settings' medium pixel configuration");
// The first adaptation should affect the frame rate: See
// Balanced_DecreaseResolution for explanation why.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(static_cast<double>(kBalancedHighFrameRateFps),
adapter.source_restrictions().max_frame_rate());
// The next three adaptations affects the resolution, because we have to reach
// kBalancedMediumResolutionPixels before a lower frame rate is considered by
// BalancedDegradationSettings. The number three is derived from the
// static_asserts above.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(kReducedPixelsFirstStep,
adapter.source_restrictions().max_pixels_per_frame());
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(kReducedPixelsSecondStep,
adapter.source_restrictions().max_pixels_per_frame());
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(kReducedPixelsThirdStep,
adapter.source_restrictions().max_pixels_per_frame());
// Thus, the next adaptation will reduce frame rate to
// kBalancedMediumFrameRateFps.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(static_cast<double>(kBalancedMediumFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(3, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(2, adapter.adaptation_counters().fps_adaptations);
// Adapt up!
// While our resolution is in the medium-range, the frame rate associated with
// the next resolution configuration up ("high") is kBalancedHighFrameRateFps
// and "balanced" prefers adapting frame rate if not already applied.
{
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(static_cast<double>(kBalancedHighFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(3, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
}
// Now that we have already achieved the next frame rate up, we act according
// to "maintain-framerate". We go back up in resolution. Due to rounding
// errors we don't end up back at kReducedPixelsSecondStep. Rather we get to
// kReducedPixelsSecondStepUp, which is off by one compared to
// kReducedPixelsSecondStep.
constexpr size_t kReducedPixelsSecondStepUp =
(kReducedPixelsThirdStep * 5) / 3;
{
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(kReducedPixelsSecondStepUp,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(2, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
}
// Now that our resolution is back in the high-range, the next frame rate to
// try out is "unlimited".
{
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(absl::nullopt, adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(2, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(0, adapter.adaptation_counters().fps_adaptations);
}
// Now only adapting resolution remains.
constexpr size_t kReducedPixelsFirstStepUp =
(kReducedPixelsSecondStepUp * 5) / 3;
{
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(kReducedPixelsFirstStepUp,
adapter.source_restrictions().target_pixels_per_frame());
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
EXPECT_EQ(0, adapter.adaptation_counters().fps_adaptations);
}
// The last step up should make us entirely unrestricted.
{
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_EQ(0, adapter.adaptation_counters().Total());
}
}
TEST(VideoStreamAdapterTest, Balanced_LimitReached) {
webrtc::test::ScopedFieldTrials balanced_field_trials(
BalancedFieldTrialConfig());
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::BALANCED);
FakeVideoStream fake_stream(
&adapter, VideoStreamAdapter::VideoInputMode::kNormalVideo,
kBalancedLowResolutionPixels, kBalancedLowFrameRateFps, absl::nullopt,
absl::nullopt);
// Attempting to adapt up while unrestricted should result in kLimitReached.
EXPECT_EQ(Adaptation::Status::kLimitReached,
adapter.GetAdaptationUp(kReasonDontCare).status());
// Adapting down once result in restricted frame rate, in this case we reach
// the lowest possible frame rate immediately: kBalancedLowFrameRateFps.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(static_cast<double>(kBalancedLowFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
// Any further adaptation must follow "maintain-framerate" rules (these are
// covered in more depth by the MaintainFramerate tests). This test does not
// assert exactly how resolution is adjusted, only that resolution always
// decreases and that we eventually reach kLimitReached.
size_t previous_resolution = kBalancedLowResolutionPixels;
bool did_reach_limit = false;
// If we have not reached the limit within 5 adaptations something is wrong...
for (int i = 0; i < 5; i++) {
Adaptation adaptation = adapter.GetAdaptationDown();
if (adaptation.status() == Adaptation::Status::kLimitReached) {
did_reach_limit = true;
break;
}
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
fake_stream.ApplyAdaptation(adaptation);
EXPECT_LT(adapter.source_restrictions().max_pixels_per_frame().value(),
previous_resolution);
previous_resolution =
adapter.source_restrictions().max_pixels_per_frame().value();
}
EXPECT_TRUE(did_reach_limit);
// Frame rate restrictions are the same as before.
EXPECT_EQ(static_cast<double>(kBalancedLowFrameRateFps),
adapter.source_restrictions().max_frame_rate());
EXPECT_EQ(1, adapter.adaptation_counters().fps_adaptations);
}
TEST(VideoStreamAdapterTest, AdaptationDisabled) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::DISABLED);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
30, absl::nullopt, absl::nullopt);
EXPECT_EQ(Adaptation::Status::kAdaptationDisabled,
adapter.GetAdaptationDown().status());
EXPECT_EQ(Adaptation::Status::kAdaptationDisabled,
adapter.GetAdaptationUp(kReasonDontCare).status());
}
TEST(VideoStreamAdapterTest, InsufficientInput) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_RESOLUTION);
// No vido is insufficient in either direction.
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNoVideo, 1280 * 720, 30,
absl::nullopt, absl::nullopt);
EXPECT_EQ(Adaptation::Status::kInsufficientInput,
adapter.GetAdaptationDown().status());
EXPECT_EQ(Adaptation::Status::kInsufficientInput,
adapter.GetAdaptationUp(kReasonDontCare).status());
// No frame rate is insufficient when going down.
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
0, absl::nullopt, absl::nullopt);
EXPECT_EQ(Adaptation::Status::kInsufficientInput,
adapter.GetAdaptationDown().status());
}
// kAwaitingPreviousAdaptation is only supported in "maintain-framerate".
TEST(VideoStreamAdapterTest, MaintainFramerate_AwaitingPreviousAdaptationDown) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
30, absl::nullopt, absl::nullopt);
// Adapt down once, but don't update the input.
adapter.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
{
// Having performed the adaptation, but not updated the input based on the
// new restrictions, adapting again in the same direction will not work.
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kAwaitingPreviousAdaptation,
adaptation.status());
}
}
// kAwaitingPreviousAdaptation is only supported in "maintain-framerate".
TEST(VideoStreamAdapterTest, MaintainFramerate_AwaitingPreviousAdaptationUp) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
FakeVideoStream fake_stream(&adapter,
VideoStreamAdapter::VideoInputMode::kNormalVideo,
1280 * 720, 30, absl::nullopt, absl::nullopt);
// Perform two adaptation down so that adapting up twice is possible.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(2, adapter.adaptation_counters().resolution_adaptations);
// Adapt up once, but don't update the input.
adapter.ApplyAdaptation(adapter.GetAdaptationUp(kReasonDontCare));
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
{
// Having performed the adaptation, but not updated the input based on the
// new restrictions, adapting again in the same direction will not work.
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kAwaitingPreviousAdaptation,
adaptation.status());
}
}
// TODO(hbos): Also add BitrateConstrained test coverage for the BALANCED
// degradation preference.
TEST(VideoStreamAdapterTest, BitrateConstrained_MaintainFramerate) {
const int kInputPixels = 1280 * 720;
const int kBitrateLimit = 1000;
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
FakeVideoStream fake_stream(
&adapter, VideoStreamAdapter::VideoInputMode::kNormalVideo, kInputPixels,
30, EncoderSettingsWithBitrateLimits(kInputPixels, kBitrateLimit),
// The target bitrate is one less than necessary
// to adapt up.
kBitrateLimit - 1);
// Adapt down so that it would be possible to adapt up if we weren't bitrate
// constrainted.
fake_stream.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_EQ(1, adapter.adaptation_counters().resolution_adaptations);
// Adapting up for reason kQuality should not work because this exceeds the
// bitrate limit.
// TODO(hbos): Why would the reason matter? If the signal was kCpu then the
// current code allows us to violate this bitrate constraint. This does not
// make any sense: either we are limited or we are not, end of story.
EXPECT_EQ(
Adaptation::Status::kIsBitrateConstrained,
adapter
.GetAdaptationUp(AdaptationObserverInterface::AdaptReason::kQuality)
.status());
}
TEST(VideoStreamAdapterTest, PeekNextRestrictions) {
VideoStreamAdapter adapter;
// Any non-disabled DegradationPreference will do.
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
FakeVideoStream fake_stream(&adapter,
VideoStreamAdapter::VideoInputMode::kNormalVideo,
1280 * 720, 30, absl::nullopt, absl::nullopt);
// When adaptation is not possible.
{
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kLimitReached, adaptation.status());
EXPECT_EQ(adapter.PeekNextRestrictions(adaptation),
adapter.source_restrictions());
}
// When we adapt down.
{
Adaptation adaptation = adapter.GetAdaptationDown();
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
VideoSourceRestrictions next_restrictions =
adapter.PeekNextRestrictions(adaptation);
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(next_restrictions, adapter.source_restrictions());
}
// When we adapt up.
{
Adaptation adaptation = adapter.GetAdaptationUp(kReasonDontCare);
EXPECT_EQ(Adaptation::Status::kValid, adaptation.status());
VideoSourceRestrictions next_restrictions =
adapter.PeekNextRestrictions(adaptation);
fake_stream.ApplyAdaptation(adaptation);
EXPECT_EQ(next_restrictions, adapter.source_restrictions());
}
}
// This test covers non-standard behavior. If the application desires
// "maintain-resolution" it should ask for it rather than relying on this
// behavior, which should become unsupported.
TEST(VideoStreamAdapterTest, BalancedScreenshareBehavesLikeMaintainResolution) {
const int kInputPixels = 1280 * 720;
const int kInputFps = 30;
VideoStreamAdapter balanced_adapter;
balanced_adapter.SetDegradationPreference(DegradationPreference::BALANCED);
balanced_adapter.SetInput(
VideoStreamAdapter::VideoInputMode::kScreenshareVideo, kInputPixels,
kInputFps, absl::nullopt, absl::nullopt);
VideoStreamAdapter maintain_resolution_adapter;
maintain_resolution_adapter.SetDegradationPreference(
DegradationPreference::MAINTAIN_RESOLUTION);
maintain_resolution_adapter.SetInput(
VideoStreamAdapter::VideoInputMode::kNormalVideo, kInputPixels, kInputFps,
absl::nullopt, absl::nullopt);
EXPECT_EQ(balanced_adapter.source_restrictions(),
maintain_resolution_adapter.source_restrictions());
balanced_adapter.ApplyAdaptation(balanced_adapter.GetAdaptationDown());
maintain_resolution_adapter.ApplyAdaptation(
maintain_resolution_adapter.GetAdaptationDown());
EXPECT_EQ(balanced_adapter.source_restrictions(),
maintain_resolution_adapter.source_restrictions());
}
TEST(VideoStreamAdapterTest,
SetDegradationPreferenceToOrFromBalancedClearsRestrictions) {
VideoStreamAdapter adapter;
EXPECT_EQ(VideoStreamAdapter::SetDegradationPreferenceResult::
kRestrictionsNotCleared,
adapter.SetDegradationPreference(
DegradationPreference::MAINTAIN_FRAMERATE));
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
30, absl::nullopt, absl::nullopt);
adapter.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_NE(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_NE(0, adapter.adaptation_counters().Total());
// Changing from non-balanced to balanced clears the restrictions.
EXPECT_EQ(
VideoStreamAdapter::SetDegradationPreferenceResult::kRestrictionsCleared,
adapter.SetDegradationPreference(DegradationPreference::BALANCED));
EXPECT_EQ(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_EQ(0, adapter.adaptation_counters().Total());
// Apply adaptation again.
adapter.ApplyAdaptation(adapter.GetAdaptationDown());
EXPECT_NE(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_NE(0, adapter.adaptation_counters().Total());
// Changing from balanced to non-balanced clears the restrictions.
EXPECT_EQ(
VideoStreamAdapter::SetDegradationPreferenceResult::kRestrictionsCleared,
adapter.SetDegradationPreference(
DegradationPreference::MAINTAIN_RESOLUTION));
EXPECT_EQ(VideoSourceRestrictions(), adapter.source_restrictions());
EXPECT_EQ(0, adapter.adaptation_counters().Total());
}
// Death tests.
// Disabled on Android because death tests misbehave on Android, see
// base/test/gtest_util.h.
#if RTC_DCHECK_IS_ON && GTEST_HAS_DEATH_TEST && !defined(WEBRTC_ANDROID)
TEST(VideoStreamAdapterDeathTest,
SetDegradationPreferenceInvalidatesAdaptations) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_FRAMERATE);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
30, absl::nullopt, absl::nullopt);
Adaptation adaptation = adapter.GetAdaptationDown();
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_RESOLUTION);
EXPECT_DEATH(adapter.ApplyAdaptation(adaptation), "");
}
TEST(VideoStreamAdapterDeathTest, SetInputInvalidatesAdaptations) {
VideoStreamAdapter adapter;
adapter.SetDegradationPreference(DegradationPreference::MAINTAIN_RESOLUTION);
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
30, absl::nullopt, absl::nullopt);
Adaptation adaptation = adapter.GetAdaptationDown();
adapter.SetInput(VideoStreamAdapter::VideoInputMode::kNormalVideo, 1280 * 720,
31, absl::nullopt, absl::nullopt);
EXPECT_DEATH(adapter.PeekNextRestrictions(adaptation), "");
}
#endif // RTC_DCHECK_IS_ON && GTEST_HAS_DEATH_TEST && !defined(WEBRTC_ANDROID)
} // namespace webrtc