Skip to main content

oximedia_codec/
rate_control_accuracy.rs

1//! Rate control accuracy verification.
2//!
3//! This module provides utilities for testing that rate control implementations
4//! achieve their target bitrate within acceptable tolerances. It measures actual
5//! output size against the configured target and computes deviation metrics.
6//!
7//! # Accuracy Criteria
8//!
9//! - **CBR mode**: Output bitrate should stay within ±5% of target over a
10//!   sliding window of at least 1 second.
11//! - **VBR mode**: Average bitrate over the entire sequence should stay within
12//!   ±10% of target.
13//! - **CRF mode**: Quality should remain stable (± 2 QP) across frames of
14//!   similar complexity.
15//!
16//! # Usage
17//!
18//! ```rust
19//! use oximedia_codec::rate_control_accuracy::{
20//!     RateControlVerifier, RcVerifyMode, VerificationResult,
21//! };
22//!
23//! let mut verifier = RateControlVerifier::new(
24//!     2_000_000,    // 2 Mbps target
25//!     30.0,         // 30 fps
26//!     RcVerifyMode::Cbr { tolerance: 0.05 },
27//! );
28//!
29//! // Feed frame sizes after encoding
30//! for _ in 0..90 {
31//!     verifier.record_frame(8000, false); // ~8000 bytes per frame
32//! }
33//!
34//! let result = verifier.verify();
35//! assert!(result.passes, "CBR should be within tolerance: {}", result.summary());
36//! ```
37
38/// Rate control verification mode.
39#[derive(Debug, Clone)]
40pub enum RcVerifyMode {
41    /// Constant Bitrate: measured bitrate must stay within `tolerance` fraction
42    /// of target over any 1-second sliding window.
43    Cbr {
44        /// Fractional tolerance (e.g. 0.05 for ±5%).
45        tolerance: f64,
46    },
47    /// Variable Bitrate: average bitrate over the full sequence must stay within
48    /// `tolerance` of target.
49    Vbr {
50        /// Fractional tolerance (e.g. 0.10 for ±10%).
51        tolerance: f64,
52    },
53    /// Constant Rate Factor: not bitrate-based but QP-stability-based.
54    Crf {
55        /// Maximum allowed QP deviation from the median.
56        max_qp_deviation: u8,
57    },
58}
59
60/// A single recorded frame's statistics.
61#[derive(Debug, Clone)]
62struct FrameRecord {
63    /// Size of encoded frame in bytes.
64    size_bytes: u32,
65    /// Whether this frame was a keyframe.
66    is_keyframe: bool,
67    /// Optional QP value (for CRF verification).
68    qp: Option<u8>,
69}
70
71/// Verifies that an encoder's rate control meets its targets.
72#[derive(Debug)]
73pub struct RateControlVerifier {
74    /// Target bitrate in bits per second.
75    target_bitrate: u64,
76    /// Frame rate in fps.
77    framerate: f64,
78    /// Verification mode.
79    mode: RcVerifyMode,
80    /// Recorded frame data.
81    frames: Vec<FrameRecord>,
82}
83
84impl RateControlVerifier {
85    /// Create a new rate control verifier.
86    #[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    /// Record one encoded frame.
97    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    /// Record one encoded frame with its QP value (for CRF mode).
106    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    /// Get the total number of recorded frames.
115    #[must_use]
116    pub fn frame_count(&self) -> usize {
117        self.frames.len()
118    }
119
120    /// Compute the overall average bitrate of the recorded sequence.
121    #[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    /// Compute the deviation of average bitrate from target.
136    ///
137    /// Returns a fraction: `(actual - target) / target`.
138    /// Positive means over-target, negative means under-target.
139    #[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    /// Compute bitrate for a sliding window of `window_frames` frames
149    /// starting at each frame position.
150    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        // Compute initial window sum
158        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        // Slide the window
165        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    /// Run verification and return a detailed result.
174    #[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); // ~1 second
185        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    /// Reset the verifier, clearing all recorded frames.
301    pub fn reset(&mut self) {
302        self.frames.clear();
303    }
304}
305
306/// Result of rate control verification.
307#[derive(Debug, Clone)]
308pub struct VerificationResult {
309    /// Whether the rate control met its target within tolerance.
310    pub passes: bool,
311    /// Measured average bitrate over the full sequence.
312    pub average_bitrate: f64,
313    /// Configured target bitrate.
314    pub target_bitrate: f64,
315    /// Maximum measured deviation (fractional for CBR/VBR, QP units for CRF).
316    pub max_deviation: f64,
317    /// Minimum bitrate observed in any 1-second window (CBR only).
318    pub min_window_bitrate: f64,
319    /// Maximum bitrate observed in any 1-second window (CBR only).
320    pub max_window_bitrate: f64,
321    /// Human-readable summary.
322    pub details: String,
323}
324
325impl VerificationResult {
326    /// Return a human-readable summary string.
327    #[must_use]
328    pub fn summary(&self) -> &str {
329        &self.details
330    }
331}
332
333// =============================================================================
334// Tests — Rate control accuracy (CBR within 5%, VBR within 10%)
335// =============================================================================
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    // ── CBR mode tests ──────────────────────────────────────────────────────
342
343    #[test]
344    fn test_cbr_perfect_bitrate() {
345        let target = 2_000_000u64; // 2 Mbps
346        let fps = 30.0;
347        let mut v = RateControlVerifier::new(target, fps, RcVerifyMode::Cbr { tolerance: 0.05 });
348
349        // Each frame: 2_000_000 / 30 / 8 ≈ 8333 bytes
350        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        // Alternate slightly above and below target
372        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        // First half: double the target bitrate
397        for _ in 0..45 {
398            v.record_frame(base_bytes * 2, false);
399        }
400        // Second half: normal
401        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    // ── VBR mode tests ──────────────────────────────────────────────────────
427
428    #[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        // Vary frame sizes but keep average near target
436        for i in 0..300 {
437            let size = if i % 60 == 0 {
438                base_bytes * 3 // keyframe burst
439            } else {
440                (base_bytes as f64 * 0.95) as u32 // compensate
441            };
442            v.record_frame(size, i % 60 == 0);
443        }
444
445        let result = v.verify();
446        let deviation = result.max_deviation;
447        // The keyframe bursts should be averaged out
448        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        // All frames 50% larger than target
462        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    // ── CRF mode tests ──────────────────────────────────────────────────────
476
477    #[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        // All frames use QP 28±1
488        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        // QP swings wildly
508        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    // ── Utility method tests ────────────────────────────────────────────────
536
537    #[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        // 10 frames at 10 fps = 1 second, each 12500 bytes = 100000 bits
542        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        // Produce exactly 1.1 Mbps (10% over)
557        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}