diff --git a/pkg/gcc/loss_based_bwe.go b/pkg/gcc/loss_based_bwe.go index 9898737c..271c8c0f 100644 --- a/pkg/gcc/loss_based_bwe.go +++ b/pkg/gcc/loss_based_bwe.go @@ -42,11 +42,13 @@ type lossBasedBandwidthEstimator struct { log logging.LeveledLogger } -func newLossBasedBWE(initialBitrate int, loggerFactory logging.LoggerFactory) *lossBasedBandwidthEstimator { +func newLossBasedBWE( + initialBitrate, minBitrate, maxBitrate int, loggerFactory logging.LoggerFactory, +) *lossBasedBandwidthEstimator { return &lossBasedBandwidthEstimator{ lock: sync.Mutex{}, - maxBitrate: 100_000_000, // 100 mbit - minBitrate: 100_000, // 100 kbit + maxBitrate: maxBitrate, + minBitrate: minBitrate, bitrate: initialBitrate, averageLoss: 0, lastLossUpdate: time.Time{}, diff --git a/pkg/gcc/loss_based_bwe_bounds_test.go b/pkg/gcc/loss_based_bwe_bounds_test.go new file mode 100644 index 00000000..9d1bd38b --- /dev/null +++ b/pkg/gcc/loss_based_bwe_bounds_test.go @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package gcc + +import ( + "testing" + "time" + + "github.com/pion/interceptor/internal/cc" + "github.com/pion/logging" + "github.com/stretchr/testify/assert" +) + +// The loss-based controller must respect a caller-supplied minimum +// bitrate under sustained loss instead of decaying to a hardcoded +// 100 kbps floor. Since the combined SendSideBWE estimate is +// min(delayEstimate, lossEstimate), a floor violation here drags the +// whole estimate below the configured SendSideBWEMinBitrate. +func TestLossBasedBWERespectsConfiguredMinBitrate(t *testing.T) { + const ( + initialBitrate = 2_500_000 + minimumBitrate = 500_000 + maximumBitrate = 100_000_000 + ) + estimator := newLossBasedBWE( + initialBitrate, minimumBitrate, maximumBitrate, logging.NewDefaultLoggerFactory(), + ) + + // Acknowledgments with a zero Arrival time count as lost packets. + lost := make([]cc.Acknowledgment, 10) + + for i := 0; i < 200; i++ { + // Rewind the rate-limit windows so every iteration is allowed + // to apply a decrease, simulating a long stretch of loss. + estimator.lock.Lock() + estimator.lastDecrease = time.Now().Add(-time.Second) + estimator.lastLossUpdate = time.Now().Add(-time.Second) + estimator.lock.Unlock() + estimator.updateLossEstimate(lost) + } + + got := estimator.getEstimate(maximumBitrate).TargetBitrate + assert.GreaterOrEqual(t, got, minimumBitrate, + "loss controller decayed below the configured minimum") + assert.Less(t, got, initialBitrate, + "sustained loss should have decreased the estimate") +} + +// The configured maximum must bound the increase path the same way. +func TestLossBasedBWERespectsConfiguredMaxBitrate(t *testing.T) { + const ( + initialBitrate = 9_000_000 + minimumBitrate = 500_000 + maximumBitrate = 10_000_000 + ) + estimator := newLossBasedBWE( + initialBitrate, minimumBitrate, maximumBitrate, logging.NewDefaultLoggerFactory(), + ) + + clean := make([]cc.Acknowledgment, 10) + for i := range clean { + clean[i].Arrival = time.Now() + } + + for i := 0; i < 200; i++ { + estimator.lock.Lock() + estimator.lastIncrease = time.Now().Add(-time.Second) + estimator.lastLossUpdate = time.Now().Add(-time.Second) + estimator.lock.Unlock() + estimator.updateLossEstimate(clean) + } + + got := estimator.getEstimate(maximumBitrate * 2).TargetBitrate + assert.LessOrEqual(t, got, maximumBitrate, + "loss controller grew above the configured maximum") + assert.Greater(t, got, initialBitrate, + "clean feedback should have increased the estimate") +} diff --git a/pkg/gcc/send_side_bwe.go b/pkg/gcc/send_side_bwe.go index c996ce2a..e014e9e8 100644 --- a/pkg/gcc/send_side_bwe.go +++ b/pkg/gcc/send_side_bwe.go @@ -136,7 +136,7 @@ func NewSendSideBWE(opts ...Option) (*SendSideBWE, error) { if send.pacer == nil { send.pacer = newLeakyBucketPacer(send.latestBitrate, send.loggerFactory) } - send.lossController = newLossBasedBWE(send.latestBitrate, send.loggerFactory) + send.lossController = newLossBasedBWE(send.latestBitrate, send.minBitrate, send.maxBitrate, send.loggerFactory) send.delayController = newDelayController(delayControllerConfig{ nowFn: time.Now, initialBitrate: send.latestBitrate,