use std::cmp::Ordering;
use std::time::{Duration, Instant};
use ustreamer_proto::quality::{EncodeMode, EncodeParams, QualityTier};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NetworkMetrics {
pub rtt: Duration,
pub packet_loss_ratio: Option<f32>,
}
impl NetworkMetrics {
pub fn new(rtt: Duration) -> Self {
Self {
rtt,
packet_loss_ratio: None,
}
}
pub fn with_packet_loss(mut self, packet_loss_ratio: f32) -> Self {
self.packet_loss_ratio = Some(packet_loss_ratio.clamp(0.0, 1.0));
self
}
}
#[derive(Debug, Clone)]
pub struct QualityConfig {
pub settle_timeout: Duration,
pub idle_timeout: Duration,
pub idle_fps: u32,
pub ultra_rtt_limit: Duration,
pub high_res_rtt_limit: Duration,
pub standard_rtt_limit: Duration,
pub ultra_loss_limit: f32,
pub high_res_loss_limit: f32,
pub standard_loss_limit: f32,
pub downgrade_hysteresis: u32,
pub upgrade_hysteresis: u32,
}
impl Default for QualityConfig {
fn default() -> Self {
Self {
settle_timeout: Duration::from_millis(200),
idle_timeout: Duration::from_secs(2),
idle_fps: 5,
ultra_rtt_limit: Duration::from_millis(8),
high_res_rtt_limit: Duration::from_millis(18),
standard_rtt_limit: Duration::from_millis(40),
ultra_loss_limit: 0.005,
high_res_loss_limit: 0.02,
standard_loss_limit: 0.05,
downgrade_hysteresis: 2,
upgrade_hysteresis: 6,
}
}
}
pub struct QualityController {
config: QualityConfig,
last_input_time: Instant,
requested_tier: QualityTier,
network_cap_tier: QualityTier,
last_network_metrics: Option<NetworkMetrics>,
lossless_sent: bool,
consecutive_degraded_samples: u32,
consecutive_recovered_samples: u32,
}
impl QualityController {
pub fn new(config: QualityConfig) -> Self {
Self {
config,
last_input_time: Instant::now(),
requested_tier: QualityTier::Standard,
network_cap_tier: QualityTier::Ultra,
last_network_metrics: None,
lossless_sent: false,
consecutive_degraded_samples: 0,
consecutive_recovered_samples: 0,
}
}
pub fn on_input(&mut self) {
self.last_input_time = Instant::now();
self.lossless_sent = false;
}
pub fn on_transport_rtt(&mut self, rtt: Duration) {
self.on_network_metrics(NetworkMetrics::new(rtt));
}
pub fn on_network_metrics(&mut self, metrics: NetworkMetrics) {
let sampled_tier = self.sampled_network_tier(metrics);
self.last_network_metrics = Some(metrics);
match tier_rank(sampled_tier).cmp(&tier_rank(self.network_cap_tier)) {
Ordering::Less => {
self.consecutive_recovered_samples = 0;
self.consecutive_degraded_samples += 1;
if self.consecutive_degraded_samples >= self.config.downgrade_hysteresis.max(1) {
self.network_cap_tier = sampled_tier;
self.consecutive_degraded_samples = 0;
}
}
Ordering::Greater => {
self.consecutive_degraded_samples = 0;
self.consecutive_recovered_samples += 1;
if self.consecutive_recovered_samples >= self.config.upgrade_hysteresis.max(1) {
self.network_cap_tier =
min_tier(step_up_tier(self.network_cap_tier), sampled_tier);
self.consecutive_recovered_samples = 0;
}
}
Ordering::Equal => {
self.consecutive_degraded_samples = 0;
self.consecutive_recovered_samples = 0;
}
}
}
pub fn requested_tier(&self) -> QualityTier {
self.requested_tier
}
pub fn network_cap_tier(&self) -> QualityTier {
self.network_cap_tier
}
pub fn current_tier(&self) -> QualityTier {
min_tier(self.requested_tier, self.network_cap_tier)
}
pub fn last_network_metrics(&self) -> Option<NetworkMetrics> {
self.last_network_metrics
}
pub fn frame_params(&mut self) -> EncodeParams {
let idle_duration = self.last_input_time.elapsed();
let mode = if idle_duration >= self.config.settle_timeout && !self.lossless_sent {
self.lossless_sent = true;
EncodeMode::LosslessRefine
} else if idle_duration >= self.config.idle_timeout {
EncodeMode::IdleLowFps
} else {
EncodeMode::Interactive
};
let (width, height, fps, bitrate, max_bitrate): (u32, u32, u32, u64, u64) =
match self.current_tier() {
QualityTier::Low => (1280, 720, 30, 5_000_000, 10_000_000),
QualityTier::Standard => (1920, 1080, 60, 15_000_000, 30_000_000),
QualityTier::HighRes => (3840, 2160, 30, 40_000_000, 80_000_000),
QualityTier::Ultra => (3840, 2160, 60, 80_000_000, 150_000_000),
};
let (bitrate, max_bitrate) = match mode {
EncodeMode::LosslessRefine => (max_bitrate, max_bitrate.saturating_mul(2)),
_ => (bitrate, max_bitrate),
};
let target_fps = match mode {
EncodeMode::IdleLowFps => self.config.idle_fps,
EncodeMode::LosslessRefine => fps,
EncodeMode::Interactive => fps,
};
EncodeParams {
width,
height,
target_fps,
bitrate_bps: bitrate,
max_bitrate_bps: max_bitrate,
mode,
force_keyframe: mode == EncodeMode::LosslessRefine,
}
}
pub fn set_tier(&mut self, tier: QualityTier) {
self.requested_tier = tier;
}
fn sampled_network_tier(&self, metrics: NetworkMetrics) -> QualityTier {
let loss = metrics.packet_loss_ratio.unwrap_or(0.0).clamp(0.0, 1.0);
if metrics.rtt > self.config.standard_rtt_limit || loss > self.config.standard_loss_limit {
QualityTier::Low
} else if metrics.rtt > self.config.high_res_rtt_limit
|| loss > self.config.high_res_loss_limit
{
QualityTier::Standard
} else if metrics.rtt > self.config.ultra_rtt_limit || loss > self.config.ultra_loss_limit {
QualityTier::HighRes
} else {
QualityTier::Ultra
}
}
}
fn tier_rank(tier: QualityTier) -> u8 {
match tier {
QualityTier::Low => 0,
QualityTier::Standard => 1,
QualityTier::HighRes => 2,
QualityTier::Ultra => 3,
}
}
fn min_tier(a: QualityTier, b: QualityTier) -> QualityTier {
if tier_rank(a) <= tier_rank(b) { a } else { b }
}
fn step_up_tier(tier: QualityTier) -> QualityTier {
match tier {
QualityTier::Low => QualityTier::Standard,
QualityTier::Standard => QualityTier::HighRes,
QualityTier::HighRes => QualityTier::Ultra,
QualityTier::Ultra => QualityTier::Ultra,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_to_standard_without_network_feedback() {
let mut controller = QualityController::new(Default::default());
let params = controller.frame_params();
assert_eq!(controller.requested_tier(), QualityTier::Standard);
assert_eq!(controller.network_cap_tier(), QualityTier::Ultra);
assert_eq!(controller.current_tier(), QualityTier::Standard);
assert_eq!(
(params.width, params.height, params.target_fps),
(1920, 1080, 60)
);
}
#[test]
fn degraded_rtt_caps_requested_ultra_tier() {
let mut controller = QualityController::new(Default::default());
controller.set_tier(QualityTier::Ultra);
controller.on_transport_rtt(Duration::from_millis(20));
assert_eq!(controller.current_tier(), QualityTier::Ultra);
controller.on_transport_rtt(Duration::from_millis(20));
assert_eq!(controller.network_cap_tier(), QualityTier::Standard);
assert_eq!(controller.current_tier(), QualityTier::Standard);
let params = controller.frame_params();
assert_eq!(
(params.width, params.height, params.target_fps),
(1920, 1080, 60)
);
}
#[test]
fn recovery_happens_one_tier_at_a_time() {
let mut controller = QualityController::new(Default::default());
controller.set_tier(QualityTier::Ultra);
for _ in 0..controller.config.downgrade_hysteresis {
controller.on_network_metrics(
NetworkMetrics::new(Duration::from_millis(1)).with_packet_loss(0.06),
);
}
assert_eq!(controller.network_cap_tier(), QualityTier::Low);
for _ in 0..controller.config.upgrade_hysteresis {
controller.on_transport_rtt(Duration::from_millis(1));
}
assert_eq!(controller.network_cap_tier(), QualityTier::Standard);
for _ in 0..controller.config.upgrade_hysteresis {
controller.on_transport_rtt(Duration::from_millis(1));
}
assert_eq!(controller.network_cap_tier(), QualityTier::HighRes);
}
#[test]
fn requested_tier_remains_a_hard_upper_bound() {
let mut controller = QualityController::new(Default::default());
controller.set_tier(QualityTier::HighRes);
for _ in 0..controller.config.upgrade_hysteresis {
controller.on_transport_rtt(Duration::from_millis(1));
}
assert_eq!(controller.network_cap_tier(), QualityTier::Ultra);
assert_eq!(controller.current_tier(), QualityTier::HighRes);
}
#[test]
fn settle_refine_forces_one_keyframe_until_next_input() {
let mut controller = QualityController::new(Default::default());
controller.last_input_time =
Instant::now() - controller.config.settle_timeout - Duration::from_millis(1);
let refine = controller.frame_params();
assert_eq!(refine.mode, EncodeMode::LosslessRefine);
assert!(refine.force_keyframe);
assert_eq!(refine.bitrate_bps, 30_000_000);
assert_eq!(refine.max_bitrate_bps, 60_000_000);
let idle = controller.frame_params();
assert_eq!(idle.mode, EncodeMode::Interactive);
assert!(!idle.force_keyframe);
controller.on_input();
assert_eq!(controller.frame_params().mode, EncodeMode::Interactive);
}
}