Skip to main content

oximedia_transcode/
two_pass.rs

1//! Two-pass encoding management.
2//!
3//! Two-pass encoding runs the encoder twice: the first pass analyzes the
4//! complexity of each frame, and the second pass uses that information to
5//! optimally distribute bits across the timeline for a given target bitrate.
6
7#![allow(dead_code)]
8
9/// Configuration for a two-pass encode.
10#[derive(Debug, Clone)]
11pub struct TwoPassConfig {
12    /// Target average bitrate in kilobits per second.
13    pub target_bitrate_kbps: u32,
14    /// Total input duration in milliseconds (used for bit-budget calculations).
15    pub input_duration_ms: u64,
16    /// Whether to perform a detailed per-frame complexity analysis in pass one.
17    pub complexity_analysis: bool,
18}
19
20impl TwoPassConfig {
21    /// Creates a new two-pass config with the given target bitrate and duration.
22    #[must_use]
23    pub fn new(target_bitrate_kbps: u32, input_duration_ms: u64) -> Self {
24        Self {
25            target_bitrate_kbps,
26            input_duration_ms,
27            complexity_analysis: true,
28        }
29    }
30
31    /// Calculates the total bit budget for the encode.
32    #[must_use]
33    pub fn total_bits(&self) -> u64 {
34        // bits = kbps * 1000 * seconds
35        let seconds = self.input_duration_ms as f64 / 1000.0;
36        (f64::from(self.target_bitrate_kbps) * 1000.0 * seconds) as u64
37    }
38}
39
40/// Results from the first pass of a two-pass encode.
41#[derive(Debug, Clone)]
42pub struct PassOneResult {
43    /// Per-frame complexity scores (0.0 = very simple, 1.0 = very complex).
44    pub complexity_map: Vec<f64>,
45    /// Mean complexity across all analyzed frames.
46    pub avg_complexity: f64,
47    /// Peak (maximum) complexity observed.
48    pub peak_complexity: f64,
49    /// How many milliseconds of content were analyzed.
50    pub duration_analyzed_ms: u64,
51}
52
53impl PassOneResult {
54    /// Creates a new pass-one result from a complexity map.
55    fn from_complexities(complexities: Vec<f64>, duration_analyzed_ms: u64) -> Self {
56        let n = complexities.len();
57        let (avg, peak) = if n == 0 {
58            (0.0, 0.0)
59        } else {
60            let sum: f64 = complexities.iter().sum();
61            let peak = complexities
62                .iter()
63                .copied()
64                .fold(f64::NEG_INFINITY, f64::max);
65            (sum / n as f64, peak)
66        };
67        Self {
68            complexity_map: complexities,
69            avg_complexity: avg,
70            peak_complexity: peak,
71            duration_analyzed_ms,
72        }
73    }
74
75    /// Allocates a bit count for the frame at `frame_idx` from the total bit budget.
76    ///
77    /// Frames with higher complexity receive proportionally more bits.
78    /// Falls back to an equal allocation if the sum of complexities is zero.
79    #[must_use]
80    pub fn allocate_bits(&self, frame_idx: usize, total_bits: u64) -> u64 {
81        let n = self.complexity_map.len();
82        if n == 0 || frame_idx >= n {
83            return 0;
84        }
85
86        let sum: f64 = self.complexity_map.iter().sum();
87        if sum <= 0.0 {
88            // Uniform allocation
89            return total_bits / n as u64;
90        }
91
92        let weight = self.complexity_map[frame_idx] / sum;
93        (total_bits as f64 * weight) as u64
94    }
95
96    /// Returns `true` if the frame at `frame_idx` is in a complex region.
97    ///
98    /// A region is considered complex if its complexity score is above the
99    /// average by more than one standard deviation.
100    #[must_use]
101    pub fn is_complex_region(&self, idx: usize) -> bool {
102        let n = self.complexity_map.len();
103        if n == 0 || idx >= n {
104            return false;
105        }
106
107        if n == 1 {
108            return self.complexity_map[0] > 0.5;
109        }
110
111        let mean = self.avg_complexity;
112        let variance: f64 = self
113            .complexity_map
114            .iter()
115            .map(|&c| (c - mean).powi(2))
116            .sum::<f64>()
117            / n as f64;
118        let std_dev = variance.sqrt();
119        self.complexity_map[idx] > mean + std_dev
120    }
121
122    /// Returns the fraction of frames classified as complex regions.
123    #[must_use]
124    pub fn complex_region_fraction(&self) -> f64 {
125        let n = self.complexity_map.len();
126        if n == 0 {
127            return 0.0;
128        }
129        let count = (0..n).filter(|&i| self.is_complex_region(i)).count();
130        count as f64 / n as f64
131    }
132}
133
134/// A two-pass encoder that manages both encoding passes.
135pub struct TwoPassEncoder {
136    /// Configuration for this encoder.
137    pub config: TwoPassConfig,
138    /// Results from the first pass, once completed.
139    pub pass_one_result: Option<PassOneResult>,
140}
141
142impl TwoPassEncoder {
143    /// Creates a new two-pass encoder from the given config.
144    #[must_use]
145    pub fn new(config: TwoPassConfig) -> Self {
146        Self {
147            config,
148            pass_one_result: None,
149        }
150    }
151
152    /// Ingests the per-frame complexity data from pass one and stores the result.
153    ///
154    /// Returns a reference to the stored `PassOneResult`.
155    pub fn analyze_pass_one(&mut self, complexities: Vec<f64>) -> &PassOneResult {
156        let duration = self.config.input_duration_ms;
157        self.pass_one_result = Some(PassOneResult::from_complexities(complexities, duration));
158        self.pass_one_result
159            .as_ref()
160            .expect("invariant: pass_one_result set just above")
161    }
162
163    /// Returns the recommended bitrate in kbps for the frame at `frame_idx` in pass two.
164    ///
165    /// If pass one has not been run, falls back to the configured target bitrate.
166    #[must_use]
167    pub fn encode_bitrate_for_frame(&self, frame_idx: usize) -> u32 {
168        let Some(pass_one) = &self.pass_one_result else {
169            return self.config.target_bitrate_kbps;
170        };
171
172        let total_bits = self.config.total_bits();
173        let n = pass_one.complexity_map.len();
174        if n == 0 {
175            return self.config.target_bitrate_kbps;
176        }
177
178        let frame_bits = pass_one.allocate_bits(frame_idx, total_bits);
179
180        // Convert from total frame bits to kbps at a nominal 1-second window
181        // (approximate: we treat the frame budget as a per-frame kbps target)
182        let duration_s = self.config.input_duration_ms as f64 / 1000.0;
183        if duration_s <= 0.0 {
184            return self.config.target_bitrate_kbps;
185        }
186        let avg_bits_per_frame = total_bits as f64 / n as f64;
187        let scale = if avg_bits_per_frame > 0.0 {
188            frame_bits as f64 / avg_bits_per_frame
189        } else {
190            1.0
191        };
192
193        // Clamp to [10%, 500%] of target to avoid extreme values
194        let scaled = (f64::from(self.config.target_bitrate_kbps) * scale).clamp(
195            f64::from(self.config.target_bitrate_kbps) * 0.1,
196            f64::from(self.config.target_bitrate_kbps) * 5.0,
197        );
198        scaled as u32
199    }
200
201    /// Returns whether pass one has been completed.
202    #[must_use]
203    pub fn pass_one_complete(&self) -> bool {
204        self.pass_one_result.is_some()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_two_pass_config_total_bits() {
214        let cfg = TwoPassConfig::new(5000, 10_000); // 5 Mbps for 10 seconds
215        assert_eq!(cfg.total_bits(), 50_000_000);
216    }
217
218    #[test]
219    fn test_two_pass_config_zero_duration() {
220        let cfg = TwoPassConfig::new(5000, 0);
221        assert_eq!(cfg.total_bits(), 0);
222    }
223
224    #[test]
225    fn test_pass_one_result_avg_complexity() {
226        let result = PassOneResult::from_complexities(vec![0.2, 0.4, 0.6, 0.8], 4000);
227        assert!((result.avg_complexity - 0.5).abs() < 1e-9);
228    }
229
230    #[test]
231    fn test_pass_one_result_peak_complexity() {
232        let result = PassOneResult::from_complexities(vec![0.1, 0.9, 0.5], 3000);
233        assert!((result.peak_complexity - 0.9).abs() < 1e-9);
234    }
235
236    #[test]
237    fn test_pass_one_result_empty() {
238        let result = PassOneResult::from_complexities(vec![], 0);
239        assert_eq!(result.avg_complexity, 0.0);
240        assert_eq!(result.peak_complexity, 0.0);
241    }
242
243    #[test]
244    fn test_allocate_bits_proportional() {
245        // Frame 1 has 3x the complexity of frame 0
246        let result = PassOneResult::from_complexities(vec![0.25, 0.75], 2000);
247        let total_bits = 10_000_000u64;
248        let bits_0 = result.allocate_bits(0, total_bits);
249        let bits_1 = result.allocate_bits(1, total_bits);
250        assert_eq!(bits_0 + bits_1, total_bits);
251        assert!(bits_1 > bits_0);
252    }
253
254    #[test]
255    fn test_allocate_bits_out_of_range() {
256        let result = PassOneResult::from_complexities(vec![0.5, 0.5], 2000);
257        assert_eq!(result.allocate_bits(99, 1_000_000), 0);
258    }
259
260    #[test]
261    fn test_is_complex_region_simple() {
262        // All frames have the same complexity → nothing is complex
263        let result = PassOneResult::from_complexities(vec![0.5, 0.5, 0.5, 0.5], 4000);
264        assert!(!result.is_complex_region(0));
265    }
266
267    #[test]
268    fn test_is_complex_region_clear_outlier() {
269        // Frame 3 is a clear outlier
270        let result = PassOneResult::from_complexities(vec![0.1, 0.1, 0.1, 0.9], 4000);
271        assert!(result.is_complex_region(3));
272        assert!(!result.is_complex_region(0));
273    }
274
275    #[test]
276    fn test_two_pass_encoder_fallback_before_pass_one() {
277        let cfg = TwoPassConfig::new(4000, 5000);
278        let encoder = TwoPassEncoder::new(cfg);
279        assert!(!encoder.pass_one_complete());
280        // Should return target bitrate before pass one
281        assert_eq!(encoder.encode_bitrate_for_frame(0), 4000);
282    }
283
284    #[test]
285    fn test_two_pass_encoder_analyze_and_encode() {
286        let cfg = TwoPassConfig::new(4000, 10_000);
287        let mut encoder = TwoPassEncoder::new(cfg);
288        encoder.analyze_pass_one(vec![0.2, 0.2, 0.9, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2]);
289        assert!(encoder.pass_one_complete());
290        // The complex frame should receive more bits → higher bitrate
291        let complex_bitrate = encoder.encode_bitrate_for_frame(2);
292        let simple_bitrate = encoder.encode_bitrate_for_frame(0);
293        assert!(complex_bitrate > simple_bitrate);
294    }
295
296    #[test]
297    fn test_complex_region_fraction() {
298        let result = PassOneResult::from_complexities(vec![0.1, 0.1, 0.1, 0.9], 4000);
299        let fraction = result.complex_region_fraction();
300        assert!(fraction > 0.0);
301        assert!(fraction <= 1.0);
302    }
303
304    #[test]
305    fn test_is_complex_region_single_frame() {
306        let result = PassOneResult::from_complexities(vec![0.8], 1000);
307        assert!(result.is_complex_region(0));
308        let result_low = PassOneResult::from_complexities(vec![0.2], 1000);
309        assert!(!result_low.is_complex_region(0));
310    }
311}