Skip to main content

oximedia_codec/multipass/
allocation.rs

1//! Bitrate allocation algorithms for multi-pass encoding.
2//!
3//! This module implements sophisticated bitrate allocation strategies that
4//! distribute bits optimally across frames based on their complexity and
5//! importance.
6
7#![forbid(unsafe_code)]
8#![allow(clippy::cast_precision_loss)]
9#![allow(clippy::cast_possible_truncation)]
10#![allow(clippy::cast_sign_loss)]
11#![allow(clippy::cast_lossless)]
12#![allow(clippy::too_many_arguments)]
13
14use crate::frame::FrameType;
15use crate::multipass::stats::{FrameStatistics, PassStatistics};
16use crate::multipass::vbv::VbvBuffer;
17
18/// Bitrate allocation strategy.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum AllocationStrategy {
21    /// Uniform allocation (equal bits per frame).
22    Uniform,
23    /// Complexity-based allocation.
24    Complexity,
25    /// Perceptual optimization (allocate more bits to important frames).
26    Perceptual,
27    /// Two-pass optimal allocation.
28    TwoPass,
29}
30
31/// Configuration for bitrate allocation.
32#[derive(Clone, Debug)]
33pub struct AllocationConfig {
34    /// Allocation strategy.
35    pub strategy: AllocationStrategy,
36    /// Target bitrate in bits per second.
37    pub target_bitrate: u64,
38    /// Frame rate numerator.
39    pub framerate_num: u32,
40    /// Frame rate denominator.
41    pub framerate_den: u32,
42    /// I-frame bit boost factor (1.0-10.0).
43    pub i_frame_boost: f64,
44    /// P-frame bit factor (relative to average).
45    pub p_frame_factor: f64,
46    /// B-frame bit factor (relative to average).
47    pub b_frame_factor: f64,
48    /// Complexity weight (0.0-1.0, how much to favor complex frames).
49    pub complexity_weight: f64,
50    /// Temporal weight (0.0-1.0, how much to favor temporally important frames).
51    pub temporal_weight: f64,
52}
53
54impl Default for AllocationConfig {
55    fn default() -> Self {
56        Self {
57            strategy: AllocationStrategy::Complexity,
58            target_bitrate: 5_000_000,
59            framerate_num: 30,
60            framerate_den: 1,
61            i_frame_boost: 3.0,
62            p_frame_factor: 1.0,
63            b_frame_factor: 0.5,
64            complexity_weight: 0.7,
65            temporal_weight: 0.3,
66        }
67    }
68}
69
70impl AllocationConfig {
71    /// Create a new allocation configuration.
72    #[must_use]
73    pub fn new(strategy: AllocationStrategy, target_bitrate: u64) -> Self {
74        Self {
75            strategy,
76            target_bitrate,
77            ..Default::default()
78        }
79    }
80
81    /// Set frame rate.
82    #[must_use]
83    pub fn with_framerate(mut self, num: u32, den: u32) -> Self {
84        self.framerate_num = num;
85        self.framerate_den = den;
86        self
87    }
88
89    /// Calculate average bits per frame.
90    #[must_use]
91    pub fn bits_per_frame(&self) -> f64 {
92        let fps = self.framerate_num as f64 / self.framerate_den as f64;
93        self.target_bitrate as f64 / fps
94    }
95}
96
97/// Bitrate allocator for video encoding.
98pub struct BitrateAllocator {
99    config: AllocationConfig,
100    first_pass_stats: Option<PassStatistics>,
101    total_allocated: u64,
102    frames_allocated: u64,
103}
104
105impl BitrateAllocator {
106    /// Create a new bitrate allocator.
107    #[must_use]
108    pub fn new(config: AllocationConfig) -> Self {
109        Self {
110            config,
111            first_pass_stats: None,
112            total_allocated: 0,
113            frames_allocated: 0,
114        }
115    }
116
117    /// Set first-pass statistics for two-pass encoding.
118    pub fn set_first_pass_stats(&mut self, stats: PassStatistics) {
119        self.first_pass_stats = Some(stats);
120    }
121
122    /// Allocate bits for a frame.
123    #[must_use]
124    pub fn allocate(
125        &mut self,
126        frame_index: u64,
127        frame_type: FrameType,
128        complexity: f64,
129    ) -> FrameAllocation {
130        let allocation = match self.config.strategy {
131            AllocationStrategy::Uniform => self.allocate_uniform(frame_type),
132            AllocationStrategy::Complexity => self.allocate_complexity(frame_type, complexity),
133            AllocationStrategy::Perceptual => self.allocate_perceptual(frame_type, complexity),
134            AllocationStrategy::TwoPass => {
135                if let Some(ref stats) = self.first_pass_stats {
136                    self.allocate_two_pass(frame_index, frame_type, stats)
137                } else {
138                    self.allocate_complexity(frame_type, complexity)
139                }
140            }
141        };
142
143        self.total_allocated += allocation.target_bits;
144        self.frames_allocated += 1;
145
146        allocation
147    }
148
149    /// Uniform allocation (equal bits per frame, adjusted for frame type).
150    fn allocate_uniform(&self, frame_type: FrameType) -> FrameAllocation {
151        let base_bits = self.config.bits_per_frame();
152        let type_factor = self.get_frame_type_factor(frame_type);
153        let target_bits = (base_bits * type_factor) as u64;
154
155        FrameAllocation {
156            target_bits,
157            min_bits: (target_bits as f64 * 0.5) as u64,
158            max_bits: (target_bits as f64 * 2.0) as u64,
159            qp_adjustment: 0.0,
160        }
161    }
162
163    /// Complexity-based allocation.
164    fn allocate_complexity(&self, frame_type: FrameType, complexity: f64) -> FrameAllocation {
165        let base_bits = self.config.bits_per_frame();
166        let type_factor = self.get_frame_type_factor(frame_type);
167
168        // Adjust based on complexity (higher complexity = more bits)
169        let complexity_factor = 0.5 + complexity;
170        let target_bits = (base_bits * type_factor * complexity_factor) as u64;
171
172        FrameAllocation {
173            target_bits,
174            min_bits: (target_bits as f64 * 0.5) as u64,
175            max_bits: (target_bits as f64 * 2.5) as u64,
176            qp_adjustment: -((complexity - 0.5) * 10.0), // More complex = lower QP
177        }
178    }
179
180    /// Perceptual allocation (favor important frames).
181    fn allocate_perceptual(&self, frame_type: FrameType, complexity: f64) -> FrameAllocation {
182        let base_bits = self.config.bits_per_frame();
183        let type_factor = self.get_frame_type_factor(frame_type);
184
185        // Perceptual importance (higher for keyframes and complex frames)
186        let perceptual_importance = match frame_type {
187            FrameType::Key => 2.0,
188            FrameType::Inter => 1.0 + complexity * 0.5,
189            FrameType::BiDir => 0.7,
190            FrameType::Switch => 1.5,
191        };
192
193        let target_bits = (base_bits * type_factor * perceptual_importance) as u64;
194
195        FrameAllocation {
196            target_bits,
197            min_bits: (target_bits as f64 * 0.6) as u64,
198            max_bits: (target_bits as f64 * 2.0) as u64,
199            qp_adjustment: -(perceptual_importance - 1.0) * 5.0,
200        }
201    }
202
203    /// Two-pass optimal allocation using first-pass statistics.
204    fn allocate_two_pass(
205        &self,
206        frame_index: u64,
207        frame_type: FrameType,
208        stats: &PassStatistics,
209    ) -> FrameAllocation {
210        // Get first-pass statistics for this frame
211        let frame_stats = stats.get_frame(frame_index);
212
213        if let Some(frame_stats) = frame_stats {
214            // Calculate target bits based on first-pass results
215            let total_bits_available = self.calculate_remaining_budget(stats);
216            let frames_remaining = (stats.total_frames - frame_index) as f64;
217
218            // Weight based on first-pass complexity and bits used
219            let weight = self.calculate_frame_weight(frame_stats, stats);
220            let total_weight = self.calculate_total_remaining_weight(frame_index, stats);
221
222            let target_bits = if total_weight > 0.0 {
223                ((total_bits_available as f64) * weight / total_weight) as u64
224            } else {
225                (total_bits_available as f64 / frames_remaining) as u64
226            };
227
228            // QP adjustment based on first-pass QP
229            let qp_adjustment = self.calculate_qp_adjustment(frame_stats, stats);
230
231            FrameAllocation {
232                target_bits,
233                min_bits: (target_bits as f64 * 0.5) as u64,
234                max_bits: (target_bits as f64 * 3.0) as u64,
235                qp_adjustment,
236            }
237        } else {
238            // Fallback if frame stats not found
239            self.allocate_complexity(frame_type, 0.5)
240        }
241    }
242
243    /// Calculate remaining bitrate budget.
244    fn calculate_remaining_budget(&self, stats: &PassStatistics) -> u64 {
245        let total_duration = stats.total_frames as f64
246            * (self.config.framerate_den as f64 / self.config.framerate_num as f64);
247        let total_bits = (self.config.target_bitrate as f64 * total_duration) as u64;
248
249        total_bits.saturating_sub(self.total_allocated)
250    }
251
252    /// Calculate weight for a frame based on first-pass data.
253    fn calculate_frame_weight(&self, frame_stats: &FrameStatistics, stats: &PassStatistics) -> f64 {
254        // Normalize complexity
255        let complexity_dist = stats.complexity_distribution();
256        let normalized_complexity = if complexity_dist.mean > 0.0 {
257            frame_stats.complexity.combined_complexity / complexity_dist.mean
258        } else {
259            1.0
260        };
261
262        // Weight by frame type
263        let type_weight = match frame_stats.frame_type {
264            FrameType::Key => self.config.i_frame_boost,
265            FrameType::Inter => self.config.p_frame_factor,
266            FrameType::BiDir => self.config.b_frame_factor,
267            FrameType::Switch => 1.5,
268        };
269
270        // Combine weights
271        self.config.complexity_weight * normalized_complexity
272            + (1.0 - self.config.complexity_weight) * type_weight
273    }
274
275    /// Calculate total weight of remaining frames.
276    fn calculate_total_remaining_weight(&self, current_index: u64, stats: &PassStatistics) -> f64 {
277        stats
278            .frames
279            .iter()
280            .filter(|f| f.frame_index >= current_index)
281            .map(|f| self.calculate_frame_weight(f, stats))
282            .sum()
283    }
284
285    /// Calculate QP adjustment for second pass.
286    fn calculate_qp_adjustment(
287        &self,
288        frame_stats: &FrameStatistics,
289        stats: &PassStatistics,
290    ) -> f64 {
291        let avg_qp = stats.avg_qp;
292        let frame_qp = frame_stats.qp;
293
294        // Adjust QP relative to first-pass average
295        (avg_qp - frame_qp) * 0.5 // Dampen the adjustment
296    }
297
298    /// Get frame type factor for bit allocation.
299    fn get_frame_type_factor(&self, frame_type: FrameType) -> f64 {
300        match frame_type {
301            FrameType::Key => self.config.i_frame_boost,
302            FrameType::Inter => self.config.p_frame_factor,
303            FrameType::BiDir => self.config.b_frame_factor,
304            FrameType::Switch => 1.5,
305        }
306    }
307
308    /// Get total bits allocated.
309    #[must_use]
310    pub fn total_allocated(&self) -> u64 {
311        self.total_allocated
312    }
313
314    /// Get number of frames allocated.
315    #[must_use]
316    pub fn frames_allocated(&self) -> u64 {
317        self.frames_allocated
318    }
319
320    /// Reset allocator state.
321    pub fn reset(&mut self) {
322        self.total_allocated = 0;
323        self.frames_allocated = 0;
324    }
325}
326
327/// Bit allocation result for a single frame.
328#[derive(Clone, Debug)]
329pub struct FrameAllocation {
330    /// Target bits for the frame.
331    pub target_bits: u64,
332    /// Minimum acceptable bits.
333    pub min_bits: u64,
334    /// Maximum allowed bits.
335    pub max_bits: u64,
336    /// Suggested QP adjustment from base.
337    pub qp_adjustment: f64,
338}
339
340impl FrameAllocation {
341    /// Clamp actual bits to acceptable range.
342    #[must_use]
343    pub fn clamp_bits(&self, actual_bits: u64) -> u64 {
344        actual_bits.clamp(self.min_bits, self.max_bits)
345    }
346
347    /// Check if actual bits are within acceptable range.
348    #[must_use]
349    pub fn is_within_range(&self, actual_bits: u64) -> bool {
350        actual_bits >= self.min_bits && actual_bits <= self.max_bits
351    }
352
353    /// Calculate error from target (positive = over, negative = under).
354    #[must_use]
355    pub fn error(&self, actual_bits: u64) -> i64 {
356        actual_bits as i64 - self.target_bits as i64
357    }
358}
359
360/// VBV-aware bitrate allocator.
361pub struct VbvAwareAllocator {
362    allocator: BitrateAllocator,
363    vbv_buffer: Option<VbvBuffer>,
364}
365
366impl VbvAwareAllocator {
367    /// Create a new VBV-aware allocator.
368    #[must_use]
369    pub fn new(config: AllocationConfig) -> Self {
370        Self {
371            allocator: BitrateAllocator::new(config),
372            vbv_buffer: None,
373        }
374    }
375
376    /// Set VBV buffer for allocation constraints.
377    pub fn set_vbv_buffer(&mut self, vbv_buffer: VbvBuffer) {
378        self.vbv_buffer = Some(vbv_buffer);
379    }
380
381    /// Allocate bits with VBV constraints.
382    #[must_use]
383    pub fn allocate(
384        &mut self,
385        frame_index: u64,
386        frame_type: FrameType,
387        complexity: f64,
388    ) -> FrameAllocation {
389        let mut allocation = self.allocator.allocate(frame_index, frame_type, complexity);
390
391        // Apply VBV constraints if buffer is set
392        if let Some(ref vbv) = self.vbv_buffer {
393            let max_allowed = vbv.max_frame_size();
394            allocation.max_bits = allocation.max_bits.min(max_allowed);
395            allocation.target_bits = allocation.target_bits.min(max_allowed);
396            allocation.min_bits = allocation.min_bits.min(allocation.target_bits);
397        }
398
399        allocation
400    }
401
402    /// Update VBV buffer after encoding.
403    pub fn update_vbv(&mut self, frame_bits: u64) {
404        if let Some(ref mut vbv) = self.vbv_buffer {
405            vbv.update(frame_bits);
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn test_allocation_config_new() {
416        let config = AllocationConfig::new(AllocationStrategy::Complexity, 5_000_000);
417        assert_eq!(config.strategy, AllocationStrategy::Complexity);
418        assert_eq!(config.target_bitrate, 5_000_000);
419    }
420
421    #[test]
422    fn test_allocation_config_bits_per_frame() {
423        let config =
424            AllocationConfig::new(AllocationStrategy::Uniform, 3_000_000).with_framerate(30, 1);
425        let bpf = config.bits_per_frame();
426        assert!((bpf - 100_000.0).abs() < 1.0); // 3M / 30 = 100k
427    }
428
429    #[test]
430    fn test_allocator_uniform() {
431        let config =
432            AllocationConfig::new(AllocationStrategy::Uniform, 3_000_000).with_framerate(30, 1);
433        let mut allocator = BitrateAllocator::new(config);
434
435        let alloc = allocator.allocate(0, FrameType::Inter, 0.5);
436        assert!(alloc.target_bits > 0);
437        assert!(alloc.min_bits < alloc.target_bits);
438        assert!(alloc.max_bits > alloc.target_bits);
439    }
440
441    #[test]
442    fn test_allocator_complexity() {
443        let config =
444            AllocationConfig::new(AllocationStrategy::Complexity, 3_000_000).with_framerate(30, 1);
445        let mut allocator = BitrateAllocator::new(config);
446
447        let low_complexity = allocator.allocate(0, FrameType::Inter, 0.2);
448        let high_complexity = allocator.allocate(1, FrameType::Inter, 0.8);
449
450        // High complexity should get more bits
451        assert!(high_complexity.target_bits > low_complexity.target_bits);
452    }
453
454    #[test]
455    fn test_allocator_keyframe_boost() {
456        let config =
457            AllocationConfig::new(AllocationStrategy::Uniform, 3_000_000).with_framerate(30, 1);
458        let mut allocator = BitrateAllocator::new(config);
459
460        let keyframe_alloc = allocator.allocate(0, FrameType::Key, 0.5);
461        let inter_alloc = allocator.allocate(1, FrameType::Inter, 0.5);
462
463        // Keyframes should get more bits
464        assert!(keyframe_alloc.target_bits > inter_alloc.target_bits);
465    }
466
467    #[test]
468    fn test_frame_allocation_clamp() {
469        let alloc = FrameAllocation {
470            target_bits: 100_000,
471            min_bits: 50_000,
472            max_bits: 200_000,
473            qp_adjustment: 0.0,
474        };
475
476        assert_eq!(alloc.clamp_bits(30_000), 50_000); // Below min
477        assert_eq!(alloc.clamp_bits(100_000), 100_000); // Within range
478        assert_eq!(alloc.clamp_bits(250_000), 200_000); // Above max
479    }
480
481    #[test]
482    fn test_frame_allocation_within_range() {
483        let alloc = FrameAllocation {
484            target_bits: 100_000,
485            min_bits: 50_000,
486            max_bits: 200_000,
487            qp_adjustment: 0.0,
488        };
489
490        assert!(!alloc.is_within_range(30_000));
491        assert!(alloc.is_within_range(100_000));
492        assert!(!alloc.is_within_range(250_000));
493    }
494}