This repository has no description
1use crate::{
2 Video,
3 rendering::rasterization::{create_pixmap, paint_svg_on_pixmap},
4 ui::Pretty,
5 video::{encoders::Encoder, engine::EngineOutput},
6};
7use anyhow::{Result, anyhow};
8use measure_time::debug_time;
9use rayon::prelude::*;
10use std::{fs::File, io::Write, ops::ControlFlow, path::PathBuf, sync::Arc};
11
12pub struct FFMpegEncoder {
13 progress: indicatif::ProgressBar,
14 process: std::process::Child,
15 output_size: (u32, u32),
16 fontdb: Option<Arc<resvg::usvg::fontdb::Database>>,
17 destination: PathBuf,
18}
19
20impl<C: Default> Video<C> {
21 pub fn setup_ffmpeg_encoder(
22 &self,
23 width: u32,
24 height: u32,
25 output_path: PathBuf,
26 ) -> Result<FFMpegEncoder> {
27 debug_time!("setup_encoder");
28 let output_path: PathBuf = output_path.into();
29
30 let mut command = std::process::Command::new("ffmpeg");
31 command
32 // Audio //
33 // Take non-0 starting point into account
34 .args(["-ss", &self.start_rendering_at.seconds_string()])
35 // File
36 .args(["-i", self.audiofile.to_str().unwrap()])
37 //
38 // Video //
39 // Raw video input
40 .args(["-f", "rawvideo"])
41 // RGBA Pixels
42 .args(["-pixel_format", "rgba"])
43 // Dimensions
44 .args(["-video_size", &format!("{width}x{height}")])
45 // FPS
46 .args(["-framerate", &self.fps.to_string()])
47 // Input from pipe
48 .args(["-i", "-"])
49 .stdin(std::process::Stdio::piped())
50 //
51 // Mapping //
52 // Audio from first input
53 .args(["-map", "0:a"])
54 // Video from second input
55 .args(["-map", "1:v"])
56 // Use shortest stream for final duration
57 .arg("-shortest")
58 //
59 // Output //
60 // Use 4:2:0 (4:4:4 is not widely supported)
61 .args(["-pix_fmt", "yuv420p"])
62 // Write to file
63 .arg(output_path.to_str().unwrap())
64 // Debug ffmpeg too if shapemaker is debugging
65 .args([
66 "-loglevel",
67 (if log::log_enabled!(log::Level::Debug) {
68 "debug"
69 } else {
70 "error"
71 }),
72 ])
73 // Put stdout/stderr here so that it doesn't mess with progress bars
74 .stdout(File::create("ffmpeg_stdout.log")?)
75 .stderr(File::create("ffmpeg_stderr.log")?);
76
77 let commandline = format!("{:?}", &command);
78
79 Ok(FFMpegEncoder {
80 destination: output_path.clone(),
81 fontdb: self.initial_canvas.fontdb.clone(),
82 output_size: (width, height),
83 progress: self.progress_bars.encoding.clone(),
84 process: command
85 .spawn()
86 .map_err(|e| anyhow!("Could not run {commandline}: {e:?}",))?,
87 })
88 }
89}
90
91impl Encoder for FFMpegEncoder {
92 fn name(&self) -> String {
93 "FFMpeg".into()
94 }
95
96 fn encode_frames(
97 &mut self,
98 outputs: Vec<EngineOutput>,
99 ) -> Result<ControlFlow<()>> {
100 let (width, height) = self.output_size;
101
102 let mut rasterizations: Vec<_> = outputs
103 .par_iter()
104 .filter_map(|output| match output {
105 EngineOutput::Finished => None,
106 EngineOutput::Frame { index, size, svg } => Some({
107 debug_time!("encode_frame");
108 // Make sure that width and height are divisible by 2, as the encoder requires it
109
110 let mut pixmap = create_pixmap(width, height);
111
112 match paint_svg_on_pixmap(
113 pixmap.as_mut(),
114 &svg.to_string(),
115 *size,
116 &self.fontdb,
117 ) {
118 Ok(..) => Some(Ok((index, pixmap.data().to_vec()))),
119 Err(e) => Some(Err(e)),
120 }
121 }),
122 })
123 .collect();
124
125 rasterizations.sort_by_cached_key(|r| match r {
126 Some(Ok((index, _))) => **index,
127 _ => 0,
128 });
129
130 for rasterization in rasterizations {
131 match rasterization {
132 None => return Ok(ControlFlow::Break(())),
133 Some(Ok((index, data))) => {
134 self.process.stdin.as_mut().unwrap().write_all(&data)?;
135 self.progress.inc(1);
136 self.progress.set_message(format!("{index}th frame",));
137 }
138 Some(Err(e)) => return Err(e),
139 }
140 }
141
142 Ok(ControlFlow::Continue(()))
143 }
144
145 fn finish(&mut self) -> Result<()> {
146 self.process.stdin.take().unwrap().flush().unwrap();
147 Ok(())
148 }
149
150 fn finish_message(&self, time_elapsed: std::time::Duration) -> String {
151 format!(
152 "video to {} in {}",
153 self.destination.pretty(),
154 time_elapsed.pretty()
155 )
156 }
157}