use std::fs; use tauri::{AppHandle, State}; use crate::config; use crate::ffmpeg::{commands, probe, runner}; use crate::recovery; use crate::types::*; use crate::AppState; #[tauri::command] pub async fn compress( input: String, output: String, settings: CompressSettings, trim: Option, app: AppHandle, state: State<'_, AppState>, ) -> Result { let ffmpeg_path = { let cfg = state.config.lock().map_err(|e| e.to_string())?; cfg.ffmpeg_path.clone() }; let ffmpeg = ffmpeg_path.unwrap_or_else(|| "ffmpeg".into()); let app_config = { let cfg = state.config.lock().map_err(|e| e.to_string())?; cfg.clone() }; let hw_info = { let hw = state.hw_info.lock().map_err(|e| e.to_string())?; hw.clone().unwrap_or(HardwareInfo { nvenc: false, qsv: false, amf: false, nvenc_codecs: Vec::new(), qsv_codecs: Vec::new(), amf_codecs: Vec::new(), }) }; let job_id = uuid::Uuid::new_v4().to_string(); let jobs = state.jobs.clone(); let info = { let input = input.clone(); let ffmpeg = ffmpeg.clone(); tokio::task::spawn_blocking(move || probe::probe_video(&input, &ffmpeg)) .await .map_err(|e| e.to_string())?? }; let duration = match &trim { Some(t) => t.end - t.start, None => info.duration, }; let has_audio = info.audio_codec.is_some(); let settings_json = serde_json::to_string(&settings).unwrap_or_default(); recovery::write_job_info(&config::temp_dir(), &input, &output, "compress", &settings_json); let (out_path, attempts) = { let input = input.clone(); let output = output.clone(); let settings = settings.clone(); let trim = trim.clone(); let ffmpeg = ffmpeg.clone(); let hw = hw_info.clone(); let cfg = app_config.clone(); let jid = job_id.clone(); let app = app.clone(); let jobs = jobs.clone(); tokio::task::spawn_blocking(move || { runner::run_compress_with_retry( &ffmpeg, &input, &output, &settings, trim.as_ref(), &hw, &cfg, &jid, duration, has_audio, &app, &jobs, ) }) .await .map_err(|e| e.to_string())?? }; recovery::delete_job_info(&config::temp_dir()); let out_info = { let path = out_path.clone(); let ffmpeg = ffmpeg.clone(); tokio::task::spawn_blocking(move || probe::probe_video(&path, &ffmpeg)) .await .map_err(|e| e.to_string())?? }; let file_size = fs::metadata(&out_path).map(|m| m.len()).unwrap_or(0); Ok(OutputInfo { path: out_path, file_size, duration: out_info.duration, width: out_info.width, height: out_info.height, video_codec: out_info.video_codec, video_bitrate: out_info.video_bitrate, audio_codec: out_info.audio_codec, audio_bitrate: out_info.audio_bitrate, attempts, }) } #[tauri::command] pub async fn trim( input: String, output: String, range: TrimRange, smart_cut: bool, strip_audio: Option, app: AppHandle, state: State<'_, AppState>, ) -> Result { let ffmpeg_path = { let cfg = state.config.lock().map_err(|e| e.to_string())?; cfg.ffmpeg_path.clone() }; let ffmpeg = ffmpeg_path.unwrap_or_else(|| "ffmpeg".into()); let job_id = uuid::Uuid::new_v4().to_string(); let jobs = state.jobs.clone(); let range_json = serde_json::to_string(&range).unwrap_or_default(); recovery::write_job_info(&config::temp_dir(), &input, &output, "trim", &range_json); let do_strip = strip_audio.unwrap_or(false); if smart_cut { do_smart_cut(&input, &output, &range, &ffmpeg, &job_id, &app, &jobs, do_strip).await?; } else { let cmd = commands::build_trim_keyframe(&input, &output, &range, do_strip); let ffmpeg_c = ffmpeg.clone(); let jid = job_id.clone(); let duration = range.end - range.start; let app_c = app.clone(); let jobs_c = jobs.clone(); tokio::task::spawn_blocking(move || { runner::run_ffmpeg(&ffmpeg_c, &cmd, &jid, duration, "encoding", &app_c, &jobs_c) }) .await .map_err(|e| e.to_string())??; } recovery::delete_job_info(&config::temp_dir()); let out_info = { let path = output.clone(); let ffmpeg = ffmpeg.clone(); tokio::task::spawn_blocking(move || probe::probe_video(&path, &ffmpeg)) .await .map_err(|e| e.to_string())?? }; let file_size = fs::metadata(&output).map(|m| m.len()).unwrap_or(0); Ok(OutputInfo { path: output, file_size, duration: out_info.duration, width: out_info.width, height: out_info.height, video_codec: out_info.video_codec, video_bitrate: out_info.video_bitrate, audio_codec: out_info.audio_codec, audio_bitrate: out_info.audio_bitrate, attempts: 1, }) } async fn do_smart_cut( input: &str, output: &str, range: &TrimRange, ffmpeg: &str, job_id: &str, app: &AppHandle, jobs: &runner::JobMap, strip_audio: bool, ) -> Result<(), String> { let info = { let input = input.to_string(); let ffmpeg = ffmpeg.to_string(); tokio::task::spawn_blocking(move || probe::probe_video(&input, &ffmpeg)) .await .map_err(|e| e.to_string())?? }; let keyframes = &info.keyframe_times; let first_kf = keyframes .iter() .find(|&&t| t >= range.start) .copied() .unwrap_or(range.start); let last_kf = keyframes .iter() .rev() .find(|&&t| t <= range.end) .copied() .unwrap_or(range.end); let has_audio = info.audio_codec.is_some() && !strip_audio; if first_kf >= last_kf || (range.end - range.start) < 5.0 { let cmd = commands::build_trim_keyframe(input, output, range, strip_audio); let ffmpeg = ffmpeg.to_string(); let jid = job_id.to_string(); let dur = range.end - range.start; let app = app.clone(); let jobs = jobs.clone(); tokio::task::spawn_blocking(move || { runner::run_ffmpeg(&ffmpeg, &cmd, &jid, dur, "encoding", &app, &jobs) }) .await .map_err(|e| e.to_string())??; return Ok(()); } let work_dir = config::ensure_temp_subdir(&format!("smartcut/{}", job_id)); let head_path = work_dir.join("head.mp4").to_string_lossy().to_string(); let middle_path = work_dir.join("middle.mp4").to_string_lossy().to_string(); let tail_path = work_dir.join("tail.mp4").to_string_lossy().to_string(); let filelist_path = work_dir.join("filelist.txt").to_string_lossy().to_string(); // head { let cmd = commands::build_smart_cut_head(input, &head_path, range.start, first_kf, has_audio); let ffmpeg = ffmpeg.to_string(); let jid = job_id.to_string(); let dur = first_kf - range.start; let app = app.clone(); let jobs = jobs.clone(); tokio::task::spawn_blocking(move || { runner::run_ffmpeg(&ffmpeg, &cmd, &jid, dur, "encoding", &app, &jobs) }) .await .map_err(|e| e.to_string())??; } // middle { let cmd = commands::build_smart_cut_middle(input, &middle_path, first_kf, last_kf); let ffmpeg = ffmpeg.to_string(); tokio::task::spawn_blocking(move || runner::run_ffmpeg_silent(&ffmpeg, &cmd)) .await .map_err(|e| e.to_string())??; } // tail { let cmd = commands::build_smart_cut_tail(input, &tail_path, last_kf, range.end, has_audio); let ffmpeg = ffmpeg.to_string(); let jid = job_id.to_string(); let dur = range.end - last_kf; let app = app.clone(); let jobs = jobs.clone(); tokio::task::spawn_blocking(move || { runner::run_ffmpeg(&ffmpeg, &cmd, &jid, dur, "encoding", &app, &jobs) }) .await .map_err(|e| e.to_string())??; } // concat let filelist_content = format!( "file '{}'\nfile '{}'\nfile '{}'", head_path.replace('\\', "/"), middle_path.replace('\\', "/"), tail_path.replace('\\', "/"), ); fs::write(&filelist_path, &filelist_content).map_err(|e| e.to_string())?; { let cmd = commands::build_concat(&filelist_path, output); let ffmpeg = ffmpeg.to_string(); tokio::task::spawn_blocking(move || runner::run_ffmpeg_silent(&ffmpeg, &cmd)) .await .map_err(|e| e.to_string())??; } // cleanup temp let _ = fs::remove_dir_all(&work_dir); Ok(()) } #[tauri::command] pub async fn cancel_job(job_id: String, state: State<'_, AppState>) -> Result<(), String> { let result = runner::cancel_job(&job_id, &state.jobs); recovery::delete_job_info(&config::temp_dir()); result }