#![allow(dead_code)]
use std::collections::VecDeque;
use crate::error::{NetError, NetResult};
#[derive(Debug, Clone)]
pub struct BufferConfig {
pub target_level_secs: f64,
pub min_safe_secs: f64,
pub max_capacity_secs: f64,
pub playback_drain_rate: f64,
pub bandwidth_history_len: usize,
}
impl Default for BufferConfig {
fn default() -> Self {
Self {
target_level_secs: 15.0,
min_safe_secs: 3.0,
max_capacity_secs: 60.0,
playback_drain_rate: 1.0,
bandwidth_history_len: 8,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FillDrainBalance {
pub fill_rate: f64,
pub drain_rate: f64,
pub net_rate: f64,
}
impl FillDrainBalance {
pub fn compute(
bandwidth_bps: f64,
segment_bitrate_bps: f64,
is_playing: bool,
) -> NetResult<Self> {
if segment_bitrate_bps <= 0.0 {
return Err(NetError::invalid_state(
"segment_bitrate_bps must be positive",
));
}
let fill_rate = bandwidth_bps / segment_bitrate_bps;
let drain_rate = if is_playing { 1.0 } else { 0.0 };
Ok(Self {
fill_rate,
drain_rate,
net_rate: fill_rate - drain_rate,
})
}
#[must_use]
pub fn is_growing(&self) -> bool {
self.net_rate > 0.0
}
#[must_use]
pub fn is_draining(&self) -> bool {
self.net_rate < 0.0
}
}
#[derive(Debug, Clone)]
pub struct BufferState {
level_secs: f64,
is_stalled: bool,
total_stall_secs: f64,
stall_count: u32,
bandwidth_history: VecDeque<f64>,
max_capacity_secs: f64,
history_len: usize,
}
impl BufferState {
#[must_use]
pub fn new(config: &BufferConfig) -> Self {
Self {
level_secs: 0.0,
is_stalled: false,
total_stall_secs: 0.0,
stall_count: 0,
bandwidth_history: VecDeque::with_capacity(config.bandwidth_history_len),
max_capacity_secs: config.max_capacity_secs,
history_len: config.bandwidth_history_len,
}
}
#[must_use]
pub fn level(&self) -> f64 {
self.level_secs
}
#[must_use]
pub fn is_stalled(&self) -> bool {
self.is_stalled
}
#[must_use]
pub fn total_stall_secs(&self) -> f64 {
self.total_stall_secs
}
#[must_use]
pub fn stall_count(&self) -> u32 {
self.stall_count
}
pub fn add_segment(&mut self, segment_secs: f64) {
self.level_secs = (self.level_secs + segment_secs).min(self.max_capacity_secs);
if self.is_stalled && self.level_secs > 0.0 {
self.is_stalled = false;
}
}
pub fn advance(&mut self, elapsed_secs: f64, drain_rate: f64) -> f64 {
if elapsed_secs <= 0.0 {
return 0.0;
}
let to_drain = elapsed_secs * drain_rate;
if to_drain <= self.level_secs {
self.level_secs -= to_drain;
0.0
} else {
let stall_secs = if drain_rate > 0.0 {
elapsed_secs - self.level_secs / drain_rate
} else {
0.0
};
self.level_secs = 0.0;
if !self.is_stalled && drain_rate > 0.0 {
self.is_stalled = true;
self.stall_count += 1;
}
self.total_stall_secs += stall_secs;
stall_secs
}
}
pub fn record_bandwidth(&mut self, bps: f64) {
if self.bandwidth_history.len() >= self.history_len {
self.bandwidth_history.pop_front();
}
self.bandwidth_history.push_back(bps);
}
#[must_use]
pub fn mean_bandwidth(&self) -> Option<f64> {
if self.bandwidth_history.is_empty() {
return None;
}
let sum: f64 = self.bandwidth_history.iter().sum();
Some(sum / self.bandwidth_history.len() as f64)
}
#[must_use]
pub fn bandwidth_stddev(&self) -> Option<f64> {
if self.bandwidth_history.len() < 2 {
return None;
}
let mean = self.mean_bandwidth()?;
let variance = self
.bandwidth_history
.iter()
.map(|&x| (x - mean).powi(2))
.sum::<f64>()
/ (self.bandwidth_history.len() - 1) as f64;
Some(variance.sqrt())
}
pub fn reset_stall_counters(&mut self) {
self.total_stall_secs = 0.0;
self.stall_count = 0;
self.is_stalled = false;
}
}
pub struct RebufferEstimator {
cv: f64,
}
impl RebufferEstimator {
#[must_use]
pub fn new(cv: f64) -> Self {
Self {
cv: cv.clamp(0.0, 2.0),
}
}
#[must_use]
pub fn from_state(state: &BufferState, fallback_cv: f64) -> Self {
let cv = match (state.mean_bandwidth(), state.bandwidth_stddev()) {
(Some(mean), Some(std)) if mean > 0.0 => std / mean,
_ => fallback_cv,
};
Self::new(cv)
}
#[must_use]
pub fn rebuffer_probability(&self, buffer_secs: f64, fill_rate: f64, segment_secs: f64) -> f64 {
if buffer_secs <= 0.0 {
return 1.0; }
if fill_rate <= 0.0 {
return 1.0; }
if segment_secs <= 0.0 {
return 0.0;
}
let r_critical = buffer_secs / (buffer_secs + segment_secs);
if fill_rate >= 1.0 {
if r_critical >= 1.0 {
return 0.0;
}
}
let sigma = fill_rate * self.cv;
if sigma <= 0.0 {
return if fill_rate < r_critical { 1.0 } else { 0.0 };
}
let z = (r_critical - fill_rate) / sigma;
gaussian_cdf(z)
}
#[must_use]
pub fn expected_stall_secs(&self, buffer_secs: f64, fill_rate: f64, segment_secs: f64) -> f64 {
let prob = self.rebuffer_probability(buffer_secs, fill_rate, segment_secs);
if prob <= 0.0 {
return 0.0;
}
if fill_rate <= 0.0 {
return segment_secs; }
let download_time = segment_secs / fill_rate;
let time_to_empty = if fill_rate < 1.0 {
buffer_secs / (1.0 - fill_rate)
} else {
f64::INFINITY
};
let stall_if_rebuffers = (download_time - time_to_empty).max(0.0);
prob * stall_if_rebuffers
}
}
fn gaussian_cdf(z: f64) -> f64 {
const P: f64 = 0.231_641_9;
const A: [f64; 5] = [
0.319_381_53,
-0.356_563_782,
1.781_477_937,
-1.821_255_978,
1.330_274_429,
];
let t = 1.0 / (1.0 + P * z.abs());
let poly = t * (A[0] + t * (A[1] + t * (A[2] + t * (A[3] + t * A[4]))));
let pdf = (-z * z / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
let tail = pdf * poly;
if z >= 0.0 {
1.0 - tail
} else {
tail
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufferPhase {
Critical,
Building,
Steady,
Full,
}
pub struct BufferModel {
config: BufferConfig,
state: BufferState,
}
impl BufferModel {
#[must_use]
pub fn new(config: BufferConfig) -> Self {
let state = BufferState::new(&config);
Self { config, state }
}
#[must_use]
pub fn phase(&self) -> BufferPhase {
let level = self.state.level_secs;
if level >= self.config.max_capacity_secs {
BufferPhase::Full
} else if level >= self.config.target_level_secs {
BufferPhase::Steady
} else if level >= self.config.min_safe_secs {
BufferPhase::Building
} else {
BufferPhase::Critical
}
}
#[must_use]
pub fn level(&self) -> f64 {
self.state.level_secs
}
#[must_use]
pub fn config(&self) -> &BufferConfig {
&self.config
}
#[must_use]
pub fn state(&self) -> &BufferState {
&self.state
}
pub fn state_mut(&mut self) -> &mut BufferState {
&mut self.state
}
pub fn tick(&mut self, elapsed_secs: f64, bandwidth_bps: Option<f64>) -> f64 {
let drain = if self.state.is_stalled {
0.0
} else {
self.config.playback_drain_rate
};
let stall = self.state.advance(elapsed_secs, drain);
if let Some(bw) = bandwidth_bps {
self.state.record_bandwidth(bw);
}
stall
}
pub fn add_segment(&mut self, segment_secs: f64) {
self.state.add_segment(segment_secs);
}
#[must_use]
pub fn rebuffer_probability(&self, fill_rate: f64, segment_secs: f64) -> f64 {
let estimator = RebufferEstimator::from_state(&self.state, 0.2);
estimator.rebuffer_probability(self.state.level_secs, fill_rate, segment_secs)
}
#[must_use]
pub fn quality_cap(&self) -> f64 {
match self.phase() {
BufferPhase::Critical => 0.0,
BufferPhase::Building => {
let range = self.config.target_level_secs - self.config.min_safe_secs;
if range <= 0.0 {
return 0.25;
}
let t = (self.state.level_secs - self.config.min_safe_secs) / range;
(t * 0.5).clamp(0.0, 0.5)
}
BufferPhase::Steady => 1.0,
BufferPhase::Full => 1.0,
}
}
pub fn reset(&mut self) {
self.state = BufferState::new(&self.config);
}
}
#[derive(Debug, Clone, Default)]
pub struct PlaybackQoE {
pub total_secs: f64,
pub stall_secs: f64,
pub stall_count: u32,
pub quality_switches: u32,
pub mean_quality_score: f64,
}
impl PlaybackQoE {
pub fn observe(&mut self, elapsed: f64, stall_secs: f64, quality_score: f64, switched: bool) {
self.total_secs += elapsed;
self.stall_secs += stall_secs;
if stall_secs > 0.0 {
self.stall_count += 1;
}
if switched {
self.quality_switches += 1;
}
if self.total_secs > 0.0 {
let w = elapsed / self.total_secs;
self.mean_quality_score = (1.0 - w) * self.mean_quality_score + w * quality_score;
}
}
#[must_use]
pub fn stall_ratio(&self) -> f64 {
if self.total_secs <= 0.0 {
0.0
} else {
(self.stall_secs / self.total_secs).clamp(0.0, 1.0)
}
}
#[must_use]
pub fn composite_score(&self) -> f64 {
let stall_penalty = (self.stall_ratio() * 5.0).clamp(0.0, 1.0);
let switch_penalty = (self.quality_switches as f64 * 0.01).clamp(0.0, 0.2);
let q = self.mean_quality_score.clamp(0.0, 1.0);
let score = 0.5 * q - 0.4 * stall_penalty - 0.1 * switch_penalty;
score.clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_model() -> BufferModel {
BufferModel::new(BufferConfig::default())
}
#[test]
fn test_initial_buffer_empty() {
let m = default_model();
assert_eq!(m.level(), 0.0);
assert_eq!(m.phase(), BufferPhase::Critical);
}
#[test]
fn test_add_segment_increases_level() {
let mut m = default_model();
m.add_segment(6.0);
assert!((m.level() - 6.0).abs() < 1e-9);
assert_eq!(m.phase(), BufferPhase::Building);
}
#[test]
fn test_tick_drains_buffer() {
let mut m = default_model();
m.add_segment(10.0);
let stall = m.tick(3.0, None);
assert_eq!(stall, 0.0);
assert!((m.level() - 7.0).abs() < 1e-9);
}
#[test]
fn test_stall_detected_when_buffer_empty() {
let mut m = default_model();
m.add_segment(2.0);
let stall = m.tick(5.0, None); assert!(stall > 0.0, "should stall");
assert_eq!(m.state().stall_count(), 1);
}
#[test]
fn test_stall_recovery_after_segment_add() {
let mut m = default_model();
m.tick(1.0, None);
assert!(m.state().is_stalled());
m.add_segment(5.0);
assert!(!m.state().is_stalled());
}
#[test]
fn test_fill_drain_balance_growing() {
let bal = FillDrainBalance::compute(4_000_000.0, 2_000_000.0, true)
.expect("bandwidth and segment size are valid positive values");
assert!((bal.fill_rate - 2.0).abs() < 1e-9);
assert!(bal.is_growing());
assert!(!bal.is_draining());
}
#[test]
fn test_rebuffer_probability_increases_low_buffer() {
let estimator = RebufferEstimator::new(0.3);
let prob_safe = estimator.rebuffer_probability(20.0, 2.0, 6.0);
let prob_empty = estimator.rebuffer_probability(0.0, 0.8, 6.0);
assert_eq!(prob_empty, 1.0, "zero buffer should give probability 1.0");
assert!(
prob_safe < 0.5,
"prob at safe buffer + high fill rate should be low: {prob_safe}"
);
}
#[test]
fn test_quality_cap_by_phase() {
let mut m = default_model();
assert_eq!(m.quality_cap(), 0.0);
m.add_segment(8.0); assert!(m.quality_cap() > 0.0 && m.quality_cap() < 1.0);
m.add_segment(20.0); assert_eq!(m.quality_cap(), 1.0);
}
#[test]
fn test_qoe_stall_ratio() {
let mut qoe = PlaybackQoE::default();
qoe.observe(10.0, 0.0, 1.0, false);
qoe.observe(10.0, 2.0, 0.5, true);
assert!((qoe.stall_ratio() - 0.1).abs() < 0.01);
assert_eq!(qoe.quality_switches, 1);
}
#[test]
fn test_bandwidth_stddev_from_history() {
let config = BufferConfig::default();
let mut state = BufferState::new(&config);
for &bw in &[1_000_000.0f64, 2_000_000.0, 1_500_000.0, 1_800_000.0] {
state.record_bandwidth(bw);
}
let mean = state.mean_bandwidth().expect("mean should be available");
assert!(mean > 0.0);
let std = state
.bandwidth_stddev()
.expect("stddev should be available");
assert!(std > 0.0);
}
}