Skip to main content

oximedia_codec/multipass/
stats.rs

1//! Statistics collection and storage for multi-pass encoding.
2//!
3//! This module handles the collection, serialization, and storage of encoding
4//! statistics during the first pass, which are then used in the second pass
5//! for optimal bitrate allocation.
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
13use crate::frame::FrameType;
14use crate::multipass::complexity::FrameComplexity;
15use std::fs::File;
16use std::io::{BufRead, BufReader, BufWriter, Write};
17use std::path::Path;
18
19/// Statistics for a single encoded frame.
20#[derive(Clone, Debug)]
21pub struct FrameStatistics {
22    /// Frame index in the stream.
23    pub frame_index: u64,
24    /// Frame type.
25    pub frame_type: FrameType,
26    /// Quantization parameter used.
27    pub qp: f64,
28    /// Actual bits used to encode this frame.
29    pub bits: u64,
30    /// Frame complexity metrics.
31    pub complexity: FrameComplexity,
32    /// Motion estimation data (average motion vector magnitude).
33    pub avg_motion: f64,
34    /// Peak Signal-to-Noise Ratio (if available).
35    pub psnr: Option<f64>,
36    /// Structural Similarity Index (if available).
37    pub ssim: Option<f64>,
38}
39
40impl FrameStatistics {
41    /// Create new frame statistics.
42    #[must_use]
43    pub fn new(
44        frame_index: u64,
45        frame_type: FrameType,
46        qp: f64,
47        bits: u64,
48        complexity: FrameComplexity,
49    ) -> Self {
50        Self {
51            frame_index,
52            frame_type,
53            qp,
54            bits,
55            complexity,
56            avg_motion: 0.0,
57            psnr: None,
58            ssim: None,
59        }
60    }
61
62    /// Set motion estimation data.
63    pub fn set_motion(&mut self, avg_motion: f64) {
64        self.avg_motion = avg_motion;
65    }
66
67    /// Set quality metrics.
68    pub fn set_quality_metrics(&mut self, psnr: f64, ssim: f64) {
69        self.psnr = Some(psnr);
70        self.ssim = Some(ssim);
71    }
72
73    /// Get bits per pixel.
74    #[must_use]
75    pub fn bits_per_pixel(&self, width: u32, height: u32) -> f64 {
76        let pixels = (width as u64) * (height as u64);
77        if pixels == 0 {
78            return 0.0;
79        }
80        self.bits as f64 / pixels as f64
81    }
82}
83
84/// First-pass statistics collection.
85#[derive(Clone, Debug)]
86pub struct PassStatistics {
87    /// All frame statistics.
88    pub frames: Vec<FrameStatistics>,
89    /// Total frames encoded.
90    pub total_frames: u64,
91    /// Total bits used.
92    pub total_bits: u64,
93    /// Average QP across all frames.
94    pub avg_qp: f64,
95    /// Average frame size in bits.
96    pub avg_frame_bits: f64,
97    /// Video width.
98    pub width: u32,
99    /// Video height.
100    pub height: u32,
101    /// Frame rate numerator.
102    pub framerate_num: u32,
103    /// Frame rate denominator.
104    pub framerate_den: u32,
105}
106
107impl PassStatistics {
108    /// Create a new pass statistics collector.
109    #[must_use]
110    pub fn new(width: u32, height: u32, framerate_num: u32, framerate_den: u32) -> Self {
111        Self {
112            frames: Vec::new(),
113            total_frames: 0,
114            total_bits: 0,
115            avg_qp: 0.0,
116            avg_frame_bits: 0.0,
117            width,
118            height,
119            framerate_num,
120            framerate_den,
121        }
122    }
123
124    /// Add a frame's statistics.
125    pub fn add_frame(&mut self, stats: FrameStatistics) {
126        self.total_bits += stats.bits;
127        self.total_frames += 1;
128        self.frames.push(stats);
129        self.update_averages();
130    }
131
132    /// Update average statistics.
133    fn update_averages(&mut self) {
134        if self.total_frames == 0 {
135            return;
136        }
137
138        self.avg_frame_bits = self.total_bits as f64 / self.total_frames as f64;
139
140        let total_qp: f64 = self.frames.iter().map(|f| f.qp).sum();
141        self.avg_qp = total_qp / self.total_frames as f64;
142    }
143
144    /// Get statistics for a specific frame.
145    #[must_use]
146    pub fn get_frame(&self, index: u64) -> Option<&FrameStatistics> {
147        self.frames.iter().find(|f| f.frame_index == index)
148    }
149
150    /// Get average bitrate in bits per second.
151    #[must_use]
152    pub fn average_bitrate(&self) -> u64 {
153        if self.total_frames == 0 {
154            return 0;
155        }
156
157        let fps = self.framerate_num as f64 / self.framerate_den as f64;
158        (self.avg_frame_bits * fps) as u64
159    }
160
161    /// Get peak bitrate (highest frame size * framerate).
162    #[must_use]
163    pub fn peak_bitrate(&self) -> u64 {
164        let max_frame_bits = self.frames.iter().map(|f| f.bits).max().unwrap_or(0);
165
166        let fps = self.framerate_num as f64 / self.framerate_den as f64;
167        (max_frame_bits as f64 * fps) as u64
168    }
169
170    /// Calculate complexity distribution across frames.
171    #[must_use]
172    pub fn complexity_distribution(&self) -> ComplexityStats {
173        if self.frames.is_empty() {
174            return ComplexityStats::default();
175        }
176
177        let mut complexities: Vec<f64> = self
178            .frames
179            .iter()
180            .map(|f| f.complexity.combined_complexity)
181            .collect();
182
183        complexities.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
184
185        let sum: f64 = complexities.iter().sum();
186        let mean = sum / complexities.len() as f64;
187
188        let variance: f64 = complexities.iter().map(|c| (c - mean).powi(2)).sum::<f64>()
189            / complexities.len() as f64;
190        let std_dev = variance.sqrt();
191
192        let median = if complexities.len() % 2 == 0 {
193            let mid = complexities.len() / 2;
194            (complexities[mid - 1] + complexities[mid]) / 2.0
195        } else {
196            complexities[complexities.len() / 2]
197        };
198
199        ComplexityStats {
200            mean,
201            std_dev,
202            median,
203            min: complexities.first().copied().unwrap_or(0.0),
204            max: complexities.last().copied().unwrap_or(0.0),
205        }
206    }
207
208    /// Save statistics to a file.
209    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
210        let file = File::create(path)?;
211        let mut writer = BufWriter::new(file);
212
213        // Write header
214        writeln!(writer, "# OxiMedia First Pass Statistics v1.0")?;
215        writeln!(writer, "width={}", self.width)?;
216        writeln!(writer, "height={}", self.height)?;
217        writeln!(
218            writer,
219            "framerate={}/{}",
220            self.framerate_num, self.framerate_den
221        )?;
222        writeln!(writer, "total_frames={}", self.total_frames)?;
223        writeln!(writer, "total_bits={}", self.total_bits)?;
224        writeln!(writer, "avg_qp={:.2}", self.avg_qp)?;
225        writeln!(writer, "avg_frame_bits={:.2}", self.avg_frame_bits)?;
226        writeln!(writer)?;
227
228        // Write per-frame data
229        writeln!(
230            writer,
231            "# frame_idx,frame_type,qp,bits,spatial,temporal,combined,sad,variance,difficulty,scene_change,avg_motion,psnr,ssim"
232        )?;
233
234        for stats in &self.frames {
235            let frame_type_str = match stats.frame_type {
236                FrameType::Key => "I",
237                FrameType::Inter => "P",
238                FrameType::BiDir => "B",
239                FrameType::Switch => "S",
240            };
241
242            write!(
243                writer,
244                "{},{},{:.2},{},{:.6},{:.6},{:.6},{},{:.2},{:.6},{},{:.6}",
245                stats.frame_index,
246                frame_type_str,
247                stats.qp,
248                stats.bits,
249                stats.complexity.spatial_complexity,
250                stats.complexity.temporal_complexity,
251                stats.complexity.combined_complexity,
252                stats.complexity.sad,
253                stats.complexity.variance,
254                stats.complexity.encoding_difficulty,
255                if stats.complexity.is_scene_change {
256                    1
257                } else {
258                    0
259                },
260                stats.avg_motion,
261            )?;
262
263            if let Some(psnr) = stats.psnr {
264                write!(writer, ",{:.2}", psnr)?;
265            } else {
266                write!(writer, ",")?;
267            }
268
269            if let Some(ssim) = stats.ssim {
270                write!(writer, ",{:.4}", ssim)?;
271            } else {
272                write!(writer, ",")?;
273            }
274
275            writeln!(writer)?;
276        }
277
278        writer.flush()?;
279        Ok(())
280    }
281
282    /// Load statistics from a file.
283    pub fn load_from_file<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
284        let file = File::open(path)?;
285        let reader = BufReader::new(file);
286
287        let mut width = 0u32;
288        let mut height = 0u32;
289        let mut framerate_num = 30u32;
290        let mut framerate_den = 1u32;
291        let mut frames = Vec::new();
292
293        for line in reader.lines() {
294            let line = line?;
295            let line = line.trim();
296
297            if line.is_empty() || line.starts_with('#') {
298                continue;
299            }
300
301            // Parse header fields
302            if line.starts_with("width=") {
303                width = line[6..].parse().unwrap_or(0);
304                continue;
305            }
306            if line.starts_with("height=") {
307                height = line[7..].parse().unwrap_or(0);
308                continue;
309            }
310            if line.starts_with("framerate=") {
311                let parts: Vec<&str> = line[10..].split('/').collect();
312                if parts.len() == 2 {
313                    framerate_num = parts[0].parse().unwrap_or(30);
314                    framerate_den = parts[1].parse().unwrap_or(1);
315                }
316                continue;
317            }
318
319            // Skip other header fields
320            if line.contains('=') {
321                continue;
322            }
323
324            // Parse frame data
325            let parts: Vec<&str> = line.split(',').collect();
326            if parts.len() < 12 {
327                continue;
328            }
329
330            let frame_index: u64 = parts[0].parse().unwrap_or(0);
331            let frame_type = match parts[1] {
332                "I" => FrameType::Key,
333                "P" => FrameType::Inter,
334                "B" => FrameType::BiDir,
335                "S" => FrameType::Switch,
336                _ => FrameType::Inter,
337            };
338            let qp: f64 = parts[2].parse().unwrap_or(28.0);
339            let bits: u64 = parts[3].parse().unwrap_or(0);
340
341            let mut complexity = FrameComplexity::new(frame_index, frame_type);
342            complexity.spatial_complexity = parts[4].parse().unwrap_or(0.5);
343            complexity.temporal_complexity = parts[5].parse().unwrap_or(0.5);
344            complexity.combined_complexity = parts[6].parse().unwrap_or(0.5);
345            complexity.sad = parts[7].parse().unwrap_or(0);
346            complexity.variance = parts[8].parse().unwrap_or(0.0);
347            complexity.encoding_difficulty = parts[9].parse().unwrap_or(1.0);
348            complexity.is_scene_change = parts[10] == "1";
349
350            let mut stats = FrameStatistics::new(frame_index, frame_type, qp, bits, complexity);
351            stats.avg_motion = parts[11].parse().unwrap_or(0.0);
352
353            if parts.len() > 12 && !parts[12].is_empty() {
354                if let Ok(psnr) = parts[12].parse::<f64>() {
355                    stats.psnr = Some(psnr);
356                }
357            }
358
359            if parts.len() > 13 && !parts[13].is_empty() {
360                if let Ok(ssim) = parts[13].parse::<f64>() {
361                    stats.ssim = Some(ssim);
362                }
363            }
364
365            frames.push(stats);
366        }
367
368        let mut stats = Self::new(width, height, framerate_num, framerate_den);
369        stats.frames = frames;
370        stats.total_frames = stats.frames.len() as u64;
371        stats.total_bits = stats.frames.iter().map(|f| f.bits).sum();
372        stats.update_averages();
373
374        Ok(stats)
375    }
376}
377
378/// Complexity statistics summary.
379#[derive(Clone, Debug, Default)]
380pub struct ComplexityStats {
381    /// Mean complexity.
382    pub mean: f64,
383    /// Standard deviation.
384    pub std_dev: f64,
385    /// Median complexity.
386    pub median: f64,
387    /// Minimum complexity.
388    pub min: f64,
389    /// Maximum complexity.
390    pub max: f64,
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    fn create_test_complexity(frame_index: u64) -> FrameComplexity {
398        FrameComplexity::new(frame_index, FrameType::Inter)
399    }
400
401    #[test]
402    fn test_frame_statistics_new() {
403        let complexity = create_test_complexity(0);
404        let stats = FrameStatistics::new(0, FrameType::Key, 28.0, 10000, complexity);
405
406        assert_eq!(stats.frame_index, 0);
407        assert_eq!(stats.qp, 28.0);
408        assert_eq!(stats.bits, 10000);
409    }
410
411    #[test]
412    fn test_pass_statistics_add_frame() {
413        let mut pass_stats = PassStatistics::new(1920, 1080, 30, 1);
414
415        let complexity = create_test_complexity(0);
416        let frame_stats = FrameStatistics::new(0, FrameType::Key, 28.0, 10000, complexity);
417
418        pass_stats.add_frame(frame_stats);
419
420        assert_eq!(pass_stats.total_frames, 1);
421        assert_eq!(pass_stats.total_bits, 10000);
422        assert_eq!(pass_stats.avg_qp, 28.0);
423    }
424
425    #[test]
426    fn test_average_bitrate() {
427        let mut pass_stats = PassStatistics::new(1920, 1080, 30, 1);
428
429        for i in 0..30 {
430            let complexity = create_test_complexity(i);
431            let frame_stats = FrameStatistics::new(i, FrameType::Inter, 28.0, 5000, complexity);
432            pass_stats.add_frame(frame_stats);
433        }
434
435        let avg_bitrate = pass_stats.average_bitrate();
436        assert_eq!(avg_bitrate, 5000 * 30); // 5000 bits/frame * 30 fps
437    }
438
439    #[test]
440    fn test_complexity_distribution() {
441        let mut pass_stats = PassStatistics::new(1920, 1080, 30, 1);
442
443        for i in 0..10 {
444            let mut complexity = create_test_complexity(i);
445            complexity.combined_complexity = i as f64 / 10.0;
446            let frame_stats = FrameStatistics::new(i, FrameType::Inter, 28.0, 5000, complexity);
447            pass_stats.add_frame(frame_stats);
448        }
449
450        let dist = pass_stats.complexity_distribution();
451        assert!(dist.mean > 0.0);
452        assert!(dist.std_dev >= 0.0);
453    }
454
455    #[test]
456    fn test_save_and_load() -> std::io::Result<()> {
457        let mut pass_stats = PassStatistics::new(1920, 1080, 30, 1);
458
459        for i in 0..5 {
460            let complexity = create_test_complexity(i);
461            let frame_stats = FrameStatistics::new(i, FrameType::Inter, 28.0, 5000, complexity);
462            pass_stats.add_frame(frame_stats);
463        }
464
465        let temp_file = "/tmp/oximedia_test_stats.txt";
466        pass_stats.save_to_file(temp_file)?;
467
468        let loaded = PassStatistics::load_from_file(temp_file)?;
469        assert_eq!(loaded.width, 1920);
470        assert_eq!(loaded.height, 1080);
471        assert_eq!(loaded.total_frames, 5);
472
473        std::fs::remove_file(temp_file)?;
474        Ok(())
475    }
476}