Extract audio from video files before loading
Video files (MP4, MKV, etc.) are now processed with ffmpeg to extract audio to a temp WAV file before loading into wavesurfer. This prevents the WebView crash caused by trying to fetch multi-GB files into memory. - New extract_audio Tauri command uses ffmpeg (sidecar-bundled or system) - Frontend detects video extensions and extracts audio automatically - User-friendly error if ffmpeg is not installed with install instructions - Reverted wavesurfer MediaElement approach in favor of clean extraction - Added FFmpeg install guide to USER_GUIDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
95
src-tauri/src/commands/media.rs
Normal file
95
src-tauri/src/commands/media.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
/// Extract audio from a video file to a WAV file using ffmpeg.
|
||||
/// Returns the path to the extracted audio file.
|
||||
#[tauri::command]
|
||||
pub fn extract_audio(file_path: String) -> Result<String, String> {
|
||||
let input = PathBuf::from(&file_path);
|
||||
if !input.exists() {
|
||||
return Err(format!("File not found: {}", file_path));
|
||||
}
|
||||
|
||||
// Output to a temp WAV file next to the original or in temp dir
|
||||
let stem = input.file_stem().unwrap_or_default().to_string_lossy();
|
||||
let output = std::env::temp_dir().join(format!("{stem}_audio.wav"));
|
||||
|
||||
eprintln!(
|
||||
"[media] Extracting audio: {} -> {}",
|
||||
input.display(),
|
||||
output.display()
|
||||
);
|
||||
|
||||
// Find ffmpeg — check sidecar extract dir first, then system PATH
|
||||
let ffmpeg = find_ffmpeg().ok_or("ffmpeg not found. Install ffmpeg or ensure it's in PATH.")?;
|
||||
|
||||
let status = Command::new(&ffmpeg)
|
||||
.args([
|
||||
"-y", // Overwrite output
|
||||
"-i",
|
||||
&file_path,
|
||||
"-vn", // No video
|
||||
"-acodec",
|
||||
"pcm_s16le", // WAV PCM 16-bit
|
||||
"-ar",
|
||||
"16000", // 16kHz (optimal for whisper)
|
||||
"-ac",
|
||||
"1", // Mono
|
||||
])
|
||||
.arg(output.to_str().unwrap())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to run ffmpeg: {e}"))?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(format!("ffmpeg exited with status {status}"));
|
||||
}
|
||||
|
||||
if !output.exists() {
|
||||
return Err("ffmpeg completed but output file not found".to_string());
|
||||
}
|
||||
|
||||
eprintln!("[media] Audio extracted successfully");
|
||||
Ok(output.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Find ffmpeg binary — check sidecar directory first, then system PATH.
|
||||
fn find_ffmpeg() -> Option<String> {
|
||||
// Check sidecar extract dir (ffmpeg is bundled with the sidecar)
|
||||
if let Some(data_dir) = crate::sidecar::DATA_DIR.get() {
|
||||
// Read sidecar version to find the right directory
|
||||
let version_file = data_dir.join("sidecar-version.txt");
|
||||
if let Ok(version) = std::fs::read_to_string(&version_file) {
|
||||
let version = version.trim();
|
||||
let sidecar_dir = data_dir.join(format!("sidecar-{version}"));
|
||||
let ffmpeg_name = if cfg!(target_os = "windows") {
|
||||
"ffmpeg.exe"
|
||||
} else {
|
||||
"ffmpeg"
|
||||
};
|
||||
let ffmpeg_path = sidecar_dir.join(ffmpeg_name);
|
||||
if ffmpeg_path.exists() {
|
||||
return Some(ffmpeg_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to system PATH
|
||||
let ffmpeg_name = if cfg!(target_os = "windows") {
|
||||
"ffmpeg.exe"
|
||||
} else {
|
||||
"ffmpeg"
|
||||
};
|
||||
if Command::new(ffmpeg_name)
|
||||
.arg("-version")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.is_ok()
|
||||
{
|
||||
return Some(ffmpeg_name.to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user