1#![allow(dead_code)]
7
8#[derive(Debug, Clone)]
10pub struct FrameMetric {
11 pub frame_index: u64,
13 pub encode_us: u64,
15 pub compressed_bytes: u64,
17 pub psnr_db: Option<f64>,
19}
20
21impl FrameMetric {
22 #[must_use]
24 pub fn new(frame_index: u64, encode_us: u64, compressed_bytes: u64) -> Self {
25 Self {
26 frame_index,
27 encode_us,
28 compressed_bytes,
29 psnr_db: None,
30 }
31 }
32
33 #[must_use]
35 pub fn with_psnr(mut self, psnr_db: f64) -> Self {
36 self.psnr_db = Some(psnr_db);
37 self
38 }
39
40 #[allow(clippy::cast_precision_loss)]
44 #[must_use]
45 pub fn instantaneous_bitrate_bps(&self, fps: f64) -> f64 {
46 self.compressed_bytes as f64 * 8.0 * fps
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct MetricsSummary {
53 pub frame_count: u64,
55 pub mean_encode_us: f64,
57 pub peak_encode_us: u64,
59 pub total_bytes: u64,
61 pub mean_psnr_db: Option<f64>,
63 pub min_psnr_db: Option<f64>,
65}
66
67impl MetricsSummary {
68 #[allow(clippy::cast_precision_loss)]
70 #[must_use]
71 pub fn mean_bitrate_bps(&self, fps: f64) -> f64 {
72 if self.frame_count == 0 || fps <= 0.0 {
73 return 0.0;
74 }
75 let total_bits = self.total_bytes as f64 * 8.0;
76 let duration_secs = self.frame_count as f64 / fps;
77 total_bits / duration_secs
78 }
79
80 #[allow(clippy::cast_precision_loss)]
82 #[must_use]
83 pub fn encode_fps(&self) -> f64 {
84 if self.mean_encode_us <= 0.0 {
85 return 0.0;
86 }
87 1_000_000.0 / self.mean_encode_us
88 }
89}
90
91#[derive(Debug, Default)]
93pub struct TranscodeMetricsCollector {
94 metrics: Vec<FrameMetric>,
95}
96
97impl TranscodeMetricsCollector {
98 #[must_use]
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 #[must_use]
106 pub fn with_capacity(cap: usize) -> Self {
107 Self {
108 metrics: Vec::with_capacity(cap),
109 }
110 }
111
112 pub fn record(&mut self, metric: FrameMetric) {
114 self.metrics.push(metric);
115 }
116
117 #[must_use]
119 pub fn frame_count(&self) -> usize {
120 self.metrics.len()
121 }
122
123 #[must_use]
125 pub fn is_empty(&self) -> bool {
126 self.metrics.is_empty()
127 }
128
129 #[allow(clippy::cast_precision_loss)]
131 pub fn summarise(&self) -> MetricsSummary {
132 let count = self.metrics.len() as u64;
133 if count == 0 {
134 return MetricsSummary {
135 frame_count: 0,
136 mean_encode_us: 0.0,
137 peak_encode_us: 0,
138 total_bytes: 0,
139 mean_psnr_db: None,
140 min_psnr_db: None,
141 };
142 }
143
144 let total_encode_us: u64 = self.metrics.iter().map(|m| m.encode_us).sum();
145 let peak_encode_us = self.metrics.iter().map(|m| m.encode_us).max().unwrap_or(0);
146 let total_bytes: u64 = self.metrics.iter().map(|m| m.compressed_bytes).sum();
147
148 let psnr_values: Vec<f64> = self.metrics.iter().filter_map(|m| m.psnr_db).collect();
149
150 let mean_psnr_db = if psnr_values.is_empty() {
151 None
152 } else {
153 Some(psnr_values.iter().sum::<f64>() / psnr_values.len() as f64)
154 };
155
156 let min_psnr_db = psnr_values.iter().copied().reduce(f64::min);
157
158 MetricsSummary {
159 frame_count: count,
160 mean_encode_us: total_encode_us as f64 / count as f64,
161 peak_encode_us,
162 total_bytes,
163 mean_psnr_db,
164 min_psnr_db,
165 }
166 }
167
168 #[must_use]
170 pub fn worst_psnr_frame(&self) -> Option<&FrameMetric> {
171 self.metrics
172 .iter()
173 .filter(|m| m.psnr_db.is_some())
174 .min_by(|a, b| {
175 a.psnr_db
176 .expect("invariant: filter ensures psnr_db is Some")
177 .partial_cmp(
178 &b.psnr_db
179 .expect("invariant: filter ensures psnr_db is Some"),
180 )
181 .unwrap_or(std::cmp::Ordering::Equal)
182 })
183 }
184
185 #[must_use]
187 pub fn slowest_frame(&self) -> Option<&FrameMetric> {
188 self.metrics.iter().max_by_key(|m| m.encode_us)
189 }
190
191 pub fn clear(&mut self) {
193 self.metrics.clear();
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn make_metric(index: u64, encode_us: u64, bytes: u64) -> FrameMetric {
202 FrameMetric::new(index, encode_us, bytes)
203 }
204
205 #[test]
206 fn test_frame_metric_creation() {
207 let m = make_metric(0, 5000, 10_000);
208 assert_eq!(m.frame_index, 0);
209 assert_eq!(m.encode_us, 5000);
210 assert_eq!(m.compressed_bytes, 10_000);
211 assert!(m.psnr_db.is_none());
212 }
213
214 #[test]
215 fn test_frame_metric_with_psnr() {
216 let m = make_metric(0, 5000, 10_000).with_psnr(42.5);
217 assert_eq!(m.psnr_db, Some(42.5));
218 }
219
220 #[test]
221 fn test_instantaneous_bitrate() {
222 let m = make_metric(0, 5000, 1000); let bps = m.instantaneous_bitrate_bps(30.0);
224 assert!((bps - 240_000.0).abs() < 0.01);
225 }
226
227 #[test]
228 fn test_collector_empty() {
229 let c = TranscodeMetricsCollector::new();
230 assert!(c.is_empty());
231 assert_eq!(c.frame_count(), 0);
232 }
233
234 #[test]
235 fn test_collector_record_increments_count() {
236 let mut c = TranscodeMetricsCollector::new();
237 c.record(make_metric(0, 1000, 500));
238 c.record(make_metric(1, 2000, 600));
239 assert_eq!(c.frame_count(), 2);
240 assert!(!c.is_empty());
241 }
242
243 #[test]
244 fn test_summarise_empty() {
245 let c = TranscodeMetricsCollector::new();
246 let s = c.summarise();
247 assert_eq!(s.frame_count, 0);
248 assert_eq!(s.total_bytes, 0);
249 assert!(s.mean_psnr_db.is_none());
250 }
251
252 #[test]
253 fn test_summarise_mean_encode_us() {
254 let mut c = TranscodeMetricsCollector::new();
255 c.record(make_metric(0, 1000, 100));
256 c.record(make_metric(1, 3000, 100));
257 let s = c.summarise();
258 assert!((s.mean_encode_us - 2000.0).abs() < f64::EPSILON);
259 }
260
261 #[test]
262 fn test_summarise_peak_encode_us() {
263 let mut c = TranscodeMetricsCollector::new();
264 c.record(make_metric(0, 1000, 100));
265 c.record(make_metric(1, 5000, 200));
266 let s = c.summarise();
267 assert_eq!(s.peak_encode_us, 5000);
268 }
269
270 #[test]
271 fn test_summarise_total_bytes() {
272 let mut c = TranscodeMetricsCollector::new();
273 c.record(make_metric(0, 1000, 400));
274 c.record(make_metric(1, 1000, 600));
275 let s = c.summarise();
276 assert_eq!(s.total_bytes, 1000);
277 }
278
279 #[test]
280 fn test_summarise_psnr() {
281 let mut c = TranscodeMetricsCollector::new();
282 c.record(make_metric(0, 100, 100).with_psnr(40.0));
283 c.record(make_metric(1, 100, 100).with_psnr(44.0));
284 let s = c.summarise();
285 assert!((s.mean_psnr_db.expect("should succeed in test") - 42.0).abs() < 0.001);
286 assert!((s.min_psnr_db.expect("should succeed in test") - 40.0).abs() < 0.001);
287 }
288
289 #[test]
290 fn test_mean_bitrate_bps() {
291 let mut c = TranscodeMetricsCollector::new();
292 for i in 0..30 {
294 c.record(make_metric(i, 1000, 1000));
295 }
296 let s = c.summarise();
297 let bps = s.mean_bitrate_bps(30.0);
298 assert!((bps - 240_000.0).abs() < 1.0);
299 }
300
301 #[test]
302 fn test_encode_fps() {
303 let mut c = TranscodeMetricsCollector::new();
304 c.record(make_metric(0, 33_333, 100));
306 c.record(make_metric(1, 33_333, 100));
307 let s = c.summarise();
308 let fps = s.encode_fps();
309 assert!((fps - 30.0).abs() < 0.1);
310 }
311
312 #[test]
313 fn test_slowest_frame() {
314 let mut c = TranscodeMetricsCollector::new();
315 c.record(make_metric(0, 1000, 100));
316 c.record(make_metric(1, 9000, 200));
317 c.record(make_metric(2, 500, 50));
318 let sf = c.slowest_frame().expect("should succeed in test");
319 assert_eq!(sf.frame_index, 1);
320 }
321
322 #[test]
323 fn test_worst_psnr_frame() {
324 let mut c = TranscodeMetricsCollector::new();
325 c.record(make_metric(0, 100, 100).with_psnr(45.0));
326 c.record(make_metric(1, 100, 100).with_psnr(35.0));
327 c.record(make_metric(2, 100, 100).with_psnr(50.0));
328 let worst = c.worst_psnr_frame().expect("should succeed in test");
329 assert_eq!(worst.frame_index, 1);
330 }
331
332 #[test]
333 fn test_clear() {
334 let mut c = TranscodeMetricsCollector::new();
335 c.record(make_metric(0, 100, 100));
336 c.clear();
337 assert!(c.is_empty());
338 }
339}