Add hysteresis to enabling of simulcast streams.
If the bandwidth is just on the edge of being able to enable a new stream, the keyframe generated when it is enabled might be large enough to trigger an overuse and force the stream off again. To avoid toggling, this CL adds hysteresis so that the available bandwidth needs to be above X% to start bitrate in order to enable the stream. It will be shut down once available bitrate falls below the original enabling bitrate. For screen content, X defaults to 35. For realtime content, X defaults to 0. Both can be individually modified via field trials. Bug: webrtc:9734 Change-Id: I941332d7be7f2a801d13d9202b2076d330e7df32 Reviewed-on: https://webrtc-review.googlesource.com/100308 Reviewed-by: Rasmus Brandt <brandtr@webrtc.org> Commit-Queue: Erik Språng <sprang@webrtc.org> Cr-Commit-Position: refs/heads/master@{#24745}
This commit is contained in:
@ -12,6 +12,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@ -37,6 +38,33 @@ static const float kShort3TlRateAllocation[kMaxTemporalStreams] = {
|
||||
|
||||
const uint32_t kLegacyScreenshareTl0BitrateKbps = 200;
|
||||
const uint32_t kLegacyScreenshareTl1BitrateKbps = 1000;
|
||||
|
||||
double GetHysteresisFactor(const VideoCodec& codec) {
|
||||
double factor = 1.0;
|
||||
|
||||
std::string field_trial_name;
|
||||
switch (codec.mode) {
|
||||
case VideoCodecMode::kRealtimeVideo:
|
||||
field_trial_name = "WebRTC-SimulcastUpswitchHysteresisPercent";
|
||||
// Default to no hysteresis for simulcast video.
|
||||
factor = 1.0;
|
||||
break;
|
||||
case VideoCodecMode::kScreensharing:
|
||||
field_trial_name = "WebRTC-SimulcastScreenshareUpswitchHysteresisPercent";
|
||||
// Default to 35% hysteresis for simulcast screenshare.
|
||||
factor = 1.35;
|
||||
break;
|
||||
}
|
||||
|
||||
std::string group_name = webrtc::field_trial::FindFullName(field_trial_name);
|
||||
int percent = 0;
|
||||
if (!group_name.empty() && sscanf(group_name.c_str(), "%d", &percent) == 1 &&
|
||||
percent >= 0) {
|
||||
factor = 1.0 + (percent / 100.0);
|
||||
}
|
||||
|
||||
return factor;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
float SimulcastRateAllocator::GetTemporalRateAllocation(int num_layers,
|
||||
@ -53,7 +81,9 @@ float SimulcastRateAllocator::GetTemporalRateAllocation(int num_layers,
|
||||
}
|
||||
|
||||
SimulcastRateAllocator::SimulcastRateAllocator(const VideoCodec& codec)
|
||||
: codec_(codec) {}
|
||||
: codec_(codec), hysteresis_factor_(GetHysteresisFactor(codec)) {}
|
||||
|
||||
SimulcastRateAllocator::~SimulcastRateAllocator() = default;
|
||||
|
||||
VideoBitrateAllocation SimulcastRateAllocator::GetAllocation(
|
||||
uint32_t total_bitrate_bps,
|
||||
@ -67,7 +97,7 @@ VideoBitrateAllocation SimulcastRateAllocator::GetAllocation(
|
||||
|
||||
void SimulcastRateAllocator::DistributeAllocationToSimulcastLayers(
|
||||
uint32_t total_bitrate_bps,
|
||||
VideoBitrateAllocation* allocated_bitrates_bps) const {
|
||||
VideoBitrateAllocation* allocated_bitrates_bps) {
|
||||
uint32_t left_to_allocate = total_bitrate_bps;
|
||||
if (codec_.maxBitrate && codec_.maxBitrate * 1000 < left_to_allocate)
|
||||
left_to_allocate = codec_.maxBitrate * 1000;
|
||||
@ -103,20 +133,37 @@ void SimulcastRateAllocator::DistributeAllocationToSimulcastLayers(
|
||||
// temporal layer 0. We'll then distribute this bitrate, across potential
|
||||
// temporal layers, when stream allocation is done.
|
||||
|
||||
bool first_allocation = false;
|
||||
if (stream_enabled_.empty()) {
|
||||
// First time allocating, this means we should not include hysteresis in
|
||||
// case this is a reconfiguration of an existing enabled stream.
|
||||
first_allocation = true;
|
||||
stream_enabled_.resize(codec_.numberOfSimulcastStreams, false);
|
||||
}
|
||||
|
||||
size_t top_active_layer = active_layer;
|
||||
// Allocate up to the target bitrate for each active simulcast layer.
|
||||
for (; active_layer < codec_.numberOfSimulcastStreams; ++active_layer) {
|
||||
const SimulcastStream& stream = codec_.simulcastStream[active_layer];
|
||||
if (!stream.active) {
|
||||
stream_enabled_[active_layer] = false;
|
||||
continue;
|
||||
}
|
||||
// If we can't allocate to the current layer we can't allocate to higher
|
||||
// layers because they require a higher minimum bitrate.
|
||||
if (left_to_allocate < stream.minBitrate * 1000) {
|
||||
uint32_t min_bitrate = stream.minBitrate * 1000;
|
||||
if (!first_allocation && !stream_enabled_[active_layer]) {
|
||||
min_bitrate = std::min(
|
||||
static_cast<uint32_t>(hysteresis_factor_ * min_bitrate + 0.5),
|
||||
stream.targetBitrate * 1000);
|
||||
}
|
||||
if (left_to_allocate < min_bitrate) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We are allocating to this layer so it is the current active allocation.
|
||||
top_active_layer = active_layer;
|
||||
stream_enabled_[active_layer] = true;
|
||||
uint32_t allocation =
|
||||
std::min(left_to_allocate, stream.targetBitrate * 1000);
|
||||
allocated_bitrates_bps->SetBitrate(active_layer, 0, allocation);
|
||||
@ -124,6 +171,11 @@ void SimulcastRateAllocator::DistributeAllocationToSimulcastLayers(
|
||||
left_to_allocate -= allocation;
|
||||
}
|
||||
|
||||
// All layers above this one are not active.
|
||||
for (; active_layer < codec_.numberOfSimulcastStreams; ++active_layer) {
|
||||
stream_enabled_[active_layer] = false;
|
||||
}
|
||||
|
||||
// Next, try allocate remaining bitrate, up to max bitrate, in top active
|
||||
// stream.
|
||||
// TODO(sprang): Allocate up to max bitrate for all layers once we have a
|
||||
|
@ -27,6 +27,7 @@ namespace webrtc {
|
||||
class SimulcastRateAllocator : public VideoBitrateAllocator {
|
||||
public:
|
||||
explicit SimulcastRateAllocator(const VideoCodec& codec);
|
||||
~SimulcastRateAllocator() override;
|
||||
|
||||
VideoBitrateAllocation GetAllocation(uint32_t total_bitrate_bps,
|
||||
uint32_t framerate) override;
|
||||
@ -37,7 +38,7 @@ class SimulcastRateAllocator : public VideoBitrateAllocator {
|
||||
private:
|
||||
void DistributeAllocationToSimulcastLayers(
|
||||
uint32_t total_bitrate_bps,
|
||||
VideoBitrateAllocation* allocated_bitrates_bps) const;
|
||||
VideoBitrateAllocation* allocated_bitrates_bps);
|
||||
void DistributeAllocationToTemporalLayers(
|
||||
uint32_t framerate,
|
||||
VideoBitrateAllocation* allocated_bitrates_bps) const;
|
||||
@ -53,6 +54,8 @@ class SimulcastRateAllocator : public VideoBitrateAllocator {
|
||||
int NumTemporalStreams(size_t simulcast_id) const;
|
||||
|
||||
const VideoCodec codec_;
|
||||
const double hysteresis_factor_;
|
||||
std::vector<bool> stream_enabled_;
|
||||
|
||||
RTC_DISALLOW_COPY_AND_ASSIGN(SimulcastRateAllocator);
|
||||
};
|
||||
|
@ -27,8 +27,11 @@ using ::testing::_;
|
||||
constexpr uint32_t kFramerateFps = 5;
|
||||
constexpr uint32_t kMinBitrateKbps = 50;
|
||||
// These correspond to kLegacyScreenshareTl(0|1)BitrateKbps in cc.
|
||||
constexpr uint32_t kTargetBitrateKbps = 200;
|
||||
constexpr uint32_t kMaxBitrateKbps = 1000;
|
||||
constexpr uint32_t kLegacyScreenshareTargetBitrateKbps = 200;
|
||||
constexpr uint32_t kLegacyScreenshareMaxBitrateKbps = 1000;
|
||||
// Bitrates for upper simulcast screenshare layer.
|
||||
constexpr uint32_t kSimulcastScreenshareMinBitrateKbps = 600;
|
||||
constexpr uint32_t kSimulcastScreenshareMaxBitrateKbps = 1250;
|
||||
|
||||
class MockTemporalLayers : public TemporalLayers {
|
||||
public:
|
||||
@ -53,7 +56,7 @@ class SimulcastRateAllocatorTest : public ::testing::TestWithParam<bool> {
|
||||
memset(&codec_, 0, sizeof(VideoCodec));
|
||||
codec_.codecType = kVideoCodecVP8;
|
||||
codec_.minBitrate = kMinBitrateKbps;
|
||||
codec_.maxBitrate = kMaxBitrateKbps;
|
||||
codec_.maxBitrate = kLegacyScreenshareMaxBitrateKbps;
|
||||
codec_.active = true;
|
||||
CreateAllocator();
|
||||
}
|
||||
@ -168,8 +171,8 @@ TEST_F(SimulcastRateAllocatorTest, NoSimulcastInactive) {
|
||||
CreateAllocator();
|
||||
|
||||
ExpectEqual(expected, GetAllocation(kMinBitrateKbps - 10));
|
||||
ExpectEqual(expected, GetAllocation(kTargetBitrateKbps));
|
||||
ExpectEqual(expected, GetAllocation(kMaxBitrateKbps + 10));
|
||||
ExpectEqual(expected, GetAllocation(kLegacyScreenshareTargetBitrateKbps));
|
||||
ExpectEqual(expected, GetAllocation(kLegacyScreenshareMaxBitrateKbps + 10));
|
||||
}
|
||||
|
||||
TEST_F(SimulcastRateAllocatorTest, SingleSimulcastBelowMin) {
|
||||
@ -177,7 +180,7 @@ TEST_F(SimulcastRateAllocatorTest, SingleSimulcastBelowMin) {
|
||||
codec_.numberOfSimulcastStreams = 1;
|
||||
const uint32_t kMin = codec_.minBitrate - 10;
|
||||
codec_.simulcastStream[0].minBitrate = kMin;
|
||||
codec_.simulcastStream[0].targetBitrate = kTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].targetBitrate = kLegacyScreenshareTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].active = true;
|
||||
CreateAllocator();
|
||||
|
||||
@ -204,13 +207,13 @@ TEST_F(SimulcastRateAllocatorTest, SingleSimulcastAboveMax) {
|
||||
TEST_F(SimulcastRateAllocatorTest, SingleSimulcastWithinLimits) {
|
||||
codec_.numberOfSimulcastStreams = 1;
|
||||
codec_.simulcastStream[0].minBitrate = kMinBitrateKbps;
|
||||
codec_.simulcastStream[0].targetBitrate = kTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].maxBitrate = kMaxBitrateKbps;
|
||||
codec_.simulcastStream[0].targetBitrate = kLegacyScreenshareTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].maxBitrate = kLegacyScreenshareMaxBitrateKbps;
|
||||
codec_.simulcastStream[0].active = true;
|
||||
CreateAllocator();
|
||||
|
||||
for (uint32_t bitrate = kMinBitrateKbps; bitrate <= kMaxBitrateKbps;
|
||||
++bitrate) {
|
||||
for (uint32_t bitrate = kMinBitrateKbps;
|
||||
bitrate <= kLegacyScreenshareMaxBitrateKbps; ++bitrate) {
|
||||
uint32_t expected[] = {bitrate};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
@ -219,15 +222,15 @@ TEST_F(SimulcastRateAllocatorTest, SingleSimulcastWithinLimits) {
|
||||
TEST_F(SimulcastRateAllocatorTest, SingleSimulcastInactive) {
|
||||
codec_.numberOfSimulcastStreams = 1;
|
||||
codec_.simulcastStream[0].minBitrate = kMinBitrateKbps;
|
||||
codec_.simulcastStream[0].targetBitrate = kTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].maxBitrate = kMaxBitrateKbps;
|
||||
codec_.simulcastStream[0].targetBitrate = kLegacyScreenshareTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].maxBitrate = kLegacyScreenshareMaxBitrateKbps;
|
||||
codec_.simulcastStream[0].active = false;
|
||||
CreateAllocator();
|
||||
|
||||
uint32_t expected[] = {0};
|
||||
ExpectEqual(expected, GetAllocation(kMinBitrateKbps - 10));
|
||||
ExpectEqual(expected, GetAllocation(kTargetBitrateKbps));
|
||||
ExpectEqual(expected, GetAllocation(kMaxBitrateKbps + 10));
|
||||
ExpectEqual(expected, GetAllocation(kLegacyScreenshareTargetBitrateKbps));
|
||||
ExpectEqual(expected, GetAllocation(kLegacyScreenshareMaxBitrateKbps + 10));
|
||||
}
|
||||
|
||||
TEST_F(SimulcastRateAllocatorTest, OneToThreeStreams) {
|
||||
@ -471,14 +474,25 @@ class ScreenshareRateAllocationTest : public SimulcastRateAllocatorTest {
|
||||
void SetupConferenceScreenshare(bool use_simulcast, bool active = true) {
|
||||
codec_.mode = VideoCodecMode::kScreensharing;
|
||||
codec_.minBitrate = kMinBitrateKbps;
|
||||
codec_.maxBitrate = kMaxBitrateKbps;
|
||||
codec_.maxBitrate =
|
||||
kLegacyScreenshareMaxBitrateKbps + kSimulcastScreenshareMaxBitrateKbps;
|
||||
if (use_simulcast) {
|
||||
codec_.numberOfSimulcastStreams = 1;
|
||||
codec_.numberOfSimulcastStreams = 2;
|
||||
codec_.simulcastStream[0].minBitrate = kMinBitrateKbps;
|
||||
codec_.simulcastStream[0].targetBitrate = kTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].maxBitrate = kMaxBitrateKbps;
|
||||
codec_.simulcastStream[0].targetBitrate =
|
||||
kLegacyScreenshareTargetBitrateKbps;
|
||||
codec_.simulcastStream[0].maxBitrate = kLegacyScreenshareMaxBitrateKbps;
|
||||
codec_.simulcastStream[0].numberOfTemporalLayers = 2;
|
||||
codec_.simulcastStream[0].active = active;
|
||||
|
||||
codec_.simulcastStream[1].minBitrate =
|
||||
kSimulcastScreenshareMinBitrateKbps;
|
||||
codec_.simulcastStream[1].targetBitrate =
|
||||
kSimulcastScreenshareMaxBitrateKbps;
|
||||
codec_.simulcastStream[1].maxBitrate =
|
||||
kSimulcastScreenshareMaxBitrateKbps;
|
||||
codec_.simulcastStream[1].numberOfTemporalLayers = 2;
|
||||
codec_.simulcastStream[1].active = active;
|
||||
} else {
|
||||
codec_.numberOfSimulcastStreams = 0;
|
||||
codec_.VP8()->numberOfTemporalLayers = 2;
|
||||
@ -495,41 +509,48 @@ TEST_P(ScreenshareRateAllocationTest, BitrateBelowTl0) {
|
||||
SetupConferenceScreenshare(GetParam());
|
||||
CreateAllocator();
|
||||
|
||||
VideoBitrateAllocation allocation =
|
||||
allocator_->GetAllocation(kTargetBitrateKbps * 1000, kFramerateFps);
|
||||
VideoBitrateAllocation allocation = allocator_->GetAllocation(
|
||||
kLegacyScreenshareTargetBitrateKbps * 1000, kFramerateFps);
|
||||
|
||||
// All allocation should go in TL0.
|
||||
EXPECT_EQ(kTargetBitrateKbps, allocation.get_sum_kbps());
|
||||
EXPECT_EQ(kTargetBitrateKbps, allocation.GetBitrate(0, 0) / 1000);
|
||||
EXPECT_EQ(kLegacyScreenshareTargetBitrateKbps, allocation.get_sum_kbps());
|
||||
EXPECT_EQ(kLegacyScreenshareTargetBitrateKbps,
|
||||
allocation.GetBitrate(0, 0) / 1000);
|
||||
}
|
||||
|
||||
TEST_P(ScreenshareRateAllocationTest, BitrateAboveTl0) {
|
||||
SetupConferenceScreenshare(GetParam());
|
||||
CreateAllocator();
|
||||
|
||||
uint32_t target_bitrate_kbps = (kTargetBitrateKbps + kMaxBitrateKbps) / 2;
|
||||
uint32_t target_bitrate_kbps =
|
||||
(kLegacyScreenshareTargetBitrateKbps + kLegacyScreenshareMaxBitrateKbps) /
|
||||
2;
|
||||
VideoBitrateAllocation allocation =
|
||||
allocator_->GetAllocation(target_bitrate_kbps * 1000, kFramerateFps);
|
||||
|
||||
// Fill TL0, then put the rest in TL1.
|
||||
EXPECT_EQ(target_bitrate_kbps, allocation.get_sum_kbps());
|
||||
EXPECT_EQ(kTargetBitrateKbps, allocation.GetBitrate(0, 0) / 1000);
|
||||
EXPECT_EQ(target_bitrate_kbps - kTargetBitrateKbps,
|
||||
EXPECT_EQ(kLegacyScreenshareTargetBitrateKbps,
|
||||
allocation.GetBitrate(0, 0) / 1000);
|
||||
EXPECT_EQ(target_bitrate_kbps - kLegacyScreenshareTargetBitrateKbps,
|
||||
allocation.GetBitrate(0, 1) / 1000);
|
||||
}
|
||||
|
||||
TEST_P(ScreenshareRateAllocationTest, BitrateAboveTl1) {
|
||||
SetupConferenceScreenshare(GetParam());
|
||||
TEST_F(ScreenshareRateAllocationTest, BitrateAboveTl1) {
|
||||
// This test is only for the non-simulcast case.
|
||||
SetupConferenceScreenshare(false);
|
||||
CreateAllocator();
|
||||
|
||||
VideoBitrateAllocation allocation =
|
||||
allocator_->GetAllocation(kMaxBitrateKbps * 2000, kFramerateFps);
|
||||
VideoBitrateAllocation allocation = allocator_->GetAllocation(
|
||||
kLegacyScreenshareMaxBitrateKbps * 2000, kFramerateFps);
|
||||
|
||||
// Fill both TL0 and TL1, but no more.
|
||||
EXPECT_EQ(kMaxBitrateKbps, allocation.get_sum_kbps());
|
||||
EXPECT_EQ(kTargetBitrateKbps, allocation.GetBitrate(0, 0) / 1000);
|
||||
EXPECT_EQ(kMaxBitrateKbps - kTargetBitrateKbps,
|
||||
allocation.GetBitrate(0, 1) / 1000);
|
||||
EXPECT_EQ(kLegacyScreenshareMaxBitrateKbps, allocation.get_sum_kbps());
|
||||
EXPECT_EQ(kLegacyScreenshareTargetBitrateKbps,
|
||||
allocation.GetBitrate(0, 0) / 1000);
|
||||
EXPECT_EQ(
|
||||
kLegacyScreenshareMaxBitrateKbps - kLegacyScreenshareTargetBitrateKbps,
|
||||
allocation.GetBitrate(0, 1) / 1000);
|
||||
}
|
||||
|
||||
// This tests when the screenshare is inactive it should be allocated 0 bitrate
|
||||
@ -539,10 +560,80 @@ TEST_P(ScreenshareRateAllocationTest, InactiveScreenshare) {
|
||||
CreateAllocator();
|
||||
|
||||
// Enough bitrate for TL0 and TL1.
|
||||
uint32_t target_bitrate_kbps = (kTargetBitrateKbps + kMaxBitrateKbps) / 2;
|
||||
uint32_t target_bitrate_kbps =
|
||||
(kLegacyScreenshareTargetBitrateKbps + kLegacyScreenshareMaxBitrateKbps) /
|
||||
2;
|
||||
VideoBitrateAllocation allocation =
|
||||
allocator_->GetAllocation(target_bitrate_kbps * 1000, kFramerateFps);
|
||||
|
||||
EXPECT_EQ(0U, allocation.get_sum_kbps());
|
||||
}
|
||||
|
||||
TEST_F(ScreenshareRateAllocationTest, Hysteresis) {
|
||||
// This test is only for the simulcast case.
|
||||
SetupConferenceScreenshare(true);
|
||||
CreateAllocator();
|
||||
|
||||
// The bitrate at which we would normally enable the upper simulcast stream.
|
||||
const uint32_t default_enable_rate_bps =
|
||||
codec_.simulcastStream[0].targetBitrate +
|
||||
codec_.simulcastStream[1].minBitrate;
|
||||
const uint32_t enable_rate_with_hysteresis_bps =
|
||||
(default_enable_rate_bps * 135) / 100;
|
||||
|
||||
{
|
||||
// On the first call to a new SimulcastRateAllocator instance, hysteresis
|
||||
// is disabled.
|
||||
const uint32_t bitrate = default_enable_rate_bps;
|
||||
uint32_t expected[] = {codec_.simulcastStream[0].targetBitrate,
|
||||
codec_.simulcastStream[1].minBitrate};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
|
||||
{
|
||||
// Go down to a bitrate below what is needed for two streams.
|
||||
const uint32_t bitrate = default_enable_rate_bps - 1;
|
||||
uint32_t expected[] = {bitrate, 0};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
|
||||
{
|
||||
// Don't enable stream as we need to get up above hysteresis threshold.
|
||||
const uint32_t bitrate = default_enable_rate_bps;
|
||||
uint32_t expected[] = {bitrate, 0};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
|
||||
{
|
||||
// Above threshold, enable second stream.
|
||||
const uint32_t bitrate = enable_rate_with_hysteresis_bps;
|
||||
uint32_t expected[] = {codec_.simulcastStream[0].targetBitrate,
|
||||
enable_rate_with_hysteresis_bps -
|
||||
codec_.simulcastStream[0].targetBitrate};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
|
||||
{
|
||||
// Go down again, still keep the second stream alive.
|
||||
const uint32_t bitrate = default_enable_rate_bps;
|
||||
uint32_t expected[] = {codec_.simulcastStream[0].targetBitrate,
|
||||
codec_.simulcastStream[1].minBitrate};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
|
||||
{
|
||||
// Go down below default enable, second stream is shut down again.
|
||||
const uint32_t bitrate = default_enable_rate_bps - 1;
|
||||
uint32_t expected[] = {bitrate, 0};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
|
||||
{
|
||||
// Go up, hysteresis is blocking us again.
|
||||
const uint32_t bitrate = default_enable_rate_bps;
|
||||
uint32_t expected[] = {bitrate, 0};
|
||||
ExpectEqual(expected, GetAllocation(bitrate));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
Reference in New Issue
Block a user