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