use super::{AbrConfig, AbrDecision, AdaptiveBitrateController, BandwidthEstimator, QualityLevel};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct BbaZones {
pub reservoir_secs: f64,
pub cushion_secs: f64,
pub max_buffer_secs: f64,
pub upswitch_margin_secs: f64,
}
impl Default for BbaZones {
fn default() -> Self {
Self {
reservoir_secs: 5.0,
cushion_secs: 25.0,
max_buffer_secs: 40.0,
upswitch_margin_secs: 2.0,
}
}
}
impl BbaZones {
#[must_use]
pub fn low_latency() -> Self {
Self {
reservoir_secs: 1.5,
cushion_secs: 8.0,
max_buffer_secs: 12.0,
upswitch_margin_secs: 0.5,
}
}
#[must_use]
pub fn in_cushion(&self, buffer_secs: f64) -> bool {
buffer_secs > self.reservoir_secs && buffer_secs <= self.cushion_secs
}
#[must_use]
pub fn in_reservoir(&self, buffer_secs: f64) -> bool {
buffer_secs <= self.reservoir_secs
}
#[must_use]
pub fn above_cushion(&self, buffer_secs: f64) -> bool {
buffer_secs > self.cushion_secs
}
}
#[derive(Debug)]
pub struct BbaController {
config: AbrConfig,
zones: BbaZones,
buffer_level: Duration,
bandwidth_estimator: BandwidthEstimator,
last_switch: Option<Instant>,
min_upswitch_interval: Duration,
min_downswitch_interval: Duration,
}
impl BbaController {
#[must_use]
pub fn with_zones(config: AbrConfig, zones: BbaZones) -> Self {
let alpha = config.mode.ema_alpha();
let bandwidth_estimator =
BandwidthEstimator::new(config.estimation_window, config.sample_ttl, alpha);
Self {
config,
zones,
buffer_level: Duration::ZERO,
bandwidth_estimator,
last_switch: None,
min_upswitch_interval: Duration::from_secs(8),
min_downswitch_interval: Duration::from_secs(2),
}
}
#[must_use]
pub fn new(config: AbrConfig) -> Self {
Self::with_zones(config, BbaZones::default())
}
#[must_use]
pub fn low_latency(config: AbrConfig) -> Self {
let zones = BbaZones::low_latency();
let mut ctrl = Self::with_zones(config, zones);
ctrl.min_upswitch_interval = Duration::from_secs(3);
ctrl.min_downswitch_interval = Duration::from_millis(500);
ctrl
}
#[must_use]
pub fn zones(&self) -> &BbaZones {
&self.zones
}
#[must_use]
pub fn buffer_secs(&self) -> f64 {
self.buffer_level.as_secs_f64()
}
#[must_use]
pub fn target_bitrate_bps(&self, levels: &[QualityLevel]) -> f64 {
if levels.is_empty() {
return 0.0;
}
let buf = self.buffer_level.as_secs_f64();
let r_min = levels.iter().map(|l| l.effective_bandwidth()).min().unwrap_or(0) as f64;
let r_max = levels.iter().map(|l| l.effective_bandwidth()).max().unwrap_or(0) as f64;
if self.zones.in_reservoir(buf) {
r_min
} else if self.zones.above_cushion(buf) {
r_max
} else {
let ratio = (buf - self.zones.reservoir_secs)
/ (self.zones.cushion_secs - self.zones.reservoir_secs);
r_min + (r_max - r_min) * ratio.clamp(0.0, 1.0)
}
}
fn best_quality_for_bps(&self, levels: &[QualityLevel], target_bps: f64) -> usize {
let mut best = 0usize;
let mut best_bw = 0u64;
for (idx, level) in levels.iter().enumerate() {
let bw = level.effective_bandwidth();
if bw as f64 <= target_bps && bw > best_bw {
best = idx;
best_bw = bw;
}
}
if let Some(min) = self.config.min_quality {
best = best.max(min);
}
if let Some(max) = self.config.max_quality {
best = best.min(max);
}
best.min(levels.len().saturating_sub(1))
}
fn can_upswitch(&self) -> bool {
match self.last_switch {
Some(t) => t.elapsed() >= self.min_upswitch_interval,
None => true,
}
}
fn can_downswitch(&self) -> bool {
match self.last_switch {
Some(t) => t.elapsed() >= self.min_downswitch_interval,
None => true,
}
}
}
impl AdaptiveBitrateController for BbaController {
fn select_quality(&self, levels: &[QualityLevel], current_index: usize) -> AbrDecision {
if levels.is_empty() {
return AbrDecision::Maintain;
}
let buf = self.buffer_level.as_secs_f64();
if self.zones.in_reservoir(buf) {
let min = self.config.min_quality.unwrap_or(0);
if current_index != min {
return AbrDecision::SwitchTo(min);
}
return AbrDecision::Maintain;
}
let estimated_bps = self.bandwidth_estimator.estimate_conservative() * 8.0;
let effective_bps = if estimated_bps > 0.0 {
self.target_bitrate_bps(levels).min(estimated_bps * 0.9)
} else {
self.target_bitrate_bps(levels)
};
let current_bw = levels
.get(current_index)
.map(|l| l.effective_bandwidth() as f64)
.unwrap_or(0.0);
let target_idx = self.best_quality_for_bps(levels, effective_bps);
if target_idx > current_index {
let margined_bps = if buf > self.zones.reservoir_secs + self.zones.upswitch_margin_secs
{
effective_bps
} else {
current_bw };
let idx_with_margin = self.best_quality_for_bps(levels, margined_bps);
if idx_with_margin > current_index && self.can_upswitch() {
return AbrDecision::SwitchTo(idx_with_margin);
}
AbrDecision::Maintain
} else if target_idx < current_index {
if self.can_downswitch() {
AbrDecision::SwitchTo(target_idx)
} else {
AbrDecision::Maintain
}
} else {
AbrDecision::Maintain
}
}
fn report_segment_download(&mut self, bytes: usize, duration: Duration) {
self.bandwidth_estimator.add_sample(bytes, duration);
}
fn report_buffer_level(&mut self, buffer_duration: Duration) {
self.buffer_level = buffer_duration;
}
fn estimated_throughput(&self) -> f64 {
self.bandwidth_estimator.estimate_conservative() * 8.0
}
fn current_buffer(&self) -> Duration {
self.buffer_level
}
fn reset(&mut self) {
self.buffer_level = Duration::ZERO;
self.bandwidth_estimator.reset();
self.last_switch = None;
}
fn config(&self) -> &AbrConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
fn quality_levels() -> Vec<QualityLevel> {
vec![
QualityLevel::new(0, 500_000),
QualityLevel::new(1, 1_500_000),
QualityLevel::new(2, 3_000_000),
QualityLevel::new(3, 6_000_000),
]
}
fn bba(reservoir: f64, cushion: f64) -> BbaController {
let zones = BbaZones {
reservoir_secs: reservoir,
cushion_secs: cushion,
max_buffer_secs: 40.0,
upswitch_margin_secs: 1.0,
};
BbaController::with_zones(AbrConfig::default(), zones)
}
#[test]
fn test_default_zones() {
let z = BbaZones::default();
assert!(z.reservoir_secs < z.cushion_secs);
assert!(z.cushion_secs < z.max_buffer_secs);
}
#[test]
fn test_low_latency_zones() {
let z = BbaZones::low_latency();
let d = BbaZones::default();
assert!(z.cushion_secs < d.cushion_secs);
}
#[test]
fn test_zone_reservoir() {
let z = BbaZones { reservoir_secs: 5.0, cushion_secs: 25.0, ..BbaZones::default() };
assert!(z.in_reservoir(0.0));
assert!(z.in_reservoir(5.0));
assert!(!z.in_reservoir(5.01));
}
#[test]
fn test_zone_cushion() {
let z = BbaZones { reservoir_secs: 5.0, cushion_secs: 25.0, ..BbaZones::default() };
assert!(z.in_cushion(10.0));
assert!(!z.in_cushion(4.9));
assert!(!z.in_cushion(25.1));
}
#[test]
fn test_zone_above_cushion() {
let z = BbaZones { reservoir_secs: 5.0, cushion_secs: 25.0, ..BbaZones::default() };
assert!(z.above_cushion(25.01));
assert!(!z.above_cushion(25.0));
}
#[test]
fn test_target_bitrate_reservoir() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_buffer_level(Duration::from_secs(3));
let target = ctrl.target_bitrate_bps(&quality_levels());
let min_bw = 500_000.0;
assert!((target - min_bw).abs() < 1.0);
}
#[test]
fn test_target_bitrate_above_cushion() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_buffer_level(Duration::from_secs(30));
let target = ctrl.target_bitrate_bps(&quality_levels());
let max_bw = 6_000_000.0;
assert!((target - max_bw).abs() < 1.0);
}
#[test]
fn test_target_bitrate_mid_cushion() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_buffer_level(Duration::from_secs(15));
let target = ctrl.target_bitrate_bps(&quality_levels());
let expected = 500_000.0 + (6_000_000.0 - 500_000.0) * 0.5;
assert!((target - expected).abs() < 1000.0);
}
#[test]
fn test_reservoir_select_lowest() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_buffer_level(Duration::from_millis(500));
let decision = ctrl.select_quality(&quality_levels(), 3);
assert_eq!(decision, AbrDecision::SwitchTo(0));
}
#[test]
fn test_reservoir_already_lowest() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_buffer_level(Duration::from_millis(500));
let decision = ctrl.select_quality(&quality_levels(), 0);
assert_eq!(decision, AbrDecision::Maintain);
}
#[test]
fn test_above_cushion_allows_highest() {
let mut ctrl = bba(5.0, 10.0);
ctrl.report_buffer_level(Duration::from_secs(15));
let decision = ctrl.select_quality(&quality_levels(), 0);
match decision {
AbrDecision::SwitchTo(idx) => assert!(idx > 0),
AbrDecision::Maintain => {} }
}
#[test]
fn test_empty_levels() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_buffer_level(Duration::from_secs(20));
assert_eq!(ctrl.select_quality(&[], 0), AbrDecision::Maintain);
}
#[test]
fn test_report_buffer_level() {
let mut ctrl = BbaController::new(AbrConfig::default());
ctrl.report_buffer_level(Duration::from_secs(12));
assert!((ctrl.buffer_secs() - 12.0).abs() < 1e-9);
}
#[test]
fn test_report_segment_download() {
let mut ctrl = BbaController::new(AbrConfig::default());
ctrl.report_segment_download(1_000_000, Duration::from_secs(1));
assert!(ctrl.estimated_throughput() > 0.0);
}
#[test]
fn test_reset() {
let mut ctrl = BbaController::new(AbrConfig::default());
ctrl.report_buffer_level(Duration::from_secs(20));
ctrl.report_segment_download(1_000_000, Duration::from_secs(1));
ctrl.reset();
assert_eq!(ctrl.current_buffer(), Duration::ZERO);
assert!((ctrl.estimated_throughput()).abs() < 1.0);
}
#[test]
fn test_config_accessor() {
let cfg = AbrConfig::default();
let ctrl = BbaController::new(cfg.clone());
let _ = ctrl.config();
}
#[test]
fn test_low_latency_ctor() {
let ctrl = BbaController::low_latency(AbrConfig::default());
assert!(ctrl.zones().reservoir_secs < 5.0);
}
#[test]
fn test_min_quality_constraint() {
let cfg = AbrConfig::default().with_min_quality(1);
let mut ctrl = BbaController::new(cfg);
ctrl.report_buffer_level(Duration::from_millis(100));
let decision = ctrl.select_quality(&quality_levels(), 3);
match decision {
AbrDecision::SwitchTo(idx) => assert!(idx >= 1),
AbrDecision::Maintain => {}
}
}
#[test]
fn test_max_quality_constraint() {
let cfg = AbrConfig::default().with_max_quality(2);
let mut ctrl = BbaController::with_zones(
cfg,
BbaZones { reservoir_secs: 1.0, cushion_secs: 3.0, ..BbaZones::default() },
);
ctrl.report_buffer_level(Duration::from_secs(10));
let decision = ctrl.select_quality(&quality_levels(), 0);
match decision {
AbrDecision::SwitchTo(idx) => assert!(idx <= 2),
AbrDecision::Maintain => {}
}
}
#[test]
fn test_target_bitrate_empty() {
let ctrl = BbaController::new(AbrConfig::default());
assert!((ctrl.target_bitrate_bps(&[])).abs() < 1e-9);
}
#[test]
fn test_downswitch_on_buffer_drop() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_buffer_level(Duration::from_secs(6));
let decision = ctrl.select_quality(&quality_levels(), 3);
match decision {
AbrDecision::SwitchTo(idx) => assert!(idx < 3),
AbrDecision::Maintain => {} }
}
#[test]
fn test_stall_protection_via_bandwidth() {
let mut ctrl = bba(5.0, 25.0);
ctrl.report_segment_download(1000, Duration::from_secs(1)); ctrl.report_buffer_level(Duration::from_secs(20)); let levels = quality_levels();
let target = ctrl.target_bitrate_bps(&levels);
assert!(target >= 0.0);
}
#[test]
fn test_current_buffer_accessor() {
let mut ctrl = BbaController::new(AbrConfig::default());
ctrl.report_buffer_level(Duration::from_secs(7));
assert_eq!(ctrl.current_buffer(), Duration::from_secs(7));
}
#[test]
fn test_best_quality_for_bps() {
let ctrl = BbaController::new(AbrConfig::default());
let levels = quality_levels();
let idx = ctrl.best_quality_for_bps(&levels, 2_000_000.0);
assert_eq!(idx, 1);
}
#[test]
fn test_with_zones_ctor() {
let zones = BbaZones { reservoir_secs: 2.0, cushion_secs: 15.0, ..BbaZones::default() };
let ctrl = BbaController::with_zones(AbrConfig::default(), zones.clone());
assert!((ctrl.zones().reservoir_secs - 2.0).abs() < 1e-9);
assert!((ctrl.zones().cushion_secs - 15.0).abs() < 1e-9);
}
}