Skip to main content

srt_runtime/
livecc.rs

1//! SRT Live Congestion Control — packet pacing (`draft-sharabayko-srt-01` §5.1,
2//! curated at `specs/rules/srt-livecc.md`).
3//!
4//! A sans-IO, `no_std` sender-side pacing controller that computes the minimum
5//! inter-packet send period (`PKT_SND_PERIOD`) from a configured maximum
6//! bandwidth (`MAX_BW`) and a running EWMA of the average payload size
7//! (`AvgPayloadSize`).
8//!
9//! ## Usage
10//!
11//! ```rust
12//! use srt_runtime::livecc::{LiveCC, MaxBwConfig};
13//! use core::time::Duration;
14//!
15//! let mut cc = LiveCC::new(Default::default());
16//! // Application sends a data packet with 1316 bytes of payload:
17//! cc.on_data_packet(1316);
18//! // On ACK reception, recompute the send period:
19//! let period = cc.on_ack_received();
20//! assert!(period > Duration::ZERO);
21//! ```
22//!
23//! ## Spec grounding
24//!
25//! - `specs/rules/srt-livecc.md` §5.1.1 — MAX_BW configuration modes (§5.1.1,
26//!   lines L3116-L3202).
27//! - `specs/rules/srt-livecc.md` §5.1.2 — LiveCC algorithm (lines L3203-L3238):
28//!   - EWMA formula: L3219 (`AvgPayloadSize = 7/8 * AvgPayloadSize + 1/8 * PacketPayloadSize`)
29//!   - Initial value cap: L3222-3223 (max 1456 bytes).
30//!   - PktSize formula: L3227-3229 (`PktSize = AvgPayloadSize + SRT header size`).
31//!   - PKT_SND_PERIOD formula: L3234 (`PktSize * 1000000 / MAX_BW`).
32//! - `specs/rules/srt-livecc.md` — SYN = 0.01 s (imported from §5.2.1,
33//!   L3421-3423, restated at L197-L199 of the curated doc).
34
35use core::time::Duration;
36
37/// The fixed SRT header size (16 bytes), defined in `draft-sharabayko-srt-01` §3,
38/// Figure 2. Used by the PktSize formula (`specs/rules/srt-livecc.md` §5.1.2
39/// step 2, L3227-3229).
40const SRT_HEADER_SIZE: u64 = 16;
41
42/// Initial cap for `AvgPayloadSize`: the maximum allowed packet payload size,
43/// which cannot be larger than 1456 bytes (`specs/rules/srt-livecc.md` §5.1.2,
44/// L3222-3223).
45const INITIAL_AVG_PAYLOAD_SIZE_CAP: u64 = 1456;
46
47/// Default MAX_BW for MAXBW_SET mode — 1 Gbps (`specs/rules/srt-livecc.md`
48/// §5.1.1, L3122-3123). Stored in bytes per second (L3156-3157): 1 Gbps =
49/// 125_000_000 bytes/s.
50const DEFAULT_MAX_BW_BYTES_PER_SEC: u64 = 125_000_000;
51
52/// Maximum bandwidth configuration mode (`specs/rules/srt-livecc.md` §5.1.1,
53/// lines L3116-L3202).
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum MaxBwConfig {
57    /// **MAXBW_SET** (§5.1.1, L3120-3128): MAX_BW set explicitly (bytes/sec).
58    /// Default: 1 Gbps = 125_000_000 bytes/sec.
59    Set(u64),
60    /// **INPUTBW_SET** (§5.1.1, L3130-3146): input rate (bytes/sec) + overhead
61    /// percentage. MAX_BW = `input_bw * (1 + overhead / 100)` (L3143).
62    InputBased {
63        /// The sender's input rate, in bytes per second.
64        input_bw: u64,
65        /// Overhead percentage (e.g. 25 means 25%).
66        overhead: u64,
67    },
68    /// **INPUTBW_ESTIMATED** (§5.1.1, L3148-3184): measured input rate +
69    /// overhead. MAX_BW = `est_input_bw * (1 + overhead / 100)` (L3154).
70    ///
71    /// Unlike [`MaxBwConfig::InputBased`], `est_input_bw` is updated
72    /// externally (e.g. the encoder's measured bitrate). The formula is
73    /// identical.
74    Estimated {
75        /// The estimated sender input rate, in bytes per second.
76        est_input_bw: u64,
77        /// Overhead percentage (e.g. 25 means 25%).
78        overhead: u64,
79    },
80    /// Unbounded — infinite bandwidth. No packet pacing is applied.
81    ///
82    /// Not a named mode in §5.1.1's table (L3197-3201), but is the implicit
83    /// effect when MAX_BW is not set or is unlimited. Represented by a
84    /// PKT_SND_PERIOD of 0 (send as fast as possible).
85    Infinite,
86}
87
88impl Default for MaxBwConfig {
89    /// Default: [`MaxBwConfig::Set`] at 1 Gbps (`specs/rules/srt-livecc.md`
90    /// §5.1.1, L3122-3123).
91    fn default() -> Self {
92        MaxBwConfig::Set(DEFAULT_MAX_BW_BYTES_PER_SEC)
93    }
94}
95
96impl MaxBwConfig {
97    /// The §5.1.1 mode name.
98    pub fn name(&self) -> &'static str {
99        match self {
100            MaxBwConfig::Set(_) => "MAXBW_SET",
101            MaxBwConfig::InputBased { .. } => "INPUTBW_SET",
102            MaxBwConfig::Estimated { .. } => "INPUTBW_ESTIMATED",
103            MaxBwConfig::Infinite => "Infinite",
104        }
105    }
106
107    /// Resolve the current `MAX_BW` value in bytes per second, or `None` for
108    /// unbounded (infinite bandwidth).
109    ///
110    /// Per `specs/rules/srt-livecc.md` §5.1.1:
111    /// - MAXBW_SET: returns the explicit value (L3120-3128).
112    /// - INPUTBW_SET: returns `input_bw * (1 + overhead / 100)` (L3143).
113    /// - INPUTBW_ESTIMATED: returns `est_input_bw * (1 + overhead / 100)` (L3154).
114    /// - Infinite: returns `None`.
115    pub fn max_bw_bytes_per_sec(&self) -> Option<u64> {
116        match *self {
117            MaxBwConfig::Set(bw) => Some(bw),
118            MaxBwConfig::InputBased { input_bw, overhead } => {
119                // L3143: MAX_BW = INPUT_BW * (1 + OVERHEAD / 100)
120                Some(apply_overhead(input_bw, overhead))
121            }
122            MaxBwConfig::Estimated {
123                est_input_bw,
124                overhead,
125            } => {
126                // L3154: MAX_BW = EST_INPUT_BW * (1 + OVERHEAD / 100)
127                Some(apply_overhead(est_input_bw, overhead))
128            }
129            MaxBwConfig::Infinite => None,
130        }
131    }
132}
133
134broadcast_common::impl_spec_display!(MaxBwConfig);
135
136/// Apply `'MAX_BW = bw * (1 + overhead / 100)'` (`specs/rules/srt-livecc.md`
137/// L3143/L3154, verbatim).
138fn apply_overhead(bw: u64, overhead: u64) -> u64 {
139    // `overhead / 100` is truncated integer division, but the spec formula
140    // `(1 + OVERHEAD / 100)` is a real multiplier — we lose precision with
141    // integer arithmetic. Compute as `bw + bw * overhead / 100` instead.
142    bw + bw * overhead / 100
143}
144
145/// SRT Live Congestion Control — sender-side pacing state
146/// (`draft-sharabayko-srt-01` §5.1.2).
147///
148/// Tracks the EWMA average payload size and recomputes the inter-packet send
149/// period on each ACK.
150///
151/// Sans-IO: all timing is caller-driven — [`LiveCC::on_data_packet`],
152/// [`LiveCC::on_ack_received`], and [`LiveCC::tick`] take explicit inputs and
153/// return computed values; the controller never reads a wall clock internally.
154#[derive(Debug, Clone)]
155pub struct LiveCC {
156    /// Average payload size (EWMA), in bytes — `AvgPayloadSize` from §5.1.2
157    /// (L3219).
158    avg_payload_size: u64,
159    /// Maximum bandwidth configuration (§5.1.1).
160    max_bw_config: MaxBwConfig,
161}
162
163impl LiveCC {
164    /// Create a new LiveCC pacing controller.
165    ///
166    /// `max_bw_config` — the MAX_BW mode (§5.1.1). Use `Default::default()`
167    /// for the default 1 Gbps fixed MAXBW_SET mode.
168    ///
169    /// Initial `AvgPayloadSize` is set to the initial cap (1456 bytes),
170    /// per `specs/rules/srt-livecc.md` §5.1.2 (L3222-3223).
171    pub fn new(max_bw_config: MaxBwConfig) -> Self {
172        LiveCC {
173            avg_payload_size: INITIAL_AVG_PAYLOAD_SIZE_CAP,
174            max_bw_config,
175        }
176    }
177
178    /// Update the EWMA average payload size on sending a data packet
179    /// (`specs/rules/srt-livecc.md` §5.1.2, L3216-3223).
180    ///
181    /// `packet_payload_size` — the payload size (bytes) of the just-sent data
182    /// packet (original or retransmitted, L3216-3217).
183    ///
184    /// Formula (L3219, verbatim):
185    ///
186    /// ```text
187    /// AvgPayloadSize = 7/8 * AvgPayloadSize + 1/8 * PacketPayloadSize
188    /// ```
189    pub fn on_data_packet(&mut self, packet_payload_size: u64) {
190        // L3219: AvgPayloadSize = 7/8 * AvgPayloadSize + 1/8 * PacketPayloadSize
191        let old = self.avg_payload_size;
192        self.avg_payload_size = (7 * old + packet_payload_size) / 8;
193    }
194
195    /// The current average payload size estimate, in bytes.
196    pub fn avg_payload_size(&self) -> u64 {
197        self.avg_payload_size
198    }
199
200    /// The current MAX_BW configuration.
201    pub fn max_bw_config(&self) -> &MaxBwConfig {
202        &self.max_bw_config
203    }
204
205    /// Reconfigure the MAX_BW mode at runtime.
206    pub fn set_max_bw_config(&mut self, config: MaxBwConfig) {
207        self.max_bw_config = config;
208    }
209
210    /// Compute the current inter-packet send period in microseconds
211    /// (`specs/rules/srt-livecc.md` §5.1.2, L3225-3238).
212    ///
213    /// Call this on ACK reception (method (2), L3225). The period is also
214    /// used after any state change (e.g. reconfiguration).
215    ///
216    /// Returns `Duration::ZERO` when bandwidth is unbounded (infinite mode),
217    /// meaning "send as fast as possible".
218    ///
219    /// Step 1 — PktSize (L3227-3229, paraphrased):
220    ///
221    /// ```text
222    /// PktSize = AvgPayloadSize + SRT header size
223    /// ```
224    ///
225    /// Step 2 — PKT_SND_PERIOD (L3234, verbatim):
226    ///
227    /// ```text
228    /// PKT_SND_PERIOD = PktSize * 1000000 / MAX_BW
229    /// ```
230    pub fn on_ack_received(&self) -> Duration {
231        let Some(max_bw) = self.max_bw_config.max_bw_bytes_per_sec() else {
232            // Infinite: no pacing.
233            return Duration::ZERO;
234        };
235        if max_bw == 0 {
236            return Duration::ZERO;
237        }
238
239        // L3227-3229: PktSize = AvgPayloadSize + SRT header size (16 bytes).
240        let pkt_size = self.avg_payload_size + SRT_HEADER_SIZE;
241
242        // L3234: PKT_SND_PERIOD = PktSize * 1000000 / MAX_BW
243        //
244        // pkt_size is in bytes, MAX_BW in bytes/sec. The factor 1_000_000
245        // converts seconds to microseconds (L3237-3238).
246        let period_us = pkt_size * 1_000_000 / max_bw;
247        Duration::from_micros(period_us)
248    }
249
250    /// Time-driven tick — currently a no-op for LiveCC (the `§5.1.2` algorithm
251    /// is driven by packet/ACK events only; an RTO timeout also triggers
252    /// `on_data_packet`, per L3240-3241).
253    ///
254    /// Provided for forward compatibility: the sender loop should call this
255    /// every cycle. Returns `None`.
256    pub fn tick(&mut self) -> Option<Duration> {
257        // Per spec, LiveCC reacts to events (data send, ACK, RTO — L3211-3214);
258        // the RTO timeout also calls on_data_packet (L3240-3241). No periodic
259        // internal computation needed.
260        None
261    }
262}
263
264impl Default for LiveCC {
265    fn default() -> Self {
266        LiveCC::new(Default::default())
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn initial_avg_payload_size_is_1456_cap() {
276        // L3222-3223: initial AvgPayloadSize is the max allowed payload, max 1456.
277        let cc = LiveCC::new(Default::default());
278        assert_eq!(cc.avg_payload_size(), 1456);
279    }
280
281    #[test]
282    fn default_bw_is_1_gbps() {
283        // L3122-3123: recommended default is 1 Gbps.
284        let cfg = MaxBwConfig::default();
285        assert_eq!(cfg.max_bw_bytes_per_sec(), Some(125_000_000));
286    }
287
288    #[test]
289    fn ewma_update_matches_hand_computed_formula() {
290        // L3219: AvgPayloadSize = 7/8 * old + 1/8 * payload.
291        // Initial: 1456. Feed a 100-byte payload.
292        // new = (7 * 1456 + 100) / 8 = (10192 + 100) / 8 = 10292 / 8 = 1286
293        let mut cc = LiveCC::new(Default::default());
294        cc.on_data_packet(100);
295        assert_eq!(cc.avg_payload_size(), 1286);
296    }
297
298    #[test]
299    fn ewma_converges_toward_constant_payload() {
300        // Feed a constant 1000-byte payload many times; the EWMA should
301        // converge toward 1000.
302        let mut cc = LiveCC::new(Default::default());
303        for _ in 0..64 {
304            cc.on_data_packet(1000);
305        }
306        let avg = cc.avg_payload_size();
307        // Converged within 1 of 1000.
308        assert!(avg.abs_diff(1000) <= 1, "expected ~1000, got {avg}");
309    }
310
311    #[test]
312    fn ewma_respects_initial_cap_regardless_of_first_feed() {
313        // The initial cap (1456) is separate from the first update.
314        // Even if we feed a tiny payload, the init was 1456, not the fed value.
315        let mut cc = LiveCC::new(Default::default());
316        cc.on_data_packet(50);
317        // new = (7 * 1456 + 50) / 8 = (10192 + 50) / 8 = 10242 / 8 = 1280
318        assert_eq!(cc.avg_payload_size(), 1280);
319    }
320
321    #[test]
322    fn pkt_snd_period_formula_matches_hand_computed() {
323        // L3234: PKT_SND_PERIOD = PktSize * 1000000 / MAX_BW.
324        //
325        // With default MAX_BW = 125_000_000 bytes/sec and init AvgPayloadSize
326        // = 1456:
327        //   PktSize = 1456 + 16 = 1472
328        //   period = 1472 * 1_000_000 / 125_000_000 = 1_472_000_000 / 125_000_000
329        //          = 11 (integer division; 11.776 truncated to 11)
330        let cc = LiveCC::new(Default::default());
331        let period = cc.on_ack_received();
332        assert_eq!(period, Duration::from_micros(11));
333    }
334
335    #[test]
336    fn pkt_snd_period_with_different_payload_size() {
337        // After feeding a 1316-byte payload seven times (to get close to
338        // converged), compute the period.
339        //
340        // EWMA after 7 steps: we don't hand-compute the exact 8-step weight
341        // here; instead we use a simpler scenario:
342        //   initial: 1456
343        //   feed 1316: avg = (7*1456 + 1316)/8 = 1438
344        //   feed 1316: avg = (7*1438 + 1316)/8 = 1422
345        //   PktSize = 1422 + 16 = 1438
346        //   period = 1438 * 1_000_000 / 125_000_000 = 11
347        let mut cc = LiveCC::new(Default::default());
348        cc.on_data_packet(1316);
349        cc.on_data_packet(1316);
350        let period = cc.on_ack_received();
351        assert_eq!(period, Duration::from_micros(11));
352    }
353
354    #[test]
355    fn input_bw_mode_formula() {
356        // L3143: MAX_BW = INPUT_BW * (1 + OVERHEAD / 100).
357        // input_bw = 10_000_000 (10 MB/s), overhead = 25%.
358        // MAX_BW = 10_000_000 + 10_000_000 * 25 / 100 = 12_500_000 bytes/sec.
359        let cfg = MaxBwConfig::InputBased {
360            input_bw: 10_000_000,
361            overhead: 25,
362        };
363        assert_eq!(cfg.max_bw_bytes_per_sec(), Some(12_500_000));
364    }
365
366    #[test]
367    fn estimated_mode_formula() {
368        // L3154: same formula as INPUTBW_SET but with EST_INPUT_BW.
369        let cfg = MaxBwConfig::Estimated {
370            est_input_bw: 5_000_000,
371            overhead: 10,
372        };
373        // MAX_BW = 5_000_000 + 5_000_000 * 10 / 100 = 5_500_000
374        assert_eq!(cfg.max_bw_bytes_per_sec(), Some(5_500_000));
375    }
376
377    #[test]
378    fn infinite_mode_returns_zero_period() {
379        let cc = LiveCC::new(MaxBwConfig::Infinite);
380        assert_eq!(cc.on_ack_received(), Duration::ZERO);
381    }
382
383    #[test]
384    fn set_mode_period_scales_with_bw() {
385        // MAXBW_SET at 62_500_000 bytes/sec (500 Mbps); init PktSize = 1472.
386        // period = 1472 * 1_000_000 / 62_500_000 = 23 (23.552 truncated).
387        let cc = LiveCC::new(MaxBwConfig::Set(62_500_000));
388        assert_eq!(cc.on_ack_received(), Duration::from_micros(23));
389    }
390
391    #[test]
392    fn runtime_reconfiguration_affects_period() {
393        let mut cc = LiveCC::new(MaxBwConfig::Infinite);
394        assert_eq!(cc.on_ack_received(), Duration::ZERO);
395
396        cc.set_max_bw_config(MaxBwConfig::Set(125_000_000));
397        assert_eq!(cc.on_ack_received(), Duration::from_micros(11));
398    }
399
400    #[test]
401    fn zero_bw_returns_zero_period() {
402        // Guard: if MAX_BW is 0, we'd divide by zero. Return ZERO instead.
403        let cc = LiveCC::new(MaxBwConfig::Set(0));
404        assert_eq!(cc.on_ack_received(), Duration::ZERO);
405    }
406
407    #[test]
408    fn tick_is_noop() {
409        let mut cc = LiveCC::new(Default::default());
410        assert_eq!(cc.tick(), None);
411        // Calling tick must not mutate state.
412        assert_eq!(cc.avg_payload_size(), 1456);
413    }
414
415    #[test]
416    fn max_bw_config_clone_and_eq() {
417        let a = MaxBwConfig::Set(100);
418        let b = MaxBwConfig::Set(100);
419        assert_eq!(a, b);
420        let c = MaxBwConfig::InputBased {
421            input_bw: 1000,
422            overhead: 20,
423        };
424        let d = MaxBwConfig::InputBased {
425            input_bw: 1001,
426            overhead: 20,
427        };
428        assert_ne!(c, d);
429    }
430
431    #[test]
432    fn overhead_formula_edge_cases() {
433        // overhead = 0: MAX_BW = bw + bw*0/100 = bw
434        assert_eq!(
435            MaxBwConfig::InputBased {
436                input_bw: 10_000,
437                overhead: 0,
438            }
439            .max_bw_bytes_per_sec(),
440            Some(10_000)
441        );
442        // overhead = 100: MAX_BW = bw + bw*100/100 = 2*bw
443        assert_eq!(
444            MaxBwConfig::InputBased {
445                input_bw: 10_000,
446                overhead: 100,
447            }
448            .max_bw_bytes_per_sec(),
449            Some(20_000)
450        );
451    }
452}