Skip to main content

vector_calls/
rate.rs

1//! Bitrate control for the encoder, fed by QUIC's own loss accounting: every
2//! datagram rides in a packet the peer acknowledges, so the sender learns what
3//! went missing without a report channel. A lossy link steps the ladder down at
4//! once; a clean one climbs back a rung at a time.
5
6/// Bitrates the encoder may use, in kbit/s. Opus picks the audio bandwidth to
7/// match: narrowband at the bottom, fullband from the middle up.
8pub const LADDER: [u32; 8] = [12, 16, 20, 24, 32, 40, 48, 64];
9pub const START_KBPS: u32 = 32;
10/// Loss in one second that costs a rung.
11const DOWN_AT_PCT: f32 = 4.0;
12/// Loss a second must stay under to count towards climbing.
13const CLEAN_PCT: f32 = 1.0;
14/// Clean seconds in a row before the next rung up.
15const CLIMB_AFTER_SECS: u32 = 8;
16/// Seconds a rung down is held before loss may take another. Loss is reported
17/// about a round trip late, so a step needs time to show in the numbers.
18const HOLD_SECS: u32 = 3;
19/// The FEC budget follows the loss actually seen, within these bounds.
20const 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 smoothed over a few seconds, for the FEC budget.
30    loss_avg: f32,
31}
32
33/// What the encoder should be set to after an observation.
34#[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    /// Sets the baseline so the first observation covers only the next second.
47    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    /// Loss seen in the last observed second, in percent.
57    pub fn loss_pct(&self) -> f32 {
58        self.loss_avg
59    }
60
61    /// Once a second, with the path's cumulative packet counts. Returns a new
62    /// setting when the encoder should change.
63    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        // Too few packets to judge: a call sends fifty a second.
69        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        // 10% loss for one second: one rung, then held.
112        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        // Clean seconds climb one rung every CLIMB_AFTER_SECS.
117        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/// One step of a video ladder. `height` caps the picture: a camera is asked for
146/// `width` by `height`, a screen is scaled to at most `height` tall (0 keeps it native).
147#[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];
164/// The top three are for games: 60 frames a second, and enough bits to keep them crisp.
165pub 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];
176/// Where a fresh camera or screen starts, and the most a relayed path may carry:
177/// the relays are not ours yet, and a video call must not be what fills them.
178pub 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
183/// Video steps down sooner than audio, since both share one congestion window and
184/// the voice must never be what gives.
185const VIDEO_DOWN_AT_PCT: f32 = 2.0;
186/// Round-trip growth over the call's floor that reads as a filling buffer.
187const VIDEO_RTT_BLOAT_MS: u32 = 50;
188/// Clean seconds before video takes the next rung: sooner than the voice, since a
189/// rung of video is worth more and costs less to give back.
190const VIDEO_CLIMB_AFTER_SECS: u32 = 5;
191
192/// What the video controller sees once a second.
193#[derive(Debug, Clone, Copy, Default)]
194pub struct VideoObservation {
195    pub sent_packets: u64,
196    pub lost_packets: u64,
197    pub rtt_ms: u32,
198    /// Audio datagrams the connection refused, cumulative: any growth is a step down.
199    pub audio_send_dropped: u64,
200    /// Frames still on the wire at the cap, or a frame reset, in the last second.
201    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    /// A rung the user chose; the ladder only leaves it to protect the voice.
217    pin: Option<usize>,
218    /// A frame rate the user chose, over whatever the rung says.
219    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    /// Hold a rung, or let the ladder run again. A held rung may sit above the
263    /// path's cap: the cap is a default for the ladder, the pin is the user's word.
264    /// Returns the rung when it moved.
265    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    /// A new ceiling (the path changed, or the peer said how much it can take).
287    /// Returns the rung when the ceiling pushed it down. The round-trip floor starts
288    /// over too: a new path has its own.
289    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    /// Once a second. Returns the rung to switch to when it changed.
300    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            // The first call sets the baselines; the history before it is not this second's.
309            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        // A pinned rung answers only to the voice: the one thing video may never cost.
318        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        // Held for a few seconds: one bad second costs one rung, not the ladder.
384        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        // A pin above the path's cap holds: the relay cap is a default, not a rule.
413        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}