1#[derive(Debug, Clone)]
40pub enum RcVerifyMode {
41 Cbr {
44 tolerance: f64,
46 },
47 Vbr {
50 tolerance: f64,
52 },
53 Crf {
55 max_qp_deviation: u8,
57 },
58}
59
60#[derive(Debug, Clone)]
62struct FrameRecord {
63 size_bytes: u32,
65 is_keyframe: bool,
67 qp: Option<u8>,
69}
70
71#[derive(Debug)]
73pub struct RateControlVerifier {
74 target_bitrate: u64,
76 framerate: f64,
78 mode: RcVerifyMode,
80 frames: Vec<FrameRecord>,
82}
83
84impl RateControlVerifier {
85 #[must_use]
87 pub fn new(target_bitrate: u64, framerate: f64, mode: RcVerifyMode) -> Self {
88 Self {
89 target_bitrate,
90 framerate,
91 mode,
92 frames: Vec::new(),
93 }
94 }
95
96 pub fn record_frame(&mut self, size_bytes: u32, is_keyframe: bool) {
98 self.frames.push(FrameRecord {
99 size_bytes,
100 is_keyframe,
101 qp: None,
102 });
103 }
104
105 pub fn record_frame_with_qp(&mut self, size_bytes: u32, is_keyframe: bool, qp: u8) {
107 self.frames.push(FrameRecord {
108 size_bytes,
109 is_keyframe,
110 qp: Some(qp),
111 });
112 }
113
114 #[must_use]
116 pub fn frame_count(&self) -> usize {
117 self.frames.len()
118 }
119
120 #[must_use]
122 pub fn average_bitrate(&self) -> f64 {
123 if self.frames.is_empty() || self.framerate <= 0.0 {
124 return 0.0;
125 }
126 let total_bits: u64 = self
127 .frames
128 .iter()
129 .map(|f| u64::from(f.size_bytes) * 8)
130 .sum();
131 let duration_seconds = self.frames.len() as f64 / self.framerate;
132 total_bits as f64 / duration_seconds
133 }
134
135 #[must_use]
140 pub fn bitrate_deviation(&self) -> f64 {
141 let avg = self.average_bitrate();
142 if self.target_bitrate == 0 {
143 return 0.0;
144 }
145 (avg - self.target_bitrate as f64) / self.target_bitrate as f64
146 }
147
148 fn sliding_window_bitrates(&self, window_frames: usize) -> Vec<f64> {
151 if self.frames.len() < window_frames || window_frames == 0 {
152 return vec![];
153 }
154 let mut results = Vec::with_capacity(self.frames.len() - window_frames + 1);
155 let duration_seconds = window_frames as f64 / self.framerate;
156
157 let mut window_bits: u64 = self.frames[..window_frames]
159 .iter()
160 .map(|f| u64::from(f.size_bytes) * 8)
161 .sum();
162 results.push(window_bits as f64 / duration_seconds);
163
164 for i in window_frames..self.frames.len() {
166 window_bits += u64::from(self.frames[i].size_bytes) * 8;
167 window_bits -= u64::from(self.frames[i - window_frames].size_bytes) * 8;
168 results.push(window_bits as f64 / duration_seconds);
169 }
170 results
171 }
172
173 #[must_use]
175 pub fn verify(&self) -> VerificationResult {
176 match &self.mode {
177 RcVerifyMode::Cbr { tolerance } => self.verify_cbr(*tolerance),
178 RcVerifyMode::Vbr { tolerance } => self.verify_vbr(*tolerance),
179 RcVerifyMode::Crf { max_qp_deviation } => self.verify_crf(*max_qp_deviation),
180 }
181 }
182
183 fn verify_cbr(&self, tolerance: f64) -> VerificationResult {
184 let window_frames = (self.framerate.ceil() as usize).max(1); let window_bitrates = self.sliding_window_bitrates(window_frames);
186
187 if window_bitrates.is_empty() {
188 return VerificationResult {
189 passes: false,
190 average_bitrate: 0.0,
191 target_bitrate: self.target_bitrate as f64,
192 max_deviation: 0.0,
193 min_window_bitrate: 0.0,
194 max_window_bitrate: 0.0,
195 details: "Not enough frames for 1-second window".to_string(),
196 };
197 }
198
199 let target = self.target_bitrate as f64;
200 let mut max_deviation = 0.0_f64;
201 let mut min_br = f64::MAX;
202 let mut max_br = f64::MIN;
203
204 for &br in &window_bitrates {
205 let dev = ((br - target) / target).abs();
206 if dev > max_deviation {
207 max_deviation = dev;
208 }
209 if br < min_br {
210 min_br = br;
211 }
212 if br > max_br {
213 max_br = br;
214 }
215 }
216
217 let passes = max_deviation <= tolerance;
218 let avg = self.average_bitrate();
219
220 VerificationResult {
221 passes,
222 average_bitrate: avg,
223 target_bitrate: target,
224 max_deviation,
225 min_window_bitrate: min_br,
226 max_window_bitrate: max_br,
227 details: format!(
228 "CBR: max window deviation={:.2}% (tolerance={:.2}%)",
229 max_deviation * 100.0,
230 tolerance * 100.0
231 ),
232 }
233 }
234
235 fn verify_vbr(&self, tolerance: f64) -> VerificationResult {
236 let avg = self.average_bitrate();
237 let target = self.target_bitrate as f64;
238 let deviation = if target > 0.0 {
239 ((avg - target) / target).abs()
240 } else {
241 0.0
242 };
243
244 VerificationResult {
245 passes: deviation <= tolerance,
246 average_bitrate: avg,
247 target_bitrate: target,
248 max_deviation: deviation,
249 min_window_bitrate: avg,
250 max_window_bitrate: avg,
251 details: format!(
252 "VBR: avg deviation={:.2}% (tolerance={:.2}%)",
253 deviation * 100.0,
254 tolerance * 100.0
255 ),
256 }
257 }
258
259 fn verify_crf(&self, max_qp_deviation: u8) -> VerificationResult {
260 let qp_values: Vec<u8> = self.frames.iter().filter_map(|f| f.qp).collect();
261
262 if qp_values.is_empty() {
263 return VerificationResult {
264 passes: false,
265 average_bitrate: self.average_bitrate(),
266 target_bitrate: self.target_bitrate as f64,
267 max_deviation: 0.0,
268 min_window_bitrate: 0.0,
269 max_window_bitrate: 0.0,
270 details: "CRF: no QP values recorded".to_string(),
271 };
272 }
273
274 let mut sorted_qp = qp_values.clone();
275 sorted_qp.sort_unstable();
276 let median_qp = sorted_qp[sorted_qp.len() / 2];
277
278 let max_dev = qp_values
279 .iter()
280 .map(|&q| (q as i16 - median_qp as i16).unsigned_abs() as u8)
281 .max()
282 .unwrap_or(0);
283
284 let passes = max_dev <= max_qp_deviation;
285
286 VerificationResult {
287 passes,
288 average_bitrate: self.average_bitrate(),
289 target_bitrate: self.target_bitrate as f64,
290 max_deviation: f64::from(max_dev),
291 min_window_bitrate: 0.0,
292 max_window_bitrate: 0.0,
293 details: format!(
294 "CRF: max QP deviation={} (limit={}), median QP={}",
295 max_dev, max_qp_deviation, median_qp
296 ),
297 }
298 }
299
300 pub fn reset(&mut self) {
302 self.frames.clear();
303 }
304}
305
306#[derive(Debug, Clone)]
308pub struct VerificationResult {
309 pub passes: bool,
311 pub average_bitrate: f64,
313 pub target_bitrate: f64,
315 pub max_deviation: f64,
317 pub min_window_bitrate: f64,
319 pub max_window_bitrate: f64,
321 pub details: String,
323}
324
325impl VerificationResult {
326 #[must_use]
328 pub fn summary(&self) -> &str {
329 &self.details
330 }
331}
332
333#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
344 fn test_cbr_perfect_bitrate() {
345 let target = 2_000_000u64; let fps = 30.0;
347 let mut v = RateControlVerifier::new(target, fps, RcVerifyMode::Cbr { tolerance: 0.05 });
348
349 let frame_bytes = (target as f64 / fps / 8.0) as u32;
351 for _ in 0..90 {
352 v.record_frame(frame_bytes, false);
353 }
354
355 let result = v.verify();
356 assert!(
357 result.passes,
358 "perfect CBR should pass: {}",
359 result.summary()
360 );
361 assert!(result.max_deviation < 0.01);
362 }
363
364 #[test]
365 fn test_cbr_within_5_percent() {
366 let target = 1_000_000u64;
367 let fps = 24.0;
368 let mut v = RateControlVerifier::new(target, fps, RcVerifyMode::Cbr { tolerance: 0.05 });
369
370 let base_bytes = (target as f64 / fps / 8.0) as u32;
371 for i in 0..120 {
373 let variation = if i % 2 == 0 {
374 (base_bytes as f64 * 1.04) as u32
375 } else {
376 (base_bytes as f64 * 0.96) as u32
377 };
378 v.record_frame(variation, i % 24 == 0);
379 }
380
381 let result = v.verify();
382 assert!(
383 result.passes,
384 "±4% variation should be within 5% tolerance: {}",
385 result.summary()
386 );
387 }
388
389 #[test]
390 fn test_cbr_exceeds_tolerance() {
391 let target = 2_000_000u64;
392 let fps = 30.0;
393 let mut v = RateControlVerifier::new(target, fps, RcVerifyMode::Cbr { tolerance: 0.05 });
394
395 let base_bytes = (target as f64 / fps / 8.0) as u32;
396 for _ in 0..45 {
398 v.record_frame(base_bytes * 2, false);
399 }
400 for _ in 0..45 {
402 v.record_frame(base_bytes, false);
403 }
404
405 let result = v.verify();
406 assert!(
407 !result.passes,
408 "2x bitrate burst should exceed 5% tolerance: {}",
409 result.summary()
410 );
411 }
412
413 #[test]
414 fn test_cbr_not_enough_frames() {
415 let mut v =
416 RateControlVerifier::new(1_000_000, 30.0, RcVerifyMode::Cbr { tolerance: 0.05 });
417 v.record_frame(5000, false);
418 let result = v.verify();
419 assert!(
420 !result.passes,
421 "too few frames should fail: {}",
422 result.summary()
423 );
424 }
425
426 #[test]
429 fn test_vbr_within_tolerance() {
430 let target = 4_000_000u64;
431 let fps = 60.0;
432 let mut v = RateControlVerifier::new(target, fps, RcVerifyMode::Vbr { tolerance: 0.10 });
433
434 let base_bytes = (target as f64 / fps / 8.0) as u32;
435 for i in 0..300 {
437 let size = if i % 60 == 0 {
438 base_bytes * 3 } else {
440 (base_bytes as f64 * 0.95) as u32 };
442 v.record_frame(size, i % 60 == 0);
443 }
444
445 let result = v.verify();
446 let deviation = result.max_deviation;
447 assert!(
449 deviation < 0.15,
450 "VBR with averaged bursts should be near target, deviation={:.2}%",
451 deviation * 100.0
452 );
453 }
454
455 #[test]
456 fn test_vbr_over_target() {
457 let target = 1_000_000u64;
458 let fps = 30.0;
459 let mut v = RateControlVerifier::new(target, fps, RcVerifyMode::Vbr { tolerance: 0.10 });
460
461 let over_bytes = ((target as f64 / fps / 8.0) * 1.5) as u32;
463 for _ in 0..90 {
464 v.record_frame(over_bytes, false);
465 }
466
467 let result = v.verify();
468 assert!(
469 !result.passes,
470 "50% over target should fail 10% tolerance: {}",
471 result.summary()
472 );
473 }
474
475 #[test]
478 fn test_crf_stable_qp() {
479 let mut v = RateControlVerifier::new(
480 0,
481 30.0,
482 RcVerifyMode::Crf {
483 max_qp_deviation: 2,
484 },
485 );
486
487 for i in 0..60 {
489 let qp = if i % 3 == 0 { 27 } else { 28 };
490 v.record_frame_with_qp(5000, false, qp);
491 }
492
493 let result = v.verify();
494 assert!(result.passes, "stable QP should pass: {}", result.summary());
495 }
496
497 #[test]
498 fn test_crf_unstable_qp() {
499 let mut v = RateControlVerifier::new(
500 0,
501 30.0,
502 RcVerifyMode::Crf {
503 max_qp_deviation: 2,
504 },
505 );
506
507 for i in 0..60 {
509 let qp = if i % 2 == 0 { 20 } else { 40 };
510 v.record_frame_with_qp(5000, false, qp);
511 }
512
513 let result = v.verify();
514 assert!(
515 !result.passes,
516 "QP swing of 20 should fail deviation limit of 2: {}",
517 result.summary()
518 );
519 }
520
521 #[test]
522 fn test_crf_no_qp_data() {
523 let mut v = RateControlVerifier::new(
524 0,
525 30.0,
526 RcVerifyMode::Crf {
527 max_qp_deviation: 2,
528 },
529 );
530 v.record_frame(5000, false);
531 let result = v.verify();
532 assert!(!result.passes, "no QP data should fail");
533 }
534
535 #[test]
538 fn test_average_bitrate_calculation() {
539 let mut v =
540 RateControlVerifier::new(1_000_000, 10.0, RcVerifyMode::Vbr { tolerance: 0.10 });
541 for _ in 0..10 {
543 v.record_frame(12500, false);
544 }
545 let avg = v.average_bitrate();
546 assert!(
547 (avg - 1_000_000.0).abs() < 1.0,
548 "average bitrate should be 1 Mbps, got {avg}"
549 );
550 }
551
552 #[test]
553 fn test_bitrate_deviation() {
554 let mut v =
555 RateControlVerifier::new(1_000_000, 10.0, RcVerifyMode::Vbr { tolerance: 0.10 });
556 let bytes_per_frame = (1_100_000.0 / 10.0 / 8.0) as u32;
558 for _ in 0..10 {
559 v.record_frame(bytes_per_frame, false);
560 }
561 let dev = v.bitrate_deviation();
562 assert!(
563 (dev - 0.1).abs() < 0.01,
564 "deviation should be ~10%, got {:.2}%",
565 dev * 100.0
566 );
567 }
568
569 #[test]
570 fn test_frame_count() {
571 let mut v =
572 RateControlVerifier::new(1_000_000, 30.0, RcVerifyMode::Cbr { tolerance: 0.05 });
573 for _ in 0..42 {
574 v.record_frame(1000, false);
575 }
576 assert_eq!(v.frame_count(), 42);
577 }
578
579 #[test]
580 fn test_reset() {
581 let mut v =
582 RateControlVerifier::new(1_000_000, 30.0, RcVerifyMode::Cbr { tolerance: 0.05 });
583 v.record_frame(1000, false);
584 v.reset();
585 assert_eq!(v.frame_count(), 0);
586 assert!(v.average_bitrate() < f64::EPSILON);
587 }
588}