use std::collections::HashMap;
use std::time::{Duration, Instant};
use super::kalman::{DelayEstimator, MAX_BITRATE_BPS, MIN_BITRATE_BPS};
use super::loss::LossEstimator;
const INITIAL_BITRATE_BPS: f64 = 300_000.0;
const CLIENT_HINT_MAX_AGE: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy)]
pub struct ClientHint {
pub bps: u64,
pub received_at: Instant,
}
#[derive(Debug)]
pub struct PerSubscriber {
pub send_times: HashMap<u64, Instant>,
pub last_arrival: Option<Instant>,
pub last_send_for_received: Option<Instant>,
pub delay: DelayEstimator,
pub loss: LossEstimator,
pub rtt: Option<Duration>,
pub native_estimate_bps: Option<f64>,
pub client_hint: Option<ClientHint>,
}
impl PerSubscriber {
pub fn new() -> Self {
Self {
send_times: HashMap::new(),
last_arrival: None,
last_send_for_received: None,
delay: DelayEstimator::new(INITIAL_BITRATE_BPS),
loss: LossEstimator::new(INITIAL_BITRATE_BPS),
rtt: None,
native_estimate_bps: None,
client_hint: None,
}
}
#[must_use]
pub fn combined_bps(&self, now: Instant) -> f64 {
let base = self.delay.bitrate_bps().min(self.loss.bitrate_bps());
let after_native = match self.native_estimate_bps {
Some(native) => base.min(native),
None => base,
};
let after_hint = match self.client_hint {
Some(h) if now.duration_since(h.received_at) < CLIENT_HINT_MAX_AGE => {
after_native.min(h.bps as f64)
}
_ => after_native,
};
after_hint.max(0.0)
}
}
impl Default for PerSubscriber {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
#[test]
fn combined_bps_takes_minimum_of_delay_and_loss() {
let now = Instant::now();
let mut sub = PerSubscriber::new();
sub.delay = DelayEstimator::new(2_000_000.0);
sub.loss = LossEstimator::new(1_000_000.0);
let combined = sub.combined_bps(now);
assert!(
combined <= 1_100_000.0,
"expected approx 1Mbps (min of 2M and 1M), got {combined}"
);
}
#[test]
fn native_estimate_acts_as_ceiling() {
let now = Instant::now();
let mut sub = PerSubscriber::new();
sub.delay = DelayEstimator::new(2_000_000.0);
sub.loss = LossEstimator::new(2_000_000.0);
sub.native_estimate_bps = Some(500_000.0);
let combined = sub.combined_bps(now);
assert!(
combined <= 500_100.0,
"native GCC ceiling not applied: {combined}"
);
}
#[test]
fn client_hint_acts_as_ceiling_when_fresh() {
let now = Instant::now();
let mut sub = PerSubscriber::new();
sub.delay = DelayEstimator::new(2_000_000.0);
sub.loss = LossEstimator::new(2_000_000.0);
sub.client_hint = Some(ClientHint { bps: 400_000, received_at: now });
let combined = sub.combined_bps(now);
assert!(
combined <= 400_100.0,
"client hint ceiling not applied: {combined}"
);
}
#[test]
fn stale_client_hint_is_ignored() {
let past = Instant::now() - Duration::from_secs(10); let now = Instant::now();
let mut sub = PerSubscriber::new();
sub.delay = DelayEstimator::new(2_000_000.0);
sub.loss = LossEstimator::new(2_000_000.0);
sub.client_hint = Some(ClientHint { bps: 100, received_at: past }); let combined = sub.combined_bps(now);
assert!(
combined > 1_000.0,
"stale client hint should be ignored, got {combined}"
);
}
}