use crate::result::ProbarError;
use std::path::Path;
pub const DEFAULT_SAMPLE_RATE: u32 = 48000;
#[must_use]
pub fn build_ffmpeg_args(video_path: &Path, sample_rate: u32) -> Vec<String> {
vec![
"-i".to_string(),
video_path.to_string_lossy().to_string(),
"-f".to_string(),
"f32le".to_string(),
"-acodec".to_string(),
"pcm_f32le".to_string(),
"-ac".to_string(),
"1".to_string(),
"-ar".to_string(),
sample_rate.to_string(),
"pipe:1".to_string(),
]
}
pub fn extract_audio(video_path: &Path, sample_rate: u32) -> Result<Vec<f32>, ProbarError> {
let args = build_ffmpeg_args(video_path, sample_rate);
let output = std::process::Command::new("ffmpeg")
.args(&args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| ProbarError::FfmpegError {
message: format!("Failed to execute ffmpeg: {e}"),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ProbarError::FfmpegError {
message: format!("ffmpeg exited with {}: {}", output.status, stderr),
});
}
let bytes = &output.stdout;
if bytes.len() % 4 != 0 {
return Err(ProbarError::FfmpegError {
message: format!(
"ffmpeg output length {} is not a multiple of 4 bytes",
bytes.len()
),
});
}
let samples: Vec<f32> = bytes
.chunks_exact(4)
.map(|chunk| {
let arr: [u8; 4] = [chunk[0], chunk[1], chunk[2], chunk[3]];
f32::from_le_bytes(arr)
})
.collect();
Ok(samples)
}
#[must_use]
pub fn default_edl_path(video_path: &Path) -> std::path::PathBuf {
let stem = video_path.file_stem().unwrap_or_default();
let edl_name = format!("{}.edl.json", stem.to_string_lossy());
match video_path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.join(edl_name),
_ => std::path::PathBuf::from(edl_name),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_build_ffmpeg_args() {
let path = Path::new("/tmp/video.mp4");
let args = build_ffmpeg_args(path, 48000);
assert_eq!(args[0], "-i");
assert_eq!(args[1], "/tmp/video.mp4");
assert_eq!(args[2], "-f");
assert_eq!(args[3], "f32le");
assert_eq!(args[4], "-acodec");
assert_eq!(args[5], "pcm_f32le");
assert_eq!(args[6], "-ac");
assert_eq!(args[7], "1");
assert_eq!(args[8], "-ar");
assert_eq!(args[9], "48000");
assert_eq!(args[10], "pipe:1");
}
#[test]
fn test_build_ffmpeg_args_custom_rate() {
let path = Path::new("test.mp4");
let args = build_ffmpeg_args(path, 44100);
assert_eq!(args[9], "44100");
}
#[test]
fn test_build_ffmpeg_args_length() {
let path = Path::new("test.mp4");
let args = build_ffmpeg_args(path, 48000);
assert_eq!(args.len(), 11);
}
#[test]
fn test_default_edl_path_mp4() {
let video = PathBuf::from("/output/demo-bench.mp4");
let edl = default_edl_path(&video);
assert_eq!(edl, PathBuf::from("/output/demo-bench.edl.json"));
}
#[test]
fn test_default_edl_path_mov() {
let video = PathBuf::from("/output/render.mov");
let edl = default_edl_path(&video);
assert_eq!(edl, PathBuf::from("/output/render.edl.json"));
}
#[test]
fn test_default_edl_path_no_parent() {
let video = PathBuf::from("video.mp4");
let edl = default_edl_path(&video);
assert_eq!(edl, PathBuf::from("video.edl.json"));
}
#[test]
fn test_default_edl_path_nested() {
let video = PathBuf::from("/a/b/c/test.mp4");
let edl = default_edl_path(&video);
assert_eq!(edl, PathBuf::from("/a/b/c/test.edl.json"));
}
#[test]
fn test_default_sample_rate() {
assert_eq!(DEFAULT_SAMPLE_RATE, 48000);
}
#[test]
fn test_extract_audio_missing_ffmpeg() {
let result = extract_audio(Path::new("/nonexistent/video.mp4"), 48000);
assert!(result.is_err());
}
}