jugar_probar/av_sync/
extraction.rs1use crate::result::ProbarError;
7use std::path::Path;
8
9pub const DEFAULT_SAMPLE_RATE: u32 = 48000;
11
12#[must_use]
16pub fn build_ffmpeg_args(video_path: &Path, sample_rate: u32) -> Vec<String> {
17 vec![
18 "-i".to_string(),
19 video_path.to_string_lossy().to_string(),
20 "-f".to_string(),
21 "f32le".to_string(),
22 "-acodec".to_string(),
23 "pcm_f32le".to_string(),
24 "-ac".to_string(),
25 "1".to_string(),
26 "-ar".to_string(),
27 sample_rate.to_string(),
28 "pipe:1".to_string(),
29 ]
30}
31
32pub fn extract_audio(video_path: &Path, sample_rate: u32) -> Result<Vec<f32>, ProbarError> {
40 let args = build_ffmpeg_args(video_path, sample_rate);
41
42 let output = std::process::Command::new("ffmpeg")
43 .args(&args)
44 .stdout(std::process::Stdio::piped())
45 .stderr(std::process::Stdio::piped())
46 .output()
47 .map_err(|e| ProbarError::FfmpegError {
48 message: format!("Failed to execute ffmpeg: {e}"),
49 })?;
50
51 if !output.status.success() {
52 let stderr = String::from_utf8_lossy(&output.stderr);
53 return Err(ProbarError::FfmpegError {
54 message: format!("ffmpeg exited with {}: {}", output.status, stderr),
55 });
56 }
57
58 let bytes = &output.stdout;
60 if bytes.len() % 4 != 0 {
61 return Err(ProbarError::FfmpegError {
62 message: format!(
63 "ffmpeg output length {} is not a multiple of 4 bytes",
64 bytes.len()
65 ),
66 });
67 }
68
69 let samples: Vec<f32> = bytes
70 .chunks_exact(4)
71 .map(|chunk| {
72 let arr: [u8; 4] = [chunk[0], chunk[1], chunk[2], chunk[3]];
73 f32::from_le_bytes(arr)
74 })
75 .collect();
76
77 Ok(samples)
78}
79
80#[must_use]
84pub fn default_edl_path(video_path: &Path) -> std::path::PathBuf {
85 let stem = video_path.file_stem().unwrap_or_default();
86 let edl_name = format!("{}.edl.json", stem.to_string_lossy());
87 match video_path.parent() {
88 Some(parent) if !parent.as_os_str().is_empty() => parent.join(edl_name),
89 _ => std::path::PathBuf::from(edl_name),
90 }
91}
92
93#[cfg(test)]
94#[allow(clippy::unwrap_used, clippy::expect_used)]
95mod tests {
96 use super::*;
97 use std::path::PathBuf;
98
99 #[test]
100 fn test_build_ffmpeg_args() {
101 let path = Path::new("/tmp/video.mp4");
102 let args = build_ffmpeg_args(path, 48000);
103 assert_eq!(args[0], "-i");
104 assert_eq!(args[1], "/tmp/video.mp4");
105 assert_eq!(args[2], "-f");
106 assert_eq!(args[3], "f32le");
107 assert_eq!(args[4], "-acodec");
108 assert_eq!(args[5], "pcm_f32le");
109 assert_eq!(args[6], "-ac");
110 assert_eq!(args[7], "1");
111 assert_eq!(args[8], "-ar");
112 assert_eq!(args[9], "48000");
113 assert_eq!(args[10], "pipe:1");
114 }
115
116 #[test]
117 fn test_build_ffmpeg_args_custom_rate() {
118 let path = Path::new("test.mp4");
119 let args = build_ffmpeg_args(path, 44100);
120 assert_eq!(args[9], "44100");
121 }
122
123 #[test]
124 fn test_build_ffmpeg_args_length() {
125 let path = Path::new("test.mp4");
126 let args = build_ffmpeg_args(path, 48000);
127 assert_eq!(args.len(), 11);
128 }
129
130 #[test]
131 fn test_default_edl_path_mp4() {
132 let video = PathBuf::from("/output/demo-bench.mp4");
133 let edl = default_edl_path(&video);
134 assert_eq!(edl, PathBuf::from("/output/demo-bench.edl.json"));
135 }
136
137 #[test]
138 fn test_default_edl_path_mov() {
139 let video = PathBuf::from("/output/render.mov");
140 let edl = default_edl_path(&video);
141 assert_eq!(edl, PathBuf::from("/output/render.edl.json"));
142 }
143
144 #[test]
145 fn test_default_edl_path_no_parent() {
146 let video = PathBuf::from("video.mp4");
147 let edl = default_edl_path(&video);
148 assert_eq!(edl, PathBuf::from("video.edl.json"));
149 }
150
151 #[test]
152 fn test_default_edl_path_nested() {
153 let video = PathBuf::from("/a/b/c/test.mp4");
154 let edl = default_edl_path(&video);
155 assert_eq!(edl, PathBuf::from("/a/b/c/test.edl.json"));
156 }
157
158 #[test]
159 fn test_default_sample_rate() {
160 assert_eq!(DEFAULT_SAMPLE_RATE, 48000);
161 }
162
163 #[test]
164 fn test_extract_audio_missing_ffmpeg() {
165 let result = extract_audio(Path::new("/nonexistent/video.mp4"), 48000);
167 assert!(result.is_err());
168 }
169}