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}