initial commit with full project
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user