Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions pkg/gcc/loss_based_bwe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
Expand Down
79 changes: 79 additions & 0 deletions pkg/gcc/loss_based_bwe_bounds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// 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")
}
2 changes: 1 addition & 1 deletion pkg/gcc/send_side_bwe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down