Skip to main content

maple_render_core/
vid_anim.rs

1use std::{
2    io::Write,
3    path::Path,
4    process::{Command, Stdio},
5};
6
7use crate::{
8    error::{Error, Result},
9    renders::Renders,
10};
11
12pub struct VidAnim {
13    renders: Renders,
14    period: f64,
15    hold: f64,
16    first_frame: i32,
17}
18
19impl VidAnim {
20    pub fn new(renders: Renders) -> Self {
21        VidAnim { renders, period: 0.1, hold: 5.0, first_frame: -1 }
22    }
23
24    pub fn set_first_frame(&mut self, index: i32) {
25        self.first_frame = index;
26    }
27
28    pub fn set_timing(&mut self, period: f64, hold: f64) {
29        self.period = period;
30        self.hold = hold;
31    }
32
33    pub fn apply<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
34        let rate = 30;
35        let frame_rate = 1.0 / rate as f64;
36
37        let frames = self.renders.length() as i32;
38        if frames == 0 {
39            return Err(Error::VideoEncode("No frames to encode".to_string()));
40        }
41
42        let first_render = self.renders.get_render(0)?;
43        let width = first_render.get().width();
44        let height = first_render.get().height();
45
46        let mut ffmpeg = Command::new("ffmpeg")
47            .args([
48                "-y",
49                "-f",
50                "rawvideo",
51                "-pixel_format",
52                "rgb24",
53                "-video_size",
54                &format!("{}x{}", width, height),
55                "-framerate",
56                &rate.to_string(),
57                "-i",
58                "-",
59                "-c:v",
60                "libx264",
61                "-pix_fmt",
62                "yuv420p",
63                "-preset",
64                "medium",
65                "-crf",
66                "23",
67            ])
68            .arg(path.as_ref())
69            .stdin(Stdio::piped())
70            .stdout(Stdio::null())
71            .stderr(Stdio::null())
72            .spawn()
73            .map_err(|e| Error::VideoEncode(format!("Failed to start ffmpeg: {}", e)))?;
74
75        let mut stdin = ffmpeg
76            .stdin
77            .take()
78            .ok_or_else(|| Error::VideoEncode("Failed to open ffmpeg stdin".to_string()))?;
79
80        let mut curr = 0.0f64;
81        let mut target = -1.0f64;
82        let mut base = 0;
83        let mut current_frame_data: Option<Vec<u8>> = None;
84
85        loop {
86            let i = if self.first_frame >= 0 { (base + self.first_frame) % frames } else { base };
87
88            if curr >= target {
89                if base >= frames {
90                    break;
91                }
92
93                let step = if i == frames - 1 { self.period + self.hold } else { self.period };
94                target = curr + step;
95
96                let render = self.renders.get_render(i)?;
97                let img = render.get();
98
99                let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
100                for pixel in img.pixels() {
101                    rgb_data.push(pixel[0]);
102                    rgb_data.push(pixel[1]);
103                    rgb_data.push(pixel[2]);
104                }
105                current_frame_data = Some(rgb_data);
106
107                self.renders.remove_render(i);
108                base += 1;
109            }
110
111            // Write current frame
112            if let Some(ref data) = current_frame_data {
113                stdin
114                    .write_all(data)
115                    .map_err(|e| Error::VideoEncode(format!("Failed to write frame: {}", e)))?;
116            }
117
118            curr += frame_rate;
119        }
120
121        drop(stdin);
122
123        let status = ffmpeg
124            .wait()
125            .map_err(|e| Error::VideoEncode(format!("Failed to wait for ffmpeg: {}", e)))?;
126
127        if !status.success() {
128            return Err(Error::VideoEncode("ffmpeg exited with error".to_string()));
129        }
130
131        Ok(())
132    }
133}