use super::{
AbrConfig, AbrDecision, AdaptiveBitrateController, BandwidthEstimator, QualityLevel,
QualitySelector,
};
use crate::abr::history::SegmentDownloadHistory;
use std::time::Duration;
#[derive(Debug)]
pub struct BolaBbrController {
config: AbrConfig,
bandwidth_estimator: BandwidthEstimator,
buffer_level: Duration,
lyapunov_v: f64,
segment_duration: f64,
quality_selector: QualitySelector,
download_history: SegmentDownloadHistory,
min_bitrate: f64,
in_startup: bool,
}
impl BolaBbrController {
#[must_use]
pub fn new(config: AbrConfig, lyapunov_v: f64, segment_duration: f64) -> Self {
let alpha = config.mode.ema_alpha();
let bandwidth_estimator =
BandwidthEstimator::new(config.estimation_window, config.sample_ttl, alpha);
Self {
config,
bandwidth_estimator,
buffer_level: Duration::ZERO,
lyapunov_v: lyapunov_v.max(0.1),
segment_duration: segment_duration.max(1.0),
quality_selector: QualitySelector::new(),
download_history: SegmentDownloadHistory::new(50),
min_bitrate: 0.0,
in_startup: true,
}
}
#[must_use]
pub fn default_params(config: AbrConfig) -> Self {
Self::new(config, 5.0, 4.0)
}
pub fn set_segment_duration(&mut self, duration: f64) {
self.segment_duration = duration.max(1.0);
}
fn utility(&self, bitrate: f64) -> f64 {
if self.min_bitrate <= 0.0 || bitrate <= 0.0 {
return 0.0;
}
(bitrate / self.min_bitrate).ln().max(0.0)
}
fn bola_objective(&self, bitrate: f64, throughput_bps: f64) -> f64 {
let q = self.buffer_level.as_secs_f64();
let utility = self.utility(bitrate);
let quality_term = self.lyapunov_v * utility;
let buffer_term = q / self.segment_duration;
let penalty = if throughput_bps > 0.0 {
bitrate / throughput_bps
} else {
0.0
};
quality_term + buffer_term - penalty
}
fn bola_select_quality(
&mut self,
levels: &[QualityLevel],
current_index: usize,
) -> AbrDecision {
if levels.is_empty() {
return AbrDecision::Maintain;
}
if self.min_bitrate <= 0.0 {
self.min_bitrate = levels
.iter()
.map(|l| l.effective_bandwidth() as f64)
.fold(f64::INFINITY, f64::min);
if self.min_bitrate <= 0.0 {
return AbrDecision::Maintain;
}
}
let throughput_bps = self.bandwidth_estimator.estimate_ema() * 8.0;
if self.buffer_level < self.config.mode.critical_buffer() && current_index > 0 {
return AbrDecision::SwitchTo(self.config.min_quality.unwrap_or(0));
}
let buffer_secs = self.buffer_level.as_secs_f64();
let min_q = self.config.min_quality.unwrap_or(0);
let max_q = self
.config
.max_quality
.unwrap_or(levels.len().saturating_sub(1))
.min(levels.len().saturating_sub(1));
let mut best_idx = min_q;
let mut best_obj = f64::NEG_INFINITY;
for idx in min_q..=max_q {
let bitrate = levels[idx].effective_bandwidth() as f64;
if throughput_bps > 0.0 {
let download_time = bitrate * self.segment_duration / throughput_bps;
if download_time > buffer_secs + self.segment_duration {
if idx > min_q {
continue;
}
}
}
let obj = self.bola_objective(bitrate, throughput_bps);
if obj > best_obj {
best_obj = obj;
best_idx = idx;
}
}
if !self
.quality_selector
.can_switch(self.config.mode.min_switch_interval())
{
return AbrDecision::Maintain;
}
if best_idx != current_index {
AbrDecision::SwitchTo(best_idx)
} else {
AbrDecision::Maintain
}
}
#[must_use]
pub fn download_history(&self) -> &SegmentDownloadHistory {
&self.download_history
}
#[must_use]
pub fn lyapunov_v(&self) -> f64 {
self.lyapunov_v
}
}
impl AdaptiveBitrateController for BolaBbrController {
fn select_quality(&self, levels: &[QualityLevel], current_index: usize) -> AbrDecision {
if levels.is_empty() {
return AbrDecision::Maintain;
}
if self.in_startup && self.bandwidth_estimator.sample_count() == 0 {
return AbrDecision::SwitchTo(self.config.min_quality.unwrap_or(0));
}
let min_bitrate = if self.min_bitrate > 0.0 {
self.min_bitrate
} else {
levels
.iter()
.map(|l| l.effective_bandwidth() as f64)
.fold(f64::INFINITY, f64::min)
.max(1.0)
};
let throughput_bps = self.bandwidth_estimator.estimate_ema() * 8.0;
if self.buffer_level < self.config.mode.critical_buffer() && current_index > 0 {
return AbrDecision::SwitchTo(self.config.min_quality.unwrap_or(0));
}
let buffer_secs = self.buffer_level.as_secs_f64();
let min_q = self.config.min_quality.unwrap_or(0);
let max_q = self
.config
.max_quality
.unwrap_or(levels.len().saturating_sub(1))
.min(levels.len().saturating_sub(1));
let utility_fn = |bitrate: f64| -> f64 {
if min_bitrate <= 0.0 || bitrate <= 0.0 {
return 0.0;
}
(bitrate / min_bitrate).ln().max(0.0)
};
let objective = |bitrate: f64| -> f64 {
let q = buffer_secs;
let u = utility_fn(bitrate);
let quality_term = self.lyapunov_v * u;
let buffer_term = q / self.segment_duration;
let penalty = if throughput_bps > 0.0 {
bitrate / throughput_bps
} else {
0.0
};
quality_term + buffer_term - penalty
};
let mut best_idx = min_q;
let mut best_obj = f64::NEG_INFINITY;
for idx in min_q..=max_q {
let bitrate = levels[idx].effective_bandwidth() as f64;
if throughput_bps > 0.0 {
let download_time = bitrate * self.segment_duration / throughput_bps;
if download_time > buffer_secs + self.segment_duration && idx > min_q {
continue;
}
}
let obj = objective(bitrate);
if obj > best_obj {
best_obj = obj;
best_idx = idx;
}
}
if !self
.quality_selector
.can_switch(self.config.mode.min_switch_interval())
{
return AbrDecision::Maintain;
}
if best_idx != current_index {
AbrDecision::SwitchTo(best_idx)
} else {
AbrDecision::Maintain
}
}
fn report_segment_download(&mut self, bytes: usize, duration: Duration) {
self.bandwidth_estimator.add_sample(bytes, duration);
let seg_dur = Duration::from_secs_f64(self.segment_duration);
self.download_history.add(0, bytes, duration, seg_dur);
if self.in_startup && self.download_history.len() >= 3 {
self.in_startup = false;
}
}
fn report_buffer_level(&mut self, buffer_duration: Duration) {
self.buffer_level = buffer_duration;
}
fn estimated_throughput(&self) -> f64 {
self.bandwidth_estimator.estimate_ema() * 8.0
}
fn current_buffer(&self) -> Duration {
self.buffer_level
}
fn reset(&mut self) {
self.bandwidth_estimator.reset();
self.buffer_level = Duration::ZERO;
self.quality_selector.reset();
self.download_history.reset();
self.min_bitrate = 0.0;
self.in_startup = true;
}
fn config(&self) -> &AbrConfig {
&self.config
}
}