1pub const LADDER: [u32; 8] = [12, 16, 20, 24, 32, 40, 48, 64];
9pub const START_KBPS: u32 = 32;
10const DOWN_AT_PCT: f32 = 4.0;
12const CLEAN_PCT: f32 = 1.0;
14const CLIMB_AFTER_SECS: u32 = 8;
16const HOLD_SECS: u32 = 3;
19const FEC_MIN_PCT: u32 = 5;
21const FEC_MAX_PCT: u32 = 30;
22
23pub struct RateControl {
24 rung: usize,
25 clean_secs: u32,
26 hold: u32,
27 last_sent: u64,
28 last_lost: u64,
29 loss_avg: f32,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct Setting {
36 pub kbps: u32,
37 pub fec_pct: u32,
38}
39
40impl RateControl {
41 pub fn new() -> Self {
42 let rung = LADDER.iter().position(|&k| k == START_KBPS).unwrap_or(0);
43 Self { rung, clean_secs: 0, hold: 0, last_sent: 0, last_lost: 0, loss_avg: 0.0 }
44 }
45
46 pub fn prime(&mut self, sent_packets: u64, lost_packets: u64) {
48 self.last_sent = sent_packets;
49 self.last_lost = lost_packets;
50 }
51
52 pub fn kbps(&self) -> u32 {
53 LADDER[self.rung]
54 }
55
56 pub fn loss_pct(&self) -> f32 {
58 self.loss_avg
59 }
60
61 pub fn observe(&mut self, sent_packets: u64, lost_packets: u64) -> Option<Setting> {
64 let sent = sent_packets.saturating_sub(self.last_sent);
65 let lost = lost_packets.saturating_sub(self.last_lost);
66 self.last_sent = sent_packets;
67 self.last_lost = lost_packets;
68 if sent < 10 {
70 return None;
71 }
72 let loss = 100.0 * lost as f32 / sent as f32;
73 self.loss_avg += (loss - self.loss_avg) * 0.3;
74 let before = self.setting();
75 if self.hold > 0 {
76 self.hold -= 1;
77 }
78 if loss >= DOWN_AT_PCT {
79 self.clean_secs = 0;
80 if self.hold == 0 && self.rung > 0 {
81 self.rung -= 1;
82 self.hold = HOLD_SECS;
83 }
84 } else if loss < CLEAN_PCT {
85 self.clean_secs += 1;
86 if self.clean_secs >= CLIMB_AFTER_SECS && self.rung + 1 < LADDER.len() {
87 self.rung += 1;
88 self.clean_secs = 0;
89 }
90 } else {
91 self.clean_secs = 0;
92 }
93 let after = self.setting();
94 (after != before).then_some(after)
95 }
96
97 fn setting(&self) -> Setting {
98 let fec = (self.loss_avg.round() as u32).clamp(FEC_MIN_PCT, FEC_MAX_PCT);
99 Setting { kbps: self.kbps(), fec_pct: fec }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn loss_steps_down_at_once_and_a_clean_link_climbs_slowly() {
109 let mut rc = RateControl::new();
110 assert_eq!(rc.kbps(), 32);
111 assert_eq!(rc.observe(50, 5).map(|s| s.kbps), Some(24));
113 assert_eq!(rc.observe(100, 10), None);
114 assert_eq!(rc.observe(150, 15), None);
115 assert_eq!(rc.observe(200, 20).map(|s| s.kbps), Some(20));
116 let mut sent = 200;
118 let mut changes = vec![];
119 for _ in 0..(CLIMB_AFTER_SECS * 4) {
120 sent += 50;
121 if let Some(s) = rc.observe(sent, 20) {
122 changes.push(s.kbps);
123 }
124 }
125 assert_eq!(changes, vec![24, 32, 40, 48]);
126 }
127
128 #[test]
129 fn the_fec_budget_follows_the_loss_and_a_quiet_second_is_ignored() {
130 let mut rc = RateControl::new();
131 assert_eq!(rc.observe(5, 0), None);
132 assert_eq!(rc.observe(55, 10).map(|s| s.fec_pct), Some(6));
133 let mut sent = 55;
134 let mut last = None;
135 for _ in 0..6 {
136 sent += 50;
137 if let Some(s) = rc.observe(sent, 10 + (sent - 55) / 5) {
138 last = Some(s);
139 }
140 }
141 assert_eq!(last.map(|s| s.fec_pct), Some(18));
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct Rung {
149 pub kbps: u32,
150 pub width: u32,
151 pub height: u32,
152 pub fps: u32,
153}
154
155pub static CAMERA_LADDER: [Rung; 7] = [
156 Rung { kbps: 150, width: 320, height: 180, fps: 15 },
157 Rung { kbps: 300, width: 480, height: 270, fps: 15 },
158 Rung { kbps: 500, width: 640, height: 360, fps: 24 },
159 Rung { kbps: 800, width: 640, height: 360, fps: 30 },
160 Rung { kbps: 1200, width: 960, height: 540, fps: 30 },
161 Rung { kbps: 1800, width: 1280, height: 720, fps: 30 },
162 Rung { kbps: 2500, width: 1280, height: 720, fps: 30 },
163];
164pub static SCREEN_LADDER: [Rung; 9] = [
166 Rung { kbps: 300, width: 0, height: 720, fps: 5 },
167 Rung { kbps: 600, width: 0, height: 1080, fps: 8 },
168 Rung { kbps: 1000, width: 0, height: 1080, fps: 10 },
169 Rung { kbps: 1500, width: 0, height: 1440, fps: 15 },
170 Rung { kbps: 2500, width: 0, height: 0, fps: 30 },
171 Rung { kbps: 4000, width: 0, height: 0, fps: 30 },
172 Rung { kbps: 6000, width: 0, height: 1080, fps: 60 },
173 Rung { kbps: 10000, width: 0, height: 0, fps: 60 },
174 Rung { kbps: 16000, width: 0, height: 0, fps: 60 },
175];
176pub const CAMERA_START: usize = 4;
179pub const CAMERA_RELAY_CAP: usize = 4;
180pub const SCREEN_START: usize = 3;
181pub const SCREEN_RELAY_CAP: usize = 3;
182
183const VIDEO_DOWN_AT_PCT: f32 = 2.0;
186const VIDEO_RTT_BLOAT_MS: u32 = 50;
188const VIDEO_CLIMB_AFTER_SECS: u32 = 5;
191
192#[derive(Debug, Clone, Copy, Default)]
194pub struct VideoObservation {
195 pub sent_packets: u64,
196 pub lost_packets: u64,
197 pub rtt_ms: u32,
198 pub audio_send_dropped: u64,
200 pub backlog: bool,
202}
203
204pub struct VideoRate {
205 ladder: &'static [Rung],
206 camera: bool,
207 rung: usize,
208 cap: usize,
209 clean_secs: u32,
210 hold: u32,
211 last_sent: u64,
212 last_lost: u64,
213 last_audio_dropped: u64,
214 min_rtt: u32,
215 primed: bool,
216 pin: Option<usize>,
218 fps: Option<u32>,
220}
221
222impl VideoRate {
223 pub fn camera(cap: usize) -> Self {
224 Self::new(&CAMERA_LADDER, true, CAMERA_START, cap)
225 }
226
227 pub fn screen(cap: usize) -> Self {
228 Self::new(&SCREEN_LADDER, false, SCREEN_START, cap)
229 }
230
231 pub fn new(ladder: &'static [Rung], camera: bool, start: usize, cap: usize) -> Self {
232 let cap = cap.min(ladder.len() - 1);
233 Self {
234 ladder,
235 camera,
236 rung: start.min(cap),
237 cap,
238 clean_secs: 0,
239 hold: 0,
240 last_sent: 0,
241 last_lost: 0,
242 last_audio_dropped: 0,
243 min_rtt: u32::MAX,
244 primed: false,
245 pin: None,
246 fps: None,
247 }
248 }
249
250 pub fn rung(&self) -> Rung {
251 let mut r = self.ladder[self.rung];
252 if let Some(fps) = self.fps {
253 r.fps = fps;
254 }
255 r
256 }
257
258 pub fn rungs(&self) -> usize {
259 self.ladder.len()
260 }
261
262 pub fn set_pin(&mut self, pin: Option<usize>) -> Option<Rung> {
266 let before = self.rung;
267 self.pin = pin.map(|p| p.min(self.ladder.len() - 1));
268 if let Some(p) = self.pin {
269 self.rung = p;
270 } else if self.rung > self.cap {
271 self.rung = self.cap;
272 }
273 self.clean_secs = 0;
274 (self.rung != before || pin.is_some()).then(|| self.rung())
275 }
276
277 pub fn set_fps(&mut self, fps: Option<u32>) -> Rung {
278 self.fps = fps;
279 self.rung()
280 }
281
282 pub fn is_camera(&self) -> bool {
283 self.camera
284 }
285
286 pub fn set_cap(&mut self, cap: usize) -> Option<Rung> {
290 self.cap = cap.min(self.ladder.len() - 1);
291 self.min_rtt = u32::MAX;
292 if self.pin.is_none() && self.rung > self.cap {
293 self.rung = self.cap;
294 return Some(self.rung());
295 }
296 None
297 }
298
299 pub fn observe(&mut self, obs: VideoObservation) -> Option<Rung> {
301 let sent = obs.sent_packets.saturating_sub(self.last_sent);
302 let lost = obs.lost_packets.saturating_sub(self.last_lost);
303 let audio_dropped = obs.audio_send_dropped.saturating_sub(self.last_audio_dropped);
304 self.last_sent = obs.sent_packets;
305 self.last_lost = obs.lost_packets;
306 self.last_audio_dropped = obs.audio_send_dropped;
307 if !self.primed {
308 self.primed = true;
310 return None;
311 }
312 if obs.rtt_ms > 0 {
313 self.min_rtt = self.min_rtt.min(obs.rtt_ms);
314 }
315 let loss = if sent >= 10 { 100.0 * lost as f32 / sent as f32 } else { 0.0 };
316 let bloated = self.min_rtt != u32::MAX && obs.rtt_ms > self.min_rtt + VIDEO_RTT_BLOAT_MS;
317 let trouble = if self.pin.is_some() {
319 audio_dropped > 0
320 } else {
321 loss >= VIDEO_DOWN_AT_PCT || audio_dropped > 0 || bloated || obs.backlog
322 };
323 let ceiling = self.pin.unwrap_or(self.cap);
324 let before = self.rung;
325 if self.hold > 0 {
326 self.hold -= 1;
327 }
328 if trouble {
329 self.clean_secs = 0;
330 if self.hold == 0 && self.rung > 0 {
331 self.rung -= 1;
332 self.hold = HOLD_SECS;
333 }
334 } else if loss < CLEAN_PCT {
335 self.clean_secs += 1;
336 if self.clean_secs >= VIDEO_CLIMB_AFTER_SECS && self.rung < ceiling {
337 self.rung += 1;
338 self.clean_secs = 0;
339 }
340 } else {
341 self.clean_secs = 0;
342 }
343 (self.rung != before).then(|| self.rung())
344 }
345}
346
347#[cfg(test)]
348mod video_tests {
349 use super::*;
350
351 fn clean(sent: u64) -> VideoObservation {
352 VideoObservation { sent_packets: sent, lost_packets: 0, rtt_ms: 40, audio_send_dropped: 0, backlog: false }
353 }
354
355 #[test]
356 fn a_clean_link_climbs_to_the_cap_and_no_further() {
357 let mut r = VideoRate::camera(CAMERA_RELAY_CAP);
358 assert_eq!(r.rung(), CAMERA_LADDER[4]);
359 let mut sent = 0;
360 let mut changes = 0;
361 for _ in 0..40 {
362 sent += 100;
363 if r.observe(clean(sent)).is_some() {
364 changes += 1;
365 }
366 }
367 assert_eq!(changes, 0, "a relayed camera never passes the relay cap");
368 assert_eq!(r.set_cap(6), None);
369 for _ in 0..VIDEO_CLIMB_AFTER_SECS {
370 sent += 100;
371 r.observe(clean(sent));
372 }
373 assert_eq!(r.rung(), CAMERA_LADDER[5]);
374 }
375
376 #[test]
377 fn audio_drops_and_bufferbloat_step_down_at_once_and_a_cap_pulls_down() {
378 let mut r = VideoRate::camera(6);
379 r.observe(clean(100));
380 r.observe(clean(200));
381 let dropped = VideoObservation { sent_packets: 300, lost_packets: 0, rtt_ms: 40, audio_send_dropped: 3, backlog: false };
382 assert_eq!(r.observe(dropped), Some(CAMERA_LADDER[3]));
383 let bloat = VideoObservation { sent_packets: 400, lost_packets: 0, rtt_ms: 140, audio_send_dropped: 3, backlog: false };
385 assert_eq!(r.observe(bloat), None);
386 let mut sent = 400;
387 for _ in 0..HOLD_SECS {
388 sent += 100;
389 r.observe(VideoObservation { sent_packets: sent, lost_packets: 0, rtt_ms: 140, audio_send_dropped: 3, backlog: false });
390 }
391 assert_eq!(r.rung(), CAMERA_LADDER[2]);
392 assert_eq!(r.set_cap(0), Some(CAMERA_LADDER[0]));
393 }
394
395 #[test]
396 fn a_pinned_rung_ignores_loss_but_still_yields_to_the_voice() {
397 let mut r = VideoRate::camera(6);
398 assert_eq!(r.set_pin(Some(6)), Some(CAMERA_LADDER[6]));
399 r.observe(clean(100));
400 let lossy = VideoObservation { sent_packets: 200, lost_packets: 20, rtt_ms: 400, audio_send_dropped: 0, backlog: true };
401 assert_eq!(r.observe(lossy), None);
402 let voice = VideoObservation { sent_packets: 300, lost_packets: 20, rtt_ms: 400, audio_send_dropped: 1, backlog: true };
403 assert_eq!(r.observe(voice), Some(CAMERA_LADDER[5]));
404 let mut sent = 300;
405 for _ in 0..(HOLD_SECS + VIDEO_CLIMB_AFTER_SECS) {
406 sent += 100;
407 r.observe(VideoObservation { sent_packets: sent, lost_packets: 20, rtt_ms: 400, audio_send_dropped: 1, backlog: true });
408 }
409 assert_eq!(r.rung(), CAMERA_LADDER[6], "climbs back to the pin once the voice is fine");
410 assert_eq!(r.set_fps(Some(15)).fps, 15);
411 assert_eq!(r.set_pin(None), None, "unpinning where the ladder already stands changes nothing");
412 let mut relayed = VideoRate::screen(SCREEN_RELAY_CAP);
414 assert_eq!(relayed.set_pin(Some(8)), Some(SCREEN_LADDER[8]));
415 assert_eq!(relayed.set_cap(SCREEN_RELAY_CAP), None);
416 assert_eq!(relayed.rung(), SCREEN_LADDER[8]);
417 assert_eq!(relayed.set_pin(None), Some(SCREEN_LADDER[SCREEN_RELAY_CAP]), "letting go drops back under the cap");
418 }
419
420 #[test]
421 fn two_percent_loss_is_enough_for_video() {
422 let mut r = VideoRate::screen(5);
423 r.observe(clean(100));
424 let lossy = VideoObservation { sent_packets: 200, lost_packets: 3, rtt_ms: 40, audio_send_dropped: 0, backlog: false };
425 assert_eq!(r.observe(lossy), Some(SCREEN_LADDER[2]));
426 }
427}