304 lines
9.9 KiB
Rust
304 lines
9.9 KiB
Rust
use std::collections::HashMap;
|
|
use std::io::{BufRead, BufReader};
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Instant;
|
|
|
|
#[cfg(windows)]
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
use tauri::{Emitter, AppHandle};
|
|
|
|
use crate::config;
|
|
use crate::ffmpeg::commands::{self, FfmpegCommand};
|
|
use crate::types::*;
|
|
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
|
|
pub type JobMap = Arc<Mutex<HashMap<String, Child>>>;
|
|
|
|
pub fn new_job_map() -> JobMap {
|
|
Arc::new(Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
pub struct RunResult {
|
|
pub success: bool,
|
|
}
|
|
|
|
fn spawn_hidden(cmd: &mut Command) -> std::io::Result<Child> {
|
|
#[cfg(windows)]
|
|
{
|
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
|
}
|
|
cmd.spawn()
|
|
}
|
|
|
|
pub fn run_ffmpeg(
|
|
ffmpeg_path: &str,
|
|
cmd: &FfmpegCommand,
|
|
job_id: &str,
|
|
total_duration: f64,
|
|
phase: &str,
|
|
app: &AppHandle,
|
|
jobs: &JobMap,
|
|
) -> Result<RunResult, String> {
|
|
let mut proc = Command::new(ffmpeg_path);
|
|
proc.args(&cmd.args)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let mut child = spawn_hidden(&mut proc)
|
|
.map_err(|e| format!("Failed to spawn ffmpeg: {}", e))?;
|
|
|
|
let stdout = child.stdout.take();
|
|
|
|
{
|
|
let mut map = jobs.lock().map_err(|e| e.to_string())?;
|
|
map.insert(job_id.to_string(), child);
|
|
}
|
|
|
|
let start = Instant::now();
|
|
let mut last_progress = ProgressEvent {
|
|
job_id: job_id.to_string(),
|
|
percent: 0.0,
|
|
fps: 0.0,
|
|
bitrate: String::new(),
|
|
size_current: 0,
|
|
time_elapsed: 0.0,
|
|
eta_seconds: 0.0,
|
|
phase: phase.to_string(),
|
|
message: None,
|
|
};
|
|
|
|
if let Some(out) = stdout {
|
|
let reader = BufReader::new(out);
|
|
for line in reader.lines() {
|
|
let line = match line {
|
|
Ok(l) => l,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
|
if parts.len() != 2 {
|
|
continue;
|
|
}
|
|
|
|
let key = parts[0].trim();
|
|
let val = parts[1].trim();
|
|
|
|
match key {
|
|
"out_time_us" => {
|
|
if let Ok(us) = val.parse::<f64>() {
|
|
let secs = us / 1_000_000.0;
|
|
let elapsed = start.elapsed().as_secs_f64();
|
|
last_progress.time_elapsed = elapsed;
|
|
|
|
if total_duration > 0.0 {
|
|
let pct = (secs / total_duration * 100.0).min(100.0).max(0.0);
|
|
last_progress.percent = pct;
|
|
|
|
if pct > 0.0 {
|
|
let remaining = elapsed / pct * (100.0 - pct);
|
|
last_progress.eta_seconds = remaining;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"fps" => {
|
|
last_progress.fps = val.parse::<f64>().unwrap_or(0.0);
|
|
}
|
|
"bitrate" => {
|
|
last_progress.bitrate = val.to_string();
|
|
}
|
|
"total_size" => {
|
|
last_progress.size_current = val.parse::<u64>().unwrap_or(0);
|
|
}
|
|
"progress" => {
|
|
let _ = app.emit("progress", &last_progress);
|
|
|
|
if val == "end" {
|
|
last_progress.percent = 100.0;
|
|
last_progress.phase = "done".into();
|
|
let _ = app.emit("progress", &last_progress);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
let status = {
|
|
let mut map = jobs.lock().map_err(|e| e.to_string())?;
|
|
if let Some(mut child) = map.remove(job_id) {
|
|
child.wait().map_err(|e| e.to_string())?
|
|
} else {
|
|
return Err("Job was cancelled".into());
|
|
}
|
|
};
|
|
|
|
Ok(RunResult {
|
|
success: status.success(),
|
|
})
|
|
}
|
|
|
|
pub fn run_ffmpeg_silent(
|
|
ffmpeg_path: &str,
|
|
cmd: &FfmpegCommand,
|
|
) -> Result<(), String> {
|
|
let mut proc = Command::new(ffmpeg_path);
|
|
proc.args(&cmd.args)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::piped());
|
|
|
|
let child = spawn_hidden(&mut proc)
|
|
.map_err(|e| format!("Failed to spawn ffmpeg: {}", e))?;
|
|
|
|
let status = child.wait_with_output().map_err(|e| e.to_string())?;
|
|
|
|
if status.status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("FFmpeg process failed".into())
|
|
}
|
|
}
|
|
|
|
pub fn cancel_job(job_id: &str, jobs: &JobMap) -> Result<(), String> {
|
|
let mut map = jobs.lock().map_err(|e| e.to_string())?;
|
|
if let Some(mut child) = map.remove(job_id) {
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
Ok(())
|
|
} else {
|
|
Err("No active job found".into())
|
|
}
|
|
}
|
|
|
|
pub fn run_compress_with_retry(
|
|
ffmpeg_path: &str,
|
|
input: &str,
|
|
output: &str,
|
|
settings: &CompressSettings,
|
|
trim: Option<&TrimRange>,
|
|
hw_info: &HardwareInfo,
|
|
app_config: &AppConfig,
|
|
job_id: &str,
|
|
total_duration: f64,
|
|
has_audio: bool,
|
|
app: &AppHandle,
|
|
jobs: &JobMap,
|
|
) -> Result<(String, u32), String> {
|
|
let encoder = commands::select_encoder(&settings.video_codec, &settings.hw_accel, hw_info);
|
|
let is_hw = commands::is_hw_encoder(&encoder);
|
|
|
|
match &settings.strategy {
|
|
SizingStrategy::CRF { value } => {
|
|
let cmd = commands::build_crf_command(
|
|
input, output, settings, *value, &encoder, trim, has_audio,
|
|
);
|
|
let result = run_ffmpeg(ffmpeg_path, &cmd, job_id, total_duration, "encoding", app, jobs)?;
|
|
if !result.success {
|
|
return Err("Encoding failed".into());
|
|
}
|
|
Ok((output.to_string(), 1))
|
|
}
|
|
|
|
SizingStrategy::TargetBitrate { kbps } => {
|
|
let cmd = commands::build_bitrate_command(
|
|
input, output, settings, *kbps, &encoder, trim, has_audio,
|
|
);
|
|
let result = run_ffmpeg(ffmpeg_path, &cmd, job_id, total_duration, "encoding", app, jobs)?;
|
|
if !result.success {
|
|
return Err("Encoding failed".into());
|
|
}
|
|
Ok((output.to_string(), 1))
|
|
}
|
|
|
|
SizingStrategy::TargetSize { mb } => {
|
|
let audio_kbps = match settings.audio_codec {
|
|
AudioCodec::None => 0,
|
|
_ => settings.audio_bitrate,
|
|
};
|
|
let mut bitrate_kbps = commands::calculate_bitrate(*mb, total_duration, audio_kbps);
|
|
let target_bytes = (*mb * 1024.0 * 1024.0) as u64;
|
|
let max_attempts = app_config.max_retry_attempts;
|
|
let threshold = app_config.retry_threshold_percent;
|
|
|
|
for attempt in 1..=max_attempts {
|
|
let phase = if attempt == 1 { "encoding" } else { "retrying" };
|
|
|
|
if is_hw {
|
|
let cmd = commands::build_compress_hw(
|
|
input, output, settings, bitrate_kbps, &encoder, trim, has_audio,
|
|
);
|
|
let result = run_ffmpeg(ffmpeg_path, &cmd, job_id, total_duration, phase, app, jobs)?;
|
|
if !result.success {
|
|
return Err("Encoding failed".into());
|
|
}
|
|
} else {
|
|
let passlog_dir = config::ensure_temp_subdir(&format!("passlog/{}", job_id));
|
|
let passlog_prefix = passlog_dir.join("ffmpeg2pass").to_string_lossy().to_string();
|
|
|
|
let pass1 = commands::build_compress_pass1(
|
|
input, settings, bitrate_kbps, &passlog_prefix,
|
|
);
|
|
let _ = app.emit("progress", &ProgressEvent {
|
|
job_id: job_id.to_string(),
|
|
percent: 0.0,
|
|
fps: 0.0,
|
|
bitrate: String::new(),
|
|
size_current: 0,
|
|
time_elapsed: 0.0,
|
|
eta_seconds: 0.0,
|
|
phase: "analyzing".into(),
|
|
message: Some(format!("Pass 1 of 2 (attempt {})", attempt)),
|
|
});
|
|
|
|
run_ffmpeg_silent(ffmpeg_path, &pass1)?;
|
|
|
|
let pass2 = commands::build_compress_pass2(
|
|
input, output, settings, bitrate_kbps, &passlog_prefix, trim, has_audio,
|
|
);
|
|
let result = run_ffmpeg(ffmpeg_path, &pass2, job_id, total_duration, phase, app, jobs)?;
|
|
if !result.success {
|
|
return Err("Encoding failed".into());
|
|
}
|
|
}
|
|
|
|
let output_size = std::fs::metadata(output)
|
|
.map(|m| m.len())
|
|
.unwrap_or(0);
|
|
|
|
if output_size <= target_bytes {
|
|
return Ok((output.to_string(), attempt));
|
|
}
|
|
|
|
let overshoot_pct = ((output_size as f64 - target_bytes as f64) / target_bytes as f64) * 100.0;
|
|
|
|
if overshoot_pct <= threshold {
|
|
return Ok((output.to_string(), attempt));
|
|
}
|
|
|
|
if attempt < max_attempts {
|
|
let _ = app.emit("compress-retry", &serde_json::json!({
|
|
"job_id": job_id,
|
|
"attempt": attempt + 1,
|
|
"reason": format!(
|
|
"Output {:.1}MB exceeds {:.1}MB target by {:.1}%",
|
|
output_size as f64 / 1024.0 / 1024.0,
|
|
mb,
|
|
overshoot_pct
|
|
),
|
|
"adjusted_bitrate": bitrate_kbps
|
|
}));
|
|
|
|
let ratio = target_bytes as f64 / output_size as f64;
|
|
bitrate_kbps = ((bitrate_kbps as f64) * ratio * 0.95) as u32;
|
|
bitrate_kbps = bitrate_kbps.max(10);
|
|
}
|
|
}
|
|
|
|
Ok((output.to_string(), max_attempts))
|
|
}
|
|
}
|
|
}
|