aether_renderer_core/ffmpeg/
gif.rs

1use std::fs;
2use std::process::Command;
3
4/// Render a GIF using palettegen + paletteuse filters
5pub fn render_gif(
6    input_pattern: &str,
7    output: &str,
8    fps: u32,
9    fade_filter: Option<&str>,
10    verbose_ffmpeg: bool,
11) -> Result<(), String> {
12    let palette_path = "palette.png";
13
14    let mut palette_args: Vec<String> = Vec::new();
15    if input_pattern.contains('*') {
16        palette_args.push("-pattern_type".into());
17        palette_args.push("glob".into());
18    }
19    palette_args.push("-i".into());
20    palette_args.push(input_pattern.into());
21    palette_args.push("-vf".into());
22    palette_args.push("fps=30,scale=640:-1:flags=lanczos,palettegen".into());
23    palette_args.push("-y".into());
24    palette_args.push(palette_path.into());
25
26    let palette_status = match {
27        let mut cmd = Command::new("ffmpeg");
28        if !verbose_ffmpeg {
29            cmd.args(["-loglevel", "warning"]);
30        }
31        cmd.args(&palette_args).status()
32    } {
33        Ok(s) => s,
34        Err(e) => {
35            if e.kind() == std::io::ErrorKind::NotFound {
36                return Err(
37                    "❌ ffmpeg not found. Please install ffmpeg and ensure it is in your PATH."
38                        .into(),
39                );
40            } else {
41                return Err(format!("❌ Failed to execute ffmpeg: {}", e));
42            }
43        }
44    };
45
46    if !palette_status.success() {
47        return Err("❌ Failed to generate palette".into());
48    }
49
50    let mut gif_filter = String::from("fps=30,scale=640:-1:flags=lanczos");
51    if let Some(filter) = fade_filter {
52        if !filter.is_empty() {
53            gif_filter.push(',');
54            gif_filter.push_str(filter);
55        }
56    }
57    let mut gif_args: Vec<String> = vec!["-framerate".into(), fps.to_string()];
58    if input_pattern.contains('*') {
59        gif_args.push("-pattern_type".into());
60        gif_args.push("glob".into());
61    }
62    gif_args.push("-i".into());
63    gif_args.push(input_pattern.into());
64    gif_args.push("-i".into());
65    gif_args.push(palette_path.into());
66    gif_args.push("-lavfi".into());
67    gif_args.push(format!("{} [x]; [x][1:v] paletteuse", gif_filter));
68    gif_args.push("-y".into());
69    gif_args.push(output.into());
70
71    let gif_status = match {
72        let mut cmd = Command::new("ffmpeg");
73        if !verbose_ffmpeg {
74            cmd.args(["-loglevel", "warning"]);
75        }
76        cmd.args(&gif_args).status()
77    } {
78        Ok(s) => s,
79        Err(e) => {
80            if e.kind() == std::io::ErrorKind::NotFound {
81                return Err(
82                    "❌ ffmpeg not found. Please install ffmpeg and ensure it is in your PATH."
83                        .into(),
84                );
85            } else {
86                return Err(format!("❌ Failed to execute ffmpeg: {}", e));
87            }
88        }
89    };
90
91    fs::remove_file(palette_path)
92        .unwrap_or_else(|e| eprintln!("⚠️ Failed to remove palette file: {}", e));
93
94    if gif_status.success() {
95        println!("✅ GIF exported: {}", output);
96        Ok(())
97    } else {
98        Err("❌ Failed to export GIF".into())
99    }
100}