Skip to main content

ustreamer_quality/
lib.rs

1//! Adaptive quality controller for the ultra-streamer pipeline.
2//!
3//! Handles:
4//! - Interaction idle detection → lossless refinement on settle
5//! - Network quality monitoring → bitrate/resolution tier switching
6//! - Framerate reduction during idle periods
7
8use std::cmp::Ordering;
9use std::time::{Duration, Instant};
10use ustreamer_proto::quality::{EncodeMode, EncodeParams, QualityTier};
11
12/// Network feedback used to cap the currently requested quality tier.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct NetworkMetrics {
15    /// Current smoothed round-trip time for the transport session.
16    pub rtt: Duration,
17    /// Estimated packet loss ratio in the range `[0.0, 1.0]`, if available.
18    pub packet_loss_ratio: Option<f32>,
19}
20
21impl NetworkMetrics {
22    /// Create a new metrics sample with RTT only.
23    pub fn new(rtt: Duration) -> Self {
24        Self {
25            rtt,
26            packet_loss_ratio: None,
27        }
28    }
29
30    /// Attach an estimated packet loss ratio to the sample.
31    pub fn with_packet_loss(mut self, packet_loss_ratio: f32) -> Self {
32        self.packet_loss_ratio = Some(packet_loss_ratio.clamp(0.0, 1.0));
33        self
34    }
35}
36
37/// Thresholds for idle detection and quality transitions.
38#[derive(Debug, Clone)]
39pub struct QualityConfig {
40    /// Time after last input before sending a single refine frame.
41    pub settle_timeout: Duration,
42    /// Time after last input before reducing framerate.
43    pub idle_timeout: Duration,
44    /// Minimum FPS during idle.
45    pub idle_fps: u32,
46    /// Maximum RTT that still permits the `Ultra` tier.
47    pub ultra_rtt_limit: Duration,
48    /// Maximum RTT that still permits the `HighRes` tier.
49    pub high_res_rtt_limit: Duration,
50    /// Maximum RTT that still permits the `Standard` tier.
51    pub standard_rtt_limit: Duration,
52    /// Maximum loss ratio that still permits the `Ultra` tier.
53    pub ultra_loss_limit: f32,
54    /// Maximum loss ratio that still permits the `HighRes` tier.
55    pub high_res_loss_limit: f32,
56    /// Maximum loss ratio that still permits the `Standard` tier.
57    pub standard_loss_limit: f32,
58    /// Consecutive degraded samples required before capping quality downward.
59    pub downgrade_hysteresis: u32,
60    /// Consecutive recovered samples required before allowing one tier of recovery.
61    pub upgrade_hysteresis: u32,
62}
63
64impl Default for QualityConfig {
65    fn default() -> Self {
66        Self {
67            settle_timeout: Duration::from_millis(200),
68            idle_timeout: Duration::from_secs(2),
69            idle_fps: 5,
70            ultra_rtt_limit: Duration::from_millis(8),
71            high_res_rtt_limit: Duration::from_millis(18),
72            standard_rtt_limit: Duration::from_millis(40),
73            ultra_loss_limit: 0.005,
74            high_res_loss_limit: 0.02,
75            standard_loss_limit: 0.05,
76            downgrade_hysteresis: 2,
77            upgrade_hysteresis: 6,
78        }
79    }
80}
81
82/// Tracks interaction state and determines encoding parameters each frame.
83pub struct QualityController {
84    config: QualityConfig,
85    last_input_time: Instant,
86    requested_tier: QualityTier,
87    network_cap_tier: QualityTier,
88    last_network_metrics: Option<NetworkMetrics>,
89    lossless_sent: bool,
90    consecutive_degraded_samples: u32,
91    consecutive_recovered_samples: u32,
92}
93
94impl QualityController {
95    pub fn new(config: QualityConfig) -> Self {
96        Self {
97            config,
98            last_input_time: Instant::now(),
99            requested_tier: QualityTier::Standard,
100            network_cap_tier: QualityTier::Ultra,
101            last_network_metrics: None,
102            lossless_sent: false,
103            consecutive_degraded_samples: 0,
104            consecutive_recovered_samples: 0,
105        }
106    }
107
108    /// Call when any input event is received.
109    pub fn on_input(&mut self) {
110        self.last_input_time = Instant::now();
111        self.lossless_sent = false;
112    }
113
114    /// Feed a transport RTT sample into the adaptive quality controller.
115    pub fn on_transport_rtt(&mut self, rtt: Duration) {
116        self.on_network_metrics(NetworkMetrics::new(rtt));
117    }
118
119    /// Feed a full network metrics sample into the adaptive quality controller.
120    ///
121    /// The controller treats `requested_tier` as the application's preferred
122    /// maximum quality, and `network_cap_tier` as the highest quality the
123    /// current transport conditions can safely sustain.
124    pub fn on_network_metrics(&mut self, metrics: NetworkMetrics) {
125        let sampled_tier = self.sampled_network_tier(metrics);
126        self.last_network_metrics = Some(metrics);
127
128        match tier_rank(sampled_tier).cmp(&tier_rank(self.network_cap_tier)) {
129            Ordering::Less => {
130                self.consecutive_recovered_samples = 0;
131                self.consecutive_degraded_samples += 1;
132                if self.consecutive_degraded_samples >= self.config.downgrade_hysteresis.max(1) {
133                    self.network_cap_tier = sampled_tier;
134                    self.consecutive_degraded_samples = 0;
135                }
136            }
137            Ordering::Greater => {
138                self.consecutive_degraded_samples = 0;
139                self.consecutive_recovered_samples += 1;
140                if self.consecutive_recovered_samples >= self.config.upgrade_hysteresis.max(1) {
141                    self.network_cap_tier =
142                        min_tier(step_up_tier(self.network_cap_tier), sampled_tier);
143                    self.consecutive_recovered_samples = 0;
144                }
145            }
146            Ordering::Equal => {
147                self.consecutive_degraded_samples = 0;
148                self.consecutive_recovered_samples = 0;
149            }
150        }
151    }
152
153    /// The currently requested application tier.
154    pub fn requested_tier(&self) -> QualityTier {
155        self.requested_tier
156    }
157
158    /// The highest tier currently allowed by recent network samples.
159    pub fn network_cap_tier(&self) -> QualityTier {
160        self.network_cap_tier
161    }
162
163    /// The effective encode tier after combining app preference and network cap.
164    pub fn current_tier(&self) -> QualityTier {
165        min_tier(self.requested_tier, self.network_cap_tier)
166    }
167
168    /// The most recent network metrics sample, if one has been provided.
169    pub fn last_network_metrics(&self) -> Option<NetworkMetrics> {
170        self.last_network_metrics
171    }
172
173    /// Get the encode parameters for the current frame.
174    pub fn frame_params(&mut self) -> EncodeParams {
175        let idle_duration = self.last_input_time.elapsed();
176
177        let mode = if idle_duration >= self.config.settle_timeout && !self.lossless_sent {
178            self.lossless_sent = true;
179            EncodeMode::LosslessRefine
180        } else if idle_duration >= self.config.idle_timeout {
181            EncodeMode::IdleLowFps
182        } else {
183            EncodeMode::Interactive
184        };
185
186        let (width, height, fps, bitrate, max_bitrate): (u32, u32, u32, u64, u64) =
187            match self.current_tier() {
188                QualityTier::Low => (1280, 720, 30, 5_000_000, 10_000_000),
189                QualityTier::Standard => (1920, 1080, 60, 15_000_000, 30_000_000),
190                QualityTier::HighRes => (3840, 2160, 30, 40_000_000, 80_000_000),
191                QualityTier::Ultra => (3840, 2160, 60, 80_000_000, 150_000_000),
192            };
193        let (bitrate, max_bitrate) = match mode {
194            EncodeMode::LosslessRefine => (max_bitrate, max_bitrate.saturating_mul(2)),
195            _ => (bitrate, max_bitrate),
196        };
197
198        let target_fps = match mode {
199            EncodeMode::IdleLowFps => self.config.idle_fps,
200            EncodeMode::LosslessRefine => fps,
201            EncodeMode::Interactive => fps,
202        };
203
204        EncodeParams {
205            width,
206            height,
207            target_fps,
208            bitrate_bps: bitrate,
209            max_bitrate_bps: max_bitrate,
210            mode,
211            force_keyframe: mode == EncodeMode::LosslessRefine,
212        }
213    }
214
215    /// Set the application's preferred maximum tier.
216    pub fn set_tier(&mut self, tier: QualityTier) {
217        self.requested_tier = tier;
218    }
219
220    fn sampled_network_tier(&self, metrics: NetworkMetrics) -> QualityTier {
221        let loss = metrics.packet_loss_ratio.unwrap_or(0.0).clamp(0.0, 1.0);
222
223        if metrics.rtt > self.config.standard_rtt_limit || loss > self.config.standard_loss_limit {
224            QualityTier::Low
225        } else if metrics.rtt > self.config.high_res_rtt_limit
226            || loss > self.config.high_res_loss_limit
227        {
228            QualityTier::Standard
229        } else if metrics.rtt > self.config.ultra_rtt_limit || loss > self.config.ultra_loss_limit {
230            QualityTier::HighRes
231        } else {
232            QualityTier::Ultra
233        }
234    }
235}
236
237fn tier_rank(tier: QualityTier) -> u8 {
238    match tier {
239        QualityTier::Low => 0,
240        QualityTier::Standard => 1,
241        QualityTier::HighRes => 2,
242        QualityTier::Ultra => 3,
243    }
244}
245
246fn min_tier(a: QualityTier, b: QualityTier) -> QualityTier {
247    if tier_rank(a) <= tier_rank(b) { a } else { b }
248}
249
250fn step_up_tier(tier: QualityTier) -> QualityTier {
251    match tier {
252        QualityTier::Low => QualityTier::Standard,
253        QualityTier::Standard => QualityTier::HighRes,
254        QualityTier::HighRes => QualityTier::Ultra,
255        QualityTier::Ultra => QualityTier::Ultra,
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn defaults_to_standard_without_network_feedback() {
265        let mut controller = QualityController::new(Default::default());
266
267        let params = controller.frame_params();
268
269        assert_eq!(controller.requested_tier(), QualityTier::Standard);
270        assert_eq!(controller.network_cap_tier(), QualityTier::Ultra);
271        assert_eq!(controller.current_tier(), QualityTier::Standard);
272        assert_eq!(
273            (params.width, params.height, params.target_fps),
274            (1920, 1080, 60)
275        );
276    }
277
278    #[test]
279    fn degraded_rtt_caps_requested_ultra_tier() {
280        let mut controller = QualityController::new(Default::default());
281        controller.set_tier(QualityTier::Ultra);
282
283        controller.on_transport_rtt(Duration::from_millis(20));
284        assert_eq!(controller.current_tier(), QualityTier::Ultra);
285
286        controller.on_transport_rtt(Duration::from_millis(20));
287        assert_eq!(controller.network_cap_tier(), QualityTier::Standard);
288        assert_eq!(controller.current_tier(), QualityTier::Standard);
289
290        let params = controller.frame_params();
291        assert_eq!(
292            (params.width, params.height, params.target_fps),
293            (1920, 1080, 60)
294        );
295    }
296
297    #[test]
298    fn recovery_happens_one_tier_at_a_time() {
299        let mut controller = QualityController::new(Default::default());
300        controller.set_tier(QualityTier::Ultra);
301
302        for _ in 0..controller.config.downgrade_hysteresis {
303            controller.on_network_metrics(
304                NetworkMetrics::new(Duration::from_millis(1)).with_packet_loss(0.06),
305            );
306        }
307        assert_eq!(controller.network_cap_tier(), QualityTier::Low);
308
309        for _ in 0..controller.config.upgrade_hysteresis {
310            controller.on_transport_rtt(Duration::from_millis(1));
311        }
312        assert_eq!(controller.network_cap_tier(), QualityTier::Standard);
313
314        for _ in 0..controller.config.upgrade_hysteresis {
315            controller.on_transport_rtt(Duration::from_millis(1));
316        }
317        assert_eq!(controller.network_cap_tier(), QualityTier::HighRes);
318    }
319
320    #[test]
321    fn requested_tier_remains_a_hard_upper_bound() {
322        let mut controller = QualityController::new(Default::default());
323        controller.set_tier(QualityTier::HighRes);
324
325        for _ in 0..controller.config.upgrade_hysteresis {
326            controller.on_transport_rtt(Duration::from_millis(1));
327        }
328
329        assert_eq!(controller.network_cap_tier(), QualityTier::Ultra);
330        assert_eq!(controller.current_tier(), QualityTier::HighRes);
331    }
332
333    #[test]
334    fn settle_refine_forces_one_keyframe_until_next_input() {
335        let mut controller = QualityController::new(Default::default());
336        controller.last_input_time =
337            Instant::now() - controller.config.settle_timeout - Duration::from_millis(1);
338
339        let refine = controller.frame_params();
340        assert_eq!(refine.mode, EncodeMode::LosslessRefine);
341        assert!(refine.force_keyframe);
342        assert_eq!(refine.bitrate_bps, 30_000_000);
343        assert_eq!(refine.max_bitrate_bps, 60_000_000);
344
345        let idle = controller.frame_params();
346        assert_eq!(idle.mode, EncodeMode::Interactive);
347        assert!(!idle.force_keyframe);
348
349        controller.on_input();
350        assert_eq!(controller.frame_params().mode, EncodeMode::Interactive);
351    }
352}