rtmp_rs/stats/
metrics.rs

1//! Statistics and metrics for RTMP sessions
2
3use std::time::{Duration, Instant};
4
5/// Session-level statistics
6#[derive(Debug, Clone, Default)]
7pub struct SessionStats {
8    /// Total bytes received
9    pub bytes_received: u64,
10    /// Total bytes sent
11    pub bytes_sent: u64,
12    /// Connection duration
13    pub duration: Duration,
14    /// Number of video frames received
15    pub video_frames: u64,
16    /// Number of audio frames received
17    pub audio_frames: u64,
18    /// Number of keyframes received
19    pub keyframes: u64,
20    /// Dropped frames count
21    pub dropped_frames: u64,
22    /// Current bitrate estimate (bits/sec)
23    pub bitrate: u64,
24}
25
26impl SessionStats {
27    /// Create new stats tracker
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Calculate bitrate from bytes and duration
33    pub fn calculate_bitrate(&mut self) {
34        let secs = self.duration.as_secs();
35        if secs > 0 {
36            self.bitrate = (self.bytes_received * 8) / secs;
37        }
38    }
39}
40
41/// Stream-level statistics
42#[derive(Debug, Clone)]
43pub struct StreamStats {
44    /// Stream key
45    pub stream_key: String,
46    /// Start time
47    pub started_at: Instant,
48    /// Total bytes received
49    pub bytes_received: u64,
50    /// Video frames received
51    pub video_frames: u64,
52    /// Audio frames received
53    pub audio_frames: u64,
54    /// Keyframes received
55    pub keyframes: u64,
56    /// Last video timestamp
57    pub last_video_ts: u32,
58    /// Last audio timestamp
59    pub last_audio_ts: u32,
60    /// Video codec info
61    pub video_codec: Option<String>,
62    /// Audio codec info
63    pub audio_codec: Option<String>,
64    /// Video width
65    pub width: Option<u32>,
66    /// Video height
67    pub height: Option<u32>,
68    /// Video framerate
69    pub framerate: Option<f64>,
70    /// Audio sample rate
71    pub audio_sample_rate: Option<u32>,
72    /// Audio channels
73    pub audio_channels: Option<u8>,
74}
75
76impl StreamStats {
77    pub fn new(stream_key: String) -> Self {
78        Self {
79            stream_key,
80            started_at: Instant::now(),
81            bytes_received: 0,
82            video_frames: 0,
83            audio_frames: 0,
84            keyframes: 0,
85            last_video_ts: 0,
86            last_audio_ts: 0,
87            video_codec: None,
88            audio_codec: None,
89            width: None,
90            height: None,
91            framerate: None,
92            audio_sample_rate: None,
93            audio_channels: None,
94        }
95    }
96
97    /// Get duration since stream started
98    pub fn duration(&self) -> Duration {
99        self.started_at.elapsed()
100    }
101
102    /// Calculate bitrate in bits per second
103    pub fn bitrate(&self) -> u64 {
104        let secs = self.duration().as_secs();
105        if secs > 0 {
106            (self.bytes_received * 8) / secs
107        } else {
108            0
109        }
110    }
111
112    /// Calculate video framerate
113    pub fn calculated_framerate(&self) -> f64 {
114        let secs = self.duration().as_secs_f64();
115        if secs > 0.0 {
116            self.video_frames as f64 / secs
117        } else {
118            0.0
119        }
120    }
121}
122
123/// Server-wide statistics
124#[derive(Debug, Clone, Default)]
125pub struct ServerStats {
126    /// Total connections ever
127    pub total_connections: u64,
128    /// Current active connections
129    pub active_connections: u64,
130    /// Total bytes received
131    pub total_bytes_received: u64,
132    /// Total bytes sent
133    pub total_bytes_sent: u64,
134    /// Active streams
135    pub active_streams: u64,
136    /// Uptime
137    pub uptime: Duration,
138}
139
140impl ServerStats {
141    pub fn new() -> Self {
142        Self::default()
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::time::Duration;
150
151    #[test]
152    fn test_session_stats_new() {
153        let stats = SessionStats::new();
154        assert_eq!(stats.bytes_received, 0);
155        assert_eq!(stats.bytes_sent, 0);
156        assert_eq!(stats.video_frames, 0);
157        assert_eq!(stats.audio_frames, 0);
158        assert_eq!(stats.keyframes, 0);
159        assert_eq!(stats.dropped_frames, 0);
160        assert_eq!(stats.bitrate, 0);
161    }
162
163    #[test]
164    fn test_session_stats_calculate_bitrate() {
165        let mut stats = SessionStats::new();
166        stats.bytes_received = 1_000_000; // 1 MB
167        stats.duration = Duration::from_secs(10);
168
169        stats.calculate_bitrate();
170
171        // 1,000,000 bytes * 8 bits / 10 seconds = 800,000 bps
172        assert_eq!(stats.bitrate, 800_000);
173    }
174
175    #[test]
176    fn test_session_stats_calculate_bitrate_zero_duration() {
177        let mut stats = SessionStats::new();
178        stats.bytes_received = 1_000_000;
179        stats.duration = Duration::from_secs(0);
180
181        stats.calculate_bitrate();
182
183        // With zero duration, bitrate should remain 0
184        assert_eq!(stats.bitrate, 0);
185    }
186
187    #[test]
188    fn test_stream_stats_new() {
189        let stats = StreamStats::new("test_stream".to_string());
190        assert_eq!(stats.stream_key, "test_stream");
191        assert_eq!(stats.bytes_received, 0);
192        assert_eq!(stats.video_frames, 0);
193        assert_eq!(stats.audio_frames, 0);
194        assert_eq!(stats.keyframes, 0);
195        assert_eq!(stats.last_video_ts, 0);
196        assert_eq!(stats.last_audio_ts, 0);
197        assert!(stats.video_codec.is_none());
198        assert!(stats.audio_codec.is_none());
199    }
200
201    #[test]
202    fn test_stream_stats_duration() {
203        let stats = StreamStats::new("test".to_string());
204
205        // Duration should be positive since started_at is set at construction
206        let duration = stats.duration();
207        assert!(duration.as_nanos() > 0 || duration == Duration::ZERO);
208    }
209
210    #[test]
211    fn test_stream_stats_bitrate_zero_duration() {
212        let stats = StreamStats::new("test".to_string());
213
214        // With essentially zero duration, bitrate should be 0
215        let bitrate = stats.bitrate();
216        // Note: this could be non-zero if enough time passed, but should be safe
217        assert!(bitrate == 0 || bitrate > 0); // Just ensure it doesn't panic
218    }
219
220    #[test]
221    fn test_stream_stats_calculated_framerate() {
222        let stats = StreamStats::new("test".to_string());
223
224        // With zero frames, framerate should be 0
225        let framerate = stats.calculated_framerate();
226        assert!(framerate >= 0.0);
227    }
228
229    #[test]
230    fn test_server_stats_new() {
231        let stats = ServerStats::new();
232        assert_eq!(stats.total_connections, 0);
233        assert_eq!(stats.active_connections, 0);
234        assert_eq!(stats.total_bytes_received, 0);
235        assert_eq!(stats.total_bytes_sent, 0);
236        assert_eq!(stats.active_streams, 0);
237    }
238
239    #[test]
240    fn test_stream_stats_with_data() {
241        let mut stats = StreamStats::new("live_stream".to_string());
242
243        // Simulate receiving some data
244        stats.bytes_received = 5_000_000; // 5 MB
245        stats.video_frames = 300;
246        stats.audio_frames = 500;
247        stats.keyframes = 10;
248        stats.last_video_ts = 10000;
249        stats.last_audio_ts = 10050;
250        stats.video_codec = Some("H.264".to_string());
251        stats.audio_codec = Some("AAC".to_string());
252        stats.width = Some(1920);
253        stats.height = Some(1080);
254        stats.framerate = Some(30.0);
255        stats.audio_sample_rate = Some(44100);
256        stats.audio_channels = Some(2);
257
258        assert_eq!(stats.video_codec, Some("H.264".to_string()));
259        assert_eq!(stats.width, Some(1920));
260        assert_eq!(stats.height, Some(1080));
261        assert_eq!(stats.audio_channels, Some(2));
262    }
263}