From 43c0cf9cf86d473dc942eaac36138a9f43c5018f Mon Sep 17 00:00:00 2001 From: Dan Tan Date: Thu, 22 Aug 2024 17:05:13 +0000 Subject: [PATCH] Support borrowing of underused audio bitrate. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Controlled via added field trial WebRTC-ElasticBitrateAllocation. Bug: webrtc:350555527 Change-Id: If57552144bd4a50421d618fd8bdab31d7c4afc35 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/359506 Reviewed-by: Erik Språng Commit-Queue: Dan Tan Cr-Commit-Position: refs/heads/main@{#42834} --- call/BUILD.gn | 2 + call/bitrate_allocator.cc | 211 +++++++++++++++++++++++++++-- call/bitrate_allocator.h | 20 ++- call/bitrate_allocator_unittest.cc | 167 +++++++++++++++++++++-- call/call.cc | 23 +++- experiments/field_trials.py | 3 + 6 files changed, 400 insertions(+), 26 deletions(-) diff --git a/call/BUILD.gn b/call/BUILD.gn index f57f9e0d3c..d4c1d564e3 100644 --- a/call/BUILD.gn +++ b/call/BUILD.gn @@ -249,6 +249,7 @@ rtc_library("bitrate_allocator") { ] deps = [ "../api:bitrate_allocation", + "../api:field_trials_view", "../api:sequence_checker", "../api/transport:network_control", "../api/units:data_rate", @@ -256,6 +257,7 @@ rtc_library("bitrate_allocator") { "../rtc_base:checks", "../rtc_base:logging", "../rtc_base:safe_minmax", + "../rtc_base/experiments:field_trial_parser", "../rtc_base/system:no_unique_address", "../system_wrappers", "../system_wrappers:field_trial", diff --git a/call/bitrate_allocator.cc b/call/bitrate_allocator.cc index 2684a1650e..e196f5629d 100644 --- a/call/bitrate_allocator.cc +++ b/call/bitrate_allocator.cc @@ -20,6 +20,7 @@ #include "api/units/data_rate.h" #include "api/units/time_delta.h" #include "rtc_base/checks.h" +#include "rtc_base/experiments/field_trial_parser.h" #include "rtc_base/logging.h" #include "rtc_base/numerics/safe_minmax.h" #include "system_wrappers/include/clock.h" @@ -317,9 +318,103 @@ std::map ZeroRateAllocation( return allocation; } +// Returns new allocation if modified, absl::nullopt otherwise. +absl::optional> MaybeApplySurplus( + const std::map& allocation, + const std::vector& allocatable_tracks, + DataRate bitrate, + DataRate upper_elastic_limit) { + if (upper_elastic_limit.IsZero()) + return absl::nullopt; + + // In this first pass looping over all `allocatable_tracks`, we aggregates + // - `surplus`: sum of unused rates for all kCanContribute* tracks, + // - `sum_demand`: sum of `bitrate_priority` for all tracks that can consume + // more bitrate to allow proportional sharing of surplus later, + // - `sum_allocated`: sum of allocated bitrates for all tracks, which might + // be larger than `bitrate` e.g. when min_bitrate_bps are enforced. + DataRate surplus = DataRate::Zero(); + double sum_demand = 0.0; + DataRate sum_allocated = DataRate::Zero(); + + for (const auto& observer_config : allocatable_tracks) { + const auto it = allocation.find(observer_config.observer); + if (it == allocation.end()) { + // No allocation for this track. + continue; + } + const DataRate allocated = DataRate::BitsPerSec(it->second); + sum_allocated += allocated; + if (const absl::optional elasticity = + observer_config.config.rate_elasticity) { + bool inactive_can_contribute_and_consume = false; + if (elasticity == TrackRateElasticity::kCanContributeUnusedRate || + elasticity == TrackRateElasticity::kCanContributeAndConsume) { + if (const absl::optional used = + observer_config.observer->GetUsedRate()) { + if (*used < allocated) { + surplus += allocated - *used; + if (elasticity == TrackRateElasticity::kCanContributeAndConsume && + *used < allocated / 2) { + inactive_can_contribute_and_consume = true; + } + } + } + } + if (!inactive_can_contribute_and_consume && + (elasticity == TrackRateElasticity::kCanConsumeExtraRate || + elasticity == TrackRateElasticity::kCanContributeAndConsume)) { + sum_demand += observer_config.config.bitrate_priority; + } + } + } + + // `sum_allocated` can exceed `bitrate` if sum minBitrates exceeds + // estimated rate. The real `surplus` should cover the difference. + DataRate overshoot = + (sum_allocated >= bitrate) ? (sum_allocated - bitrate) : DataRate::Zero(); + if (sum_demand < 0.0001 || overshoot > surplus) { + // No demand for extra bitrate or no available surplus. + return absl::nullopt; + } + surplus -= overshoot; + + auto new_allocation = allocation; + // We loop over all allocatable_tracks again, and proportionally assign + // `surplus` to each track according to `bitrate_priority`. + for (const auto& observer_config : allocatable_tracks) { + auto it = new_allocation.find(observer_config.observer); + if (it == new_allocation.end()) { + // No allocation for this track. + continue; + } + absl::optional elasticity = + observer_config.config.rate_elasticity; + if (elasticity == TrackRateElasticity::kCanConsumeExtraRate || + elasticity == TrackRateElasticity::kCanContributeAndConsume) { + DataRate allocated = DataRate::BitsPerSec(it->second); + if (allocated < upper_elastic_limit) { + allocated += + surplus * (observer_config.config.bitrate_priority / sum_demand); + if (allocated > upper_elastic_limit) + allocated = upper_elastic_limit; + } + DataRate max_bitrate = + DataRate::BitsPerSec(observer_config.config.max_bitrate_bps); + if (allocated > max_bitrate) { + allocated = max_bitrate; + } + // Save new allocated rate back to `new_allocation`. + it->second = allocated.bps(); + } + } + return new_allocation; +} + std::map AllocateBitrates( const std::vector& allocatable_tracks, - uint32_t bitrate) { + uint32_t bitrate, + DataRate upper_elastic_limit) { if (allocatable_tracks.empty()) return std::map(); @@ -342,8 +437,13 @@ std::map AllocateBitrates( // All observers will get their min bitrate plus a share of the rest. This // share is allocated to each observer based on its bitrate_priority. - if (bitrate <= sum_max_bitrates) - return NormalRateAllocation(allocatable_tracks, bitrate, sum_min_bitrates); + if (bitrate <= sum_max_bitrates) { + auto allocation = + NormalRateAllocation(allocatable_tracks, bitrate, sum_min_bitrates); + return MaybeApplySurplus(allocation, allocatable_tracks, + DataRate::BitsPerSec(bitrate), upper_elastic_limit) + .value_or(allocation); + } // All observers will get up to transmission_max_bitrate_multiplier_ x max. return MaxRateAllocation(allocatable_tracks, bitrate, sum_max_bitrates); @@ -351,7 +451,8 @@ std::map AllocateBitrates( } // namespace -BitrateAllocator::BitrateAllocator(LimitObserver* limit_observer) +BitrateAllocator::BitrateAllocator(LimitObserver* limit_observer, + DataRate upper_elastic_rate_limit) : limit_observer_(limit_observer), last_target_bps_(0), last_stable_target_bps_(0), @@ -360,7 +461,8 @@ BitrateAllocator::BitrateAllocator(LimitObserver* limit_observer) last_rtt_(0), last_bwe_period_ms_(1000), num_pause_events_(0), - last_bwe_log_time_(0) { + last_bwe_log_time_(0), + upper_elastic_rate_limit_(upper_elastic_rate_limit) { sequenced_checker_.Detach(); } @@ -394,9 +496,10 @@ void BitrateAllocator::OnNetworkEstimateChanged(TargetTransferRate msg) { last_bwe_log_time_ = now; } - auto allocation = AllocateBitrates(allocatable_tracks_, last_target_bps_); - auto stable_bitrate_allocation = - AllocateBitrates(allocatable_tracks_, last_stable_target_bps_); + auto allocation = AllocateBitrates(allocatable_tracks_, last_target_bps_, + upper_elastic_rate_limit_); + auto stable_bitrate_allocation = AllocateBitrates( + allocatable_tracks_, last_stable_target_bps_, DataRate::Zero()); for (auto& config : allocatable_tracks_) { uint32_t allocated_bitrate = allocation[config.observer]; @@ -439,6 +542,7 @@ void BitrateAllocator::OnNetworkEstimateChanged(TargetTransferRate msg) { if (allocated_bitrate > 0) config.media_ratio = MediaRatio(allocated_bitrate, protection_bitrate); config.allocated_bitrate_bps = allocated_bitrate; + config.last_used_bitrate = config.observer->GetUsedRate(); } UpdateAllocationLimits(); } @@ -461,9 +565,10 @@ void BitrateAllocator::AddObserver(BitrateAllocatorObserver* observer, if (last_target_bps_ > 0) { // Calculate a new allocation and update all observers. - auto allocation = AllocateBitrates(allocatable_tracks_, last_target_bps_); - auto stable_bitrate_allocation = - AllocateBitrates(allocatable_tracks_, last_stable_target_bps_); + auto allocation = AllocateBitrates(allocatable_tracks_, last_target_bps_, + upper_elastic_rate_limit_); + auto stable_bitrate_allocation = AllocateBitrates( + allocatable_tracks_, last_stable_target_bps_, DataRate::Zero()); for (auto& config : allocatable_tracks_) { uint32_t allocated_bitrate = allocation[config.observer]; uint32_t allocated_stable_bitrate = @@ -477,6 +582,7 @@ void BitrateAllocator::AddObserver(BitrateAllocatorObserver* observer, update.bwe_period = TimeDelta::Millis(last_bwe_period_ms_); uint32_t protection_bitrate = config.observer->OnBitrateUpdated(update); config.allocated_bitrate_bps = allocated_bitrate; + config.last_used_bitrate = config.observer->GetUsedRate(); if (allocated_bitrate > 0) config.media_ratio = MediaRatio(allocated_bitrate, protection_bitrate); } @@ -496,6 +602,78 @@ void BitrateAllocator::AddObserver(BitrateAllocatorObserver* observer, UpdateAllocationLimits(); } +bool BitrateAllocator::RecomputeAllocationIfNeeded() { + RTC_DCHECK_RUN_ON(&sequenced_checker_); + + if (upper_elastic_rate_limit_.IsZero()) { + return false; + } + + bool need_recompute = false; + bool has_contributor = false; + bool has_consumer = false; + + // Recomputes if there is a kCanContribute* track whose current bitrate usage + // has a jump (i.e., increase only) larger than 20% of allocated_bitrate. + constexpr double kUsageJumpRatioThreshold = 0.2; + for (auto& config : allocatable_tracks_) { + if (config.config.rate_elasticity.has_value()) { + const TrackRateElasticity elasticity = *config.config.rate_elasticity; + if (elasticity == TrackRateElasticity::kCanContributeUnusedRate || + elasticity == TrackRateElasticity::kCanContributeAndConsume) { + DataRate current_usage = + config.observer->GetUsedRate().value_or(DataRate::Zero()); + DataRate last_usage = + config.last_used_bitrate.value_or(DataRate::Zero()); + if (!last_usage.IsZero()) { + has_contributor = true; + DataRate recompute_threshold = + DataRate::BitsPerSec(config.LastAllocatedBitrate()) * + kUsageJumpRatioThreshold; + if (current_usage > last_usage + recompute_threshold) { + need_recompute = true; + } + } + } + if (elasticity == TrackRateElasticity::kCanConsumeExtraRate || + elasticity == TrackRateElasticity::kCanContributeAndConsume) { + has_consumer = true; + } + } + } + if (has_contributor == false || has_consumer == false) + return false; + + if (need_recompute && last_target_bps_ > 0) { + // Calculate a new allocation and update all observers. + auto allocation = AllocateBitrates(allocatable_tracks_, last_target_bps_, + upper_elastic_rate_limit_); + auto stable_bitrate_allocation = AllocateBitrates( + allocatable_tracks_, last_stable_target_bps_, DataRate::Zero()); + for (auto& config : allocatable_tracks_) { + DataRate allocated_bitrate = + DataRate::BitsPerSec(allocation[config.observer]); + DataRate allocated_stable_bitrate = + DataRate::BitsPerSec(stable_bitrate_allocation[config.observer]); + BitrateAllocationUpdate update; + update.target_bitrate = allocated_bitrate; + update.stable_target_bitrate = allocated_stable_bitrate; + update.packet_loss_ratio = last_fraction_loss_ / 256.0; + update.round_trip_time = TimeDelta::Millis(last_rtt_); + update.bwe_period = TimeDelta::Millis(last_bwe_period_ms_); + DataRate protection_bitrate = + DataRate::BitsPerSec(config.observer->OnBitrateUpdated(update)); + config.allocated_bitrate_bps = allocated_bitrate.bps(); + config.last_used_bitrate = config.observer->GetUsedRate(); + if (allocated_bitrate.bps() > 0) + config.media_ratio = + MediaRatio(allocated_bitrate.bps(), protection_bitrate.bps()); + } + UpdateAllocationLimits(); + } + return true; +} + void BitrateAllocator::UpdateAllocationLimits() { BitrateAllocationLimits limits; for (const auto& config : allocatable_tracks_) { @@ -590,4 +768,15 @@ uint32_t bitrate_allocator_impl::AllocatableTrack::MinBitrateWithHysteresis() return min_bitrate; } +// TODO(b/350555527): Remove after experiment +const char kElasticBitrateAllocator[] = "WebRTC-ElasticBitrateAllocation"; +DataRate GetElasticRateAllocationFieldTrialParameter( + const FieldTrialsView& field_trials) { + FieldTrialParameter elastic_rate_limit("upper_limit", + DataRate::Zero()); + std::string trial_string = field_trials.Lookup(kElasticBitrateAllocator); + ParseFieldTrial({&elastic_rate_limit}, trial_string); + return elastic_rate_limit.Get(); +} + } // namespace webrtc diff --git a/call/bitrate_allocator.h b/call/bitrate_allocator.h index 366ac3f82b..07d92d1df2 100644 --- a/call/bitrate_allocator.h +++ b/call/bitrate_allocator.h @@ -20,6 +20,7 @@ #include #include "api/call/bitrate_allocation.h" +#include "api/field_trials_view.h" #include "api/sequence_checker.h" #include "api/transport/network_types.h" #include "api/units/data_rate.h" @@ -92,12 +93,11 @@ struct AllocatableTrack { : observer(observer), config(allocation_config), allocated_bitrate_bps(-1), - last_used_bitrate_bps(-1), media_ratio(1.0) {} BitrateAllocatorObserver* observer; MediaStreamAllocationConfig config; int64_t allocated_bitrate_bps; - int64_t last_used_bitrate_bps; + absl::optional last_used_bitrate; double media_ratio; // Part of the total bitrate used for media [0.0, 1.0]. uint32_t LastAllocatedBitrate() const; @@ -122,7 +122,11 @@ class BitrateAllocator : public BitrateAllocatorInterface { virtual ~LimitObserver() = default; }; - explicit BitrateAllocator(LimitObserver* limit_observer); + // `upper_elastic_rate_limit` specifies the rate ceiling an observer can + // reach when unused bits are added. A value of zero disables borrowing of + // unused rates. + BitrateAllocator(LimitObserver* limit_observer, + DataRate upper_elastic_rate_limit); ~BitrateAllocator() override; void UpdateStartRate(uint32_t start_rate_bps); @@ -140,6 +144,11 @@ class BitrateAllocator : public BitrateAllocatorInterface { void AddObserver(BitrateAllocatorObserver* observer, MediaStreamAllocationConfig config) override; + // Checks and recomputes bitrate allocation if necessary (when an + // elastic/audio bitrate increases significantly). Returns whether there is an + // active contributing and active consuming stream. + bool RecomputeAllocationIfNeeded(); + // Removes a previously added observer, but will not trigger a new bitrate // allocation. void RemoveObserver(BitrateAllocatorObserver* observer) override; @@ -176,7 +185,12 @@ class BitrateAllocator : public BitrateAllocatorInterface { int num_pause_events_ RTC_GUARDED_BY(&sequenced_checker_); int64_t last_bwe_log_time_ RTC_GUARDED_BY(&sequenced_checker_); BitrateAllocationLimits current_limits_ RTC_GUARDED_BY(&sequenced_checker_); + const DataRate upper_elastic_rate_limit_ RTC_GUARDED_BY(&sequenced_checker_); }; +// TODO(b/350555527): Remove after experiment +DataRate GetElasticRateAllocationFieldTrialParameter( + const FieldTrialsView& field_trials); + } // namespace webrtc #endif // CALL_BITRATE_ALLOCATOR_H_ diff --git a/call/bitrate_allocator_unittest.cc b/call/bitrate_allocator_unittest.cc index 0ceacd0a85..9c6599a17b 100644 --- a/call/bitrate_allocator_unittest.cc +++ b/call/bitrate_allocator_unittest.cc @@ -16,6 +16,7 @@ #include "absl/strings/string_view.h" #include "system_wrappers/include/clock.h" +#include "test/explicit_key_value_config.h" #include "test/gmock.h" #include "test/gtest.h" @@ -85,6 +86,13 @@ class TestBitrateObserver : public BitrateAllocatorObserver { double protection_ratio_; }; +class TestContributingBitrateObserver : public TestBitrateObserver { + public: + TestContributingBitrateObserver() : rate_usage_(DataRate::Zero()) {} + absl::optional GetUsedRate() const override { return rate_usage_; } + DataRate rate_usage_; +}; + constexpr int64_t kDefaultProbingIntervalMs = 3000; const double kDefaultBitratePriority = 1.0; @@ -108,21 +116,24 @@ TargetTransferRate CreateTargetRateMessage(uint32_t target_bitrate_bps, class BitrateAllocatorTest : public ::testing::Test { protected: - BitrateAllocatorTest() : allocator_(new BitrateAllocator(&limit_observer_)) { + BitrateAllocatorTest() + : allocator_(new BitrateAllocator(&limit_observer_, DataRate::Zero())) { allocator_->OnNetworkEstimateChanged( CreateTargetRateMessage(300000u, 0, 0, kDefaultProbingIntervalMs)); } ~BitrateAllocatorTest() {} - void AddObserver(BitrateAllocatorObserver* observer, - uint32_t min_bitrate_bps, - uint32_t max_bitrate_bps, - uint32_t pad_up_bitrate_bps, - bool enforce_min_bitrate, - double bitrate_priority) { + void AddObserver( + BitrateAllocatorObserver* observer, + uint32_t min_bitrate_bps, + uint32_t max_bitrate_bps, + uint32_t pad_up_bitrate_bps, + bool enforce_min_bitrate, + double bitrate_priority, + absl::optional rate_elasticity = absl::nullopt) { allocator_->AddObserver( - observer, - {min_bitrate_bps, max_bitrate_bps, pad_up_bitrate_bps, - /* priority_bitrate */ 0, enforce_min_bitrate, bitrate_priority}); + observer, {min_bitrate_bps, max_bitrate_bps, pad_up_bitrate_bps, + /* priority_bitrate */ 0, enforce_min_bitrate, + bitrate_priority, rate_elasticity}); } MediaStreamAllocationConfig DefaultConfig() const { MediaStreamAllocationConfig default_config; @@ -134,6 +145,12 @@ class BitrateAllocatorTest : public ::testing::Test { default_config.bitrate_priority = kDefaultBitratePriority; return default_config; } + void ReconfigureAllocator(DataRate elastic_rate_upper_limit) { + allocator_.reset( + new BitrateAllocator(&limit_observer_, elastic_rate_upper_limit)); + allocator_->OnNetworkEstimateChanged( + CreateTargetRateMessage(300000u, 0, 0, kDefaultProbingIntervalMs)); + } NiceMock limit_observer_; std::unique_ptr allocator_; @@ -300,7 +317,7 @@ TEST_F(BitrateAllocatorTest, RemoveObserverTriggersLimitObserver) { class BitrateAllocatorTestNoEnforceMin : public ::testing::Test { protected: BitrateAllocatorTestNoEnforceMin() - : allocator_(new BitrateAllocator(&limit_observer_)) { + : allocator_(new BitrateAllocator(&limit_observer_, DataRate::Zero())) { allocator_->OnNetworkEstimateChanged( CreateTargetRateMessage(300000u, 0, 0, kDefaultProbingIntervalMs)); } @@ -1037,4 +1054,132 @@ TEST_F(BitrateAllocatorTest, PriorityRateThreeObserversTwoAllocatedToMax) { allocator_->RemoveObserver(&observer_high); } +TEST_F(BitrateAllocatorTest, ElasticRateAllocationCanBorrowUnsedRate) { + test::ExplicitKeyValueConfig field_trials( + "WebRTC-ElasticBitrateAllocation/upper_limit:200bps/"); + ReconfigureAllocator( + GetElasticRateAllocationFieldTrialParameter(field_trials)); + TestBitrateObserver observer_consume; + TestContributingBitrateObserver observer_contribute; + AddObserver(&observer_consume, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanConsumeExtraRate); + AddObserver(&observer_contribute, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanContributeUnusedRate); + + observer_contribute.rate_usage_ = DataRate::BitsPerSec(20); + allocator_->OnNetworkEstimateChanged( + CreateTargetRateMessage(100, 0, 0, kDefaultProbingIntervalMs)); + + // observer_contribute is allocated 50 but only used 20, so 30 is borrowed to + // observer_consume who gets 50+30=80. + EXPECT_EQ(80u, observer_consume.last_bitrate_bps_); + EXPECT_EQ(50u, observer_contribute.last_bitrate_bps_); + + allocator_->RemoveObserver(&observer_consume); + allocator_->RemoveObserver(&observer_contribute); +} + +TEST_F(BitrateAllocatorTest, ElasticRateAllocationDefaultsInactive) { + test::ExplicitKeyValueConfig field_trials(""); + ReconfigureAllocator( + GetElasticRateAllocationFieldTrialParameter(field_trials)); + TestBitrateObserver observer_consume; + TestContributingBitrateObserver observer_contribute; + AddObserver(&observer_consume, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanConsumeExtraRate); + AddObserver(&observer_contribute, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanContributeUnusedRate); + + observer_contribute.rate_usage_ = DataRate::BitsPerSec(20); + allocator_->OnNetworkEstimateChanged( + CreateTargetRateMessage(100, 0, 0, kDefaultProbingIntervalMs)); + + EXPECT_EQ(50u, observer_consume.last_bitrate_bps_); + EXPECT_EQ(50u, observer_contribute.last_bitrate_bps_); + + allocator_->RemoveObserver(&observer_consume); + allocator_->RemoveObserver(&observer_contribute); +} + +TEST_F(BitrateAllocatorTest, ElasticRateAllocationDontExceedMaxBitrate) { + test::ExplicitKeyValueConfig field_trials( + "WebRTC-ElasticBitrateAllocation/upper_limit:200bps/"); + ReconfigureAllocator( + GetElasticRateAllocationFieldTrialParameter(field_trials)); + TestBitrateObserver observer_consume; + TestContributingBitrateObserver observer_contribute; + AddObserver(&observer_consume, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanConsumeExtraRate); + AddObserver(&observer_contribute, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanContributeUnusedRate); + + observer_contribute.rate_usage_ = DataRate::BitsPerSec(20); + allocator_->OnNetworkEstimateChanged( + CreateTargetRateMessage(140, 0, 0, kDefaultProbingIntervalMs)); + + // observer_contribute is allocated 70 but only used 20, so 50 is borrowed to + // observer_consume who could get 70+50=120, but is capped by max-bitrate to + // 100. + EXPECT_EQ(100u, observer_consume.last_bitrate_bps_); + EXPECT_EQ(70u, observer_contribute.last_bitrate_bps_); + + allocator_->RemoveObserver(&observer_consume); + allocator_->RemoveObserver(&observer_contribute); +} + +TEST_F(BitrateAllocatorTest, ElasticRateAllocationStayWithinUpperLimit) { + uint32_t upper_limit = 70; + test::ExplicitKeyValueConfig field_trials( + "WebRTC-ElasticBitrateAllocation/upper_limit:" + + std::to_string(upper_limit) + "bps/"); + ReconfigureAllocator( + GetElasticRateAllocationFieldTrialParameter(field_trials)); + TestBitrateObserver observer_consume; + TestContributingBitrateObserver observer_contribute; + AddObserver(&observer_consume, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanConsumeExtraRate); + AddObserver(&observer_contribute, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanContributeUnusedRate); + + observer_contribute.rate_usage_ = DataRate::BitsPerSec(20); + allocator_->OnNetworkEstimateChanged( + CreateTargetRateMessage(100, 0, 0, kDefaultProbingIntervalMs)); + + // observer_contribute is allocated 50 but only used 20, so 30 is borrowed to + // observer_consume who could get 30+50=80, but is capped by upper_limit. + EXPECT_EQ(upper_limit, observer_consume.last_bitrate_bps_); + EXPECT_EQ(50u, observer_contribute.last_bitrate_bps_); + + allocator_->RemoveObserver(&observer_consume); + allocator_->RemoveObserver(&observer_contribute); +} + +TEST_F(BitrateAllocatorTest, ElasticRateAllocationDontReduceAllocation) { + uint32_t upper_limit = 70; + test::ExplicitKeyValueConfig field_trials( + "WebRTC-ElasticBitrateAllocation/upper_limit:" + + std::to_string(upper_limit) + "bps/"); + ReconfigureAllocator( + GetElasticRateAllocationFieldTrialParameter(field_trials)); + TestBitrateObserver observer_consume; + TestContributingBitrateObserver observer_contribute; + AddObserver(&observer_consume, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanConsumeExtraRate); + AddObserver(&observer_contribute, 10, 100, 0, false, 1.0, + TrackRateElasticity::kCanContributeUnusedRate); + + observer_contribute.rate_usage_ = DataRate::BitsPerSec(20); + allocator_->OnNetworkEstimateChanged( + CreateTargetRateMessage(200, 0, 0, kDefaultProbingIntervalMs)); + + // observer_contribute is allocated 100 but only used 20, so 80 can be + // borrowed to observer_consume. But observer_consume already has 100 + // (above upper_limit), so no bitrate is borrowed. + EXPECT_EQ(100u, observer_consume.last_bitrate_bps_); + EXPECT_EQ(100u, observer_contribute.last_bitrate_bps_); + + allocator_->RemoveObserver(&observer_consume); + allocator_->RemoveObserver(&observer_contribute); +} + } // namespace webrtc diff --git a/call/call.cc b/call/call.cc index 066223f01e..953fdda399 100644 --- a/call/call.cc +++ b/call/call.cc @@ -29,6 +29,7 @@ #include "api/sequence_checker.h" #include "api/task_queue/pending_task_safety_flag.h" #include "api/transport/network_control.h" +#include "api/units/time_delta.h" #include "audio/audio_receive_stream.h" #include "audio/audio_send_stream.h" #include "audio/audio_state.h" @@ -420,6 +421,7 @@ class Call final : public webrtc::Call, ReceiveSideCongestionController receive_side_cc_; RepeatingTaskHandle receive_side_cc_periodic_task_; + RepeatingTaskHandle elastic_bandwidth_allocation_task_; const std::unique_ptr receive_time_calculator_; @@ -660,7 +662,9 @@ Call::Call(CallConfig config, : nullptr), num_cpu_cores_(CpuInfo::DetectNumberOfCores()), call_stats_(new CallStats(&env_.clock(), worker_thread_)), - bitrate_allocator_(new BitrateAllocator(this)), + bitrate_allocator_(new BitrateAllocator( + this, + GetElasticRateAllocationFieldTrialParameter(env_.field_trials()))), config_(std::move(config)), audio_network_state_(kNetworkDown), video_network_state_(kNetworkDown), @@ -697,6 +701,22 @@ Call::Call(CallConfig config, worker_thread_, [receive_side_cc] { return receive_side_cc->MaybeProcess(); }, TaskQueueBase::DelayPrecision::kLow, &env_.clock()); + + // TODO(b/350555527): Remove after experiment + if (GetElasticRateAllocationFieldTrialParameter(env_.field_trials()) != + DataRate::Zero()) { + elastic_bandwidth_allocation_task_ = RepeatingTaskHandle::Start( + worker_thread_, + [this] { + TimeDelta next_schedule_interval = TimeDelta::Millis(25); + if (bitrate_allocator_) { + if (!bitrate_allocator_->RecomputeAllocationIfNeeded()) + next_schedule_interval = TimeDelta::Millis(300); + } + return next_schedule_interval; + }, + TaskQueueBase::DelayPrecision::kLow, &env_.clock()); + } } Call::~Call() { @@ -709,6 +729,7 @@ Call::~Call() { RTC_CHECK(video_receive_streams_.empty()); receive_side_cc_periodic_task_.Stop(); + elastic_bandwidth_allocation_task_.Stop(); call_stats_->DeregisterStatsObserver(&receive_side_cc_); send_stats_.SetFirstPacketTime(transport_send_->GetFirstPacketTime()); diff --git a/experiments/field_trials.py b/experiments/field_trials.py index be0f62db22..dda60dca6b 100755 --- a/experiments/field_trials.py +++ b/experiments/field_trials.py @@ -74,6 +74,9 @@ ACTIVE_FIELD_TRIALS: FrozenSet[FieldTrial] = frozenset([ FieldTrial('WebRTC-DisableRtxRateLimiter', 42225500, date(2024, 4, 1)), + FieldTrial('WebRTC-ElasticBitrateAllocation', + 350555527, + date(2025, 3, 1)), FieldTrial('WebRTC-EncoderDataDumpDirectory', 296242528, date(2024, 4, 1)),