use super::{
AUDIO_ONLY_BPS, HIGH_MIN_BPS, LOW_MIN_BPS, MEDIUM_MIN_BPS, SUSPEND_STREAK, SUSPEND_VIDEO_BPS,
UPGRADE_STREAK,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(docsrs, doc(cfg(feature = "pacer")))]
#[non_exhaustive]
pub enum PacerConfigError {
UpgradeStreakZero,
SuspendStreakZero,
BitrateOrderingViolation,
}
impl core::fmt::Display for PacerConfigError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UpgradeStreakZero => write!(f, "upgrade_streak must be >= 1"),
Self::SuspendStreakZero => write!(f, "suspend_streak must be >= 1"),
Self::BitrateOrderingViolation => write!(
f,
"bitrate fields must satisfy: suspend_video_bps <= audio_only_bps <= low_min_bps <= medium_min_bps <= high_min_bps"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(docsrs, doc(cfg(feature = "pacer")))]
pub struct PacerConfig {
pub suspend_video_bps: u64,
pub audio_only_bps: u64,
pub low_min_bps: u64,
pub medium_min_bps: u64,
pub high_min_bps: u64,
pub suspend_streak: u8,
pub upgrade_streak: u8,
}
impl PacerConfig {
pub fn validate(&self) -> Result<(), PacerConfigError> {
if self.upgrade_streak == 0 {
return Err(PacerConfigError::UpgradeStreakZero);
}
if self.suspend_streak == 0 {
return Err(PacerConfigError::SuspendStreakZero);
}
if !(self.suspend_video_bps <= self.audio_only_bps
&& self.audio_only_bps <= self.low_min_bps
&& self.low_min_bps <= self.medium_min_bps
&& self.medium_min_bps <= self.high_min_bps)
{
return Err(PacerConfigError::BitrateOrderingViolation);
}
Ok(())
}
}
impl Default for PacerConfig {
fn default() -> Self {
Self {
suspend_video_bps: SUSPEND_VIDEO_BPS,
audio_only_bps: AUDIO_ONLY_BPS,
low_min_bps: LOW_MIN_BPS,
medium_min_bps: MEDIUM_MIN_BPS,
high_min_bps: HIGH_MIN_BPS,
suspend_streak: SUSPEND_STREAK,
upgrade_streak: UPGRADE_STREAK,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_values_match_constants() {
let cfg = PacerConfig::default();
assert_eq!(cfg.suspend_video_bps, SUSPEND_VIDEO_BPS);
assert_eq!(cfg.audio_only_bps, AUDIO_ONLY_BPS);
assert_eq!(cfg.low_min_bps, LOW_MIN_BPS);
assert_eq!(cfg.medium_min_bps, MEDIUM_MIN_BPS);
assert_eq!(cfg.high_min_bps, HIGH_MIN_BPS);
assert_eq!(cfg.suspend_streak, SUSPEND_STREAK);
assert_eq!(cfg.upgrade_streak, UPGRADE_STREAK);
}
#[test]
fn default_config_validates_ok() {
assert!(PacerConfig::default().validate().is_ok());
}
#[test]
fn validate_upgrade_streak_zero() {
let cfg = PacerConfig {
upgrade_streak: 0,
..PacerConfig::default()
};
assert_eq!(cfg.validate(), Err(PacerConfigError::UpgradeStreakZero));
}
#[test]
fn validate_suspend_streak_zero() {
let cfg = PacerConfig {
suspend_streak: 0,
..PacerConfig::default()
};
assert_eq!(cfg.validate(), Err(PacerConfigError::SuspendStreakZero));
}
#[test]
fn validate_audio_only_below_suspend_video() {
let cfg = PacerConfig {
audio_only_bps: 5_000, ..PacerConfig::default()
};
assert_eq!(
cfg.validate(),
Err(PacerConfigError::BitrateOrderingViolation)
);
}
#[test]
fn validate_low_min_below_audio_only() {
let cfg = PacerConfig {
low_min_bps: 50_000, ..PacerConfig::default()
};
assert_eq!(
cfg.validate(),
Err(PacerConfigError::BitrateOrderingViolation)
);
}
#[test]
fn validate_medium_min_below_low_min() {
let cfg = PacerConfig {
medium_min_bps: 100_000, ..PacerConfig::default()
};
assert_eq!(
cfg.validate(),
Err(PacerConfigError::BitrateOrderingViolation)
);
}
#[test]
fn validate_high_min_below_medium_min() {
let cfg = PacerConfig {
high_min_bps: 200_000, ..PacerConfig::default()
};
assert_eq!(
cfg.validate(),
Err(PacerConfigError::BitrateOrderingViolation)
);
}
#[test]
fn validate_equal_thresholds_ok() {
let cfg = PacerConfig {
suspend_video_bps: 10_000,
audio_only_bps: 10_000,
low_min_bps: 10_000,
medium_min_bps: 10_000,
high_min_bps: 10_000,
suspend_streak: 1,
upgrade_streak: 1,
};
assert!(cfg.validate().is_ok());
}
#[test]
fn struct_update_syntax_works() {
let cfg = PacerConfig {
upgrade_streak: 5,
..PacerConfig::default()
};
assert_eq!(cfg.upgrade_streak, 5);
assert_eq!(cfg.audio_only_bps, AUDIO_ONLY_BPS);
assert_eq!(cfg.suspend_streak, SUSPEND_STREAK);
}
#[test]
fn custom_config_drives_pacer_behavior() {
use crate::bwe::{PacerAction, SubscriberPacer};
let cfg = PacerConfig {
upgrade_streak: 1,
..PacerConfig::default()
};
let mut pacer = SubscriberPacer::with_config(cfg);
let action = pacer.update(MEDIUM_MIN_BPS + 1);
assert_eq!(
action,
PacerAction::ChangeLayer(crate::ids::SfuRid::MEDIUM),
"upgrade_streak=1 must upgrade in first tick"
);
}
#[test]
fn custom_audio_only_threshold() {
use crate::bwe::{PacerAction, SubscriberPacer};
let custom_audio_only = 200_000u64;
let cfg = PacerConfig {
audio_only_bps: custom_audio_only,
low_min_bps: custom_audio_only + 50_000,
..PacerConfig::default()
};
let mut pacer = SubscriberPacer::with_config(cfg);
let action = pacer.update(100_000);
assert_eq!(action, PacerAction::GoAudioOnly);
}
}