initial commit with full project

This commit is contained in:
2026-04-26 17:50:04 +03:00
parent 021f0bcb23
commit 824591084b
71 changed files with 34115 additions and 537 deletions
+312
View File
@@ -0,0 +1,312 @@
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
}