use anyhow::{Context, Result, anyhow}; use std::sync::mpsc::Sender; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; /// Executa o FFmpeg de forma assíncrona, capturando stderr em tempo real. /// As linhas de saída são enviadas pelo canal `progress_tx` (std::sync::mpsc). /// Um sinal enviado em `cancel_rx` encerra o processo imediatamente. pub async fn run_ffmpeg_async( args: Vec, progress_tx: Sender, cancel_rx: tokio::sync::oneshot::Receiver<()>, ) -> Result<()> { let mut child = Command::new("ffmpeg") .args(&args) .stdin(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) .stdout(std::process::Stdio::null()) .spawn() .context("Falha ao iniciar ffmpeg")?; // Lê stderr em stream para exibir progresso em tempo real if let Some(stderr) = child.stderr.take() { let reader = BufReader::new(stderr); let mut lines = reader.lines(); let tx = progress_tx.clone(); tokio::spawn(async move { while let Ok(Some(line)) = lines.next_line().await { let _ = tx.send(line); } }); } tokio::select! { result = child.wait() => { let status = result.context("Erro ao aguardar ffmpeg")?; if !status.success() { return Err(anyhow!( "FFmpeg encerrou com código de saída: {:?}", status.code() )); } Ok(()) } _ = cancel_rx => { let _ = child.kill().await; Err(anyhow!("Cancelado pelo usuário")) } } } /// Verifica se um binário está disponível no PATH. pub fn check_binary_available(binary: &str) -> Result<()> { std::process::Command::new(binary) .arg("-version") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() .map_err(|_| { anyhow!( "'{}' não encontrado no PATH. Por favor, instale o FFmpeg.", binary ) })?; Ok(()) } /// Verifica se ffmpeg e ffprobe estão disponíveis. pub fn validate_dependencies() -> Result<()> { check_binary_available("ffmpeg")?; check_binary_available("ffprobe")?; Ok(()) }