Skip to main content

oximedia_codec/rate_control/
simple_rc.rs

1//! Simplified rate control algorithms for video encoding.
2//!
3//! This module provides straightforward rate control implementations suitable
4//! for educational use and lightweight encoders. For production-grade rate
5//! control with lookahead, AQ, and two-pass support, see the other submodules.
6//!
7//! # Modes Supported
8//!
9//! - **CRF / Constant Quality**: quality-driven QP derivation
10//! - **ABR / Average Bitrate**: complexity-weighted bit allocation
11//! - **CBR / Constant Bitrate**: ABR with VBV buffer clamping
12//! - **VBR / Variable Bitrate**: min/max clamped ABR
13//! - **Two-Pass ABR**: complexity-histogram scaling on the second pass
14
15#![allow(clippy::cast_lossless)]
16#![allow(clippy::cast_precision_loss)]
17#![allow(clippy::cast_possible_truncation)]
18#![allow(clippy::cast_sign_loss)]
19
20use std::collections::VecDeque;
21
22// ──────────────────────────────────────────────
23// Rate control mode
24// ──────────────────────────────────────────────
25
26/// Simplified rate control mode for video encoding.
27///
28/// Each variant carries all parameters needed to drive the corresponding
29/// algorithm without requiring a separate configuration struct.
30#[derive(Debug, Clone)]
31pub enum SimpleRateControlMode {
32    /// Constant-quality mode (CRF / CQ).
33    ///
34    /// The `crf` value (0–63, lower = higher quality) drives a QP calculation.
35    /// Bitrate is not constrained; it grows with content complexity.
36    ConstantQuality {
37        /// Constant Rate Factor (0–63).
38        crf: u8,
39    },
40
41    /// Average-bitrate mode (ABR).
42    ///
43    /// The encoder targets `target_kbps` averaged over the whole clip.
44    /// Individual frames may deviate based on content complexity.
45    AverageBitrate {
46        /// Target average bitrate in kbps.
47        target_kbps: u32,
48    },
49
50    /// Constant-bitrate mode (CBR) with a VBV (Video Buffer Verifier) buffer.
51    ///
52    /// Per-frame bits are clamped so that the VBV never overflows (fullness > 0.9)
53    /// or starves (fullness < 0.3).
54    ConstantBitrate {
55        /// Target constant bitrate in kbps.
56        target_kbps: u32,
57        /// VBV buffer size in kilobits.
58        vbv_size_kb: u32,
59    },
60
61    /// Variable-bitrate mode (VBR).
62    ///
63    /// Like ABR but the instantaneous bitrate is clamped to `[min_kbps, max_kbps]`.
64    VariableBitrate {
65        /// Minimum instantaneous bitrate in kbps.
66        min_kbps: u32,
67        /// Maximum instantaneous bitrate in kbps.
68        max_kbps: u32,
69        /// Long-term average target in kbps.
70        target_kbps: u32,
71    },
72
73    /// Two-pass average-bitrate mode.
74    ///
75    /// On the second pass, bits are allocated proportional to the per-frame
76    /// complexity ratios recorded during the first pass.  `first_pass_stats`
77    /// is a newline-separated string of `f32` complexity values, one per
78    /// frame (e.g. produced by [`SimpleRateController::record_frame`]).
79    TwoPass {
80        /// Target average bitrate in kbps.
81        target_kbps: u32,
82        /// Serialised first-pass complexity data (one `f32` per line).
83        first_pass_stats: Option<String>,
84    },
85}
86
87// ──────────────────────────────────────────────
88// Configuration
89// ──────────────────────────────────────────────
90
91/// Configuration for [`SimpleRateController`].
92#[derive(Debug, Clone)]
93pub struct SimpleRateControlConfig {
94    /// Rate control algorithm and its parameters.
95    pub mode: SimpleRateControlMode,
96    /// Frames per second of the encoded video.
97    pub fps: f32,
98    /// Encoded picture resolution as `(width, height)` in pixels.
99    pub resolution: (u32, u32),
100    /// IDR / keyframe interval in frames.
101    pub keyframe_interval: u32,
102    /// Number of B-frames between reference frames.
103    pub b_frames: u8,
104    /// Scene-change sensitivity (0 = disabled, 100 = most sensitive).
105    pub scene_change_sensitivity: u8,
106}
107
108impl SimpleRateControlConfig {
109    /// Construct a constant-quality configuration.
110    pub fn crf(crf: u8, fps: f32, resolution: (u32, u32)) -> Self {
111        Self {
112            mode: SimpleRateControlMode::ConstantQuality { crf },
113            fps,
114            resolution,
115            keyframe_interval: 120,
116            b_frames: 2,
117            scene_change_sensitivity: 40,
118        }
119    }
120
121    /// Construct an average-bitrate configuration.
122    pub fn abr(target_kbps: u32, fps: f32, resolution: (u32, u32)) -> Self {
123        Self {
124            mode: SimpleRateControlMode::AverageBitrate { target_kbps },
125            fps,
126            resolution,
127            keyframe_interval: 120,
128            b_frames: 2,
129            scene_change_sensitivity: 40,
130        }
131    }
132
133    /// Construct a constant-bitrate configuration.
134    pub fn cbr(target_kbps: u32, vbv_size_kb: u32, fps: f32, resolution: (u32, u32)) -> Self {
135        Self {
136            mode: SimpleRateControlMode::ConstantBitrate {
137                target_kbps,
138                vbv_size_kb,
139            },
140            fps,
141            resolution,
142            keyframe_interval: 120,
143            b_frames: 0,
144            scene_change_sensitivity: 20,
145        }
146    }
147}
148
149// ──────────────────────────────────────────────
150// Statistics
151// ──────────────────────────────────────────────
152
153/// Per-session rate control statistics snapshot.
154#[derive(Debug, Clone)]
155pub struct SimpleRateControlStats {
156    /// Number of frames encoded so far.
157    pub frames_encoded: u64,
158    /// Total bits produced by the encoder (sum of `actual_bits` passed to
159    /// [`SimpleRateController::record_frame`]).
160    pub total_bits: u64,
161    /// Running mean of bits per frame.
162    pub avg_bits_per_frame: f64,
163    /// Running mean of the complexity values supplied to the controller.
164    pub avg_complexity: f32,
165    /// Current VBV buffer fullness in `[0.0, 1.0]`.  0 = empty, 1 = full.
166    pub vbv_fullness: f64,
167    /// Target bitrate in kbps derived from the mode, or 0 for CRF mode.
168    pub target_bitrate_kbps: u32,
169    /// Actual measured bitrate in kbps based on bits spent and frames encoded.
170    pub actual_bitrate_kbps: f64,
171}
172
173// ──────────────────────────────────────────────
174// Controller
175// ──────────────────────────────────────────────
176
177/// Maximum number of recent complexity samples kept for rolling mean.
178const COMPLEXITY_HISTORY_LEN: usize = 64;
179
180/// A simplified, self-contained rate controller.
181///
182/// # Usage
183///
184/// ```rust
185/// use oximedia_codec::rate_control::{SimpleRateControlConfig, SimpleRateController};
186///
187/// let cfg = SimpleRateControlConfig::abr(4000, 30.0, (1920, 1080));
188/// let mut rc = SimpleRateController::new(cfg);
189///
190/// for i in 0..300_u32 {
191///     let complexity = 0.5_f32;                     // supplied by analyser
192///     let bits = rc.allocate_frame_bits(complexity);
193///     rc.record_frame(bits, complexity);
194/// }
195///
196/// let stats = rc.stats();
197/// assert!(stats.frames_encoded == 300);
198/// ```
199pub struct SimpleRateController {
200    /// Immutable configuration supplied at construction time.
201    config: SimpleRateControlConfig,
202    /// Running frame counter (incremented in `record_frame`).
203    frame_count: u64,
204    /// Cumulative bits recorded via `record_frame`.
205    bits_spent: u64,
206    /// Pre-computed target bits per frame (0 for CRF).
207    target_bits: u64,
208    /// Current VBV buffer fullness as a fraction `[0.0, 1.0]`.
209    vbv_fullness: f64,
210    /// Sliding window of recent complexity values for mean estimation.
211    complexity_history: VecDeque<f32>,
212    /// Decoded first-pass complexity histogram for two-pass ABR.
213    /// Each element corresponds to a frame index.
214    first_pass_complexities: Vec<f32>,
215    /// Cached mean of `first_pass_complexities`.
216    first_pass_mean: f32,
217}
218
219impl SimpleRateController {
220    /// Create a new controller from `config`.
221    ///
222    /// The VBV buffer starts at 50 % fullness for CBR mode and at 0 for all
223    /// other modes.
224    pub fn new(config: SimpleRateControlConfig) -> Self {
225        let target_bits = compute_target_bits_per_frame(&config);
226        let vbv_fullness = match &config.mode {
227            SimpleRateControlMode::ConstantBitrate { .. } => 0.5,
228            _ => 0.0,
229        };
230
231        let (first_pass_complexities, first_pass_mean) = parse_first_pass_stats(&config.mode);
232
233        Self {
234            config,
235            frame_count: 0,
236            bits_spent: 0,
237            target_bits,
238            vbv_fullness,
239            complexity_history: VecDeque::with_capacity(COMPLEXITY_HISTORY_LEN),
240            first_pass_complexities,
241            first_pass_mean,
242        }
243    }
244
245    // ── Internal helpers ────────────────────────────────────────────────────
246
247    /// Rolling mean of the complexity history.
248    ///
249    /// Falls back to `1.0` when the history is empty to avoid division by zero.
250    fn avg_complexity(&self) -> f32 {
251        if self.complexity_history.is_empty() {
252            return 1.0;
253        }
254        let sum: f32 = self.complexity_history.iter().sum();
255        sum / self.complexity_history.len() as f32
256    }
257
258    // ── Public API ───────────────────────────────────────────────────────────
259
260    /// Return the pre-computed target bits-per-frame for the current mode.
261    ///
262    /// CRF mode always returns 0 because bit budget is implicitly derived from
263    /// the QP rather than a bitrate target.
264    pub fn target_bits_per_frame(&self) -> u64 {
265        self.target_bits
266    }
267
268    /// Decide how many bits to allocate for the next frame given its
269    /// `complexity` estimate (any non-negative float, relative scale).
270    ///
271    /// The return value is a **recommendation** in bits; the caller feeds the
272    /// actual encoded size back via `record_frame`.
273    pub fn allocate_frame_bits(&mut self, complexity: f32) -> u32 {
274        let complexity = complexity.max(0.0001_f32);
275        match &self.config.mode.clone() {
276            SimpleRateControlMode::ConstantQuality { crf } => {
277                let (w, h) = self.config.resolution;
278                let qp = (*crf as f32).max(1.0);
279                let bits = (w as f64 * h as f64 / (qp * qp) as f64) as u64;
280                bits.min(u32::MAX as u64) as u32
281            }
282
283            SimpleRateControlMode::AverageBitrate { .. } => {
284                let avg = self.avg_complexity();
285                let ratio = (complexity / avg) as f64;
286                let allocated = (self.target_bits as f64 * ratio).round() as u64;
287                // Allow up to 4× target to absorb complexity peaks.
288                allocated.clamp(1, self.target_bits.saturating_mul(4)) as u32
289            }
290
291            SimpleRateControlMode::VariableBitrate {
292                min_kbps, max_kbps, ..
293            } => {
294                let min_bits = (*min_kbps as f64 * 1000.0 / self.config.fps as f64) as u64;
295                let max_bits = (*max_kbps as f64 * 1000.0 / self.config.fps as f64) as u64;
296                let avg = self.avg_complexity();
297                let ratio = (complexity / avg) as f64;
298                let allocated = (self.target_bits as f64 * ratio).round() as u64;
299                allocated.clamp(min_bits.max(1), max_bits.max(1)) as u32
300            }
301
302            SimpleRateControlMode::ConstantBitrate { vbv_size_kb, .. } => {
303                let avg = self.avg_complexity();
304                let ratio = (complexity / avg) as f64;
305                let mut allocated = (self.target_bits as f64 * ratio).round() as u64;
306
307                // VBV clamping: reduce bits when buffer is getting full,
308                // increase when it is running low.
309                let vbv_bits = (*vbv_size_kb as u64).saturating_mul(1000);
310                if self.vbv_fullness > 0.9 {
311                    // Buffer almost full — cut bits to at most 50 % of target.
312                    let cap = (self.target_bits as f64 * 0.5).round() as u64;
313                    allocated = allocated.min(cap);
314                } else if self.vbv_fullness < 0.3 {
315                    // Buffer running low — allow up to 150 % of target.
316                    let floor = (self.target_bits as f64 * 1.5).round() as u64;
317                    allocated = allocated.max(floor);
318                }
319
320                // Hard cap at VBV buffer size to prevent overflow in a single frame.
321                allocated.clamp(1, vbv_bits.max(1)) as u32
322            }
323
324            SimpleRateControlMode::TwoPass { .. } => {
325                let idx = self.frame_count as usize;
326                if !self.first_pass_complexities.is_empty() && self.first_pass_mean > 0.0 {
327                    let frame_cplx = if idx < self.first_pass_complexities.len() {
328                        self.first_pass_complexities[idx]
329                    } else {
330                        self.first_pass_mean
331                    };
332                    let ratio = (frame_cplx / self.first_pass_mean) as f64;
333                    let allocated = (self.target_bits as f64 * ratio).round() as u64;
334                    allocated.clamp(1, self.target_bits.saturating_mul(4)) as u32
335                } else {
336                    // No first-pass data: fall back to flat allocation.
337                    self.target_bits.clamp(1, u32::MAX as u64) as u32
338                }
339            }
340        }
341    }
342
343    /// Record the outcome of encoding a frame.
344    ///
345    /// - `actual_bits`: real encoded size in bits.
346    /// - `complexity`: the same complexity estimate passed to
347    ///   `allocate_frame_bits` for this frame.
348    ///
349    /// This updates internal accounting (VBV fullness, bit budget, complexity
350    /// history) so that subsequent allocations are informed by history.
351    pub fn record_frame(&mut self, actual_bits: u32, complexity: f32) {
352        self.bits_spent = self.bits_spent.saturating_add(actual_bits as u64);
353        self.frame_count = self.frame_count.saturating_add(1);
354
355        // Update sliding complexity window.
356        if self.complexity_history.len() >= COMPLEXITY_HISTORY_LEN {
357            self.complexity_history.pop_front();
358        }
359        self.complexity_history.push_back(complexity.max(0.0));
360
361        // Update VBV buffer for CBR mode.
362        if let SimpleRateControlMode::ConstantBitrate {
363            target_kbps,
364            vbv_size_kb,
365        } = &self.config.mode
366        {
367            let vbv_capacity = (*vbv_size_kb as f64) * 1000.0;
368            if vbv_capacity > 0.0 {
369                let drained = actual_bits as f64;
370                let refilled = (*target_kbps as f64 * 1000.0) / self.config.fps as f64;
371                // Buffer is refilled by the channel rate and drained by encoded bits.
372                let delta = (refilled - drained) / vbv_capacity;
373                self.vbv_fullness = (self.vbv_fullness + delta).clamp(0.0, 1.0);
374            }
375        }
376    }
377
378    /// Current VBV buffer fullness: 0.0 = empty, 1.0 = full.
379    ///
380    /// For non-CBR modes this always returns 0.0.
381    pub fn vbv_status(&self) -> f64 {
382        self.vbv_fullness
383    }
384
385    /// Return `true` if the current frame index is a keyframe position.
386    ///
387    /// Uses the `keyframe_interval` from the configuration.  Frame index 0
388    /// (the very first frame) is always considered a keyframe.
389    pub fn is_keyframe(&self) -> bool {
390        let interval = self.config.keyframe_interval.max(1) as u64;
391        self.frame_count % interval == 0
392    }
393
394    /// Snapshot of accumulated statistics.
395    pub fn stats(&self) -> SimpleRateControlStats {
396        let target_bitrate_kbps = extract_target_kbps(&self.config.mode);
397        let avg_bits_per_frame = if self.frame_count == 0 {
398            0.0
399        } else {
400            self.bits_spent as f64 / self.frame_count as f64
401        };
402        let actual_bitrate_kbps = avg_bits_per_frame * self.config.fps as f64 / 1000.0;
403
404        SimpleRateControlStats {
405            frames_encoded: self.frame_count,
406            total_bits: self.bits_spent,
407            avg_bits_per_frame,
408            avg_complexity: self.avg_complexity(),
409            vbv_fullness: self.vbv_fullness,
410            target_bitrate_kbps,
411            actual_bitrate_kbps,
412        }
413    }
414}
415
416// ──────────────────────────────────────────────
417// Free-standing helpers
418// ──────────────────────────────────────────────
419
420/// Compute the per-frame bit budget for bitrate-based modes.
421///
422/// Returns 0 for CRF mode (bit budget is implicit in QP).
423fn compute_target_bits_per_frame(config: &SimpleRateControlConfig) -> u64 {
424    let fps = config.fps.max(0.001_f32) as f64;
425    match &config.mode {
426        SimpleRateControlMode::ConstantQuality { .. } => 0,
427        SimpleRateControlMode::AverageBitrate { target_kbps }
428        | SimpleRateControlMode::ConstantBitrate { target_kbps, .. }
429        | SimpleRateControlMode::VariableBitrate { target_kbps, .. }
430        | SimpleRateControlMode::TwoPass { target_kbps, .. } => {
431            (*target_kbps as f64 * 1000.0 / fps).round() as u64
432        }
433    }
434}
435
436/// Extract a representative `target_kbps` value for stats reporting.
437///
438/// Returns 0 for CRF because there is no bitrate target.
439fn extract_target_kbps(mode: &SimpleRateControlMode) -> u32 {
440    match mode {
441        SimpleRateControlMode::ConstantQuality { .. } => 0,
442        SimpleRateControlMode::AverageBitrate { target_kbps }
443        | SimpleRateControlMode::ConstantBitrate { target_kbps, .. }
444        | SimpleRateControlMode::VariableBitrate { target_kbps, .. }
445        | SimpleRateControlMode::TwoPass { target_kbps, .. } => *target_kbps,
446    }
447}
448
449/// Parse the `first_pass_stats` string for TwoPass mode.
450///
451/// Returns `(complexities, mean)`.  Both are empty / zero when the mode is not
452/// TwoPass or when the stats string is absent.
453fn parse_first_pass_stats(mode: &SimpleRateControlMode) -> (Vec<f32>, f32) {
454    if let SimpleRateControlMode::TwoPass {
455        first_pass_stats: Some(stats),
456        ..
457    } = mode
458    {
459        let values: Vec<f32> = stats
460            .lines()
461            .filter_map(|l| l.trim().parse::<f32>().ok())
462            .collect();
463        let mean = if values.is_empty() {
464            0.0
465        } else {
466            values.iter().sum::<f32>() / values.len() as f32
467        };
468        return (values, mean);
469    }
470    (Vec::new(), 0.0)
471}
472
473// ──────────────────────────────────────────────
474// Tests
475// ──────────────────────────────────────────────
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    fn make_crf(crf: u8) -> SimpleRateController {
482        SimpleRateController::new(SimpleRateControlConfig::crf(crf, 30.0, (1920, 1080)))
483    }
484
485    fn make_abr(kbps: u32) -> SimpleRateController {
486        SimpleRateController::new(SimpleRateControlConfig::abr(kbps, 30.0, (1920, 1080)))
487    }
488
489    fn make_cbr(kbps: u32, vbv_kb: u32) -> SimpleRateController {
490        SimpleRateController::new(SimpleRateControlConfig::cbr(
491            kbps,
492            vbv_kb,
493            30.0,
494            (1920, 1080),
495        ))
496    }
497
498    // ── 1. CRF allocation is non-zero ────────────────────────────────────────
499
500    #[test]
501    fn crf_allocation_nonzero() {
502        let mut rc = make_crf(23);
503        let bits = rc.allocate_frame_bits(1.0);
504        assert!(bits > 0, "CRF allocation must be positive");
505    }
506
507    // ── 2. CRF: lower crf (higher quality) → more bits ──────────────────────
508
509    #[test]
510    fn crf_lower_crf_more_bits() {
511        let mut hi_q = make_crf(10);
512        let mut lo_q = make_crf(40);
513        let hi = hi_q.allocate_frame_bits(1.0);
514        let lo = lo_q.allocate_frame_bits(1.0);
515        assert!(hi > lo, "crf=10 should yield more bits than crf=40");
516    }
517
518    // ── 3. target_bits_per_frame is correct for ABR ─────────────────────────
519
520    #[test]
521    fn abr_target_bits_per_frame() {
522        let rc = make_abr(4000);
523        // 4000 kbps / 30 fps = ~133_333 bits/frame
524        let expected = (4_000_000_u64 / 30) as f64;
525        let got = rc.target_bits_per_frame() as f64;
526        assert!(
527            (got - expected).abs() / expected < 0.01,
528            "ABR target bits/frame off: got {got}, expected ~{expected}"
529        );
530    }
531
532    // ── 4. CRF: target_bits_per_frame is 0 ──────────────────────────────────
533
534    #[test]
535    fn crf_target_bits_zero() {
536        let rc = make_crf(28);
537        assert_eq!(rc.target_bits_per_frame(), 0);
538    }
539
540    // ── 5. ABR: higher complexity → more bits ────────────────────────────────
541
542    #[test]
543    fn abr_high_complexity_more_bits() {
544        let mut rc = make_abr(4000);
545        // Prime the history with moderate complexity.
546        for _ in 0..16 {
547            rc.record_frame(100_000, 1.0);
548        }
549        let low = rc.allocate_frame_bits(0.5);
550        let high = rc.allocate_frame_bits(2.0);
551        assert!(
552            high > low,
553            "higher complexity should yield more bits in ABR"
554        );
555    }
556
557    // ── 6. record_frame increments frame_count ───────────────────────────────
558
559    #[test]
560    fn record_frame_increments_count() {
561        let mut rc = make_abr(2000);
562        for _ in 0..10 {
563            rc.record_frame(50_000, 1.0);
564        }
565        assert_eq!(rc.stats().frames_encoded, 10);
566    }
567
568    // ── 7. record_frame accumulates total_bits ───────────────────────────────
569
570    #[test]
571    fn record_frame_accumulates_bits() {
572        let mut rc = make_abr(2000);
573        for _ in 0..5 {
574            rc.record_frame(10_000, 1.0);
575        }
576        assert_eq!(rc.stats().total_bits, 50_000);
577    }
578
579    // ── 8. CBR: VBV fullness moves within [0, 1] ────────────────────────────
580
581    #[test]
582    fn cbr_vbv_fullness_in_range() {
583        let mut rc = make_cbr(4000, 8000);
584        for i in 0..60 {
585            let bits = if i % 5 == 0 { 1_000_000 } else { 50_000 };
586            rc.record_frame(bits, 1.0);
587            let f = rc.vbv_status();
588            assert!((0.0..=1.0).contains(&f), "VBV fullness out of range: {f}");
589        }
590    }
591
592    // ── 9. CBR: VBV reduces allocation when near-full ───────────────────────
593
594    #[test]
595    fn cbr_vbv_reduces_when_full() {
596        let mut rc = make_cbr(4000, 1000); // tiny VBV
597                                           // Force VBV to near-full by encoding many frames with zero bits.
598        for _ in 0..100 {
599            rc.record_frame(0, 1.0); // no bits drained, VBV fills up
600        }
601        let bits_full = rc.allocate_frame_bits(1.0);
602        let target = rc.target_bits_per_frame() as u32;
603        // Should be at most 50 % of target when full.
604        assert!(
605            bits_full <= target.saturating_mul(5) / 10 + 1,
606            "CBR should reduce bits when VBV is full; got {bits_full}, target {target}"
607        );
608    }
609
610    // ── 10. is_keyframe at correct intervals ─────────────────────────────────
611
612    #[test]
613    fn is_keyframe_at_correct_intervals() {
614        let cfg = SimpleRateControlConfig {
615            mode: SimpleRateControlMode::AverageBitrate { target_kbps: 2000 },
616            fps: 30.0,
617            resolution: (1280, 720),
618            keyframe_interval: 10,
619            b_frames: 0,
620            scene_change_sensitivity: 0,
621        };
622        let mut rc = SimpleRateController::new(cfg);
623        // frame_count=0, 0%10==0 → keyframe.
624        assert!(rc.is_keyframe(), "frame_count=0 should be keyframe");
625        // Advance through frames 1–9 (non-keyframe positions).
626        for i in 1..10u64 {
627            rc.record_frame(10_000, 1.0); // increments frame_count
628            assert_eq!(rc.frame_count, i);
629            assert!(!rc.is_keyframe(), "frame_count={i} should NOT be keyframe");
630        }
631        // One more record brings frame_count to 10 → keyframe.
632        rc.record_frame(10_000, 1.0);
633        assert_eq!(rc.frame_count, 10);
634        assert!(rc.is_keyframe(), "frame_count=10 should be keyframe");
635    }
636
637    // ── 11. stats returns correct avg_bits_per_frame ─────────────────────────
638
639    #[test]
640    fn stats_avg_bits_per_frame() {
641        let mut rc = make_abr(2000);
642        rc.record_frame(200_000, 1.0);
643        rc.record_frame(100_000, 1.0);
644        let s = rc.stats();
645        assert!((s.avg_bits_per_frame - 150_000.0).abs() < 1.0);
646    }
647
648    // ── 12. VBR: allocation is clamped between min and max ──────────────────
649
650    #[test]
651    fn vbr_allocation_clamped() {
652        let cfg = SimpleRateControlConfig {
653            mode: SimpleRateControlMode::VariableBitrate {
654                min_kbps: 1000,
655                max_kbps: 8000,
656                target_kbps: 4000,
657            },
658            fps: 30.0,
659            resolution: (1920, 1080),
660            keyframe_interval: 120,
661            b_frames: 2,
662            scene_change_sensitivity: 40,
663        };
664        let mut rc = SimpleRateController::new(cfg);
665        // Prime history.
666        for _ in 0..16 {
667            rc.record_frame(130_000, 1.0);
668        }
669        let bits_low = rc.allocate_frame_bits(0.01); // almost no complexity
670        let bits_high = rc.allocate_frame_bits(100.0); // extreme complexity
671        let min_bits = (1000_u64 * 1000 / 30) as u32;
672        let max_bits = (8000_u64 * 1000 / 30) as u32;
673        assert!(
674            bits_low >= min_bits.saturating_sub(1),
675            "VBR low allocation {bits_low} below min {min_bits}"
676        );
677        assert!(
678            bits_high <= max_bits + 1,
679            "VBR high allocation {bits_high} above max {max_bits}"
680        );
681    }
682
683    // ── 13. Two-pass uses first-pass stats ───────────────────────────────────
684
685    #[test]
686    fn two_pass_uses_first_pass_stats() {
687        let stats = "0.5\n1.0\n2.0\n0.5\n1.0";
688        let cfg = SimpleRateControlConfig {
689            mode: SimpleRateControlMode::TwoPass {
690                target_kbps: 4000,
691                first_pass_stats: Some(stats.to_owned()),
692            },
693            fps: 30.0,
694            resolution: (1920, 1080),
695            keyframe_interval: 120,
696            b_frames: 2,
697            scene_change_sensitivity: 40,
698        };
699        let mut rc = SimpleRateController::new(cfg);
700
701        // Frame 0: complexity 0.5 (below mean 1.0) → fewer bits than target.
702        let bits_easy = rc.allocate_frame_bits(0.5);
703        rc.record_frame(bits_easy, 0.5);
704
705        // Frame 1: complexity 1.0 (at mean) → ~target.
706        let bits_avg = rc.allocate_frame_bits(1.0);
707        rc.record_frame(bits_avg, 1.0);
708
709        // Frame 2: complexity 2.0 (above mean) → more bits than target.
710        let bits_hard = rc.allocate_frame_bits(2.0);
711
712        assert!(
713            bits_hard > bits_easy,
714            "Two-pass: complex frame should get more bits than easy frame"
715        );
716    }
717
718    // ── 14. stats actual_bitrate_kbps is non-zero after recording ────────────
719
720    #[test]
721    fn stats_actual_bitrate_nonzero() {
722        let mut rc = make_abr(4000);
723        for _ in 0..30 {
724            rc.record_frame(133_333, 1.0);
725        }
726        let s = rc.stats();
727        assert!(
728            s.actual_bitrate_kbps > 0.0,
729            "actual bitrate should be positive"
730        );
731    }
732
733    // ── 15. Two-pass without first-pass data falls back to flat allocation ───
734
735    #[test]
736    fn two_pass_no_stats_flat_fallback() {
737        let cfg = SimpleRateControlConfig {
738            mode: SimpleRateControlMode::TwoPass {
739                target_kbps: 2000,
740                first_pass_stats: None,
741            },
742            fps: 25.0,
743            resolution: (1280, 720),
744            keyframe_interval: 50,
745            b_frames: 2,
746            scene_change_sensitivity: 40,
747        };
748        let mut rc = SimpleRateController::new(cfg);
749        let bits = rc.allocate_frame_bits(1.0);
750        assert!(bits > 0, "fallback two-pass allocation must be positive");
751        // Should equal target_bits_per_frame (flat).
752        let target = rc.target_bits_per_frame() as u32;
753        assert_eq!(
754            bits, target,
755            "no-stats two-pass should produce flat allocation equal to target"
756        );
757    }
758}