Skip to content

Commit

Permalink
Add hysteresis to enabling of simulcast streams.
Browse files Browse the repository at this point in the history
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}
  • Loading branch information
Erik Språng authored and Commit Bot committed Sep 14, 2018
1 parent 60570dc commit 3064f31
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 38 deletions.
58 changes: 55 additions & 3 deletions modules/video_coding/utility/simulcast_rate_allocator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -103,27 +133,49 @@ 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);
RTC_DCHECK_LE(allocation, left_to_allocate);
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
Expand Down
5 changes: 4 additions & 1 deletion modules/video_coding/utility/simulcast_rate_allocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
};
Expand Down
159 changes: 125 additions & 34 deletions modules/video_coding/utility/simulcast_rate_allocator_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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();
}
Expand Down Expand Up @@ -168,16 +171,16 @@ 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) {
// With simulcast, use the min bitrate from the ss spec instead of the global.
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();

Expand All @@ -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));
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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

0 comments on commit 3064f31

Please sign in to comment.