313 lines
9.2 KiB
Rust
313 lines
9.2 KiB
Rust
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<TrimRange>,
|
|
app: AppHandle,
|
|
state: State<'_, AppState>,
|
|
) -> Result<OutputInfo, String> {
|
|
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<bool>,
|
|
app: AppHandle,
|
|
state: State<'_, AppState>,
|
|
) -> Result<OutputInfo, String> {
|
|
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
|
|
}
|