Skip to main content

oximedia_codec/
vbr_twopass.rs

1//! VBR two-pass encoding state tracking.
2//!
3//! Two-pass VBR encoding works in two phases:
4//!
5//! 1. **First pass** — the encoder analyses each frame, recording complexity
6//!    metrics (e.g. SAD, variance, intra cost) without actually producing output
7//!    bitstream.  The results are accumulated into a [`TwoPassFirstPassStats`].
8//!
9//! 2. **Second pass** — using the first-pass statistics the encoder computes an
10//!    optimal bitrate allocation per frame, then encodes each frame with that
11//!    target.  [`TwoPassBitrateAllocator`] performs this allocation.
12//!
13//! # Algorithm
14//!
15//! The allocator implements *complexity-proportional* bit allocation:
16//!
17//! ```text
18//! target_bits(i) = total_bits * complexity(i) / sum(complexity)
19//! ```
20//!
21//! with GOP-level equalisation to avoid budget overflow across group boundaries.
22
23#![allow(dead_code)]
24#![allow(clippy::cast_precision_loss)]
25
26// ---------------------------------------------------------------------------
27// Per-frame complexity record (first pass)
28// ---------------------------------------------------------------------------
29
30/// Per-frame statistics recorded during the first encoding pass.
31#[derive(Debug, Clone)]
32pub struct FirstPassFrameStats {
33    /// Frame index (display order).
34    pub frame_index: u64,
35    /// Mean intra cost (bits per pixel at a reference QP).
36    pub intra_cost: f32,
37    /// Mean inter cost (bits per pixel for the best motion prediction).
38    pub inter_cost: f32,
39    /// Intra/inter cost ratio; values > 1.0 indicate scene-change candidates.
40    pub intra_inter_ratio: f32,
41    /// Motion-compensated average block SAD.
42    pub mean_sad: f32,
43    /// Estimated number of bits if encoded as a key frame.
44    pub key_frame_bits: u64,
45    /// Whether this frame was marked as a scene change in the first pass.
46    pub is_scene_change: bool,
47}
48
49impl FirstPassFrameStats {
50    /// Create a minimal stats record for a frame.
51    #[must_use]
52    pub fn new(frame_index: u64, intra_cost: f32, inter_cost: f32) -> Self {
53        let ratio = if inter_cost > 0.0 {
54            intra_cost / inter_cost
55        } else {
56            1.0
57        };
58        Self {
59            frame_index,
60            intra_cost,
61            inter_cost,
62            intra_inter_ratio: ratio,
63            mean_sad: 0.0,
64            key_frame_bits: 0,
65            is_scene_change: ratio > 2.5,
66        }
67    }
68
69    /// Effective complexity weight for bit allocation.
70    ///
71    /// Uses the inter cost as the primary signal; scene-change frames get a
72    /// boost so that the I-frame receives proportionally more bits.
73    #[must_use]
74    pub fn complexity_weight(&self) -> f32 {
75        let base = self.inter_cost.max(0.001);
76        if self.is_scene_change {
77            base * 1.8
78        } else {
79            base
80        }
81    }
82}
83
84// ---------------------------------------------------------------------------
85// First-pass accumulator
86// ---------------------------------------------------------------------------
87
88/// Accumulated first-pass statistics for an entire encode session.
89#[derive(Debug, Default, Clone)]
90pub struct TwoPassFirstPassStats {
91    frames: Vec<FirstPassFrameStats>,
92}
93
94impl TwoPassFirstPassStats {
95    /// Create an empty statistics container.
96    #[must_use]
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Add one frame's statistics.
102    pub fn push(&mut self, stats: FirstPassFrameStats) {
103        self.frames.push(stats);
104    }
105
106    /// Total number of analysed frames.
107    #[must_use]
108    pub fn frame_count(&self) -> usize {
109        self.frames.len()
110    }
111
112    /// Access per-frame statistics by index.
113    #[must_use]
114    pub fn get(&self, index: usize) -> Option<&FirstPassFrameStats> {
115        self.frames.get(index)
116    }
117
118    /// Iterate over all per-frame statistics.
119    pub fn iter(&self) -> impl Iterator<Item = &FirstPassFrameStats> {
120        self.frames.iter()
121    }
122
123    /// Sum of all complexity weights (denominator for proportional allocation).
124    #[must_use]
125    pub fn total_complexity(&self) -> f32 {
126        self.frames.iter().map(|f| f.complexity_weight()).sum()
127    }
128
129    /// Mean intra/inter ratio across all frames.
130    #[must_use]
131    pub fn mean_intra_inter_ratio(&self) -> f32 {
132        if self.frames.is_empty() {
133            return 1.0;
134        }
135        self.frames.iter().map(|f| f.intra_inter_ratio).sum::<f32>() / self.frames.len() as f32
136    }
137
138    /// Number of scene changes detected in the first pass.
139    #[must_use]
140    pub fn scene_change_count(&self) -> usize {
141        self.frames.iter().filter(|f| f.is_scene_change).count()
142    }
143}
144
145// ---------------------------------------------------------------------------
146// Second-pass allocator
147// ---------------------------------------------------------------------------
148
149/// Configuration for the second-pass bitrate allocator.
150#[derive(Debug, Clone)]
151pub struct TwoPassAllocatorConfig {
152    /// Target average bitrate in bits per second.
153    pub target_bitrate_bps: u64,
154    /// Output frame rate (frames per second).
155    pub frame_rate: f64,
156    /// GOP size (number of frames between key frames).
157    pub gop_size: u32,
158    /// Maximum bitrate multiplier relative to average.
159    ///
160    /// A value of 2.0 means the peak bitrate may be up to 2× the average.
161    pub max_bitrate_ratio: f32,
162    /// Minimum QP value.
163    pub min_qp: u32,
164    /// Maximum QP value.
165    pub max_qp: u32,
166}
167
168impl Default for TwoPassAllocatorConfig {
169    fn default() -> Self {
170        Self {
171            target_bitrate_bps: 4_000_000,
172            frame_rate: 30.0,
173            gop_size: 120,
174            max_bitrate_ratio: 2.5,
175            min_qp: 16,
176            max_qp: 51,
177        }
178    }
179}
180
181/// Per-frame allocation result from the second pass.
182#[derive(Debug, Clone)]
183pub struct FrameAllocationResult {
184    /// Frame index (display order).
185    pub frame_index: u64,
186    /// Target number of bits for this frame.
187    pub target_bits: u64,
188    /// Suggested quantiser parameter.
189    pub suggested_qp: u32,
190    /// Whether this frame should be encoded as a key frame.
191    pub force_keyframe: bool,
192}
193
194/// Two-pass VBR bitrate allocator.
195///
196/// Use [`TwoPassBitrateAllocator::allocate`] to produce per-frame allocation
197/// plans from first-pass statistics.
198#[derive(Debug, Clone)]
199pub struct TwoPassBitrateAllocator {
200    cfg: TwoPassAllocatorConfig,
201}
202
203impl TwoPassBitrateAllocator {
204    /// Create a new allocator with the given configuration.
205    #[must_use]
206    pub fn new(cfg: TwoPassAllocatorConfig) -> Self {
207        Self { cfg }
208    }
209
210    /// Create an allocator with default configuration.
211    #[must_use]
212    pub fn default_allocator() -> Self {
213        Self::new(TwoPassAllocatorConfig::default())
214    }
215
216    /// Allocate bits for all frames in a session.
217    ///
218    /// Returns one [`FrameAllocationResult`] per frame in the first-pass stats,
219    /// in the same order.
220    #[must_use]
221    pub fn allocate(&self, stats: &TwoPassFirstPassStats) -> Vec<FrameAllocationResult> {
222        let n = stats.frame_count();
223        if n == 0 {
224            return Vec::new();
225        }
226
227        let total_complexity = stats.total_complexity();
228        let seconds = n as f64 / self.cfg.frame_rate.max(1.0);
229        let total_bits = (self.cfg.target_bitrate_bps as f64 * seconds) as u64;
230
231        let max_frame_bits =
232            ((total_bits as f64 / n as f64) * f64::from(self.cfg.max_bitrate_ratio)) as u64;
233
234        let mut results = Vec::with_capacity(n);
235
236        for frame_stats in stats.iter() {
237            let weight = frame_stats.complexity_weight();
238            let raw_bits = if total_complexity > 0.0 {
239                ((weight as f64 / total_complexity as f64) * total_bits as f64) as u64
240            } else {
241                total_bits / n as u64
242            };
243
244            let target_bits = raw_bits.min(max_frame_bits).max(512);
245
246            // Heuristic QP from bits-per-pixel: higher bits → lower QP
247            let bpp = target_bits as f64 / (frame_stats.inter_cost.max(0.001) as f64 * 100.0);
248            let suggested_qp = self.bits_to_qp(bpp);
249
250            results.push(FrameAllocationResult {
251                frame_index: frame_stats.frame_index,
252                target_bits,
253                suggested_qp,
254                force_keyframe: frame_stats.is_scene_change,
255            });
256        }
257
258        results
259    }
260
261    /// Heuristic mapping from bits-per-pixel to QP.
262    fn bits_to_qp(&self, bpp: f64) -> u32 {
263        // Simple log-linear model: QP ≈ 36 - 12 * log2(bpp / 0.1)
264        let qp_f = 36.0 - 12.0 * (bpp / 0.1 + 1.0).log2();
265        (qp_f.round() as u32).clamp(self.cfg.min_qp, self.cfg.max_qp)
266    }
267}
268
269// ---------------------------------------------------------------------------
270// Two-pass encode session state machine
271// ---------------------------------------------------------------------------
272
273/// State of the two-pass VBR encode session.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum TwoPassState {
276    /// First pass: frame analysis in progress.
277    FirstPass,
278    /// Transition: first pass complete, allocator ready.
279    AllocationReady,
280    /// Second pass: frame encoding in progress.
281    SecondPass,
282    /// Encode complete.
283    Complete,
284}
285
286/// Top-level two-pass VBR encode session.
287///
288/// Manages state transitions and coordinates between first-pass analysis
289/// and second-pass encoding.
290#[derive(Debug)]
291pub struct TwoPassSession {
292    cfg: TwoPassAllocatorConfig,
293    state: TwoPassState,
294    first_pass_stats: TwoPassFirstPassStats,
295    allocations: Vec<FrameAllocationResult>,
296    second_pass_index: usize,
297}
298
299impl TwoPassSession {
300    /// Create a new session with the given allocator configuration.
301    #[must_use]
302    pub fn new(cfg: TwoPassAllocatorConfig) -> Self {
303        Self {
304            cfg,
305            state: TwoPassState::FirstPass,
306            first_pass_stats: TwoPassFirstPassStats::new(),
307            allocations: Vec::new(),
308            second_pass_index: 0,
309        }
310    }
311
312    /// Current state of the session.
313    #[must_use]
314    pub fn state(&self) -> TwoPassState {
315        self.state
316    }
317
318    /// Record one frame's first-pass statistics.
319    ///
320    /// Returns an error string if the session is not in `FirstPass` state.
321    pub fn record_first_pass_frame(
322        &mut self,
323        stats: FirstPassFrameStats,
324    ) -> Result<(), &'static str> {
325        if self.state != TwoPassState::FirstPass {
326            return Err("session is not in first-pass state");
327        }
328        self.first_pass_stats.push(stats);
329        Ok(())
330    }
331
332    /// Finalise the first pass and compute bit allocations.
333    ///
334    /// After calling this method the session moves to `AllocationReady` state.
335    pub fn finish_first_pass(&mut self) {
336        if self.state != TwoPassState::FirstPass {
337            return;
338        }
339        let allocator = TwoPassBitrateAllocator::new(self.cfg.clone());
340        self.allocations = allocator.allocate(&self.first_pass_stats);
341        self.state = TwoPassState::AllocationReady;
342    }
343
344    /// Begin the second pass.
345    ///
346    /// Transitions the session from `AllocationReady` to `SecondPass`.
347    pub fn begin_second_pass(&mut self) -> Result<(), &'static str> {
348        if self.state != TwoPassState::AllocationReady {
349            return Err("first pass not yet finalised");
350        }
351        self.second_pass_index = 0;
352        self.state = TwoPassState::SecondPass;
353        Ok(())
354    }
355
356    /// Get the allocation for the next second-pass frame.
357    ///
358    /// Returns `None` when all frames have been consumed (session complete).
359    pub fn next_frame_allocation(&mut self) -> Option<&FrameAllocationResult> {
360        if self.state != TwoPassState::SecondPass {
361            return None;
362        }
363        if self.second_pass_index >= self.allocations.len() {
364            self.state = TwoPassState::Complete;
365            return None;
366        }
367        let result = &self.allocations[self.second_pass_index];
368        self.second_pass_index += 1;
369        if self.second_pass_index >= self.allocations.len() {
370            self.state = TwoPassState::Complete;
371        }
372        Some(result)
373    }
374
375    /// Access the first-pass statistics.
376    #[must_use]
377    pub fn first_pass_stats(&self) -> &TwoPassFirstPassStats {
378        &self.first_pass_stats
379    }
380
381    /// Access all computed frame allocations.
382    #[must_use]
383    pub fn allocations(&self) -> &[FrameAllocationResult] {
384        &self.allocations
385    }
386}
387
388// ---------------------------------------------------------------------------
389// Simplified TwoPassStateTracker — convenience API for two-pass VBR
390// ---------------------------------------------------------------------------
391
392/// Lightweight two-pass VBR state tracker.
393///
394/// Provides a simplified API for recording per-frame complexity values during
395/// the first pass and computing per-frame bitrate targets during the second
396/// pass.
397///
398/// # Usage
399///
400/// ```rust
401/// use oximedia_codec::vbr_twopass::TwoPassStateTracker;
402///
403/// let mut state = TwoPassStateTracker::new();
404/// state.record_first_pass(1.5); // frame 0
405/// state.record_first_pass(0.8); // frame 1
406/// state.record_first_pass(2.3); // frame 2
407///
408/// let bits_frame0 = state.compute_bitrate(1.5, 4_000_000);
409/// assert!(bits_frame0 > 0);
410/// ```
411#[derive(Debug, Clone, Default)]
412pub struct TwoPassStateTracker {
413    /// Complexity values accumulated during the first pass.
414    first_pass_complexities: Vec<f32>,
415}
416
417impl TwoPassStateTracker {
418    /// Create a new, empty state tracker.
419    #[must_use]
420    pub fn new() -> Self {
421        Self::default()
422    }
423
424    /// Record a complexity value for one frame during the first pass.
425    ///
426    /// `complexity` must be a non-negative floating-point value.  Typical
427    /// values are SAD-normalised intra/inter costs in the range 0.1 – 20.0.
428    pub fn record_first_pass(&mut self, complexity: f32) {
429        self.first_pass_complexities.push(complexity.max(0.0));
430    }
431
432    /// Compute the per-frame bitrate target for a frame with the given complexity.
433    ///
434    /// Uses complexity-proportional allocation:
435    /// ```text
436    /// target_bits = budget * complexity / sum(all_complexities)
437    /// ```
438    ///
439    /// If the accumulated first-pass data is empty the entire `budget` is
440    /// returned (no-op case).
441    ///
442    /// # Parameters
443    /// - `complexity` – complexity value of the current frame (should have been
444    ///   recorded via [`Self::record_first_pass`]).
445    /// - `budget`     – total bit budget for the session (bits).
446    ///
447    /// # Returns
448    /// Target number of bits for this frame, as a `u32`.  Clamped to at least 1
449    /// and at most `budget`.
450    #[must_use]
451    pub fn compute_bitrate(&self, complexity: f32, budget: u32) -> u32 {
452        let total: f32 = self.first_pass_complexities.iter().sum();
453        if total <= 0.0 || budget == 0 {
454            return budget;
455        }
456        let share = complexity.max(0.0) / total;
457        let bits = (budget as f64 * share as f64).round() as u64;
458        bits.clamp(1, u64::from(budget)) as u32
459    }
460
461    /// Returns the number of frames recorded in the first pass.
462    #[must_use]
463    pub fn frame_count(&self) -> usize {
464        self.first_pass_complexities.len()
465    }
466
467    /// Returns the total accumulated complexity.
468    #[must_use]
469    pub fn total_complexity(&self) -> f32 {
470        self.first_pass_complexities.iter().sum()
471    }
472
473    /// Returns the average complexity per frame.
474    #[must_use]
475    pub fn mean_complexity(&self) -> f32 {
476        let n = self.first_pass_complexities.len();
477        if n == 0 {
478            return 0.0;
479        }
480        self.total_complexity() / n as f32
481    }
482}
483
484// ---------------------------------------------------------------------------
485// Tests
486// ---------------------------------------------------------------------------
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    fn make_stats(n: usize) -> TwoPassFirstPassStats {
493        let mut stats = TwoPassFirstPassStats::new();
494        for i in 0..n {
495            let intra = 1.0 + (i as f32) * 0.1;
496            let inter = 0.5 + (i as f32) * 0.05;
497            stats.push(FirstPassFrameStats::new(i as u64, intra, inter));
498        }
499        stats
500    }
501
502    #[test]
503    fn test_first_pass_stats_frame_count() {
504        let stats = make_stats(10);
505        assert_eq!(stats.frame_count(), 10);
506    }
507
508    #[test]
509    fn test_total_complexity_positive() {
510        let stats = make_stats(5);
511        assert!(stats.total_complexity() > 0.0);
512    }
513
514    #[test]
515    fn test_allocator_correct_count() {
516        let stats = make_stats(30);
517        let alloc = TwoPassBitrateAllocator::default_allocator();
518        let results = alloc.allocate(&stats);
519        assert_eq!(results.len(), 30);
520    }
521
522    #[test]
523    fn test_allocator_empty_stats() {
524        let stats = TwoPassFirstPassStats::new();
525        let alloc = TwoPassBitrateAllocator::default_allocator();
526        let results = alloc.allocate(&stats);
527        assert!(results.is_empty());
528    }
529
530    #[test]
531    fn test_allocator_bits_in_range() {
532        let stats = make_stats(100);
533        let alloc = TwoPassBitrateAllocator::default_allocator();
534        let results = alloc.allocate(&stats);
535        for r in &results {
536            assert!(r.target_bits >= 512, "target_bits should be at least 512");
537        }
538    }
539
540    #[test]
541    fn test_allocator_qp_in_range() {
542        let stats = make_stats(20);
543        let alloc = TwoPassBitrateAllocator::default_allocator();
544        let results = alloc.allocate(&stats);
545        for r in &results {
546            assert!(r.suggested_qp >= 16 && r.suggested_qp <= 51);
547        }
548    }
549
550    #[test]
551    fn test_session_state_transitions() {
552        let mut session = TwoPassSession::new(TwoPassAllocatorConfig::default());
553        assert_eq!(session.state(), TwoPassState::FirstPass);
554
555        let frame = FirstPassFrameStats::new(0, 1.5, 0.8);
556        session
557            .record_first_pass_frame(frame)
558            .expect("should succeed");
559        session.finish_first_pass();
560        assert_eq!(session.state(), TwoPassState::AllocationReady);
561
562        session.begin_second_pass().expect("should succeed");
563        assert_eq!(session.state(), TwoPassState::SecondPass);
564    }
565
566    #[test]
567    fn test_session_second_pass_iterates() {
568        let mut session = TwoPassSession::new(TwoPassAllocatorConfig::default());
569        for i in 0..5u64 {
570            let f = FirstPassFrameStats::new(i, 1.0, 0.5);
571            session.record_first_pass_frame(f).expect("ok");
572        }
573        session.finish_first_pass();
574        session.begin_second_pass().expect("ok");
575
576        let mut count = 0;
577        while let Some(_alloc) = session.next_frame_allocation() {
578            count += 1;
579        }
580        assert_eq!(count, 5);
581        assert_eq!(session.state(), TwoPassState::Complete);
582    }
583
584    #[test]
585    fn test_first_pass_record_after_finish_errors() {
586        let mut session = TwoPassSession::new(TwoPassAllocatorConfig::default());
587        session.finish_first_pass();
588        let f = FirstPassFrameStats::new(0, 1.0, 0.5);
589        assert!(session.record_first_pass_frame(f).is_err());
590    }
591
592    #[test]
593    fn test_scene_change_flagged_on_high_ratio() {
594        let stats = FirstPassFrameStats::new(0, 5.0, 0.5);
595        // ratio = 10.0 > 2.5
596        assert!(stats.is_scene_change);
597    }
598
599    #[test]
600    fn test_scene_change_not_flagged_on_low_ratio() {
601        let stats = FirstPassFrameStats::new(0, 1.0, 0.8);
602        // ratio = 1.25 < 2.5
603        assert!(!stats.is_scene_change);
604    }
605
606    #[test]
607    fn test_mean_intra_inter_ratio() {
608        let mut s = TwoPassFirstPassStats::new();
609        s.push(FirstPassFrameStats::new(0, 2.0, 1.0)); // ratio 2.0
610        s.push(FirstPassFrameStats::new(1, 4.0, 1.0)); // ratio 4.0
611        let mean = s.mean_intra_inter_ratio();
612        assert!((mean - 3.0).abs() < 0.01);
613    }
614
615    // TwoPassStateTracker tests
616    #[test]
617    fn two_pass_tracker_new_is_empty() {
618        let t = TwoPassStateTracker::new();
619        assert_eq!(t.frame_count(), 0);
620    }
621
622    #[test]
623    fn two_pass_tracker_record_adds_frames() {
624        let mut t = TwoPassStateTracker::new();
625        t.record_first_pass(1.0);
626        t.record_first_pass(2.0);
627        assert_eq!(t.frame_count(), 2);
628    }
629
630    #[test]
631    fn two_pass_tracker_compute_bitrate_proportional() {
632        let mut t = TwoPassStateTracker::new();
633        t.record_first_pass(1.0);
634        t.record_first_pass(1.0);
635        // Each frame has equal complexity so each gets half the budget
636        let bits = t.compute_bitrate(1.0, 1000);
637        assert_eq!(bits, 500);
638    }
639
640    #[test]
641    fn two_pass_tracker_compute_bitrate_empty_returns_budget() {
642        let t = TwoPassStateTracker::new();
643        let bits = t.compute_bitrate(1.0, 999);
644        assert_eq!(bits, 999);
645    }
646
647    #[test]
648    fn two_pass_tracker_compute_bitrate_at_least_one() {
649        let mut t = TwoPassStateTracker::new();
650        t.record_first_pass(1000.0);
651        // Tiny complexity vs huge total → at least 1
652        let bits = t.compute_bitrate(0.001, 1000);
653        assert!(bits >= 1);
654    }
655
656    #[test]
657    fn two_pass_tracker_total_complexity() {
658        let mut t = TwoPassStateTracker::new();
659        t.record_first_pass(2.0);
660        t.record_first_pass(3.0);
661        assert!((t.total_complexity() - 5.0).abs() < 1e-5);
662    }
663
664    #[test]
665    fn two_pass_tracker_mean_complexity() {
666        let mut t = TwoPassStateTracker::new();
667        t.record_first_pass(4.0);
668        t.record_first_pass(6.0);
669        assert!((t.mean_complexity() - 5.0).abs() < 1e-5);
670    }
671}