simple_editor\media/
proxy.rs1use crate::engine::export::{self, Progress};
9use crate::media::ffpipe;
10use std::io::BufRead;
11use std::path::PathBuf;
12use std::process::Stdio;
13use std::sync::Arc;
14
15pub fn dir() -> PathBuf {
16 crate::settings::Settings::cache_dir().join("proxies")
17}
18
19pub fn proxy_path(src: &str, height: u32) -> PathBuf {
21 use std::hash::{Hash, Hasher};
22 let mtime = std::fs::metadata(src)
23 .and_then(|m| m.modified())
24 .ok()
25 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
26 .map(|d| d.as_secs())
27 .unwrap_or(0);
28 let mut h = std::collections::hash_map::DefaultHasher::new();
29 (src.to_ascii_lowercase(), mtime, height).hash(&mut h);
30 dir().join(format!("{:016x}.mp4", h.finish()))
31}
32
33pub fn generate(src: String, dst: PathBuf, height: u32) -> Arc<Progress> {
36 export::spawn_job("proxy", move |prog| run(&src, &dst, height, prog))
37}
38
39fn run(src: &str, dst: &PathBuf, height: u32, prog: &Progress) -> Result<(), String> {
40 let ffmpeg = ffpipe::ffmpeg_exe().ok_or("ffmpeg.exe not found")?;
41 std::fs::create_dir_all(dir()).map_err(|e| format!("proxies dir: {e}"))?;
42 let dur = crate::engine::convert::probe_seconds(std::path::Path::new(src)).unwrap_or(0.0);
43 let tmp = export::temp_output(dst);
44 let mut cmd = ffpipe::command(&ffmpeg);
45 cmd.args(["-y", "-hide_banner", "-loglevel", "error", "-progress", "pipe:1"]);
46 cmd.arg("-i").arg(src);
47 let vf = format!("scale=-2:{}", height.max(120));
49 cmd.args(["-vf", &vf, "-c:v", "libx264", "-preset", "veryfast", "-g", "1", "-crf", "20"]);
50 cmd.args(["-pix_fmt", "yuv420p", "-an", "-movflags", "+faststart"]);
51 cmd.arg(&tmp.0);
52 cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
53 let mut child = cmd.spawn().map_err(|e| format!("ffmpeg: {e}"))?;
54 let tail = export::stderr_tail(&mut child);
55 if let Some(out) = child.stdout.take() {
56 prog.set(0.0, "Building proxy…");
57 for line in std::io::BufReader::new(out).lines() {
58 let Ok(line) = line else { break };
59 if prog.is_cancelled() {
60 let _ = child.kill();
61 break;
62 }
63 if let Some(us) = line.strip_prefix("out_time_us=").and_then(|v| v.trim().parse::<f64>().ok()) {
64 let f = if dur > 0.0 { (us / 1e6 / dur).clamp(0.0, 1.0).min(0.99) as f32 } else { 0.0 };
65 prog.set(f, "Building proxy…");
66 }
67 }
68 }
69 export::wait_ffmpeg(&mut child, tail, prog)?;
70 tmp.commit(dst.as_path())
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
79 fn proxy_path_is_deterministic() {
80 let a = proxy_path("C:\\missing\\clip.mp4", 720);
81 assert_eq!(a, proxy_path("C:\\missing\\clip.mp4", 720));
82 assert_eq!(a, proxy_path("C:\\MISSING\\CLIP.mp4", 720), "case-insensitive paths hash alike");
83 assert_ne!(a, proxy_path("C:\\missing\\clip.mp4", 540));
84 assert_ne!(a, proxy_path("C:\\missing\\other.mp4", 720));
85 assert!(a.extension().is_some_and(|e| e == "mp4"));
86 }
87}