This repository has no description
1use super::Video;
2use crate::ui::{Log, Pretty};
3use crate::video::encoders::Encoder;
4use crate::video::encoders::vgv::VGVTranscodeMode;
5use crate::video::engine::EngineOutput;
6use anyhow::{Result, anyhow};
7use measure_time::debug_time;
8use std::path::PathBuf;
9use std::thread;
10
11impl<AdditionalContext: Default> Video<AdditionalContext> {
12 pub fn encode(
13 &mut self,
14 output_file: impl Into<PathBuf> + Clone,
15 ) -> Result<std::time::Duration> {
16 debug_time!("encode");
17
18 let encoder = self.setup_encoder(output_file.clone())?;
19 let encoder_name = encoder.name();
20
21 let time_taken = self.encode_with(encoder)?;
22
23 let _ = notify_rust::Notification::new()
24 .appname("Shapemaker")
25 .summary(&format!("{} is ready", &output_file.into().pretty()))
26 .body(&format!(
27 "Encoded with {encoder_name} in {}",
28 time_taken.pretty()
29 ))
30 .show();
31
32 Ok(time_taken)
33 }
34
35 fn setup_encoder(
36 &mut self,
37 output_path: impl Into<PathBuf>,
38 ) -> Result<Box<dyn Encoder + Send>> {
39 let (width, height) =
40 self.initial_canvas.resolution_to_size_even(self.resolution);
41
42 let destination = output_path.into();
43 let pb = &self.progress_bars.encoding;
44
45 if destination.exists() {
46 std::fs::remove_file(&destination)?;
47 }
48
49 std::fs::create_dir_all(
50 &destination
51 .parent()
52 .expect("Given output file has no parent"),
53 )?;
54
55 Ok(match destination.full_extension() {
56 ".vgv.html" => {
57 self.progress_bars.encoding.log(
58 "Selecting",
59 &format!(
60 "VGV encoder with HTML transcoding as {} ends with .vgv.html",
61 destination.pretty(),
62 ),
63 );
64
65 Box::new(self.setup_vgv_encoder(
66 VGVTranscodeMode::ToHTML,
67 width as _,
68 height as _,
69 &self.initial_canvas,
70 destination,
71 )?)
72 }
73 ".vgv" => {
74 self.progress_bars.encoding.log(
75 "Selecting",
76 &format!(
77 "VGV encoder as {} ends with .vgv (use .vgv.html for HTML transcoding)",
78 destination.pretty(),
79 ),
80 );
81
82 Box::new(self.setup_vgv_encoder(
83 VGVTranscodeMode::None,
84 width as _,
85 height as _,
86 &self.initial_canvas,
87 destination,
88 )?)
89 }
90 _ => {
91 pb.log(
92 "Selecting",
93 &format!(
94 "FFMpeg encoder as {} ends with {}",
95 destination.pretty(),
96 destination.full_extension()
97 ),
98 );
99
100 self.initial_canvas.load_fonts()?;
101 Box::new(self.setup_ffmpeg_encoder(width, height, destination)?)
102 }
103 })
104 }
105
106 pub fn encode_with(
107 &mut self,
108 mut encoder: Box<dyn Encoder + Send>,
109 ) -> Result<std::time::Duration> {
110 debug_time!("encode_with");
111
112 self.progress.remove(&self.progress_bars.loading);
113
114 let pb = self.progress_bars.encoding.clone();
115
116 pb.set_length(self.ms_to_frames(self.duration_ms()) as _);
117 pb.set_message("");
118
119 let (tx, rx) = std::sync::mpsc::sync_channel::<EngineOutput>(1_000);
120
121 let encoder_thread =
122 thread::spawn(move || -> Result<std::time::Duration> {
123 for output in rx.iter() {
124 match output {
125 EngineOutput::Finished => break,
126 EngineOutput::Frame { .. } => {
127 pb.inc(1);
128 pb.set_message(encoder.progress_message(
129 pb.position(),
130 pb.length().unwrap(),
131 ));
132 }
133 }
134
135 encoder.encode_frame(output)?;
136 }
137
138 let time_taken = pb.elapsed();
139 let finish_message = encoder.finish_message(time_taken);
140
141 encoder.finish()?;
142
143 pb.finish();
144 pb.log("Encoded", &finish_message);
145
146 Ok(time_taken)
147 });
148
149 self.render_with_overrides(tx)?;
150
151 let time_taken = encoder_thread
152 .join()
153 .map_err(|e| anyhow!("Encoder thread panicked: {e:?}"))
154 .flatten()?;
155
156 let _ = self.progress.clear();
157
158 Ok(time_taken)
159 }
160
161 #[allow(dead_code)]
162 fn add_audio_track(&mut self, _output_file: String) -> Result<()> {
163 todo!(
164 "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)"
165 );
166 }
167}
168
169// Because .extension() sucks
170
171trait FullExtension {
172 fn full_extension(&self) -> &str;
173}
174
175impl FullExtension for PathBuf {
176 fn full_extension(&self) -> &str {
177 let filename = self
178 .file_name()
179 .and_then(|f| f.to_str())
180 .unwrap_or_default();
181 let parts: Vec<&str> = filename.split('.').collect();
182 if parts.len() <= 1 {
183 ""
184 } else {
185 &filename[filename.find('.').unwrap()..]
186 }
187 }
188}