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 format!("/tmp/transcode_stats_{job_id}.log")
318}
319
320pub fn cleanup_temp_files(job_id: &str) -> Result<()> {
322 let stats_file = temp_stats_file(job_id);
323 if Path::new(&stats_file).exists() {
324 std::fs::remove_file(&stats_file)?;
325 }
326 Ok(())
327}
328
329#[must_use]
331pub fn calculate_compression_ratio(input_size: u64, output_size: u64) -> f64 {
332 if output_size == 0 {
333 return 0.0;
334 }
335 input_size as f64 / output_size as f64
336}
337
338#[must_use]
340pub fn format_compression_ratio(ratio: f64) -> String {
341 if ratio >= 1.0 {
342 format!("{ratio:.2}x smaller")
343 } else {
344 format!("{:.2}x larger", 1.0 / ratio)
345 }
346}
347
348#[must_use]
350pub fn calculate_space_savings(input_size: u64, output_size: u64) -> i64 {
351 input_size as i64 - output_size as i64
352}
353
354#[must_use]
356pub fn format_space_savings(savings: i64) -> String {
357 if savings > 0 {
358 format!("{} saved", format_file_size(savings as u64))
359 } else {
360 format!("{} larger", format_file_size((-savings) as u64))
361 }
362}
363
364pub fn parse_duration(duration_str: &str) -> Result<f64> {
366 let parts: Vec<&str> = duration_str.split(':').collect();
367
368 let seconds = match parts.len() {
369 1 => {
370 parts[0].parse::<f64>().map_err(|_| {
372 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
373 "Invalid duration format".to_string(),
374 ))
375 })?
376 }
377 2 => {
378 let minutes = parts[0].parse::<f64>().map_err(|_| {
380 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
381 "Invalid duration format".to_string(),
382 ))
383 })?;
384 let secs = parts[1].parse::<f64>().map_err(|_| {
385 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
386 "Invalid duration format".to_string(),
387 ))
388 })?;
389 minutes * 60.0 + secs
390 }
391 3 => {
392 let hours = parts[0].parse::<f64>().map_err(|_| {
394 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
395 "Invalid duration format".to_string(),
396 ))
397 })?;
398 let minutes = parts[1].parse::<f64>().map_err(|_| {
399 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
400 "Invalid duration format".to_string(),
401 ))
402 })?;
403 let secs = parts[2].parse::<f64>().map_err(|_| {
404 TranscodeError::ValidationError(crate::ValidationError::InvalidInputFormat(
405 "Invalid duration format".to_string(),
406 ))
407 })?;
408 hours * 3600.0 + minutes * 60.0 + secs
409 }
410 _ => {
411 return Err(TranscodeError::ValidationError(
412 crate::ValidationError::InvalidInputFormat("Invalid duration format".to_string()),
413 ))
414 }
415 };
416
417 Ok(seconds)
418}
419
420#[must_use]
422pub fn format_framerate(num: u32, den: u32) -> String {
423 let fps = f64::from(num) / f64::from(den);
424 if den == 1 {
425 format!("{num} fps")
426 } else {
427 format!("{fps:.2} fps")
428 }
429}
430
431#[must_use]
433pub fn is_standard_framerate(num: u32, den: u32) -> bool {
434 matches!(
435 (num, den),
436 (24 | 25 | 30 | 50 | 60, 1) | (24000 | 30000 | 60000, 1001)
437 )
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_estimate_encoding_time() {
446 let time = estimate_encoding_time(60.0, crate::QualityMode::Medium, (1920, 1080), false);
447 assert!(time > 0.0);
448 }
449
450 #[test]
451 fn test_estimate_file_size() {
452 let size = estimate_file_size(60.0, 5_000_000, 128_000);
453 assert_eq!(size, (60.0 * 5_128_000.0 / 8.0) as u64);
454 }
455
456 #[test]
457 fn test_format_duration() {
458 assert_eq!(format_duration(90.0), "01:30");
459 assert_eq!(format_duration(3665.0), "01:01:05");
460 }
461
462 #[test]
463 fn test_format_file_size() {
464 assert_eq!(format_file_size(1024), "1.00 KB");
465 assert_eq!(format_file_size(1024 * 1024), "1.00 MB");
466 assert_eq!(format_file_size(1024 * 1024 * 1024), "1.00 GB");
467 }
468
469 #[test]
470 fn test_format_bitrate() {
471 assert_eq!(format_bitrate(1_000_000), "1.00 Mbps");
472 assert_eq!(format_bitrate(128_000), "128 kbps");
473 }
474
475 #[test]
476 fn test_get_file_extension() {
477 assert_eq!(get_file_extension("video.mp4"), Some("mp4".to_string()));
478 assert_eq!(get_file_extension("VIDEO.MP4"), Some("mp4".to_string()));
479 assert_eq!(get_file_extension("video"), None);
480 }
481
482 #[test]
483 fn test_container_from_extension() {
484 assert_eq!(
485 container_from_extension("video.mp4"),
486 Some("mp4".to_string())
487 );
488 assert_eq!(
489 container_from_extension("video.mkv"),
490 Some("matroska".to_string())
491 );
492 assert_eq!(
493 container_from_extension("video.webm"),
494 Some("webm".to_string())
495 );
496 }
497
498 #[test]
499 fn test_suggest_codecs() {
500 assert_eq!(suggest_video_codec("mp4"), Some("h264".to_string()));
501 assert_eq!(suggest_video_codec("webm"), Some("vp9".to_string()));
502 assert_eq!(suggest_audio_codec("mp4"), Some("aac".to_string()));
503 assert_eq!(suggest_audio_codec("webm"), Some("opus".to_string()));
504 }
505
506 #[test]
507 fn test_calculate_aspect_ratio() {
508 assert_eq!(calculate_aspect_ratio(1920, 1080), (16, 9));
509 assert_eq!(calculate_aspect_ratio(1280, 720), (16, 9));
510 assert_eq!(calculate_aspect_ratio(1920, 800), (12, 5));
511 }
512
513 #[test]
514 fn test_format_aspect_ratio() {
515 assert_eq!(format_aspect_ratio(1920, 1080), "16:9");
516 assert_eq!(format_aspect_ratio(1280, 720), "16:9");
517 }
518
519 #[test]
520 fn test_is_standard_resolution() {
521 assert!(is_standard_resolution(1920, 1080));
522 assert!(is_standard_resolution(1280, 720));
523 assert!(!is_standard_resolution(1000, 1000));
524 }
525
526 #[test]
527 fn test_resolution_name() {
528 assert_eq!(resolution_name(1920, 1080), "Full HD (1080p)");
529 assert_eq!(resolution_name(3840, 2160), "4K (2160p)");
530 assert_eq!(resolution_name(1000, 1000), "1000x1000");
531 }
532
533 #[test]
534 fn test_calculate_optimal_tiles() {
535 let (cols, rows) = calculate_optimal_tiles(1920, 1080, 8);
536 assert!(cols > 0 && rows > 0);
537 }
538
539 #[test]
540 fn test_suggest_bitrate() {
541 let bitrate = suggest_bitrate(1920, 1080, 30.0, crate::QualityMode::Medium);
542 assert!(bitrate > 0);
543 }
544
545 #[test]
546 fn test_calculate_compression_ratio() {
547 assert_eq!(calculate_compression_ratio(1000, 500), 2.0);
548 assert_eq!(calculate_compression_ratio(500, 1000), 0.5);
549 }
550
551 #[test]
552 fn test_parse_duration() {
553 assert_eq!(parse_duration("60").expect("should succeed in test"), 60.0);
554 assert_eq!(
555 parse_duration("01:30").expect("should succeed in test"),
556 90.0
557 );
558 assert_eq!(
559 parse_duration("01:01:30").expect("should succeed in test"),
560 3690.0
561 );
562 }
563
564 #[test]
565 fn test_format_framerate() {
566 assert_eq!(format_framerate(30, 1), "30 fps");
567 assert_eq!(format_framerate(30000, 1001), "29.97 fps");
568 }
569
570 #[test]
571 fn test_is_standard_framerate() {
572 assert!(is_standard_framerate(30, 1));
573 assert!(is_standard_framerate(60, 1));
574 assert!(is_standard_framerate(30000, 1001));
575 assert!(!is_standard_framerate(45, 1));
576 }
577}