ranim/cmd/render/
file_writer.rs1use std::{
2 io::Write,
3 path::PathBuf,
4 process::{Child, ChildStdin, Command, Stdio},
5};
6
7use crate::OutputFormat;
8use tracing::info;
9
10pub(crate) trait OutputFormatExt {
12 fn encoding_params(&self) -> (&'static str, &'static str, &'static str);
14 fn extra_args(&self) -> &'static [&'static str];
16 fn has_alpha(&self) -> bool;
18 fn supports_eq_filter(&self) -> bool;
20}
21
22impl OutputFormatExt for OutputFormat {
23 fn encoding_params(&self) -> (&'static str, &'static str, &'static str) {
24 match self {
25 Self::Mp4 => ("libx264", "yuv420p", "mp4"),
26 Self::Webm => ("libvpx-vp9", "yuva420p", "webm"),
27 Self::Mov => ("prores_ks", "yuva444p10le", "mov"),
28 Self::Gif => ("gif", "rgb8", "gif"),
29 }
30 }
31
32 fn extra_args(&self) -> &'static [&'static str] {
33 match self {
34 Self::Mov => &["-profile:v", "4444"],
35 _ => &[],
36 }
37 }
38
39 fn has_alpha(&self) -> bool {
40 matches!(self, Self::Webm | Self::Mov)
41 }
42
43 fn supports_eq_filter(&self) -> bool {
44 !self.has_alpha()
45 }
46}
47
48#[derive(Debug, Clone)]
49pub struct FileWriterBuilder {
50 pub file_path: PathBuf,
51 pub width: u32,
52 pub height: u32,
53 pub fps: u32,
54 pub vf_args: Vec<String>,
55
56 pub video_codec: String,
57 pub pixel_format: String,
58 pub extra_codec_args: Vec<String>,
59}
60
61impl Default for FileWriterBuilder {
62 fn default() -> Self {
63 Self {
64 file_path: PathBuf::from("output.mp4"),
65 width: 1920,
66 height: 1080,
67 fps: 60,
68
69 vf_args: vec!["eq=saturation=1.0:gamma=1.0".to_string()],
70 video_codec: "libx264".to_string(),
71 pixel_format: "yuv420p".to_string(),
72 extra_codec_args: Vec::new(),
73 }
74 }
75}
76
77#[allow(unused)]
78impl FileWriterBuilder {
79 pub fn with_file_path(mut self, file_path: PathBuf) -> Self {
80 self.file_path = file_path;
81 self
82 }
83
84 pub fn with_size(mut self, width: u32, height: u32) -> Self {
85 self.width = width;
86 self.height = height;
87 self
88 }
89
90 pub fn with_fps(mut self, fps: u32) -> Self {
91 self.fps = fps;
92 self
93 }
94
95 pub fn with_output_format(mut self, format: OutputFormat) -> Self {
96 let (codec, pix_fmt, ext) = format.encoding_params();
97 self.video_codec = codec.to_string();
98 self.pixel_format = pix_fmt.to_string();
99 self.extra_codec_args = format.extra_args().iter().map(|s| s.to_string()).collect();
100 self.file_path = self.file_path.with_extension(ext);
102 if !format.supports_eq_filter() {
104 self.vf_args.clear();
105 }
106 if format == OutputFormat::Gif && self.fps > 50 {
109 self.fps = 50;
110 }
111 self
112 }
113
114 pub fn enable_fast_encoding(mut self) -> Self {
115 self.video_codec = "libx264rgb".to_string();
116 self.pixel_format = "rgb32".to_string();
117 self
118 }
119
120 pub fn output_gif(mut self) -> Self {
121 self.file_path = self.file_path.with_file_name(format!(
123 "{}.gif",
124 self.file_path.file_stem().unwrap().to_string_lossy()
125 ));
126 self.fps = 30;
127 self.video_codec = "gif".to_string();
128 self.pixel_format = "rgb8".to_string();
129 self
130 }
131
132 pub fn build(self) -> FileWriter {
133 let parent = self.file_path.parent().unwrap();
134 if !parent.exists() {
135 std::fs::create_dir_all(parent).unwrap();
136 }
137
138 let mut command = if which::which("ffmpeg").is_ok() {
139 info!("using ffmpeg found from path env");
140 Command::new("ffmpeg")
141 } else {
142 info!("using ffmpeg from current working dir");
143 Command::new("./ffmpeg")
144 };
145
146 let size = format!("{}x{}", self.width, self.height);
147 let fps = self.fps.to_string();
148 let file_path = self.file_path.to_string_lossy().to_string();
149
150 command.args([
152 "-y", "-f", "rawvideo", "-s", &size, "-pix_fmt", "rgba", "-r", &fps, "-i", "-",
153 ]);
154 command.args(["-an", "-loglevel", "error", "-vcodec", &self.video_codec]);
156 command.args(&self.extra_codec_args);
157 command.args(["-pix_fmt", &self.pixel_format]);
158 if !self.vf_args.is_empty() {
159 let vf = self.vf_args.join(",");
160 command.args(["-vf", &vf]);
161 }
162 command.arg(&file_path);
164 command.stdin(Stdio::piped());
165
166 let mut child = command.spawn().expect("Failed to spawn ffmpeg");
167 FileWriter {
168 child_in: child.stdin.take(),
169 child,
170 }
171 }
172}
173
174pub struct FileWriter {
175 child: Child,
176 child_in: Option<ChildStdin>,
177}
178
179impl Drop for FileWriter {
180 fn drop(&mut self) {
181 self.child_in
182 .as_mut()
183 .unwrap()
184 .flush()
185 .expect("Failed to flush ffmpeg");
186 drop(self.child_in.take());
187 self.child.wait().expect("Failed to wait ffmpeg");
188 }
189}
190
191impl FileWriter {
192 pub fn write_frame(&mut self, frame: &[u8]) {
197 self.child_in
198 .as_mut()
199 .unwrap()
200 .write_all(frame)
201 .expect("Failed to write frame");
202 }
203}