use core::time::Duration;
const SRT_HEADER_SIZE: u64 = 16;
const INITIAL_AVG_PAYLOAD_SIZE_CAP: u64 = 1456;
const DEFAULT_MAX_BW_BYTES_PER_SEC: u64 = 125_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MaxBwConfig {
Set(u64),
InputBased {
input_bw: u64,
overhead: u64,
},
Estimated {
est_input_bw: u64,
overhead: u64,
},
Infinite,
}
impl Default for MaxBwConfig {
fn default() -> Self {
MaxBwConfig::Set(DEFAULT_MAX_BW_BYTES_PER_SEC)
}
}
impl MaxBwConfig {
pub fn name(&self) -> &'static str {
match self {
MaxBwConfig::Set(_) => "MAXBW_SET",
MaxBwConfig::InputBased { .. } => "INPUTBW_SET",
MaxBwConfig::Estimated { .. } => "INPUTBW_ESTIMATED",
MaxBwConfig::Infinite => "Infinite",
}
}
pub fn max_bw_bytes_per_sec(&self) -> Option<u64> {
match *self {
MaxBwConfig::Set(bw) => Some(bw),
MaxBwConfig::InputBased { input_bw, overhead } => {
Some(apply_overhead(input_bw, overhead))
}
MaxBwConfig::Estimated {
est_input_bw,
overhead,
} => {
Some(apply_overhead(est_input_bw, overhead))
}
MaxBwConfig::Infinite => None,
}
}
}
broadcast_common::impl_spec_display!(MaxBwConfig);
fn apply_overhead(bw: u64, overhead: u64) -> u64 {
bw + bw * overhead / 100
}
#[derive(Debug, Clone)]
pub struct LiveCC {
avg_payload_size: u64,
max_bw_config: MaxBwConfig,
}
impl LiveCC {
pub fn new(max_bw_config: MaxBwConfig) -> Self {
LiveCC {
avg_payload_size: INITIAL_AVG_PAYLOAD_SIZE_CAP,
max_bw_config,
}
}
pub fn on_data_packet(&mut self, packet_payload_size: u64) {
let old = self.avg_payload_size;
self.avg_payload_size = (7 * old + packet_payload_size) / 8;
}
pub fn avg_payload_size(&self) -> u64 {
self.avg_payload_size
}
pub fn max_bw_config(&self) -> &MaxBwConfig {
&self.max_bw_config
}
pub fn set_max_bw_config(&mut self, config: MaxBwConfig) {
self.max_bw_config = config;
}
pub fn on_ack_received(&self) -> Duration {
let Some(max_bw) = self.max_bw_config.max_bw_bytes_per_sec() else {
return Duration::ZERO;
};
if max_bw == 0 {
return Duration::ZERO;
}
let pkt_size = self.avg_payload_size + SRT_HEADER_SIZE;
let period_us = pkt_size * 1_000_000 / max_bw;
Duration::from_micros(period_us)
}
pub fn tick(&mut self) -> Option<Duration> {
None
}
}
impl Default for LiveCC {
fn default() -> Self {
LiveCC::new(Default::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_avg_payload_size_is_1456_cap() {
let cc = LiveCC::new(Default::default());
assert_eq!(cc.avg_payload_size(), 1456);
}
#[test]
fn default_bw_is_1_gbps() {
let cfg = MaxBwConfig::default();
assert_eq!(cfg.max_bw_bytes_per_sec(), Some(125_000_000));
}
#[test]
fn ewma_update_matches_hand_computed_formula() {
let mut cc = LiveCC::new(Default::default());
cc.on_data_packet(100);
assert_eq!(cc.avg_payload_size(), 1286);
}
#[test]
fn ewma_converges_toward_constant_payload() {
let mut cc = LiveCC::new(Default::default());
for _ in 0..64 {
cc.on_data_packet(1000);
}
let avg = cc.avg_payload_size();
assert!(avg.abs_diff(1000) <= 1, "expected ~1000, got {avg}");
}
#[test]
fn ewma_respects_initial_cap_regardless_of_first_feed() {
let mut cc = LiveCC::new(Default::default());
cc.on_data_packet(50);
assert_eq!(cc.avg_payload_size(), 1280);
}
#[test]
fn pkt_snd_period_formula_matches_hand_computed() {
let cc = LiveCC::new(Default::default());
let period = cc.on_ack_received();
assert_eq!(period, Duration::from_micros(11));
}
#[test]
fn pkt_snd_period_with_different_payload_size() {
let mut cc = LiveCC::new(Default::default());
cc.on_data_packet(1316);
cc.on_data_packet(1316);
let period = cc.on_ack_received();
assert_eq!(period, Duration::from_micros(11));
}
#[test]
fn input_bw_mode_formula() {
let cfg = MaxBwConfig::InputBased {
input_bw: 10_000_000,
overhead: 25,
};
assert_eq!(cfg.max_bw_bytes_per_sec(), Some(12_500_000));
}
#[test]
fn estimated_mode_formula() {
let cfg = MaxBwConfig::Estimated {
est_input_bw: 5_000_000,
overhead: 10,
};
assert_eq!(cfg.max_bw_bytes_per_sec(), Some(5_500_000));
}
#[test]
fn infinite_mode_returns_zero_period() {
let cc = LiveCC::new(MaxBwConfig::Infinite);
assert_eq!(cc.on_ack_received(), Duration::ZERO);
}
#[test]
fn set_mode_period_scales_with_bw() {
let cc = LiveCC::new(MaxBwConfig::Set(62_500_000));
assert_eq!(cc.on_ack_received(), Duration::from_micros(23));
}
#[test]
fn runtime_reconfiguration_affects_period() {
let mut cc = LiveCC::new(MaxBwConfig::Infinite);
assert_eq!(cc.on_ack_received(), Duration::ZERO);
cc.set_max_bw_config(MaxBwConfig::Set(125_000_000));
assert_eq!(cc.on_ack_received(), Duration::from_micros(11));
}
#[test]
fn zero_bw_returns_zero_period() {
let cc = LiveCC::new(MaxBwConfig::Set(0));
assert_eq!(cc.on_ack_received(), Duration::ZERO);
}
#[test]
fn tick_is_noop() {
let mut cc = LiveCC::new(Default::default());
assert_eq!(cc.tick(), None);
assert_eq!(cc.avg_payload_size(), 1456);
}
#[test]
fn max_bw_config_clone_and_eq() {
let a = MaxBwConfig::Set(100);
let b = MaxBwConfig::Set(100);
assert_eq!(a, b);
let c = MaxBwConfig::InputBased {
input_bw: 1000,
overhead: 20,
};
let d = MaxBwConfig::InputBased {
input_bw: 1001,
overhead: 20,
};
assert_ne!(c, d);
}
#[test]
fn overhead_formula_edge_cases() {
assert_eq!(
MaxBwConfig::InputBased {
input_bw: 10_000,
overhead: 0,
}
.max_bw_bytes_per_sec(),
Some(10_000)
);
assert_eq!(
MaxBwConfig::InputBased {
input_bw: 10_000,
overhead: 100,
}
.max_bw_bytes_per_sec(),
Some(20_000)
);
}
}