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    /// Returns the collected statistics from pass one as a serializable report.
208    #[must_use]
209    pub fn statistics(&self) -> Option<TwoPassStatistics> {
210        let pass_one = self.pass_one_result.as_ref()?;
211        let total_bits = self.config.total_bits();
212
213        Some(TwoPassStatistics::from_pass_one(
214            pass_one,
215            &self.config,
216            total_bits,
217        ))
218    }
219}
220
221/// Comprehensive statistics collected from the two-pass encoding process.
222///
223/// This provides detailed information about the content complexity, bit
224/// allocation, and quality metrics gathered during the first pass that
225/// guides the second pass encoding.
226#[derive(Debug, Clone)]
227pub struct TwoPassStatistics {
228    /// Total number of frames analyzed.
229    pub frame_count: usize,
230    /// Mean complexity score across all frames (0.0-1.0).
231    pub mean_complexity: f64,
232    /// Peak (maximum) complexity observed.
233    pub peak_complexity: f64,
234    /// Minimum complexity observed.
235    pub min_complexity: f64,
236    /// Standard deviation of complexity scores.
237    pub complexity_std_dev: f64,
238    /// Fraction of frames classified as complex regions (0.0-1.0).
239    pub complex_region_fraction: f64,
240    /// Total bit budget in bits.
241    pub total_bit_budget: u64,
242    /// Average bits per frame.
243    pub avg_bits_per_frame: u64,
244    /// Maximum bits allocated to any single frame.
245    pub max_bits_per_frame: u64,
246    /// Minimum bits allocated to any single frame.
247    pub min_bits_per_frame: u64,
248    /// Target bitrate in kbps.
249    pub target_bitrate_kbps: u32,
250    /// Content duration in milliseconds.
251    pub duration_ms: u64,
252    /// Complexity histogram (10 bins covering 0.0-1.0).
253    pub complexity_histogram: [u32; 10],
254    /// Scene change indices (frames where complexity jumps significantly).
255    pub scene_change_indices: Vec<usize>,
256}
257
258impl TwoPassStatistics {
259    /// Constructs statistics from pass one results.
260    fn from_pass_one(pass_one: &PassOneResult, config: &TwoPassConfig, total_bits: u64) -> Self {
261        let n = pass_one.complexity_map.len();
262        let (min_c, max_c, std_dev) = if n == 0 {
263            (0.0, 0.0, 0.0)
264        } else {
265            let min = pass_one
266                .complexity_map
267                .iter()
268                .copied()
269                .fold(f64::INFINITY, f64::min);
270            let max = pass_one.peak_complexity;
271            let mean = pass_one.avg_complexity;
272            let variance: f64 = pass_one
273                .complexity_map
274                .iter()
275                .map(|&c| (c - mean).powi(2))
276                .sum::<f64>()
277                / n as f64;
278            (min, max, variance.sqrt())
279        };
280
281        // Compute per-frame bit allocations
282        let mut max_bits: u64 = 0;
283        let mut min_bits: u64 = u64::MAX;
284        for i in 0..n {
285            let bits = pass_one.allocate_bits(i, total_bits);
286            if bits > max_bits {
287                max_bits = bits;
288            }
289            if bits < min_bits {
290                min_bits = bits;
291            }
292        }
293        if n == 0 {
294            min_bits = 0;
295        }
296
297        let avg_bits = if n > 0 { total_bits / n as u64 } else { 0 };
298
299        // Build complexity histogram (10 bins: [0.0-0.1), [0.1-0.2), ... [0.9-1.0])
300        let mut histogram = [0u32; 10];
301        for &c in &pass_one.complexity_map {
302            let bin = ((c * 10.0).floor() as usize).min(9);
303            histogram[bin] += 1;
304        }
305
306        // Detect scene changes: frames where complexity jumps by more than
307        // 2x the standard deviation from the previous frame
308        let scene_changes = Self::detect_scene_changes(&pass_one.complexity_map, std_dev);
309
310        Self {
311            frame_count: n,
312            mean_complexity: pass_one.avg_complexity,
313            peak_complexity: max_c,
314            min_complexity: min_c,
315            complexity_std_dev: std_dev,
316            complex_region_fraction: pass_one.complex_region_fraction(),
317            total_bit_budget: total_bits,
318            avg_bits_per_frame: avg_bits,
319            max_bits_per_frame: max_bits,
320            min_bits_per_frame: min_bits,
321            target_bitrate_kbps: config.target_bitrate_kbps,
322            duration_ms: config.input_duration_ms,
323            complexity_histogram: histogram,
324            scene_change_indices: scene_changes,
325        }
326    }
327
328    /// Detects scene changes by finding frames where complexity changes
329    /// by more than 2 standard deviations from the previous frame.
330    fn detect_scene_changes(complexities: &[f64], std_dev: f64) -> Vec<usize> {
331        let threshold = 2.0 * std_dev;
332        if threshold <= 0.0 || complexities.len() < 2 {
333            return Vec::new();
334        }
335
336        let mut changes = Vec::new();
337        for i in 1..complexities.len() {
338            let delta = (complexities[i] - complexities[i - 1]).abs();
339            if delta > threshold {
340                changes.push(i);
341            }
342        }
343        changes
344    }
345
346    /// Returns the compression ratio (mean complexity / peak complexity).
347    ///
348    /// Values close to 1.0 indicate uniform content; values close to 0.0
349    /// indicate highly variable content that benefits most from two-pass.
350    #[must_use]
351    pub fn content_uniformity(&self) -> f64 {
352        if self.peak_complexity <= 0.0 {
353            return 1.0;
354        }
355        self.mean_complexity / self.peak_complexity
356    }
357
358    /// Returns the bit allocation ratio (max bits / avg bits).
359    ///
360    /// Higher values indicate more aggressive bit redistribution.
361    #[must_use]
362    pub fn bit_allocation_ratio(&self) -> f64 {
363        if self.avg_bits_per_frame == 0 {
364            return 1.0;
365        }
366        self.max_bits_per_frame as f64 / self.avg_bits_per_frame as f64
367    }
368
369    /// Returns `true` if the content would significantly benefit from
370    /// two-pass encoding (high complexity variance).
371    #[must_use]
372    pub fn benefits_from_two_pass(&self) -> bool {
373        // High variance relative to mean indicates benefit
374        if self.mean_complexity <= 0.0 {
375            return false;
376        }
377        let cv = self.complexity_std_dev / self.mean_complexity;
378        cv > 0.3 // coefficient of variation > 30%
379    }
380
381    /// Returns the number of detected scene changes.
382    #[must_use]
383    pub fn scene_change_count(&self) -> usize {
384        self.scene_change_indices.len()
385    }
386
387    /// Returns a human-readable summary of the statistics.
388    #[must_use]
389    pub fn summary(&self) -> String {
390        format!(
391            "Frames: {} | Complexity: mean={:.3}, peak={:.3}, std={:.3} | \
392             Bits: budget={}, avg/frame={}, max/frame={} | \
393             Scene changes: {} | Two-pass benefit: {}",
394            self.frame_count,
395            self.mean_complexity,
396            self.peak_complexity,
397            self.complexity_std_dev,
398            self.total_bit_budget,
399            self.avg_bits_per_frame,
400            self.max_bits_per_frame,
401            self.scene_change_count(),
402            if self.benefits_from_two_pass() {
403                "high"
404            } else {
405                "low"
406            },
407        )
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_two_pass_config_total_bits() {
417        let cfg = TwoPassConfig::new(5000, 10_000); // 5 Mbps for 10 seconds
418        assert_eq!(cfg.total_bits(), 50_000_000);
419    }
420
421    #[test]
422    fn test_two_pass_config_zero_duration() {
423        let cfg = TwoPassConfig::new(5000, 0);
424        assert_eq!(cfg.total_bits(), 0);
425    }
426
427    #[test]
428    fn test_pass_one_result_avg_complexity() {
429        let result = PassOneResult::from_complexities(vec![0.2, 0.4, 0.6, 0.8], 4000);
430        assert!((result.avg_complexity - 0.5).abs() < 1e-9);
431    }
432
433    #[test]
434    fn test_pass_one_result_peak_complexity() {
435        let result = PassOneResult::from_complexities(vec![0.1, 0.9, 0.5], 3000);
436        assert!((result.peak_complexity - 0.9).abs() < 1e-9);
437    }
438
439    #[test]
440    fn test_pass_one_result_empty() {
441        let result = PassOneResult::from_complexities(vec![], 0);
442        assert_eq!(result.avg_complexity, 0.0);
443        assert_eq!(result.peak_complexity, 0.0);
444    }
445
446    #[test]
447    fn test_allocate_bits_proportional() {
448        // Frame 1 has 3x the complexity of frame 0
449        let result = PassOneResult::from_complexities(vec![0.25, 0.75], 2000);
450        let total_bits = 10_000_000u64;
451        let bits_0 = result.allocate_bits(0, total_bits);
452        let bits_1 = result.allocate_bits(1, total_bits);
453        assert_eq!(bits_0 + bits_1, total_bits);
454        assert!(bits_1 > bits_0);
455    }
456
457    #[test]
458    fn test_allocate_bits_out_of_range() {
459        let result = PassOneResult::from_complexities(vec![0.5, 0.5], 2000);
460        assert_eq!(result.allocate_bits(99, 1_000_000), 0);
461    }
462
463    #[test]
464    fn test_is_complex_region_simple() {
465        // All frames have the same complexity → nothing is complex
466        let result = PassOneResult::from_complexities(vec![0.5, 0.5, 0.5, 0.5], 4000);
467        assert!(!result.is_complex_region(0));
468    }
469
470    #[test]
471    fn test_is_complex_region_clear_outlier() {
472        // Frame 3 is a clear outlier
473        let result = PassOneResult::from_complexities(vec![0.1, 0.1, 0.1, 0.9], 4000);
474        assert!(result.is_complex_region(3));
475        assert!(!result.is_complex_region(0));
476    }
477
478    #[test]
479    fn test_two_pass_encoder_fallback_before_pass_one() {
480        let cfg = TwoPassConfig::new(4000, 5000);
481        let encoder = TwoPassEncoder::new(cfg);
482        assert!(!encoder.pass_one_complete());
483        // Should return target bitrate before pass one
484        assert_eq!(encoder.encode_bitrate_for_frame(0), 4000);
485    }
486
487    #[test]
488    fn test_two_pass_encoder_analyze_and_encode() {
489        let cfg = TwoPassConfig::new(4000, 10_000);
490        let mut encoder = TwoPassEncoder::new(cfg);
491        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]);
492        assert!(encoder.pass_one_complete());
493        // The complex frame should receive more bits → higher bitrate
494        let complex_bitrate = encoder.encode_bitrate_for_frame(2);
495        let simple_bitrate = encoder.encode_bitrate_for_frame(0);
496        assert!(complex_bitrate > simple_bitrate);
497    }
498
499    #[test]
500    fn test_complex_region_fraction() {
501        let result = PassOneResult::from_complexities(vec![0.1, 0.1, 0.1, 0.9], 4000);
502        let fraction = result.complex_region_fraction();
503        assert!(fraction > 0.0);
504        assert!(fraction <= 1.0);
505    }
506
507    #[test]
508    fn test_is_complex_region_single_frame() {
509        let result = PassOneResult::from_complexities(vec![0.8], 1000);
510        assert!(result.is_complex_region(0));
511        let result_low = PassOneResult::from_complexities(vec![0.2], 1000);
512        assert!(!result_low.is_complex_region(0));
513    }
514
515    // ── TwoPassStatistics tests ─────────────────────────────────────────
516
517    #[test]
518    fn test_statistics_basic() {
519        let cfg = TwoPassConfig::new(5000, 10_000);
520        let mut encoder = TwoPassEncoder::new(cfg);
521        encoder.analyze_pass_one(vec![0.2, 0.4, 0.6, 0.8]);
522        let stats = encoder
523            .statistics()
524            .expect("should have stats after pass one");
525        assert_eq!(stats.frame_count, 4);
526        assert!((stats.mean_complexity - 0.5).abs() < 1e-9);
527        assert!((stats.peak_complexity - 0.8).abs() < 1e-9);
528        assert!((stats.min_complexity - 0.2).abs() < 1e-9);
529        assert!(stats.complexity_std_dev > 0.0);
530        assert_eq!(stats.target_bitrate_kbps, 5000);
531        assert_eq!(stats.duration_ms, 10_000);
532    }
533
534    #[test]
535    fn test_statistics_bit_budget() {
536        let cfg = TwoPassConfig::new(5000, 10_000);
537        let mut encoder = TwoPassEncoder::new(cfg);
538        encoder.analyze_pass_one(vec![0.5, 0.5, 0.5, 0.5]);
539        let stats = encoder.statistics().expect("should have stats");
540        assert_eq!(stats.total_bit_budget, 50_000_000);
541        assert_eq!(stats.avg_bits_per_frame, 12_500_000);
542    }
543
544    #[test]
545    fn test_statistics_none_before_pass_one() {
546        let cfg = TwoPassConfig::new(4000, 5000);
547        let encoder = TwoPassEncoder::new(cfg);
548        assert!(encoder.statistics().is_none());
549    }
550
551    #[test]
552    fn test_statistics_content_uniformity_uniform() {
553        let cfg = TwoPassConfig::new(5000, 10_000);
554        let mut encoder = TwoPassEncoder::new(cfg);
555        encoder.analyze_pass_one(vec![0.5, 0.5, 0.5, 0.5]);
556        let stats = encoder.statistics().expect("should have stats");
557        assert!((stats.content_uniformity() - 1.0).abs() < 1e-9);
558    }
559
560    #[test]
561    fn test_statistics_content_uniformity_variable() {
562        let cfg = TwoPassConfig::new(5000, 10_000);
563        let mut encoder = TwoPassEncoder::new(cfg);
564        encoder.analyze_pass_one(vec![0.1, 0.1, 0.1, 0.9]);
565        let stats = encoder.statistics().expect("should have stats");
566        assert!(stats.content_uniformity() < 0.5);
567    }
568
569    #[test]
570    fn test_statistics_bit_allocation_ratio_uniform() {
571        let cfg = TwoPassConfig::new(5000, 10_000);
572        let mut encoder = TwoPassEncoder::new(cfg);
573        encoder.analyze_pass_one(vec![0.5, 0.5, 0.5, 0.5]);
574        let stats = encoder.statistics().expect("should have stats");
575        // Uniform content -> equal allocation -> ratio ~1.0
576        assert!((stats.bit_allocation_ratio() - 1.0).abs() < 0.01);
577    }
578
579    #[test]
580    fn test_statistics_bit_allocation_ratio_variable() {
581        let cfg = TwoPassConfig::new(5000, 10_000);
582        let mut encoder = TwoPassEncoder::new(cfg);
583        encoder.analyze_pass_one(vec![0.1, 0.1, 0.1, 0.9]);
584        let stats = encoder.statistics().expect("should have stats");
585        // Complex frame should get more bits -> ratio > 1
586        assert!(stats.bit_allocation_ratio() > 1.5);
587    }
588
589    #[test]
590    fn test_statistics_benefits_from_two_pass_variable() {
591        let cfg = TwoPassConfig::new(5000, 10_000);
592        let mut encoder = TwoPassEncoder::new(cfg);
593        encoder.analyze_pass_one(vec![0.1, 0.1, 0.1, 0.9, 0.1, 0.1, 0.1, 0.9]);
594        let stats = encoder.statistics().expect("should have stats");
595        assert!(stats.benefits_from_two_pass());
596    }
597
598    #[test]
599    fn test_statistics_not_benefits_from_two_pass_uniform() {
600        let cfg = TwoPassConfig::new(5000, 10_000);
601        let mut encoder = TwoPassEncoder::new(cfg);
602        encoder.analyze_pass_one(vec![0.5, 0.5, 0.5, 0.5, 0.5]);
603        let stats = encoder.statistics().expect("should have stats");
604        assert!(!stats.benefits_from_two_pass());
605    }
606
607    #[test]
608    fn test_statistics_complexity_histogram() {
609        let cfg = TwoPassConfig::new(5000, 10_000);
610        let mut encoder = TwoPassEncoder::new(cfg);
611        // 4 frames at 0.15, 1 at 0.85
612        encoder.analyze_pass_one(vec![0.15, 0.15, 0.15, 0.15, 0.85]);
613        let stats = encoder.statistics().expect("should have stats");
614        // Bin 1 (0.1-0.2) should have 4 frames
615        assert_eq!(stats.complexity_histogram[1], 4);
616        // Bin 8 (0.8-0.9) should have 1 frame
617        assert_eq!(stats.complexity_histogram[8], 1);
618    }
619
620    #[test]
621    fn test_statistics_scene_changes() {
622        let cfg = TwoPassConfig::new(5000, 10_000);
623        let mut encoder = TwoPassEncoder::new(cfg);
624        // Clear scene change at index 3 (0.1 -> 0.9)
625        encoder.analyze_pass_one(vec![0.1, 0.1, 0.1, 0.9, 0.9, 0.9, 0.1, 0.1]);
626        let stats = encoder.statistics().expect("should have stats");
627        assert!(stats.scene_change_count() > 0);
628        assert!(stats.scene_change_indices.contains(&3));
629    }
630
631    #[test]
632    fn test_statistics_no_scene_changes_uniform() {
633        let cfg = TwoPassConfig::new(5000, 10_000);
634        let mut encoder = TwoPassEncoder::new(cfg);
635        encoder.analyze_pass_one(vec![0.5, 0.5, 0.5, 0.5]);
636        let stats = encoder.statistics().expect("should have stats");
637        assert_eq!(stats.scene_change_count(), 0);
638    }
639
640    #[test]
641    fn test_statistics_summary_not_empty() {
642        let cfg = TwoPassConfig::new(5000, 10_000);
643        let mut encoder = TwoPassEncoder::new(cfg);
644        encoder.analyze_pass_one(vec![0.2, 0.4, 0.6, 0.8]);
645        let stats = encoder.statistics().expect("should have stats");
646        let summary = stats.summary();
647        assert!(!summary.is_empty());
648        assert!(summary.contains("Frames: 4"));
649        assert!(summary.contains("Scene changes:"));
650    }
651
652    #[test]
653    fn test_statistics_empty_complexities() {
654        let cfg = TwoPassConfig::new(5000, 10_000);
655        let mut encoder = TwoPassEncoder::new(cfg);
656        encoder.analyze_pass_one(vec![]);
657        let stats = encoder.statistics().expect("should have stats");
658        assert_eq!(stats.frame_count, 0);
659        assert_eq!(stats.avg_bits_per_frame, 0);
660        assert_eq!(stats.min_bits_per_frame, 0);
661        assert!((stats.content_uniformity() - 1.0).abs() < 1e-9);
662        assert!((stats.bit_allocation_ratio() - 1.0).abs() < 1e-9);
663        assert!(!stats.benefits_from_two_pass());
664    }
665}