1use crate::{Result, TranscodeError};
4use std::path::Path;
5
6#[must_use]
19pub fn estimate_encoding_time(
20 duration: f64,
21 quality: crate::QualityMode,
22 resolution: (u32, u32),
23 hw_accel: bool,
24) -> f64 {
25 let base_speed_factor = quality.speed_factor();
26
27 let pixel_count = f64::from(resolution.0 * resolution.1);
29 let resolution_factor = pixel_count / (1920.0 * 1080.0);
30
31 let hw_factor = if hw_accel { 0.3 } else { 1.0 };
33
34 duration * base_speed_factor * resolution_factor * hw_factor
35}
36
37#[must_use]
49pub fn estimate_file_size(duration: f64, video_bitrate: u64, audio_bitrate: u64) -> u64 {
50 let total_bitrate = video_bitrate + audio_bitrate;
51 let bits = (duration * total_bitrate as f64) as u64;
52 bits / 8 }
54
55#[must_use]
57pub fn format_duration(seconds: f64) -> String {
58 let hours = (seconds / 3600.0) as u64;
59 let minutes = ((seconds % 3600.0) / 60.0) as u64;
60 let secs = (seconds % 60.0) as u64;
61
62 if hours > 0 {
63 format!("{hours:02}:{minutes:02}:{secs:02}")
64 } else {
65 format!("{minutes:02}:{secs:02}")
66 }
67}
68
69#[must_use]
71pub fn format_file_size(bytes: u64) -> String {
72 const KB: u64 = 1024;
73 const MB: u64 = KB * 1024;
74 const GB: u64 = MB * 1024;
75 const TB: u64 = GB * 1024;
76
77 if bytes >= TB {
78 format!("{:.2} TB", bytes as f64 / TB as f64)
79 } else if bytes >= GB {
80 format!("{:.2} GB", bytes as f64 / GB as f64)
81 } else if bytes >= MB {
82 format!("{:.2} MB", bytes as f64 / MB as f64)
83 } else if bytes >= KB {
84 format!("{:.2} KB", bytes as f64 / KB as f64)
85 } else {
86 format!("{bytes} B")
87 }
88}
89
90#[must_use]
92pub fn format_bitrate(bps: u64) -> String {
93 const KBPS: u64 = 1000;
94 const MBPS: u64 = KBPS * 1000;
95
96 if bps >= MBPS {
97 format!("{:.2} Mbps", bps as f64 / MBPS as f64)
98 } else if bps >= KBPS {
99 format!("{:.0} kbps", bps as f64 / KBPS as f64)
100 } else {
101 format!("{bps} bps")
102 }
103}
104
105pub fn validate_input_file(path: &str) -> Result<()> {
111 let path_obj = Path::new(path);
112
113 if !path_obj.exists() {
114 return Err(TranscodeError::InvalidInput(format!(
115 "File does not exist: {path}"
116 )));
117 }
118
119 if !path_obj.is_file() {
120 return Err(TranscodeError::InvalidInput(format!(
121 "Path is not a file: {path}"
122 )));
123 }
124
125 match std::fs::metadata(path_obj) {
126 Ok(metadata) => {
127 if metadata.len() == 0 {
128 return Err(TranscodeError::InvalidInput(format!(
129 "File is empty: {path}"
130 )));
131 }
132 }
133 Err(e) => {
134 return Err(TranscodeError::InvalidInput(format!(
135 "Cannot read file {path}: {e}"
136 )));
137 }
138 }
139
140 Ok(())
141}
142
143#[must_use]
145pub fn get_file_extension(path: &str) -> Option<String> {
146 Path::new(path)
147 .extension()
148 .and_then(|e| e.to_str())
149 .map(str::to_lowercase)
150}
151
152#[must_use]
154pub fn container_from_extension(path: &str) -> Option<String> {
155 let ext = get_file_extension(path)?;
156
157 match ext.as_str() {
158 "mp4" | "m4v" => Some("mp4".to_string()),
159 "mkv" => Some("matroska".to_string()),
160 "webm" => Some("webm".to_string()),
161 "avi" => Some("avi".to_string()),
162 "mov" => Some("mov".to_string()),
163 "flv" => Some("flv".to_string()),
164 "wmv" => Some("asf".to_string()),
165 "ogv" => Some("ogg".to_string()),
166 _ => None,
167 }
168}
169
170#[must_use]
172pub fn suggest_video_codec(container: &str) -> Option<String> {
173 match container.to_lowercase().as_str() {
174 "mp4" | "m4v" => Some("h264".to_string()),
175 "webm" => Some("vp9".to_string()),
176 "mkv" => Some("vp9".to_string()),
177 "ogv" => Some("theora".to_string()),
178 _ => None,
179 }
180}
181
182#[must_use]
184pub fn suggest_audio_codec(container: &str) -> Option<String> {
185 match container.to_lowercase().as_str() {
186 "mp4" | "m4v" => Some("aac".to_string()),
187 "webm" => Some("opus".to_string()),
188 "mkv" => Some("opus".to_string()),
189 "ogv" => Some("vorbis".to_string()),
190 _ => None,
191 }
192}
193
194#[must_use]
196pub fn calculate_aspect_ratio(width: u32, height: u32) -> (u32, u32) {
197 fn gcd(mut a: u32, mut b: u32) -> u32 {
198 while b != 0 {
199 let temp = b;
200 b = a % b;
201 a = temp;
202 }
203 a
204 }
205
206 let divisor = gcd(width, height);
207 (width / divisor, height / divisor)
208}
209
210#[must_use]
212pub fn format_aspect_ratio(width: u32, height: u32) -> String {
213 let (w, h) = calculate_aspect_ratio(width, height);
214 format!("{w}:{h}")
215}
216
217#[must_use]
219pub fn is_standard_resolution(width: u32, height: u32) -> bool {
220 matches!(
221 (width, height),
222 (1920, 1080)
223 | (1280, 720)
224 | (3840, 2160)
225 | (2560, 1440)
226 | (854, 480)
227 | (640, 360)
228 | (426, 240)
229 )
230}
231
232#[must_use]
234pub fn resolution_name(width: u32, height: u32) -> String {
235 match (width, height) {
236 (3840, 2160) => "4K (2160p)".to_string(),
237 (2560, 1440) => "2K (1440p)".to_string(),
238 (1920, 1080) => "Full HD (1080p)".to_string(),
239 (1280, 720) => "HD (720p)".to_string(),
240 (854, 480) => "SD (480p)".to_string(),
241 (640, 360) => "nHD (360p)".to_string(),
242 (426, 240) => "240p".to_string(),
243 _ => format!("{width}x{height}"),
244 }
245}
246
247#[must_use]
249pub fn calculate_optimal_tiles(width: u32, height: u32, threads: u32) -> (u8, u8) {
250 let pixel_count = width * height;
251
252 if pixel_count < 1280 * 720 {
254 return (1, 1);
255 }
256
257 let tiles = match threads {
259 1..=2 => 1,
260 3..=4 => 2,
261 5..=8 => 4,
262 9..=16 => 8,
263 _ => 16,
264 };
265
266 let cols = tiles.min(8);
268 let rows = (tiles / cols).min(8);
269
270 (cols as u8, rows as u8)
271}
272
273#[must_use]
275pub fn suggest_bitrate(width: u32, height: u32, fps: f64, quality: crate::QualityMode) -> u64 {
276 let pixel_count = u64::from(width * height);
277 let motion_factor = if fps > 30.0 { 1.5 } else { 1.0 };
278
279 let base_bitrate = match quality {
280 crate::QualityMode::Low => pixel_count / 1500,
281 crate::QualityMode::Medium => pixel_count / 1000,
282 crate::QualityMode::High => pixel_count / 750,
283 crate::QualityMode::VeryHigh => pixel_count / 500,
284 crate::QualityMode::Custom => pixel_count / 1000,
285 };
286
287 (base_bitrate as f64 * motion_factor) as u64
288}
289
290pub fn validate_resolution_constraints(
292 input_width: u32,
293 input_height: u32,
294 output_width: u32,
295 output_height: u32,
296) -> Result<()> {
297 if output_width > input_width || output_height > input_height {
299 }
301
302 let input_ratio = f64::from(input_width) / f64::from(input_height);
304 let output_ratio = f64::from(output_width) / f64::from(output_height);
305 let ratio_diff = (input_ratio - output_ratio).abs();
306
307 if ratio_diff > 0.01 {
308 }
310
311 Ok(())
312}
313
314#[must_use]
316pub fn temp_stats_file(job_id: &str) -> String {
317 std::env::temp_dir()
318 .join(format!("oximedia-transcode-stats-{job_id}.log"))
319 .to_string_lossy()
320 .into_owned()
321}
322
323pub fn cleanup_temp_files(job_id: &str) -> Result<()> {
325 let stats_file = temp_stats_file(job_id);
326 if Path::new(&stats_file).exists() {
327 std::fs::remove_file(&stats_file)?;
328 }
329 Ok(())
330}
331
332#[must_use]
334pub fn calculate_compression_ratio(input_size: u64, output_size: u64) -> f64 {
335 if output_size == 0 {
336 return 0.0;
337 }
338 input_size as f64 / output_size as f64
339}
340
341#[must_use]
343pub fn format_compression_ratio(ratio: f64) -> String {
344 if ratio >= 1.0 {
345 format!("{ratio:.2}x smaller")
346 } else {
347 format!("{:.2}x larger", 1.0 / ratio)
348 }
349}
350
351#[must_use]
353pub fn calculate_space_savings(input_size: u64, output_size: u64) -> i64 {
354 input_size as i64 - output_size as i64
355}
356
357#[must_use]
359pub fn format_space_savings(savings: i64) -> String {
360 if savings > 0 {
361 format!("{} saved", format_file_size(savings as u64))
362 } else {
363 format!("{} larger", format_file_size((-savings) as u64))
364 }
365}
366
367pub fn parse_duration(duration_str: &str) -> Result<f64> {
369 let parts: Vec<&str> = duration_str.split(':').collect();
370
371 let seconds = match parts.len() {
372 1 => {
373 parts[0].parse::<f64>().map_err(|_| {
375 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
376 "Invalid duration format".to_string(),
377 ))
378 })?
379 }
380 2 => {
381 let minutes = parts[0].parse::<f64>().map_err(|_| {
383 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
384 "Invalid duration format".to_string(),
385 ))
386 })?;
387 let secs = parts[1].parse::<f64>().map_err(|_| {
388 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
389 "Invalid duration format".to_string(),
390 ))
391 })?;
392 minutes * 60.0 + secs
393 }
394 3 => {
395 let hours = parts[0].parse::<f64>().map_err(|_| {
397 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
398 "Invalid duration format".to_string(),
399 ))
400 })?;
401 let minutes = parts[1].parse::<f64>().map_err(|_| {
402 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
403 "Invalid duration format".to_string(),
404 ))
405 })?;
406 let secs = parts[2].parse::<f64>().map_err(|_| {
407 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
408 "Invalid duration format".to_string(),
409 ))
410 })?;
411 hours * 3600.0 + minutes * 60.0 + secs
412 }
413 _ => {
414 return Err(TranscodeError::ValidationError(
415 crate::ValidationError::InvalidInputFormat("Invalid duration format".to_string()),
416 ))
417 }
418 };
419
420 Ok(seconds)
421}
422
423#[must_use]
425pub fn format_framerate(num: u32, den: u32) -> String {
426 let fps = f64::from(num) / f64::from(den);
427 if den == 1 {
428 format!("{num} fps")
429 } else {
430 format!("{fps:.2} fps")
431 }
432}
433
434#[must_use]
436pub fn is_standard_framerate(num: u32, den: u32) -> bool {
437 matches!(
438 (num, den),
439 (24 | 25 | 30 | 50 | 60, 1) | (24000 | 30000 | 60000, 1001)
440 )
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_estimate_encoding_time() {
449 let time = estimate_encoding_time(60.0, crate::QualityMode::Medium, (1920, 1080), false);
450 assert!(time > 0.0);
451 }
452
453 #[test]
454 fn test_estimate_file_size() {
455 let size = estimate_file_size(60.0, 5_000_000, 128_000);
456 assert_eq!(size, (60.0 * 5_128_000.0 / 8.0) as u64);
457 }
458
459 #[test]
460 fn test_format_duration() {
461 assert_eq!(format_duration(90.0), "01:30");
462 assert_eq!(format_duration(3665.0), "01:01:05");
463 }
464
465 #[test]
466 fn test_format_file_size() {
467 assert_eq!(format_file_size(1024), "1.00 KB");
468 assert_eq!(format_file_size(1024 * 1024), "1.00 MB");
469 assert_eq!(format_file_size(1024 * 1024 * 1024), "1.00 GB");
470 }
471
472 #[test]
473 fn test_format_bitrate() {
474 assert_eq!(format_bitrate(1_000_000), "1.00 Mbps");
475 assert_eq!(format_bitrate(128_000), "128 kbps");
476 }
477
478 #[test]
479 fn test_get_file_extension() {
480 assert_eq!(get_file_extension("video.mp4"), Some("mp4".to_string()));
481 assert_eq!(get_file_extension("VIDEO.MP4"), Some("mp4".to_string()));
482 assert_eq!(get_file_extension("video"), None);
483 }
484
485 #[test]
486 fn test_container_from_extension() {
487 assert_eq!(
488 container_from_extension("video.mp4"),
489 Some("mp4".to_string())
490 );
491 assert_eq!(
492 container_from_extension("video.mkv"),
493 Some("matroska".to_string())
494 );
495 assert_eq!(
496 container_from_extension("video.webm"),
497 Some("webm".to_string())
498 );
499 }
500
501 #[test]
502 fn test_suggest_codecs() {
503 assert_eq!(suggest_video_codec("mp4"), Some("h264".to_string()));
504 assert_eq!(suggest_video_codec("webm"), Some("vp9".to_string()));
505 assert_eq!(suggest_audio_codec("mp4"), Some("aac".to_string()));
506 assert_eq!(suggest_audio_codec("webm"), Some("opus".to_string()));
507 }
508
509 #[test]
510 fn test_calculate_aspect_ratio() {
511 assert_eq!(calculate_aspect_ratio(1920, 1080), (16, 9));
512 assert_eq!(calculate_aspect_ratio(1280, 720), (16, 9));
513 assert_eq!(calculate_aspect_ratio(1920, 800), (12, 5));
514 }
515
516 #[test]
517 fn test_format_aspect_ratio() {
518 assert_eq!(format_aspect_ratio(1920, 1080), "16:9");
519 assert_eq!(format_aspect_ratio(1280, 720), "16:9");
520 }
521
522 #[test]
523 fn test_is_standard_resolution() {
524 assert!(is_standard_resolution(1920, 1080));
525 assert!(is_standard_resolution(1280, 720));
526 assert!(!is_standard_resolution(1000, 1000));
527 }
528
529 #[test]
530 fn test_resolution_name() {
531 assert_eq!(resolution_name(1920, 1080), "Full HD (1080p)");
532 assert_eq!(resolution_name(3840, 2160), "4K (2160p)");
533 assert_eq!(resolution_name(1000, 1000), "1000x1000");
534 }
535
536 #[test]
537 fn test_calculate_optimal_tiles() {
538 let (cols, rows) = calculate_optimal_tiles(1920, 1080, 8);
539 assert!(cols > 0 && rows > 0);
540 }
541
542 #[test]
543 fn test_suggest_bitrate() {
544 let bitrate = suggest_bitrate(1920, 1080, 30.0, crate::QualityMode::Medium);
545 assert!(bitrate > 0);
546 }
547
548 #[test]
549 fn test_calculate_compression_ratio() {
550 assert_eq!(calculate_compression_ratio(1000, 500), 2.0);
551 assert_eq!(calculate_compression_ratio(500, 1000), 0.5);
552 }
553
554 #[test]
555 fn test_parse_duration() {
556 assert_eq!(parse_duration("60").expect("should succeed in test"), 60.0);
557 assert_eq!(
558 parse_duration("01:30").expect("should succeed in test"),
559 90.0
560 );
561 assert_eq!(
562 parse_duration("01:01:30").expect("should succeed in test"),
563 3690.0
564 );
565 }
566
567 #[test]
568 fn test_format_framerate() {
569 assert_eq!(format_framerate(30, 1), "30 fps");
570 assert_eq!(format_framerate(30000, 1001), "29.97 fps");
571 }
572
573 #[test]
574 fn test_is_standard_framerate() {
575 assert!(is_standard_framerate(30, 1));
576 assert!(is_standard_framerate(60, 1));
577 assert!(is_standard_framerate(30000, 1001));
578 assert!(!is_standard_framerate(45, 1));
579 }
580}