1#[derive(Debug, Clone, Copy)]
42pub struct FrameMetric {
43 pub size_bytes: u32,
45 pub qp: u8,
47 pub is_keyframe: bool,
49}
50
51#[derive(Debug, Clone)]
53pub struct PassRecorder {
54 label: String,
56 frames: Vec<FrameMetric>,
58}
59
60impl PassRecorder {
61 #[must_use]
63 pub fn new(label: &str) -> Self {
64 Self {
65 label: label.to_string(),
66 frames: Vec::new(),
67 }
68 }
69
70 pub fn record(&mut self, metric: FrameMetric) {
72 self.frames.push(metric);
73 }
74
75 #[must_use]
77 pub fn frame_count(&self) -> usize {
78 self.frames.len()
79 }
80
81 #[must_use]
83 pub fn label(&self) -> &str {
84 &self.label
85 }
86
87 #[must_use]
89 pub fn average_qp(&self) -> f64 {
90 if self.frames.is_empty() {
91 return 0.0;
92 }
93 let sum: u64 = self.frames.iter().map(|f| u64::from(f.qp)).sum();
94 sum as f64 / self.frames.len() as f64
95 }
96
97 #[must_use]
99 pub fn qp_std_dev(&self) -> f64 {
100 if self.frames.len() < 2 {
101 return 0.0;
102 }
103 let mean = self.average_qp();
104 let variance: f64 = self
105 .frames
106 .iter()
107 .map(|f| {
108 let d = f64::from(f.qp) - mean;
109 d * d
110 })
111 .sum::<f64>()
112 / (self.frames.len() - 1) as f64;
113 variance.sqrt()
114 }
115
116 #[must_use]
118 pub fn total_bytes(&self) -> u64 {
119 self.frames.iter().map(|f| u64::from(f.size_bytes)).sum()
120 }
121
122 #[must_use]
124 pub fn average_frame_size(&self) -> f64 {
125 if self.frames.is_empty() {
126 return 0.0;
127 }
128 self.total_bytes() as f64 / self.frames.len() as f64
129 }
130
131 #[must_use]
133 pub fn size_std_dev(&self) -> f64 {
134 if self.frames.len() < 2 {
135 return 0.0;
136 }
137 let mean = self.average_frame_size();
138 let variance: f64 = self
139 .frames
140 .iter()
141 .map(|f| {
142 let d = f64::from(f.size_bytes) - mean;
143 d * d
144 })
145 .sum::<f64>()
146 / (self.frames.len() - 1) as f64;
147 variance.sqrt()
148 }
149
150 #[must_use]
152 pub fn size_min_max_ratio(&self) -> f64 {
153 if self.frames.is_empty() {
154 return 1.0;
155 }
156 let min = self.frames.iter().map(|f| f.size_bytes).min().unwrap_or(1);
157 let max = self.frames.iter().map(|f| f.size_bytes).max().unwrap_or(1);
158 if max == 0 {
159 return 1.0;
160 }
161 f64::from(min) / f64::from(max)
162 }
163
164 #[must_use]
166 pub fn frames(&self) -> &[FrameMetric] {
167 &self.frames
168 }
169
170 pub fn reset(&mut self) {
172 self.frames.clear();
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct MultipassComparison {
179 pub reference_label: String,
181 pub candidate_label: String,
183 pub ref_avg_qp: f64,
185 pub cand_avg_qp: f64,
187 pub ref_qp_std_dev: f64,
189 pub cand_qp_std_dev: f64,
191 pub ref_total_bytes: u64,
193 pub cand_total_bytes: u64,
195 pub ref_size_std_dev: f64,
197 pub cand_size_std_dev: f64,
199 pub candidate_qp_equal_or_better: bool,
201 pub candidate_qp_more_consistent: bool,
203 pub candidate_smoother_bitrate: bool,
205}
206
207impl MultipassComparison {
208 #[must_use]
211 pub fn compare(reference: &PassRecorder, candidate: &PassRecorder) -> Self {
212 let ref_avg_qp = reference.average_qp();
213 let cand_avg_qp = candidate.average_qp();
214 let ref_qp_std = reference.qp_std_dev();
215 let cand_qp_std = candidate.qp_std_dev();
216 let ref_size_std = reference.size_std_dev();
217 let cand_size_std = candidate.size_std_dev();
218
219 Self {
220 reference_label: reference.label().to_string(),
221 candidate_label: candidate.label().to_string(),
222 ref_avg_qp,
223 cand_avg_qp,
224 ref_qp_std_dev: ref_qp_std,
225 cand_qp_std_dev: cand_qp_std,
226 ref_total_bytes: reference.total_bytes(),
227 cand_total_bytes: candidate.total_bytes(),
228 ref_size_std_dev: ref_size_std,
229 cand_size_std_dev: cand_size_std,
230 candidate_qp_equal_or_better: cand_avg_qp <= ref_avg_qp + 0.5,
231 candidate_qp_more_consistent: cand_qp_std <= ref_qp_std + 0.5,
232 candidate_smoother_bitrate: cand_size_std <= ref_size_std * 1.1,
233 }
234 }
235
236 #[must_use]
238 pub fn summary(&self) -> String {
239 format!(
240 "{} vs {}: avg_qp {:.1} vs {:.1}, qp_std {:.2} vs {:.2}, \
241 size_std {:.0} vs {:.0}, bytes {} vs {}",
242 self.reference_label,
243 self.candidate_label,
244 self.ref_avg_qp,
245 self.cand_avg_qp,
246 self.ref_qp_std_dev,
247 self.cand_qp_std_dev,
248 self.ref_size_std_dev,
249 self.cand_size_std_dev,
250 self.ref_total_bytes,
251 self.cand_total_bytes,
252 )
253 }
254}
255
256#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn uniform_pass(label: &str, n: usize, size: u32, qp: u8) -> PassRecorder {
266 let mut rec = PassRecorder::new(label);
267 for i in 0..n {
268 rec.record(FrameMetric {
269 size_bytes: size,
270 qp,
271 is_keyframe: i == 0,
272 });
273 }
274 rec
275 }
276
277 fn varying_pass(label: &str, n: usize, base_size: u32, qp_range: (u8, u8)) -> PassRecorder {
279 let mut rec = PassRecorder::new(label);
280 for i in 0..n {
281 let t = i as f64 / n as f64;
282 let variation = (t * std::f64::consts::PI * 4.0).sin();
284 let qp = (qp_range.0 as f64
285 + (qp_range.1 as f64 - qp_range.0 as f64) * (variation + 1.0) / 2.0)
286 as u8;
287 let size = (base_size as f64 * (1.0 + variation * 0.3)) as u32;
288 rec.record(FrameMetric {
289 size_bytes: size,
290 qp,
291 is_keyframe: i % 30 == 0,
292 });
293 }
294 rec
295 }
296
297 #[test]
298 fn test_pass_recorder_basic() {
299 let mut rec = PassRecorder::new("test");
300 rec.record(FrameMetric {
301 size_bytes: 1000,
302 qp: 28,
303 is_keyframe: true,
304 });
305 rec.record(FrameMetric {
306 size_bytes: 500,
307 qp: 30,
308 is_keyframe: false,
309 });
310 assert_eq!(rec.frame_count(), 2);
311 assert_eq!(rec.label(), "test");
312 assert_eq!(rec.total_bytes(), 1500);
313 }
314
315 #[test]
316 fn test_average_qp() {
317 let rec = uniform_pass("test", 10, 1000, 28);
318 assert!((rec.average_qp() - 28.0).abs() < f64::EPSILON);
319 }
320
321 #[test]
322 fn test_qp_std_dev_uniform() {
323 let rec = uniform_pass("test", 10, 1000, 28);
324 assert!(
325 rec.qp_std_dev() < f64::EPSILON,
326 "uniform QP should have zero std dev"
327 );
328 }
329
330 #[test]
331 fn test_qp_std_dev_varying() {
332 let mut rec = PassRecorder::new("test");
333 rec.record(FrameMetric {
334 size_bytes: 1000,
335 qp: 20,
336 is_keyframe: false,
337 });
338 rec.record(FrameMetric {
339 size_bytes: 1000,
340 qp: 40,
341 is_keyframe: false,
342 });
343 let std_dev = rec.qp_std_dev();
344 assert!(
345 std_dev > 10.0,
346 "QP 20 vs 40 should have large std dev, got {std_dev}"
347 );
348 }
349
350 #[test]
351 fn test_size_std_dev_uniform() {
352 let rec = uniform_pass("test", 20, 5000, 28);
353 assert!(
354 rec.size_std_dev() < f64::EPSILON,
355 "uniform size should have zero std dev"
356 );
357 }
358
359 #[test]
360 fn test_size_min_max_ratio() {
361 let mut rec = PassRecorder::new("test");
362 rec.record(FrameMetric {
363 size_bytes: 1000,
364 qp: 28,
365 is_keyframe: false,
366 });
367 rec.record(FrameMetric {
368 size_bytes: 2000,
369 qp: 28,
370 is_keyframe: false,
371 });
372 let ratio = rec.size_min_max_ratio();
373 assert!(
374 (ratio - 0.5).abs() < f64::EPSILON,
375 "min/max ratio should be 0.5, got {ratio}"
376 );
377 }
378
379 #[test]
380 fn test_comparison_identical_passes() {
381 let single = uniform_pass("single", 90, 5000, 28);
382 let multi = uniform_pass("multi", 90, 5000, 28);
383 let cmp = MultipassComparison::compare(&single, &multi);
384
385 assert!(cmp.candidate_qp_equal_or_better);
386 assert!(cmp.candidate_qp_more_consistent);
387 assert!(cmp.candidate_smoother_bitrate);
388 }
389
390 #[test]
391 fn test_comparison_multipass_better_qp() {
392 let single = uniform_pass("single", 90, 5000, 30);
393 let multi = uniform_pass("multi", 90, 5000, 26); let cmp = MultipassComparison::compare(&single, &multi);
395
396 assert!(
397 cmp.candidate_qp_equal_or_better,
398 "multipass with lower QP should be detected as better"
399 );
400 }
401
402 #[test]
403 fn test_comparison_multipass_more_consistent() {
404 let single = varying_pass("single", 120, 5000, (20, 40));
405 let multi = varying_pass("multi", 120, 5000, (26, 30)); let cmp = MultipassComparison::compare(&single, &multi);
407
408 assert!(
409 cmp.cand_qp_std_dev < cmp.ref_qp_std_dev,
410 "multipass should have lower QP variance: {} vs {}",
411 cmp.cand_qp_std_dev,
412 cmp.ref_qp_std_dev
413 );
414 }
415
416 #[test]
417 fn test_comparison_smoother_bitrate() {
418 let mut single = PassRecorder::new("single");
420 for i in 0..60 {
421 let size = if i % 10 == 0 { 15000 } else { 3000 };
422 single.record(FrameMetric {
423 size_bytes: size,
424 qp: 28,
425 is_keyframe: i % 10 == 0,
426 });
427 }
428
429 let multi = uniform_pass("multi", 60, 5000, 28);
431 let cmp = MultipassComparison::compare(&single, &multi);
432
433 assert!(
434 cmp.candidate_smoother_bitrate,
435 "uniform multipass should have smoother bitrate: {} vs {}",
436 cmp.cand_size_std_dev, cmp.ref_size_std_dev
437 );
438 }
439
440 #[test]
441 fn test_comparison_summary_format() {
442 let single = uniform_pass("single", 10, 5000, 28);
443 let multi = uniform_pass("multi", 10, 5000, 26);
444 let cmp = MultipassComparison::compare(&single, &multi);
445 let summary = cmp.summary();
446
447 assert!(summary.contains("single"));
448 assert!(summary.contains("multi"));
449 assert!(summary.contains("avg_qp"));
450 }
451
452 #[test]
453 fn test_pass_recorder_reset() {
454 let mut rec = uniform_pass("test", 10, 5000, 28);
455 assert_eq!(rec.frame_count(), 10);
456 rec.reset();
457 assert_eq!(rec.frame_count(), 0);
458 assert!(rec.total_bytes() == 0);
459 }
460
461 #[test]
462 fn test_empty_recorder_defaults() {
463 let rec = PassRecorder::new("empty");
464 assert!(rec.average_qp() < f64::EPSILON);
465 assert!(rec.qp_std_dev() < f64::EPSILON);
466 assert!(rec.average_frame_size() < f64::EPSILON);
467 assert!((rec.size_min_max_ratio() - 1.0).abs() < f64::EPSILON);
468 }
469}