1#![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#[derive(Clone, Debug)]
21pub struct FrameStatistics {
22 pub frame_index: u64,
24 pub frame_type: FrameType,
26 pub qp: f64,
28 pub bits: u64,
30 pub complexity: FrameComplexity,
32 pub avg_motion: f64,
34 pub psnr: Option<f64>,
36 pub ssim: Option<f64>,
38}
39
40impl FrameStatistics {
41 #[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 pub fn set_motion(&mut self, avg_motion: f64) {
64 self.avg_motion = avg_motion;
65 }
66
67 pub fn set_quality_metrics(&mut self, psnr: f64, ssim: f64) {
69 self.psnr = Some(psnr);
70 self.ssim = Some(ssim);
71 }
72
73 #[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#[derive(Clone, Debug)]
86pub struct PassStatistics {
87 pub frames: Vec<FrameStatistics>,
89 pub total_frames: u64,
91 pub total_bits: u64,
93 pub avg_qp: f64,
95 pub avg_frame_bits: f64,
97 pub width: u32,
99 pub height: u32,
101 pub framerate_num: u32,
103 pub framerate_den: u32,
105}
106
107impl PassStatistics {
108 #[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 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 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 #[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 #[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 #[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 #[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 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 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 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 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 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 if line.contains('=') {
321 continue;
322 }
323
324 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#[derive(Clone, Debug, Default)]
380pub struct ComplexityStats {
381 pub mean: f64,
383 pub std_dev: f64,
385 pub median: f64,
387 pub min: f64,
389 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); }
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}