Skip to main content

oximedia_net/srt/
stats.rs

1//! SRT stream statistics and quality monitoring.
2
3#![allow(dead_code)]
4
5use std::time::Instant;
6
7/// Per-direction statistics.
8#[derive(Debug, Clone, Default)]
9pub struct DirectionStats {
10    /// Total packets sent or expected.
11    pub packets_sent: u64,
12    /// Total packets received.
13    pub packets_received: u64,
14    /// Packets lost (not received).
15    pub packets_lost: u64,
16    /// Packets retransmitted.
17    pub packets_retransmitted: u64,
18    /// Total bytes sent.
19    pub bytes_sent: u64,
20    /// Total bytes received.
21    pub bytes_received: u64,
22    /// Packet loss rate in [0.0, 1.0].
23    pub packet_loss_rate: f64,
24    /// Retransmit rate in [0.0, 1.0].
25    pub retransmit_rate: f64,
26    /// Estimated bandwidth in bits per second.
27    pub bandwidth_bps: f64,
28}
29
30impl DirectionStats {
31    /// Recalculate `packet_loss_rate` from packet counts.
32    pub fn update_loss_rate(&mut self) {
33        let total = self.packets_sent;
34        if total == 0 {
35            self.packet_loss_rate = 0.0;
36        } else {
37            self.packet_loss_rate = self.packets_lost as f64 / total as f64;
38        }
39    }
40
41    /// Recalculate `retransmit_rate` from packet counts.
42    pub fn update_retransmit_rate(&mut self) {
43        let total = self.packets_sent;
44        if total == 0 {
45            self.retransmit_rate = 0.0;
46        } else {
47            self.retransmit_rate = self.packets_retransmitted as f64 / total as f64;
48        }
49    }
50}
51
52/// RTT (Round-Trip Time) statistics using Welford's online algorithm.
53#[derive(Debug, Clone)]
54pub struct RttStats {
55    /// Most recent RTT sample in milliseconds.
56    pub current_ms: f64,
57    /// Minimum RTT observed.
58    pub min_ms: f64,
59    /// Maximum RTT observed.
60    pub max_ms: f64,
61    /// Running mean RTT.
62    pub mean_ms: f64,
63    /// Running variance of RTT.
64    pub variance_ms: f64,
65    /// Number of samples recorded.
66    pub sample_count: u64,
67}
68
69impl Default for RttStats {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl RttStats {
76    /// Creates a new, empty `RttStats`.
77    pub fn new() -> Self {
78        Self {
79            current_ms: 0.0,
80            min_ms: f64::MAX,
81            max_ms: 0.0,
82            mean_ms: 0.0,
83            variance_ms: 0.0,
84            sample_count: 0,
85        }
86    }
87
88    /// Update statistics with a new RTT sample using Welford's online algorithm.
89    pub fn update(&mut self, sample_ms: f64) {
90        self.current_ms = sample_ms;
91        self.sample_count += 1;
92
93        // Update min/max
94        if sample_ms < self.min_ms {
95            self.min_ms = sample_ms;
96        }
97        if sample_ms > self.max_ms {
98            self.max_ms = sample_ms;
99        }
100
101        // Welford's online algorithm for mean and variance
102        let n = self.sample_count as f64;
103        let delta = sample_ms - self.mean_ms;
104        self.mean_ms += delta / n;
105        let delta2 = sample_ms - self.mean_ms;
106        // M2 accumulator (variance * (n-1))
107        self.variance_ms += delta * delta2;
108    }
109
110    /// Returns the population variance (variance_ms / sample_count).
111    ///
112    /// Returns 0.0 if fewer than 2 samples have been recorded.
113    pub fn population_variance(&self) -> f64 {
114        if self.sample_count < 2 {
115            return 0.0;
116        }
117        self.variance_ms / self.sample_count as f64
118    }
119}
120
121/// Buffer fill statistics for send and receive buffers.
122#[derive(Debug, Clone, Default)]
123pub struct BufferStats {
124    /// Bytes currently in the send buffer.
125    pub send_buffer_level: usize,
126    /// Bytes currently in the receive buffer.
127    pub recv_buffer_level: usize,
128    /// Capacity of the send buffer in bytes.
129    pub send_buffer_capacity: usize,
130    /// Capacity of the receive buffer in bytes.
131    pub recv_buffer_capacity: usize,
132    /// Send buffer utilization in [0.0, 1.0].
133    pub send_buffer_utilization: f64,
134    /// Receive buffer utilization in [0.0, 1.0].
135    pub recv_buffer_utilization: f64,
136}
137
138impl BufferStats {
139    /// Recalculate both utilization values from current levels and capacities.
140    pub fn update_utilization(&mut self) {
141        self.send_buffer_utilization = if self.send_buffer_capacity == 0 {
142            0.0
143        } else {
144            self.send_buffer_level as f64 / self.send_buffer_capacity as f64
145        };
146
147        self.recv_buffer_utilization = if self.recv_buffer_capacity == 0 {
148            0.0
149        } else {
150            self.recv_buffer_level as f64 / self.recv_buffer_capacity as f64
151        };
152    }
153}
154
155/// Complete SRT stream statistics snapshot.
156#[derive(Debug, Clone)]
157pub struct SrtStreamStats {
158    /// Send-direction statistics.
159    pub send: DirectionStats,
160    /// Receive-direction statistics.
161    pub recv: DirectionStats,
162    /// Round-trip time statistics.
163    pub rtt: RttStats,
164    /// Buffer utilization statistics.
165    pub buffer: BufferStats,
166    /// Connection uptime in milliseconds (snapshot at creation, use `uptime()` for live).
167    pub uptime_ms: u64,
168    /// Latency negotiated during the SRT handshake.
169    pub negotiated_latency_ms: u32,
170    /// Whether AES encryption is active on this stream.
171    pub encryption_enabled: bool,
172    /// Whether forward-error correction is active on this stream.
173    pub fec_enabled: bool,
174    /// Timestamp when the connection was established.
175    pub connected_at: Instant,
176}
177
178impl SrtStreamStats {
179    /// Create a new statistics object for a connection with the given negotiated latency.
180    pub fn new(latency_ms: u32) -> Self {
181        Self {
182            send: DirectionStats::default(),
183            recv: DirectionStats::default(),
184            rtt: RttStats::new(),
185            buffer: BufferStats::default(),
186            uptime_ms: 0,
187            negotiated_latency_ms: latency_ms,
188            encryption_enabled: false,
189            fec_enabled: false,
190            connected_at: Instant::now(),
191        }
192    }
193
194    /// Returns the elapsed time since `connected_at`.
195    pub fn uptime(&self) -> std::time::Duration {
196        self.connected_at.elapsed()
197    }
198
199    /// Returns `true` if the connection is considered healthy:
200    /// packet loss < 1 % and RTT < 200 ms.
201    pub fn is_healthy(&self) -> bool {
202        self.send.packet_loss_rate < 0.01
203            && self.recv.packet_loss_rate < 0.01
204            && self.rtt.current_ms < 200.0
205    }
206
207    /// Returns a quality score in [0.0, 1.0].
208    ///
209    /// Composite of loss rate, RTT, and buffer utilization.
210    /// 1.0 means perfect, 0.0 means worst.
211    pub fn quality_score(&self) -> f32 {
212        // Loss component: perfect at 0, zero at 10%+ loss
213        let loss = self
214            .send
215            .packet_loss_rate
216            .max(self.recv.packet_loss_rate)
217            .min(0.1);
218        let loss_score = 1.0 - (loss / 0.1);
219
220        // RTT component: perfect at 0 ms, zero at 500 ms+
221        let rtt_score = (1.0 - (self.rtt.current_ms / 500.0).min(1.0)).max(0.0);
222
223        // Buffer component: perfect at 0 utilization, zero at 100%
224        let buf_util = self
225            .buffer
226            .send_buffer_utilization
227            .max(self.buffer.recv_buffer_utilization)
228            .min(1.0);
229        let buf_score = 1.0 - buf_util;
230
231        // Weighted average: loss 50%, RTT 35%, buffer 15%
232        let score = 0.5 * loss_score + 0.35 * rtt_score + 0.15 * buf_score;
233        score.clamp(0.0, 1.0) as f32
234    }
235
236    /// Generate a human-readable status report.
237    pub fn report(&self) -> String {
238        let quality = StreamQuality::from_stats(self);
239        format!(
240            "SRT Stream Report\n\
241             Quality:    {}\n\
242             RTT:        {:.1} ms (min={:.1}, max={:.1})\n\
243             Loss (tx):  {:.2}%\n\
244             Loss (rx):  {:.2}%\n\
245             Latency:    {} ms (negotiated)\n\
246             Encrypted:  {}\n\
247             FEC:        {}\n\
248             Uptime:     {:.1}s",
249            quality.name(),
250            self.rtt.current_ms,
251            self.rtt.min_ms.min(self.rtt.max_ms), // guard against MAX sentinel
252            self.rtt.max_ms,
253            self.send.packet_loss_rate * 100.0,
254            self.recv.packet_loss_rate * 100.0,
255            self.negotiated_latency_ms,
256            self.encryption_enabled,
257            self.fec_enabled,
258            self.uptime().as_secs_f64(),
259        )
260    }
261}
262
263/// Qualitative assessment of an SRT connection.
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub enum StreamQuality {
266    /// Loss < 0.1 %, RTT < 20 ms.
267    Excellent,
268    /// Loss < 1 %, RTT < 50 ms.
269    Good,
270    /// Loss < 5 %, RTT < 100 ms.
271    Fair,
272    /// Loss < 10 %, RTT < 200 ms.
273    Poor,
274    /// Worse than Poor.
275    Critical,
276}
277
278impl StreamQuality {
279    /// Derive quality level from a statistics snapshot.
280    pub fn from_stats(stats: &SrtStreamStats) -> Self {
281        let loss = stats.send.packet_loss_rate.max(stats.recv.packet_loss_rate);
282        let rtt = stats.rtt.current_ms;
283
284        if loss < 0.001 && rtt < 20.0 {
285            Self::Excellent
286        } else if loss < 0.01 && rtt < 50.0 {
287            Self::Good
288        } else if loss < 0.05 && rtt < 100.0 {
289            Self::Fair
290        } else if loss < 0.1 && rtt < 200.0 {
291            Self::Poor
292        } else {
293            Self::Critical
294        }
295    }
296
297    /// Short human-readable name for the quality level.
298    pub fn name(&self) -> &'static str {
299        match self {
300            Self::Excellent => "Excellent",
301            Self::Good => "Good",
302            Self::Fair => "Fair",
303            Self::Poor => "Poor",
304            Self::Critical => "Critical",
305        }
306    }
307
308    /// Returns `true` for any quality level that can sustain a usable stream
309    /// (Excellent through Poor).
310    pub fn is_usable(&self) -> bool {
311        !matches!(self, Self::Critical)
312    }
313}
314
315// ─────────────────────────────────────────────────────────────────────────────
316// Tests
317// ─────────────────────────────────────────────────────────────────────────────
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_rtt_stats_update() {
325        let mut rtt = RttStats::new();
326        rtt.update(10.0);
327        rtt.update(20.0);
328        rtt.update(30.0);
329        // Mean should be approximately 20.0
330        assert!((rtt.mean_ms - 20.0).abs() < 1e-9, "mean={}", rtt.mean_ms);
331        assert_eq!(rtt.sample_count, 3);
332        assert_eq!(rtt.current_ms, 30.0);
333    }
334
335    #[test]
336    fn test_rtt_stats_min_max() {
337        let mut rtt = RttStats::new();
338        rtt.update(50.0);
339        rtt.update(10.0);
340        rtt.update(100.0);
341        assert_eq!(rtt.min_ms, 10.0);
342        assert_eq!(rtt.max_ms, 100.0);
343    }
344
345    #[test]
346    fn test_direction_stats_loss_rate() {
347        let mut d = DirectionStats::default();
348        d.packets_sent = 100;
349        d.packets_lost = 10;
350        d.update_loss_rate();
351        assert!((d.packet_loss_rate - 0.1).abs() < 1e-9);
352    }
353
354    #[test]
355    fn test_buffer_stats_utilization() {
356        let mut b = BufferStats::default();
357        b.send_buffer_level = 512;
358        b.send_buffer_capacity = 1024;
359        b.recv_buffer_level = 0;
360        b.recv_buffer_capacity = 1024;
361        b.update_utilization();
362        assert!((b.send_buffer_utilization - 0.5).abs() < 1e-9);
363        assert!((b.recv_buffer_utilization - 0.0).abs() < 1e-9);
364    }
365
366    #[test]
367    fn test_stream_stats_healthy() {
368        let stats = SrtStreamStats::new(120);
369        // Default: all zeros → healthy
370        assert!(stats.is_healthy());
371    }
372
373    #[test]
374    fn test_stream_quality_from_stats() {
375        let mut stats = SrtStreamStats::new(120);
376        stats.send.packet_loss_rate = 0.005; // 0.5%
377        stats.rtt.current_ms = 30.0;
378        let quality = StreamQuality::from_stats(&stats);
379        assert_eq!(quality, StreamQuality::Good);
380    }
381
382    #[test]
383    fn test_stream_quality_is_usable() {
384        assert!(StreamQuality::Excellent.is_usable());
385        assert!(StreamQuality::Good.is_usable());
386        assert!(StreamQuality::Fair.is_usable());
387        assert!(StreamQuality::Poor.is_usable());
388        assert!(!StreamQuality::Critical.is_usable());
389    }
390
391    #[test]
392    fn test_quality_score_range() {
393        // All-zero stats → near-perfect score
394        let mut stats = SrtStreamStats::new(120);
395        let score = stats.quality_score();
396        assert!((0.0..=1.0).contains(&score));
397
398        // Worst-case stats
399        stats.send.packet_loss_rate = 1.0;
400        stats.recv.packet_loss_rate = 1.0;
401        stats.rtt.current_ms = 1000.0;
402        stats.buffer.send_buffer_utilization = 1.0;
403        stats.buffer.recv_buffer_utilization = 1.0;
404        let score_bad = stats.quality_score();
405        assert!((0.0..=1.0).contains(&score_bad));
406        assert!(score > score_bad);
407    }
408}