This repository has no description
1use super::{hooks::milliseconds_to_timestamp, Video};
2use crate::rendering::svg;
3use crate::Canvas;
4use anyhow::Result;
5use itertools::Itertools;
6use measure_time::debug_time;
7use std::fs::File;
8use std::io::Write;
9use std::thread;
10use std::time::Duration;
11use std::{fs::create_dir_all, path::PathBuf};
12
13impl<AdditionalContext: Default> Video<AdditionalContext> {
14 pub fn encode_to_vgv(
15 &mut self,
16 output_file: impl Into<PathBuf>,
17 ) -> Result<()> {
18 debug_time!("encode_to_vgv");
19 let output_file: PathBuf = output_file.into();
20
21 if output_file.exists() {
22 std::fs::remove_file(&output_file)?;
23 }
24
25 create_dir_all(
26 &output_file
27 .parent()
28 .expect("Given output file has no parent"),
29 )?;
30
31 let mut file = File::create(&output_file)?;
32
33 self.progress_bar.set_position(0);
34 self.progress_bar.set_prefix("Rendering");
35 self.progress_bar.set_message("");
36
37 self.initial_canvas.load_fonts()?;
38 let initial_canvas = self.initial_canvas.clone();
39 let resolution = self.resolution;
40 let (width, height) = initial_canvas.resolution_to_size_even(resolution);
41
42 let (tx, rx) =
43 std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(1_000);
44
45 let pb = self.progress_bar.clone();
46
47 let mut vgv_encoder = vgv::Encoder::new(
48 vgv::InitializationParameters {
49 w: width as _,
50 h: height as _,
51 d: (1000.0 / self.fps as f64) as _,
52 bg: initial_canvas
53 .background
54 .unwrap_or_default()
55 .render(&initial_canvas.colormap),
56 },
57 format!(
58 r#"width={w} height={h} viewBox="-{pad} -{pad} {w} {h}""#,
59 w = initial_canvas.width(),
60 h = initial_canvas.height(),
61 pad = initial_canvas.canvas_outer_padding
62 ),
63 );
64
65 let vgv_thread = thread::spawn(move || {
66 for (time, svg) in rx.iter() {
67 if svg.is_empty() {
68 break;
69 }
70
71 vgv_encoder.encode_svg(match svg {
72 svg::Node::Text(text) => text,
73 svg::Node::SVG(svg) => svg,
74 svg::Node::Element(element) => element
75 .children
76 .iter()
77 .map(|child| child.to_string())
78 .join(""),
79 });
80
81 pb.set_position(time.as_millis() as _);
82 pb.set_message(milliseconds_to_timestamp(time.as_millis() as _));
83 }
84
85 vgv_encoder.dump(&mut file);
86 });
87
88 self.render_all_frames(tx)?;
89
90 vgv_thread.join().expect("VGV thread panicked");
91
92 Ok(())
93 }
94
95 fn setup_encoder(
96 &mut self,
97 output_path: impl Into<PathBuf>,
98 ) -> anyhow::Result<std::process::Child> {
99 debug_time!("setup_encoder");
100 let output_path: PathBuf = output_path.into();
101
102 let (width, height) =
103 self.initial_canvas.resolution_to_size_even(self.resolution);
104
105 Ok(std::process::Command::new("ffmpeg")
106 .arg("-i")
107 .arg(self.audiofile.to_str().unwrap())
108 .arg("-f")
109 .arg("rawvideo")
110 .arg("-pixel_format")
111 .arg("rgba")
112 .arg("-video_size")
113 .arg(format!("{width}x{height}"))
114 .arg("-framerate")
115 .arg(format!("{}", self.fps))
116 .arg("-i")
117 .arg("-")
118 .arg("-map")
119 .arg("0:a")
120 .arg("-map")
121 .arg("1:v")
122 .arg("-shortest")
123 .arg(output_path.to_str().unwrap())
124 .arg("-loglevel")
125 .arg(if log::log_enabled!(log::Level::Debug) {
126 "debug"
127 } else {
128 "error"
129 })
130 .stdin(std::process::Stdio::piped())
131 .stdout(File::create("ffmpeg_stdout.log")?)
132 .stderr(File::create("ffmpeg_stderr.log")?)
133 .spawn()?)
134 }
135
136 pub fn encode(&mut self, output_file: impl Into<PathBuf>) -> Result<()> {
137 debug_time!("encode");
138
139 let output_file: PathBuf = output_file.into();
140
141 if output_file.exists() {
142 std::fs::remove_file(&output_file)?;
143 }
144
145 create_dir_all(
146 &output_file
147 .parent()
148 .expect("Given output file has no parent"),
149 )?;
150
151 let mut encoder = self.setup_encoder(&output_file)?;
152
153 self.progress_bar.set_position(0);
154 self.progress_bar.set_prefix("Rendering");
155 self.progress_bar.set_message("");
156
157 self.initial_canvas.load_fonts()?;
158 let initial_canvas = self.initial_canvas.clone();
159 let resolution = self.resolution;
160
161 let (tx, rx) =
162 std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(1_000);
163
164 let pb = self.progress_bar.clone();
165
166 let encoder_thread = thread::spawn(move || {
167 for (time, svg) in rx.iter() {
168 if svg.is_empty() {
169 break;
170 }
171
172 encode_frame(
173 &mut encoder,
174 resolution,
175 time,
176 &initial_canvas,
177 &svg.to_string(),
178 )
179 .unwrap();
180
181 pb.set_position(time.as_millis() as _);
182 pb.set_message(milliseconds_to_timestamp(time.as_millis() as _));
183 }
184
185 encoder.stdin.take().unwrap().flush().unwrap();
186 });
187
188 self.render_all_frames(tx)?;
189
190 encoder_thread.join().expect("Encoder thread panicked");
191
192 Ok(())
193 }
194
195 #[allow(dead_code)]
196 fn add_audio_track(&mut self, _output_file: String) -> Result<()> {
197 todo!("Look into https://github.com/zmwangx/rust-ffmpeg/blob/master/examples/transcode-x264.rs and maybe contribute to video-rs (see https://github.com/oddity-ai/video-rs/issues/44)");
198 }
199}
200
201fn encode_frame(
202 encoder: &mut std::process::Child,
203 resolution: u32,
204 _timestamp: Duration,
205 canvas: &Canvas,
206 svg: &String,
207) -> anyhow::Result<()> {
208 debug_time!("encode_frame");
209 // Make sure that width and height are divisible by 2, as the encoder requires it
210 let (width, height) = canvas.resolution_to_size_even(resolution);
211
212 let pixmap = canvas.svg_to_pixmap(width, height, svg)?;
213 // Send frame
214 encoder.stdin.as_mut().unwrap().write_all(&pixmap.data())?;
215
216 // let frame =
217 // canvas.pixmap_to_hwc_frame((width as usize, height as usize), &pixmap)?;
218 // Ok(encoder.encode(&frame, timestamp)?)
219 Ok(())
220}