Skip to content

Commit 5dc650b

Browse files
committed
add downgrade decay and emergency drop
1 parent 88d9992 commit 5dc650b

File tree

2 files changed

+112
-35
lines changed

2 files changed

+112
-35
lines changed

pulsebeam/src/bitrate.rs

Lines changed: 110 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,12 @@ pub struct BitrateControllerConfig {
55
pub min_bitrate: Bitrate,
66
pub max_bitrate: Bitrate,
77
pub default_bitrate: Bitrate,
8-
9-
// Safety headroom (e.g. 0.85 of estimate)
108
pub headroom_factor: f64,
11-
12-
// Immediate downgrade if target drops below this % of current (e.g. 0.90)
13-
pub downgrade_threshold: f64,
14-
15-
// Number of ticks to hold a higher target before committing
16-
// Since input is already smoothed, 5-8 ticks is usually enough stability.
9+
// Max percentage to decrease per tick (e.g. 0.95 = 5% max drop)
10+
pub max_decay_factor: f64,
11+
// Drop immediately if target is below this % of current (e.g. 0.50)
12+
pub emergency_drop_threshold: f64,
1713
pub required_up_samples: usize,
18-
19-
// Step size to prevent micro-fluctuations (e.g. 50kbps)
2014
pub quantization_step: Bitrate,
2115
}
2216

@@ -27,8 +21,9 @@ impl Default for BitrateControllerConfig {
2721
max_bitrate: Bitrate::mbps(5),
2822
default_bitrate: Bitrate::kbps(300),
2923
headroom_factor: 1.0,
30-
downgrade_threshold: 0.90,
31-
required_up_samples: 1,
24+
max_decay_factor: 0.95,
25+
emergency_drop_threshold: 0.50,
26+
required_up_samples: 5,
3227
quantization_step: Bitrate::kbps(10),
3328
}
3429
}
@@ -44,8 +39,7 @@ pub struct BitrateController {
4439

4540
impl Default for BitrateController {
4641
fn default() -> Self {
47-
let config = BitrateControllerConfig::default();
48-
Self::new(config)
42+
Self::new(BitrateControllerConfig::default())
4943
}
5044
}
5145

@@ -60,12 +54,9 @@ impl BitrateController {
6054
}
6155

6256
pub fn update(&mut self, available_bandwidth: Bitrate) -> Bitrate {
63-
// 1. Give headroom
6457
let raw_bw = available_bandwidth.as_f64();
6558
let safe_bw = raw_bw * self.config.headroom_factor;
6659

67-
// 2. Quantize (Floor)
68-
// This stabilizes the input "jitter" into steps
6960
let step = self.config.quantization_step.as_f64();
7061
let quantized_target = (safe_bw / step).floor() * step;
7162

@@ -74,30 +65,23 @@ impl BitrateController {
7465
self.config.max_bitrate.as_f64(),
7566
);
7667

77-
// 3. Logic
78-
79-
// A: Immediate Downgrade
80-
// If external BWE says we dropped significant bandwidth, believe it immediately.
81-
if target < self.current_bitrate * self.config.downgrade_threshold {
82-
self.current_bitrate = target;
68+
if target < self.current_bitrate {
8369
self.reset_stability();
84-
return self.current();
85-
}
8670

87-
// B: Debounced Upgrade
88-
// If target is higher, wait for N samples to confirm it's not a temporary spike.
89-
if target > self.current_bitrate {
71+
if target < self.current_bitrate * self.config.emergency_drop_threshold {
72+
self.current_bitrate = target;
73+
} else {
74+
let decay_limit = self.current_bitrate * self.config.max_decay_factor;
75+
self.current_bitrate = target.max(decay_limit);
76+
}
77+
} else if target > self.current_bitrate {
9078
match self.pending_target {
9179
Some(mut pending) => {
92-
// Track the "Floor" of the new bandwidth during the window.
93-
// If bandwidth dips during the wait, lower our expectations
94-
// to that dip, but keep the counter running.
9580
if target < pending {
9681
pending = target;
9782
self.pending_target = Some(pending);
9883
}
9984

100-
// If the dip made it drop below current, the upgrade is invalid.
10185
if pending <= self.current_bitrate {
10286
self.reset_stability();
10387
} else {
@@ -117,7 +101,6 @@ impl BitrateController {
117101
self.reset_stability();
118102
}
119103
} else {
120-
// Target is roughly equal or slightly below (within threshold). Stable.
121104
self.reset_stability();
122105
}
123106

@@ -133,3 +116,97 @@ impl BitrateController {
133116
Bitrate::from(self.current_bitrate)
134117
}
135118
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use super::*;
123+
124+
#[test]
125+
fn test_steady_state() {
126+
let mut ctrl = BitrateController::default();
127+
let bw = Bitrate::kbps(500);
128+
129+
// Stabilize at 500
130+
for _ in 0..10 {
131+
ctrl.update(bw);
132+
}
133+
assert_eq!(ctrl.current().as_f64(), 500_000.0);
134+
}
135+
136+
#[test]
137+
fn test_transient_drop_filtering() {
138+
let mut ctrl = BitrateController::default();
139+
// Start stable at 1000 kbps
140+
for _ in 0..10 {
141+
ctrl.update(Bitrate::kbps(1000));
142+
}
143+
assert_eq!(ctrl.current().as_f64(), 1_000_000.0);
144+
145+
// One tick drop to 300 kbps (simulating bad estimate/jitter)
146+
// With decay 0.95, should only drop to ~950k, not 300k
147+
let res = ctrl.update(Bitrate::kbps(300));
148+
assert!(res.as_f64() > 900_000.0);
149+
assert!(res.as_f64() < 1_000_000.0);
150+
151+
// Input recovers immediately
152+
let res = ctrl.update(Bitrate::kbps(1000));
153+
154+
// Should climb back up (or stay high) rather than waiting for full debounce
155+
// because we never actually dropped low.
156+
assert!(res.as_f64() > 900_000.0);
157+
}
158+
159+
#[test]
160+
fn test_real_congestion_decay() {
161+
let mut ctrl = BitrateController::default();
162+
for _ in 0..10 {
163+
ctrl.update(Bitrate::kbps(1000));
164+
}
165+
166+
// Sustained drop to 500
167+
for _ in 0..10 {
168+
ctrl.update(Bitrate::kbps(500));
169+
}
170+
171+
// Should be sliding down
172+
let current = ctrl.current().as_f64();
173+
assert!(current < 1_000_000.0);
174+
assert!(current > 500_000.0);
175+
}
176+
177+
#[test]
178+
fn test_emergency_drop() {
179+
let mut ctrl = BitrateController::default();
180+
for _ in 0..10 {
181+
ctrl.update(Bitrate::kbps(2000));
182+
}
183+
184+
// Massive drop (e.g. WiFi loss), < 50%
185+
let res = ctrl.update(Bitrate::kbps(300));
186+
187+
// Should snap immediately
188+
assert_eq!(res.as_f64(), 300_000.0);
189+
}
190+
191+
#[test]
192+
fn test_debounce_up() {
193+
let config = BitrateControllerConfig {
194+
required_up_samples: 3,
195+
..Default::default()
196+
};
197+
let mut ctrl = BitrateController::new(config);
198+
199+
// Start 300
200+
ctrl.update(Bitrate::kbps(300));
201+
202+
// Jump to 500
203+
ctrl.update(Bitrate::kbps(500)); // Tick 1 (Pending)
204+
assert_eq!(ctrl.current().as_f64(), 300_000.0);
205+
206+
ctrl.update(Bitrate::kbps(500)); // Tick 2
207+
assert_eq!(ctrl.current().as_f64(), 300_000.0);
208+
209+
ctrl.update(Bitrate::kbps(500)); // Tick 3 (Commit)
210+
assert_eq!(ctrl.current().as_f64(), 500_000.0);
211+
}
212+
}

pulsebeam/src/rtp/monitor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,9 @@ impl BitrateEstimate {
351351
max_bitrate: Bitrate::mbps(5),
352352
default_bitrate: Bitrate::kbps(100),
353353
headroom_factor: 1.0,
354-
downgrade_threshold: 0.95,
355354
required_up_samples: 1,
356355
quantization_step: Bitrate::kbps(10),
356+
..Default::default()
357357
};
358358

359359
let controller = BitrateController::new(config);
@@ -1219,7 +1219,7 @@ mod test {
12191219
// The BitrateController should have completely ignored the bump.
12201220
// It might have risen by 1 quantization step (10kbps), but not more.
12211221
assert!(
1222-
(after_spike - baseline).abs() < 50_000.0,
1222+
(after_spike - baseline).abs() < 5.0,
12231223
"Estimator flapped due to keyframe!"
12241224
);
12251225
}

0 commit comments

Comments
 (0)