Skip to main content

oximedia_codec/rate_control/
allocation.rs

1// Copyright 2024 OxiMedia Project
2// Licensed under the Apache License, Version 2.0
3
4//! Bitrate allocation strategies for rate control.
5//!
6//! This module provides sophisticated algorithms for allocating bits across frames:
7//!
8//! - **Complexity-Based Allocation** - Allocate bits proportional to frame complexity
9//! - **GOP-Level Allocation** - Distribute bits across GOPs
10//! - **Hierarchical Allocation** - B-pyramid and hierarchical GOP structures
11//! - **Multi-Pass Allocation** - Use first-pass statistics for optimal allocation
12//! - **Constrained Allocation** - VBV/HRD buffer constraints
13//! - **Adaptive Allocation** - Real-time adjustment based on encoding results
14//!
15//! # Architecture
16//!
17//! ```text
18//! Total Bits → GOP Allocator → Frame Allocator → Block Allocator
19//!      ↓             ↓               ↓                 ↓
20//!   Budget      GOP Budget    Frame Target    Block Weights
21//! ```
22
23#![allow(clippy::cast_lossless)]
24#![allow(clippy::cast_precision_loss)]
25#![allow(clippy::cast_possible_truncation)]
26#![allow(clippy::cast_sign_loss)]
27#![allow(clippy::similar_names)]
28#![allow(clippy::too_many_arguments)]
29#![allow(clippy::struct_excessive_bools)]
30#![forbid(unsafe_code)]
31
32use crate::frame::FrameType;
33
34/// Bitrate allocation strategy.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum AllocationStrategy {
37    /// Equal allocation across all frames.
38    Uniform,
39    /// Complexity-based allocation.
40    ComplexityBased,
41    /// Multi-pass allocation using statistics.
42    MultiPass,
43    /// Hierarchical allocation for B-pyramid.
44    Hierarchical,
45    /// Constrained allocation with VBV compliance.
46    Constrained,
47    /// Adaptive allocation with real-time feedback.
48    Adaptive,
49}
50
51impl Default for AllocationStrategy {
52    fn default() -> Self {
53        Self::ComplexityBased
54    }
55}
56
57/// Bitrate allocator for distributing bits across frames.
58#[derive(Clone, Debug)]
59pub struct BitrateAllocator {
60    /// Allocation strategy.
61    strategy: AllocationStrategy,
62    /// Target bitrate in bits per second.
63    target_bitrate: u64,
64    /// Frame rate.
65    framerate: f64,
66    /// GOP length.
67    gop_length: u32,
68    /// I-frame to P-frame bit ratio.
69    i_p_ratio: f32,
70    /// P-frame to B-frame bit ratio.
71    p_b_ratio: f32,
72    /// Complexity weight factor.
73    complexity_weight: f32,
74    /// Historical complexity data.
75    complexity_history: Vec<f32>,
76    /// Historical bit usage.
77    bit_usage_history: Vec<u64>,
78    /// Maximum history size.
79    max_history: usize,
80    /// VBV buffer size (if constrained).
81    vbv_buffer_size: Option<u64>,
82    /// Bit reservoir for smoothing.
83    bit_reservoir: i64,
84    /// Maximum reservoir size.
85    max_reservoir: i64,
86    /// Current GOP index.
87    current_gop: u32,
88    /// Frames in current GOP.
89    frames_in_gop: u32,
90    /// Bits used in current GOP.
91    bits_used_in_gop: u64,
92    /// Target bits for current GOP.
93    gop_target_bits: u64,
94}
95
96impl BitrateAllocator {
97    /// Create a new bitrate allocator.
98    #[must_use]
99    pub fn new(target_bitrate: u64, framerate: f64, gop_length: u32) -> Self {
100        let max_reservoir = (target_bitrate as i64) * 2;
101
102        Self {
103            strategy: AllocationStrategy::default(),
104            target_bitrate,
105            framerate,
106            gop_length,
107            i_p_ratio: 3.0,
108            p_b_ratio: 2.0,
109            complexity_weight: 1.0,
110            complexity_history: Vec::new(),
111            bit_usage_history: Vec::new(),
112            max_history: 100,
113            vbv_buffer_size: None,
114            bit_reservoir: 0,
115            max_reservoir,
116            current_gop: 0,
117            frames_in_gop: 0,
118            bits_used_in_gop: 0,
119            gop_target_bits: 0,
120        }
121    }
122
123    /// Set allocation strategy.
124    pub fn set_strategy(&mut self, strategy: AllocationStrategy) {
125        self.strategy = strategy;
126    }
127
128    /// Set I/P frame bit ratio.
129    pub fn set_i_p_ratio(&mut self, ratio: f32) {
130        self.i_p_ratio = ratio.max(1.0);
131    }
132
133    /// Set P/B frame bit ratio.
134    pub fn set_p_b_ratio(&mut self, ratio: f32) {
135        self.p_b_ratio = ratio.max(1.0);
136    }
137
138    /// Set complexity weight factor.
139    pub fn set_complexity_weight(&mut self, weight: f32) {
140        self.complexity_weight = weight.clamp(0.0, 2.0);
141    }
142
143    /// Set VBV buffer size for constrained allocation.
144    pub fn set_vbv_buffer_size(&mut self, size: u64) {
145        self.vbv_buffer_size = Some(size);
146        if self.strategy == AllocationStrategy::Uniform {
147            self.strategy = AllocationStrategy::Constrained;
148        }
149    }
150
151    /// Calculate target bits for a frame.
152    #[must_use]
153    pub fn allocate_frame_bits(
154        &self,
155        frame_type: FrameType,
156        complexity: f32,
157        frame_in_gop: u32,
158    ) -> AllocationResult {
159        let base_bits = self.calculate_base_allocation();
160
161        let allocated_bits = match self.strategy {
162            AllocationStrategy::Uniform => self.allocate_uniform(base_bits, frame_type),
163            AllocationStrategy::ComplexityBased => {
164                self.allocate_complexity_based(base_bits, frame_type, complexity)
165            }
166            AllocationStrategy::MultiPass => {
167                self.allocate_multipass(base_bits, frame_type, complexity)
168            }
169            AllocationStrategy::Hierarchical => {
170                self.allocate_hierarchical(base_bits, frame_type, frame_in_gop)
171            }
172            AllocationStrategy::Constrained => {
173                self.allocate_constrained(base_bits, frame_type, complexity)
174            }
175            AllocationStrategy::Adaptive => {
176                self.allocate_adaptive(base_bits, frame_type, complexity)
177            }
178        };
179
180        let mut max_bits = allocated_bits * 4;
181
182        // Clamp max_bits to VBV buffer size when constrained
183        if let Some(vbv_size) = self.vbv_buffer_size {
184            max_bits = max_bits.min(vbv_size);
185        }
186
187        AllocationResult {
188            target_bits: allocated_bits,
189            min_bits: allocated_bits / 4,
190            max_bits,
191            frame_type,
192            complexity_factor: complexity / self.average_complexity(),
193            reservoir_adjustment: self.calculate_reservoir_adjustment(allocated_bits),
194        }
195    }
196
197    /// Calculate base bit allocation per frame.
198    fn calculate_base_allocation(&self) -> u64 {
199        if self.framerate <= 0.0 {
200            return 0;
201        }
202        (self.target_bitrate as f64 / self.framerate) as u64
203    }
204
205    /// Uniform allocation (simple frame type based).
206    fn allocate_uniform(&self, base_bits: u64, frame_type: FrameType) -> u64 {
207        match frame_type {
208            FrameType::Key => (base_bits as f32 * self.i_p_ratio) as u64,
209            FrameType::Inter => base_bits,
210            FrameType::BiDir => (base_bits as f32 / self.p_b_ratio) as u64,
211            FrameType::Switch => (base_bits as f32 * 1.5) as u64,
212        }
213    }
214
215    /// Complexity-based allocation.
216    fn allocate_complexity_based(
217        &self,
218        base_bits: u64,
219        frame_type: FrameType,
220        complexity: f32,
221    ) -> u64 {
222        // Get base allocation for frame type
223        let type_bits = self.allocate_uniform(base_bits, frame_type);
224
225        // Apply complexity factor
226        let avg_complexity = self.average_complexity();
227        if avg_complexity <= 0.0 {
228            return type_bits;
229        }
230
231        let complexity_ratio = complexity / avg_complexity;
232        let complexity_multiplier = 1.0 + (complexity_ratio - 1.0) * self.complexity_weight;
233
234        (type_bits as f32 * complexity_multiplier.clamp(0.5, 2.0)) as u64
235    }
236
237    /// Multi-pass allocation using statistics.
238    fn allocate_multipass(&self, base_bits: u64, frame_type: FrameType, complexity: f32) -> u64 {
239        // Use historical data to improve allocation
240        if self.complexity_history.is_empty() {
241            return self.allocate_complexity_based(base_bits, frame_type, complexity);
242        }
243
244        // Calculate total complexity for proportional allocation
245        let total_complexity: f32 = self.complexity_history.iter().sum();
246        if total_complexity <= 0.0 {
247            return self.allocate_complexity_based(base_bits, frame_type, complexity);
248        }
249
250        // Total bits for this GOP
251        let gop_bits = base_bits * self.gop_length as u64;
252
253        // Proportional allocation based on complexity
254        let frame_proportion = complexity / total_complexity;
255        let mut allocated = (gop_bits as f32 * frame_proportion) as u64;
256
257        // Apply frame type multiplier
258        allocated = match frame_type {
259            FrameType::Key => (allocated as f32 * self.i_p_ratio) as u64,
260            FrameType::BiDir => (allocated as f32 / self.p_b_ratio) as u64,
261            _ => allocated,
262        };
263
264        allocated.max(base_bits / 4)
265    }
266
267    /// Hierarchical allocation for B-pyramid structures.
268    fn allocate_hierarchical(
269        &self,
270        base_bits: u64,
271        frame_type: FrameType,
272        frame_in_gop: u32,
273    ) -> u64 {
274        let type_bits = self.allocate_uniform(base_bits, frame_type);
275
276        match frame_type {
277            FrameType::BiDir => {
278                // B-frames in pyramid: lower levels get more bits
279                let pyramid_level = self.calculate_pyramid_level(frame_in_gop);
280                let level_multiplier = 1.0 + (pyramid_level as f32 * 0.2);
281                (type_bits as f32 * level_multiplier) as u64
282            }
283            _ => type_bits,
284        }
285    }
286
287    /// Calculate B-frame pyramid level.
288    fn calculate_pyramid_level(&self, frame_in_gop: u32) -> u32 {
289        // Simplified pyramid level calculation
290        // Level 0 = reference B-frames (higher quality)
291        // Level N = highest level (lower quality)
292        let mut level = 0;
293        let mut pos = frame_in_gop;
294
295        while pos % 2 == 0 && pos > 0 {
296            level += 1;
297            pos /= 2;
298        }
299
300        level
301    }
302
303    /// Constrained allocation with VBV compliance.
304    fn allocate_constrained(&self, base_bits: u64, frame_type: FrameType, complexity: f32) -> u64 {
305        let mut allocated = self.allocate_complexity_based(base_bits, frame_type, complexity);
306
307        // Apply VBV constraints if enabled
308        if let Some(vbv_size) = self.vbv_buffer_size {
309            // Estimate current buffer level
310            let buffer_level = self.estimate_buffer_level();
311
312            // If buffer is getting full, reduce allocation
313            let fullness_ratio = buffer_level as f32 / vbv_size as f32;
314            if fullness_ratio > 0.8 {
315                let reduction = (fullness_ratio - 0.8) * 5.0; // Aggressive reduction
316                allocated = (allocated as f32 * (1.0 - reduction).max(0.5)) as u64;
317            }
318
319            // Don't exceed buffer capacity
320            allocated = allocated.min(vbv_size);
321        }
322
323        allocated
324    }
325
326    /// Adaptive allocation with real-time feedback.
327    fn allocate_adaptive(&self, base_bits: u64, frame_type: FrameType, complexity: f32) -> u64 {
328        let mut allocated = self.allocate_complexity_based(base_bits, frame_type, complexity);
329
330        // Adjust based on recent bit usage accuracy
331        if !self.bit_usage_history.is_empty() {
332            let recent_usage: u64 = self.bit_usage_history.iter().rev().take(10).sum();
333            let recent_target = base_bits * 10.min(self.bit_usage_history.len()) as u64;
334
335            if recent_target > 0 {
336                let usage_ratio = recent_usage as f32 / recent_target as f32;
337
338                // If consistently overshooting, reduce allocation
339                // If undershooting, increase allocation
340                if usage_ratio > 1.2 {
341                    allocated = (allocated as f32 * 0.9) as u64;
342                } else if usage_ratio < 0.8 {
343                    allocated = (allocated as f32 * 1.1) as u64;
344                }
345            }
346        }
347
348        // Apply reservoir adjustment
349        let reservoir_adjustment = self.calculate_reservoir_adjustment(allocated);
350        ((allocated as i64) + reservoir_adjustment).max(base_bits as i64 / 4) as u64
351    }
352
353    /// Calculate reservoir adjustment.
354    fn calculate_reservoir_adjustment(&self, target: u64) -> i64 {
355        if self.bit_reservoir == 0 {
356            return 0;
357        }
358
359        let reservoir_ratio = self.bit_reservoir as f32 / self.max_reservoir as f32;
360
361        // Use reservoir when it's full, save to reservoir when empty
362        let adjustment = (target as f32 * reservoir_ratio * 0.1) as i64;
363
364        // Clamp to prevent extreme adjustments
365        adjustment.clamp(-(target as i64 / 4), target as i64 / 4)
366    }
367
368    /// Estimate current VBV buffer level.
369    fn estimate_buffer_level(&self) -> u64 {
370        // Simplified estimation based on recent bit usage
371        if let Some(vbv_size) = self.vbv_buffer_size {
372            let bits_per_frame = self.calculate_base_allocation();
373            let recent_frames = 10.min(self.bit_usage_history.len());
374
375            if recent_frames == 0 {
376                return vbv_size / 2; // Assume half full initially
377            }
378
379            let recent_bits: u64 = self
380                .bit_usage_history
381                .iter()
382                .rev()
383                .take(recent_frames)
384                .sum();
385            let target_bits = bits_per_frame * recent_frames as u64;
386
387            // Buffer level increases when using less than target
388            if recent_bits < target_bits {
389                let saved = target_bits - recent_bits;
390                (vbv_size / 2 + saved).min(vbv_size)
391            } else {
392                let overage = recent_bits - target_bits;
393                (vbv_size / 2).saturating_sub(overage)
394            }
395        } else {
396            0
397        }
398    }
399
400    /// Get average complexity from history.
401    fn average_complexity(&self) -> f32 {
402        if self.complexity_history.is_empty() {
403            return 1.0;
404        }
405
406        let sum: f32 = self.complexity_history.iter().sum();
407        (sum / self.complexity_history.len() as f32).max(0.01)
408    }
409
410    /// Update allocator with actual frame results.
411    pub fn update(&mut self, complexity: f32, bits_used: u64) {
412        // Update complexity history
413        self.complexity_history.push(complexity);
414        if self.complexity_history.len() > self.max_history {
415            self.complexity_history.remove(0);
416        }
417
418        // Update bit usage history
419        self.bit_usage_history.push(bits_used);
420        if self.bit_usage_history.len() > self.max_history {
421            self.bit_usage_history.remove(0);
422        }
423
424        // Update bit reservoir
425        let target_per_frame = self.calculate_base_allocation();
426        self.bit_reservoir += target_per_frame as i64 - bits_used as i64;
427        self.bit_reservoir = self
428            .bit_reservoir
429            .clamp(-self.max_reservoir, self.max_reservoir);
430
431        // Update GOP tracking
432        self.frames_in_gop += 1;
433        self.bits_used_in_gop += bits_used;
434    }
435
436    /// Start a new GOP.
437    pub fn start_new_gop(&mut self) {
438        self.current_gop += 1;
439        self.frames_in_gop = 0;
440        self.bits_used_in_gop = 0;
441        self.gop_target_bits = self.calculate_base_allocation() * self.gop_length as u64;
442    }
443
444    /// Get current GOP allocation status.
445    #[must_use]
446    pub fn gop_status(&self) -> GopAllocationStatus {
447        let remaining_frames = self.gop_length.saturating_sub(self.frames_in_gop);
448        let remaining_bits = self.gop_target_bits.saturating_sub(self.bits_used_in_gop);
449
450        GopAllocationStatus {
451            gop_index: self.current_gop,
452            frames_encoded: self.frames_in_gop,
453            frames_remaining: remaining_frames,
454            bits_used: self.bits_used_in_gop,
455            bits_remaining: remaining_bits,
456            target_bits: self.gop_target_bits,
457            on_target: self.is_gop_on_target(),
458        }
459    }
460
461    /// Check if GOP allocation is on target.
462    fn is_gop_on_target(&self) -> bool {
463        if self.frames_in_gop == 0 {
464            return true;
465        }
466
467        let expected_bits = (self.gop_target_bits as f32
468            * (self.frames_in_gop as f32 / self.gop_length as f32))
469            as u64;
470
471        let accuracy = self.bits_used_in_gop as f32 / expected_bits as f32;
472        accuracy > 0.8 && accuracy < 1.2
473    }
474
475    /// Reset the allocator state.
476    pub fn reset(&mut self) {
477        self.complexity_history.clear();
478        self.bit_usage_history.clear();
479        self.bit_reservoir = 0;
480        self.current_gop = 0;
481        self.frames_in_gop = 0;
482        self.bits_used_in_gop = 0;
483        self.gop_target_bits = 0;
484    }
485}
486
487/// Result of frame allocation.
488#[derive(Clone, Debug)]
489pub struct AllocationResult {
490    /// Target bits for the frame.
491    pub target_bits: u64,
492    /// Minimum bits (underflow prevention).
493    pub min_bits: u64,
494    /// Maximum bits (overflow prevention).
495    pub max_bits: u64,
496    /// Frame type.
497    pub frame_type: FrameType,
498    /// Complexity factor relative to average.
499    pub complexity_factor: f32,
500    /// Reservoir adjustment applied.
501    pub reservoir_adjustment: i64,
502}
503
504impl AllocationResult {
505    /// Check if actual bits are within acceptable range.
506    #[must_use]
507    pub fn is_within_range(&self, actual_bits: u64) -> bool {
508        actual_bits >= self.min_bits && actual_bits <= self.max_bits
509    }
510
511    /// Calculate allocation accuracy.
512    #[must_use]
513    pub fn accuracy(&self, actual_bits: u64) -> f32 {
514        if self.target_bits == 0 {
515            return 1.0;
516        }
517        actual_bits as f32 / self.target_bits as f32
518    }
519}
520
521/// GOP allocation status.
522#[derive(Clone, Debug)]
523pub struct GopAllocationStatus {
524    /// GOP index.
525    pub gop_index: u32,
526    /// Frames encoded in this GOP.
527    pub frames_encoded: u32,
528    /// Frames remaining in this GOP.
529    pub frames_remaining: u32,
530    /// Bits used so far.
531    pub bits_used: u64,
532    /// Bits remaining in budget.
533    pub bits_remaining: u64,
534    /// Target bits for this GOP.
535    pub target_bits: u64,
536    /// Whether allocation is on target.
537    pub on_target: bool,
538}
539
540impl GopAllocationStatus {
541    /// Get the average bits per frame so far.
542    #[must_use]
543    pub fn average_bits_per_frame(&self) -> u64 {
544        if self.frames_encoded == 0 {
545            return 0;
546        }
547        self.bits_used / self.frames_encoded as u64
548    }
549
550    /// Get recommended bits per remaining frame.
551    #[must_use]
552    pub fn recommended_bits_per_frame(&self) -> u64 {
553        if self.frames_remaining == 0 {
554            return 0;
555        }
556        self.bits_remaining / self.frames_remaining as u64
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    #[test]
565    fn test_allocator_creation() {
566        let allocator = BitrateAllocator::new(5_000_000, 30.0, 250);
567        assert_eq!(allocator.target_bitrate, 5_000_000);
568        assert_eq!(allocator.gop_length, 250);
569    }
570
571    #[test]
572    fn test_base_allocation() {
573        let allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
574        let base = allocator.calculate_base_allocation();
575        assert_eq!(base, 100_000); // 3M / 30 fps
576    }
577
578    #[test]
579    fn test_uniform_allocation() {
580        let allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
581        let base = 100_000;
582
583        let i_bits = allocator.allocate_uniform(base, FrameType::Key);
584        let p_bits = allocator.allocate_uniform(base, FrameType::Inter);
585        let b_bits = allocator.allocate_uniform(base, FrameType::BiDir);
586
587        assert!(i_bits > p_bits); // I-frames get more bits
588        assert!(p_bits > b_bits); // P-frames get more than B-frames
589    }
590
591    #[test]
592    fn test_complexity_based_allocation() {
593        let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
594
595        // Add some complexity history
596        allocator.update(1.0, 100_000);
597        allocator.update(2.0, 150_000);
598        allocator.update(1.5, 125_000);
599
600        let result = allocator.allocate_frame_bits(FrameType::Inter, 2.0, 0);
601        let low_complexity_result = allocator.allocate_frame_bits(FrameType::Inter, 0.5, 0);
602
603        assert!(result.target_bits > low_complexity_result.target_bits);
604    }
605
606    #[test]
607    fn test_hierarchical_allocation() {
608        let allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
609        let base = 100_000;
610
611        // B-frames at different pyramid levels
612        let level0 = allocator.allocate_hierarchical(base, FrameType::BiDir, 2);
613        let level1 = allocator.allocate_hierarchical(base, FrameType::BiDir, 4);
614
615        // Lower pyramid levels should get more bits
616        assert!(level1 >= level0);
617    }
618
619    #[test]
620    fn test_reservoir_management() {
621        let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
622
623        // Undershoot target - reservoir should grow
624        allocator.update(1.0, 80_000); // Target is 100_000
625        assert!(allocator.bit_reservoir > 0);
626
627        // Overshoot target - reservoir should shrink
628        allocator.update(1.0, 120_000);
629        assert!(allocator.bit_reservoir < 20_000);
630    }
631
632    #[test]
633    fn test_vbv_constraint() {
634        let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
635        allocator.set_vbv_buffer_size(1_000_000);
636        allocator.set_strategy(AllocationStrategy::Constrained);
637
638        let result = allocator.allocate_frame_bits(FrameType::Key, 5.0, 0);
639
640        // Should not exceed VBV buffer size
641        assert!(result.max_bits <= 1_000_000);
642    }
643
644    #[test]
645    fn test_adaptive_allocation() {
646        let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
647        allocator.set_strategy(AllocationStrategy::Adaptive);
648
649        // Consistently overshoot
650        for _ in 0..10 {
651            allocator.update(1.0, 130_000); // Target is ~100_000
652        }
653
654        let result = allocator.allocate_frame_bits(FrameType::Inter, 1.0, 0);
655
656        // Should adapt by reducing allocation
657        assert!(result.target_bits < 100_000);
658    }
659
660    #[test]
661    fn test_gop_tracking() {
662        let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 10);
663        allocator.start_new_gop();
664
665        for i in 0..5 {
666            allocator.update(1.0, 100_000);
667            let status = allocator.gop_status();
668            assert_eq!(status.frames_encoded, i + 1);
669            assert_eq!(status.frames_remaining, 10 - (i + 1));
670        }
671
672        let status = allocator.gop_status();
673        assert_eq!(status.frames_encoded, 5);
674        assert_eq!(status.bits_used, 500_000);
675    }
676
677    #[test]
678    fn test_allocation_result() {
679        let result = AllocationResult {
680            target_bits: 100_000,
681            min_bits: 25_000,
682            max_bits: 400_000,
683            frame_type: FrameType::Inter,
684            complexity_factor: 1.5,
685            reservoir_adjustment: 0,
686        };
687
688        assert!(result.is_within_range(100_000));
689        assert!(result.is_within_range(50_000));
690        assert!(!result.is_within_range(500_000));
691        assert!(!result.is_within_range(10_000));
692
693        let accuracy = result.accuracy(110_000);
694        assert!((accuracy - 1.1).abs() < 0.01);
695    }
696
697    #[test]
698    fn test_gop_status() {
699        let status = GopAllocationStatus {
700            gop_index: 1,
701            frames_encoded: 5,
702            frames_remaining: 5,
703            bits_used: 500_000,
704            bits_remaining: 500_000,
705            target_bits: 1_000_000,
706            on_target: true,
707        };
708
709        assert_eq!(status.average_bits_per_frame(), 100_000);
710        assert_eq!(status.recommended_bits_per_frame(), 100_000);
711    }
712
713    #[test]
714    fn test_strategy_switching() {
715        let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
716
717        allocator.set_strategy(AllocationStrategy::Uniform);
718        let uniform_result = allocator.allocate_frame_bits(FrameType::Inter, 2.0, 0);
719
720        allocator.set_strategy(AllocationStrategy::ComplexityBased);
721        allocator.update(1.0, 100_000);
722        let complexity_result = allocator.allocate_frame_bits(FrameType::Inter, 2.0, 0);
723
724        // Complexity-based should differ from uniform for high complexity
725        assert_ne!(uniform_result.target_bits, complexity_result.target_bits);
726    }
727
728    #[test]
729    fn test_reset() {
730        let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
731
732        allocator.update(1.0, 100_000);
733        allocator.update(2.0, 150_000);
734        assert!(!allocator.complexity_history.is_empty());
735
736        allocator.reset();
737
738        assert!(allocator.complexity_history.is_empty());
739        assert!(allocator.bit_usage_history.is_empty());
740        assert_eq!(allocator.bit_reservoir, 0);
741    }
742}