use std::collections::VecDeque;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use moq_net::Timestamp;
const RATE_WINDOW: Duration = Duration::from_secs(1);
const FRAMERATES: [f64; 11] = [
24_000.0 / 1001.0,
24.0,
25.0,
30_000.0 / 1001.0,
30.0,
48.0,
50.0,
60_000.0 / 1001.0,
60.0,
100.0,
120.0,
];
const FRAMERATE_TOLERANCE: f64 = 0.0005;
const JITTER_WINDOW: Duration = Duration::from_secs(10);
#[derive(Clone, Default, Debug, PartialEq)]
#[non_exhaustive]
pub struct Estimate {
pub jitter: Option<Duration>,
pub bitrate: Option<u64>,
pub delay: Option<Duration>,
pub framerate: Option<f64>,
}
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
}
pub fn with_delay(mut self, delay: impl Into<Option<Duration>>) -> Self {
self.delay = delay.into();
self
}
pub fn with_framerate(mut self, framerate: impl Into<Option<f64>>) -> Self {
self.framerate = framerate.into();
self
}
}
#[derive(Default)]
pub struct Estimator {
jitter: Jitter,
bitrate: Bitrate,
framerate: Framerate,
baseline: Window,
broadcast: Baseline,
delay: Duration,
}
impl Estimator {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn with_broadcast(broadcast: Baseline) -> Self {
Self {
broadcast,
..Self::default()
}
}
pub fn write(&mut self, timestamp: Timestamp, bytes: usize) {
let timestamp = nanos(timestamp);
self.bitrate.write(timestamp, bytes);
self.framerate.write(timestamp);
}
pub fn flush(&mut self, timestamp: Timestamp, now: Instant) {
let (lateness, earliest) = self.broadcast.observe(timestamp, now);
let minimum = self.baseline.observe(now, lateness);
self.jitter.max = self.jitter.max.max(nanos_between(minimum, lateness));
self.delay = self.delay.max(nanos_between(earliest, minimum));
}
pub fn cut(&mut self, end: Option<Timestamp>) {
self.bitrate.cut(end.map(nanos));
self.framerate.cut();
}
pub fn discontinuity(&mut self) {
self.bitrate.discontinuity();
self.framerate.discontinuity();
self.baseline = Window::default();
self.broadcast.discontinuity();
}
pub fn reorder(&mut self, delay: Timestamp) {
self.burst(Duration::from(delay));
}
pub(crate) fn burst(&mut self, duration: Duration) {
self.jitter.max = self.jitter.max.max(duration);
}
pub fn estimate(&self) -> Estimate {
Estimate {
jitter: self.jitter.current(),
bitrate: self.bitrate.current(),
delay: (!self.delay.is_zero()).then_some(self.delay),
framerate: self.framerate.max,
}
}
}
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 < RATE_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
}
}
#[derive(Default)]
struct Framerate {
high: Option<u128>,
open: Option<u128>,
frames: u64,
max: Option<f64>,
}
impl Framerate {
fn write(&mut self, ts: u128) {
self.high = Some(self.high.map_or(ts, |high| high.max(ts)));
self.frames += 1;
}
fn cut(&mut self) {
let Some(high) = self.high else {
return;
};
let Some(open) = self.open else {
self.open = Some(high);
self.frames = 0;
return;
};
let Some(duration) = elapsed(open, high).filter(|duration| *duration >= RATE_WINDOW) else {
return;
};
let measured = self.frames as f64 / duration.as_secs_f64();
let snapped = FRAMERATES
.into_iter()
.map(|rate| (rate, (measured - rate).abs() / rate))
.filter(|(_, error)| *error <= FRAMERATE_TOLERANCE)
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(rate, _)| rate);
if let Some(framerate) = snapped
&& self.max.is_none_or(|max| framerate > max)
{
self.max = Some(framerate);
}
self.open = Some(high);
self.frames = 0;
}
fn discontinuity(&mut self) {
*self = Self {
max: self.max,
..Self::default()
};
}
}
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 {
max: Duration,
}
impl Jitter {
fn current(&self) -> Option<Duration> {
(!self.max.is_zero()).then_some(self.max)
}
}
#[derive(Clone, Default)]
pub(crate) struct Baseline(Arc<Mutex<Shared>>);
#[derive(Default)]
struct Shared {
epoch: Option<Instant>,
window: Window,
}
impl Baseline {
fn observe(&self, timestamp: Timestamp, now: Instant) -> (i128, i128) {
let mut shared = self.0.lock().unwrap_or_else(PoisonError::into_inner);
let epoch = *shared.epoch.get_or_insert(now);
let elapsed = match now.checked_duration_since(epoch) {
Some(duration) => duration.as_nanos() as i128,
None => -(epoch.duration_since(now).as_nanos() as i128),
};
let lateness = elapsed - timestamp.as_nanos() as i128;
(lateness, shared.window.observe(now, lateness))
}
fn discontinuity(&self) {
self.0.lock().unwrap_or_else(PoisonError::into_inner).window = Window::default();
}
}
#[derive(Default)]
struct Window {
samples: VecDeque<Sample>,
}
struct Sample {
at: Instant,
lateness: i128,
}
impl Window {
fn observe(&mut self, now: Instant, lateness: i128) -> i128 {
while self
.samples
.front()
.is_some_and(|sample| now.saturating_duration_since(sample.at) > JITTER_WINDOW)
{
self.samples.pop_front();
}
while self.samples.back().is_some_and(|sample| sample.lateness >= lateness) {
self.samples.pop_back();
}
self.samples.push_back(Sample { at: now, lateness });
self.samples.front().expect("the current sample was inserted").lateness
}
}
fn nanos_between(from: i128, to: i128) -> Duration {
Duration::from_nanos(u64::try_from(to - from).unwrap_or(if to > from { u64::MAX } else { 0 }))
}
#[cfg(test)]
mod tests {
use super::*;
fn micros(value: u64) -> Timestamp {
Timestamp::from_micros(value).unwrap()
}
#[test]
fn decode_order_pts_gap_never_becomes_provisional_jitter() {
let mut estimator = Estimator::new();
for pts in [0, 120_000, 40_000, 80_000] {
estimator.write(micros(pts), 1);
}
assert_eq!(estimator.estimate().jitter, None);
estimator.reorder(micros(80_000));
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(80)));
}
#[test]
fn batch_flush_at_its_end_reports_its_media_span() {
let mut estimator = Estimator::new();
let anchor = Instant::now();
estimator.flush(micros(0), anchor);
estimator.flush(micros(0), anchor + Duration::from_millis(120));
estimator.flush(micros(40_000), anchor + Duration::from_millis(120));
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(120)));
}
#[test]
fn constant_encoder_delay_is_not_jitter() {
let mut estimator = Estimator::new();
let anchor = Instant::now();
estimator.flush(micros(0), anchor + Duration::from_millis(200));
estimator.flush(micros(240_000), anchor + Duration::from_millis(440));
assert_eq!(estimator.estimate().jitter, None);
}
#[test]
fn discontinuity_resets_the_flush_baseline() {
let mut estimator = Estimator::new();
let anchor = Instant::now();
estimator.flush(micros(0), anchor);
estimator.flush(micros(40_000), anchor + Duration::from_millis(50));
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(10)));
estimator.discontinuity();
estimator.flush(micros(40_000), anchor + Duration::from_millis(2_050));
estimator.flush(micros(80_000), anchor + Duration::from_millis(2_090));
assert_eq!(estimator.estimate().jitter, Some(Duration::from_millis(10)));
}
#[test]
fn baseline_expires_drift() {
let mut estimator = Estimator::new();
let anchor = Instant::now();
for second in 0..100u64 {
estimator.flush(
micros(second * 1_000_000),
anchor + Duration::from_millis(second * 1_001),
);
}
let jitter = estimator.estimate().jitter.unwrap();
assert!(jitter <= Duration::from_millis(10), "{jitter:?}");
}
#[test]
fn early_flush_keeps_lowering_the_baseline() {
let mut estimator = Estimator::new();
let anchor = Instant::now();
for second in 0..100u64 {
estimator.flush(micros(second * 2_000_000), anchor + Duration::from_secs(second));
}
assert_eq!(estimator.estimate(), Estimate::default());
}
fn pair(seconds: u64, fast: impl Fn(u64) -> u64, slow: impl Fn(u64) -> u64) -> (Estimate, Estimate) {
let broadcast = Baseline::default();
let mut estimators = [
Estimator::with_broadcast(broadcast.clone()),
Estimator::with_broadcast(broadcast),
];
let anchor = Instant::now();
for frame in 0..seconds * 10 {
let pts = frame * 100;
for (estimator, lateness) in estimators.iter_mut().zip([fast(pts), slow(pts)]) {
estimator.flush(micros(pts * 1_000), anchor + Duration::from_millis(pts + lateness));
}
}
let [fast, slow] = estimators.map(|estimator| estimator.estimate());
(fast, slow)
}
#[test]
fn a_constant_offset_is_delay_on_the_slower_track() {
let (fast, slow) = pair(5, |_| 0, |_| 200);
assert_eq!(fast, Estimate::default());
assert_eq!(slow, Estimate::default().with_delay(Duration::from_millis(200)));
}
#[test]
fn a_common_drift_stays_bounded() {
let (fast, slow) = pair(100, |pts| pts / 1_000, |pts| 200 + pts / 1_000);
assert_eq!(fast.delay, None);
assert_eq!(slow.delay, Some(Duration::from_millis(200)));
for jitter in [fast.jitter, slow.jitter] {
assert!(jitter.unwrap() <= Duration::from_millis(10), "{jitter:?}");
}
}
#[test]
fn the_earliest_track_changing_does_not_under_buffer() {
let drift = |pts: u64| pts.saturating_sub(10_000).min(50_000) / 100;
let (drifted, steady) = pair(80, drift, |_| 200);
assert_eq!(steady.delay, Some(Duration::from_millis(200)));
assert_eq!(drifted.delay, Some(Duration::from_millis(300)));
}
#[test]
fn a_discontinuity_clears_the_broadcast_baseline() {
let broadcast = Baseline::default();
let mut audio = Estimator::with_broadcast(broadcast.clone());
let mut video = Estimator::with_broadcast(broadcast);
let anchor = Instant::now();
audio.flush(micros(0), anchor);
video.flush(micros(0), anchor + Duration::from_millis(50));
assert_eq!(video.estimate().delay, Some(Duration::from_millis(50)));
audio.discontinuity();
video.discontinuity();
video.flush(micros(40_000), anchor + Duration::from_millis(2_090));
audio.flush(micros(40_000), anchor + Duration::from_millis(2_040));
video.flush(micros(80_000), anchor + Duration::from_millis(2_130));
assert_eq!(video.estimate().delay, Some(Duration::from_millis(50)));
assert_eq!(audio.estimate().delay, None);
}
#[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, None);
}
fn feed_groups(estimator: &mut Estimator, groups: std::ops::Range<u64>, order: &[i64]) {
for group in groups {
let keyframe = 25 + (group * order.len() as u64) as i64;
for (i, &offset) in order.iter().enumerate() {
let ts = micros(((keyframe + offset) as u64) * 40_000);
if i == 0 {
estimator.cut(Some(ts));
}
estimator.write(ts, 1_000);
}
}
}
#[test]
fn framerate_waits_for_the_window() {
let mut estimator = Estimator::new();
let order: Vec<i64> = (0..12).collect();
feed_groups(&mut estimator, 0..1, &order);
estimator.cut(None);
assert_eq!(estimator.estimate().framerate, None);
feed_groups(&mut estimator, 1..5, &order);
assert_eq!(estimator.estimate().framerate, Some(25.0));
}
#[test]
fn framerate_counts_reordered_groups() {
let mut estimator = Estimator::new();
let mut order = vec![0];
for quad in 0..(23 / 3) {
let base = quad * 3;
order.extend([base + 3, base + 1, base + 2]);
}
order.extend(22..24);
assert_eq!(order.len(), 24);
feed_groups(&mut estimator, 0..5, &order);
assert_eq!(estimator.estimate().framerate, Some(25.0));
}
#[test]
fn framerate_counts_leading_pictures() {
let mut estimator = Estimator::new();
let mut order = vec![0, -3, -2, -1];
order.extend(1..21);
assert_eq!(order.len(), 24);
feed_groups(&mut estimator, 0..5, &order);
assert_eq!(estimator.estimate().framerate, Some(25.0));
}
#[test]
fn framerate_snaps_to_ntsc() {
let mut estimator = Estimator::new();
for i in 0..300u64 {
let ts = Timestamp::from_scale(i * 3003, 90_000).unwrap();
estimator.cut(Some(ts));
estimator.write(ts, 1_000);
estimator.cut(None);
}
assert_eq!(estimator.estimate().framerate, Some(30_000.0 / 1001.0));
}
#[test]
fn framerate_skips_nonstandard_rates() {
let mut estimator = Estimator::new();
for i in 0..100u64 {
let ts = micros(i * 37_037);
estimator.cut(Some(ts));
estimator.write(ts, 1_000);
}
assert_eq!(estimator.estimate().framerate, None);
}
#[test]
fn framerate_reports_the_maximum() {
let mut estimator = Estimator::new();
let order: Vec<i64> = (0..25).collect();
feed_groups(&mut estimator, 0..3, &order);
assert_eq!(estimator.estimate().framerate, Some(25.0));
for i in 0..40u64 {
let ts = micros(4_000_000 + i * 41_667);
estimator.cut(Some(ts));
estimator.write(ts, 1_000);
}
assert_eq!(estimator.estimate().framerate, Some(25.0), "24 fps doesn't lower it");
}
#[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");
}
#[test]
fn discontinuity_resets_the_framerate_window() {
let mut estimator = Estimator::new();
for i in 0..10u64 {
estimator.write(micros(i * 40_000), 1_000);
}
estimator.discontinuity();
for i in 0..60u64 {
let ts = micros(2_400_000_000 + i * 40_000);
estimator.cut(Some(ts));
estimator.write(ts, 1_000);
}
assert_eq!(estimator.estimate().framerate, Some(25.0));
}
}