use std::time::Duration;
use moq_net::Timestamp;
const BITRATE_WINDOW: Duration = Duration::from_secs(1);
#[derive(Clone, Default, Debug, PartialEq)]
#[non_exhaustive]
pub struct Estimate {
pub jitter: Option<Duration>,
pub bitrate: Option<u64>,
}
impl Estimate {
pub fn with_jitter(mut self, jitter: impl Into<Option<Duration>>) -> Self {
self.jitter = jitter.into();
self
}
pub fn with_bitrate(mut self, bitrate: impl Into<Option<u64>>) -> Self {
self.bitrate = bitrate.into();
self
}
}
#[derive(Default)]
pub struct Estimator {
jitter: Jitter,
bitrate: Bitrate,
}
impl Estimator {
pub fn new() -> Self {
Self::default()
}
pub fn write(&mut self, timestamp: Timestamp, bytes: usize) {
let timestamp = nanos(timestamp);
self.bitrate.write(timestamp, bytes);
self.jitter.write(timestamp);
}
pub fn cut(&mut self, end: Option<Timestamp>) {
self.bitrate.cut(end.map(nanos));
}
pub fn discontinuity(&mut self) {
self.bitrate.discontinuity();
self.jitter.discontinuity();
}
pub fn reorder(&mut self, delay: Timestamp) {
self.jitter.reorder(delay);
}
pub fn estimate(&self) -> Estimate {
Estimate {
jitter: self.jitter.current(),
bitrate: self.bitrate.current(),
}
}
}
fn nanos(timestamp: Timestamp) -> u128 {
timestamp.as_nanos()
}
fn elapsed(start: u128, end: u128) -> Option<Duration> {
let delta = end.checked_sub(start).filter(|delta| *delta > 0)?;
Some(Duration::from_nanos(u64::try_from(delta).unwrap_or(u64::MAX)))
}
#[derive(Default)]
struct Bitrate {
span: Option<Span>,
window_bytes: u64,
window_duration: Duration,
max: Option<u64>,
}
impl Bitrate {
fn write(&mut self, ts: u128, bytes: usize) {
let span = self.span.get_or_insert(Span {
start: ts,
max: ts,
bytes: 0,
});
span.start = span.start.min(ts);
span.max = span.max.max(ts);
span.bytes = span.bytes.saturating_add(bytes as u64);
}
fn cut(&mut self, end: Option<u128>) {
let Some(span) = self.span.as_ref() else {
return;
};
let duration = end
.and_then(|end| elapsed(span.start, end))
.or_else(|| elapsed(span.start, span.max));
let Some(duration) = duration else {
return;
};
let span = self.span.take().expect("span is present");
self.window_bytes = self.window_bytes.saturating_add(span.bytes);
self.window_duration += duration;
if self.window_duration < BITRATE_WINDOW {
return;
}
let bitrate = bits_per_second(self.window_bytes, self.window_duration);
self.window_bytes = 0;
self.window_duration = Duration::ZERO;
if self.max.is_none_or(|max| bitrate > max) {
self.max = Some(bitrate);
}
}
fn discontinuity(&mut self) {
self.span = None;
}
fn current(&self) -> Option<u64> {
self.max
}
}
struct Span {
start: u128,
max: u128,
bytes: u64,
}
fn bits_per_second(bytes: u64, duration: Duration) -> u64 {
let nanos = duration.as_nanos();
if nanos == 0 {
return 0;
}
let bits_per_second = (bytes as u128).saturating_mul(8).saturating_mul(1_000_000_000) / nanos;
bits_per_second.min(u64::MAX as u128) as u64
}
#[derive(Default)]
struct Jitter {
last: Option<u128>,
min_duration: Option<Duration>,
max_reorder: Duration,
}
impl Jitter {
fn write(&mut self, ts: u128) {
if let Some(last) = self.last.replace(ts)
&& let Some(duration) = elapsed(last, ts)
{
self.min_duration = Some(match self.min_duration {
Some(min) => min.min(duration),
None => duration,
});
}
}
fn reorder(&mut self, delay: Timestamp) {
self.max_reorder = self.max_reorder.max(Duration::from(delay));
}
fn discontinuity(&mut self) {
self.last = None;
}
fn current(&self) -> Option<Duration> {
let jitter = self.min_duration.unwrap_or(Duration::ZERO).max(self.max_reorder);
(!jitter.is_zero()).then_some(jitter)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn micros(value: u64) -> Timestamp {
Timestamp::from_micros(value).unwrap()
}
#[test]
fn reports_the_minimum_frame_spacing() {
let mut estimator = Estimator::new();
estimator.write(micros(1_000), 1);
assert_eq!(estimator.estimate().jitter, None, "one frame has no spacing");
estimator.write(micros(41_000), 1);
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(40)));
estimator.write(micros(81_000), 1);
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(40)));
estimator.write(micros(101_000), 1);
assert_eq!(
estimator.estimate().jitter,
Some(Duration::from_millis(20)),
"the minimum wins"
);
}
#[test]
fn reorder_delay_wins_over_frame_spacing() {
let mut estimator = Estimator::new();
estimator.write(micros(0), 1);
estimator.write(micros(16_000), 1);
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(16)));
estimator.reorder(micros(48_000));
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(48)));
estimator.write(micros(32_000), 1);
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(48)));
}
#[test]
fn ignores_non_monotonic_presentation_spacing() {
let mut estimator = Estimator::new();
estimator.write(micros(100_000), 1);
estimator.write(micros(80_000), 1);
assert_eq!(estimator.estimate().jitter, None);
estimator.write(micros(120_000), 1);
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(40)));
}
#[test]
fn bitrate_waits_for_the_window_and_reports_the_maximum() {
let mut estimator = Estimator::new();
estimator.write(micros(0), 100_000);
estimator.write(micros(500_000), 100_000);
estimator.cut(Some(micros(1_000_000)));
assert_eq!(estimator.estimate().bitrate, Some(1_600_000));
estimator.write(micros(1_000_000), 25_000);
estimator.cut(Some(micros(2_000_000)));
assert_eq!(estimator.estimate().bitrate, Some(1_600_000));
estimator.write(micros(2_000_000), 250_000);
estimator.cut(Some(micros(3_000_000)));
assert_eq!(estimator.estimate().bitrate, Some(2_000_000));
}
#[test]
fn unbounded_cuts_never_drop_bytes() {
let mut estimator = Estimator::new();
for i in 0..40u64 {
let ts = micros(i * 40_000);
estimator.cut(Some(ts));
estimator.write(ts, 5_000);
estimator.cut(None);
}
assert_eq!(estimator.estimate().bitrate, Some(1_000_000));
}
#[test]
fn mixed_timescales_still_measure() {
let mut estimator = Estimator::new();
estimator.write(Timestamp::from_millis(0).unwrap(), 5_000);
for i in 1..40u64 {
let ts = micros(i * 40_000);
estimator.cut(Some(ts));
estimator.write(ts, 5_000);
}
let estimate = estimator.estimate();
assert_eq!(estimate.bitrate, Some(1_000_000));
assert_eq!(estimate.jitter, Some(Duration::from_millis(40)));
}
#[test]
fn discontinuity_drops_the_open_span() {
let mut estimator = Estimator::new();
estimator.write(micros(0), 100_000);
estimator.discontinuity();
estimator.write(micros(2_400_000_000), 100_000);
estimator.cut(Some(micros(2_401_000_000)));
assert_eq!(
estimator.estimate().bitrate,
Some(800_000),
"only the post-break span counts"
);
assert_eq!(estimator.estimate().jitter, None, "the gap is not a frame duration");
}
}