1mod probe;
11#[cfg(test)]
12mod test;
13
14use std::{
15 io::{BufRead, BufReader},
16 os::unix::{fs::MetadataExt, process::CommandExt},
17 path::PathBuf,
18 process::{Command, Stdio},
19 str::Utf8Error,
20};
21use tracing::{debug, error, trace};
22
23use crate::probe::{AspectRatio, Resolution};
24
25pub struct PhotoOutputFormat {
26 pub codec: PhotoCodec,
27 pub quality: u8,
28 pub default_video_duration: u32,
29}
30
31pub struct VideoOutputFormat {
32 pub video_codec: VideoCodec,
33 pub audio_codec: AudioCodec,
34 pub video_kbitrate: u16,
35 pub audio_kbitrate: u8,
36}
37
38#[derive(Default)]
39pub enum VideoCodec {
40 #[default]
41 AV1,
42}
43
44#[derive(Default)]
45pub enum AudioCodec {
46 #[default]
47 OPUS,
48}
49
50#[derive(Default)]
51pub enum PhotoCodec {
52 #[default]
53 WEBP,
54}
55
56impl std::fmt::Display for AudioCodec {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "libopus")
59 }
60}
61
62impl std::fmt::Display for VideoCodec {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "libsvtav1")
65 }
66}
67
68impl PhotoCodec {
69 pub fn to_container_str(&self) -> &str {
70 match &self {
71 Self::WEBP => "webp",
72 }
73 }
74}
75
76impl VideoCodec {
77 pub fn to_container_str(&self) -> &str {
78 match &self {
79 Self::AV1 => "mp4",
80 }
81 }
82}
83
84#[derive(Default)]
85pub struct Encoder {
86 pub video_format: VideoOutputFormat,
87 pub photo_format: PhotoOutputFormat,
88}
89
90#[derive(Debug, thiserror::Error)]
91pub enum Error {
92 #[error("File not supported or does not exist, {0}")]
93 WrongPath(String),
94 #[error("Spawning ffmpeg and listening to stdin returned error, is ffmpeg installed? {0}")]
95 Sys(String),
96 #[error("Listening to process lines threw error, wha..? {0}")]
97 IO(#[from] std::io::Error),
98 #[error("Something went wrong during conversion of file, {0}")]
99 Ffmpeg(String),
100 #[error("Something went wrong during probing metadata of file, {0}")]
101 Ffprobe(String),
102 #[error("String conversion error, {0}")]
103 Utf8(#[from] Utf8Error),
104}
105
106pub struct EncoderCommandBuffer {
107 pub input_file_path: PathBuf,
108 pub output_file_path: PathBuf,
109 pub pass1_logfile: PathBuf,
110 pub vcodec_str: String,
111 pub acodec_str: String,
112 pub vcodec_rate_str: String,
113 pub acodec_rate_str: String,
114 pub commands: Vec<Command>,
115}
116
117impl Encoder {
118 pub fn new(
119 video_format: Option<VideoOutputFormat>,
120 photo_format: Option<PhotoOutputFormat>,
121 ) -> Self {
122 Self {
123 video_format: video_format.unwrap_or(VideoOutputFormat::default()),
124 photo_format: photo_format.unwrap_or(PhotoOutputFormat::default()),
125 }
126 }
127
128 fn create_common_command_args<'a>(
129 &self,
130 com_buffer: &'a mut EncoderCommandBuffer,
131 mut prepend_args: Option<Vec<String>>,
132 mut append_args: Option<Vec<String>>,
133 ) -> Result<Vec<String>, Error> {
134 let mut common_args: Vec<String> =
135 vec!["-progress", "-", "-nostats", "-stats_period", "50ms", "-y"]
136 .into_iter()
137 .map(|s| s.to_owned())
138 .collect();
139
140 if let Some(prepend) = prepend_args.as_mut() {
141 common_args.append(prepend)
142 }
143
144 common_args.append(
145 &mut vec![
146 "-i",
147 com_buffer.input_file_path.to_str().ok_or(Error::WrongPath(
148 "filepath to string returned none, UNICODE shenanigans?".to_owned(),
149 ))?,
150 "-vcodec",
151 &com_buffer.vcodec_str,
152 "-acodec",
153 &com_buffer.acodec_str,
154 "-cpu-used",
155 "0",
156 "-threads",
157 "16",
158 "-b:v",
159 &com_buffer.vcodec_rate_str,
160 "-b:a",
161 &com_buffer.acodec_rate_str,
162 "-passlogfile",
163 &com_buffer
164 .pass1_logfile
165 .as_os_str()
166 .to_str()
167 .ok_or(Error::WrongPath(
168 "filepath to string returned none, UNICODE shenanigans?".to_owned(),
169 ))?,
170 ]
171 .into_iter()
172 .map(|s| s.to_owned())
173 .collect(),
174 );
175
176 common_args.append(&mut match self.video_format.video_codec {
177 VideoCodec::AV1 => vec!["-preset", "8", "-svtav1-params", "rc=1:scd=1"]
178 .into_iter()
179 .map(|s| s.to_owned())
180 .collect(),
181 });
182
183 if let Some(append) = append_args.as_mut() {
184 common_args.append(append)
185 }
186
187 Ok(common_args)
188 }
189
190 fn create_base_command_buffers<'a>(
191 &self,
192 input_file_path: PathBuf,
193 prepend_args: Option<Vec<&'a str>>,
194 append_args: Option<Vec<&'a str>>,
195 ) -> Result<EncoderCommandBuffer, Error> {
196 match input_file_path.try_exists() {
197 Ok(exists) => {
198 if !exists {
199 return Err(Error::WrongPath(
200 "File doesn't exist according to PathBuf.try_exists(), aiaiai".to_string(),
201 ));
202 }
203 }
204 Err(e) => return Err(e.into()),
205 }
206
207 let mut output_file_path = input_file_path.clone();
208
209 let mut pass1 = Command::new("ffmpeg");
210 let mut pass2 = Command::new("ffmpeg");
211
212 let mut pass1_logfile = input_file_path.clone();
213
214 pass1_logfile.set_extension("");
215
216 output_file_path.set_extension(self.video_format.video_codec.to_container_str());
217
218 output_file_path.set_file_name(
219 "encoded_".to_owned() + output_file_path.file_name().unwrap().to_str().unwrap(),
220 );
221
222 let mut args_pass1 = vec![];
223 let mut args_pass2 = vec![];
224
225 let vcodec_str = self.video_format.video_codec.to_string();
226 let acodec_str = self.video_format.audio_codec.to_string();
227 let vcodec_rate_str = format!("{}k", self.video_format.video_kbitrate);
228 let acodec_rate_str = format!("{}k", self.video_format.audio_kbitrate);
229
230 let mut com_buf = EncoderCommandBuffer {
231 input_file_path,
232 output_file_path,
233 pass1_logfile,
234 vcodec_str,
235 acodec_str,
236 vcodec_rate_str,
237 acodec_rate_str,
238 commands: vec![],
239 };
240
241 let prepend_args = prepend_args.map(|v| v.into_iter().map(|s| s.to_owned()).collect());
242 let append_args = append_args.map(|v| v.into_iter().map(|s| s.to_owned()).collect());
243 let mut common_args =
244 self.create_common_command_args(&mut com_buf, prepend_args, append_args)?;
245
246 args_pass1.append(common_args.clone().as_mut());
247 args_pass2.append(&mut common_args);
248
249 args_pass1.append(
250 &mut vec![
251 "-pass",
252 "1",
253 "-f",
254 self.video_format.video_codec.to_container_str(),
255 ]
256 .into_iter()
257 .map(|s| s.to_owned())
258 .collect(),
259 );
260
261 if cfg!(windows) {
262 args_pass1.push("NUL".to_owned());
263 } else {
264 args_pass1.push("/dev/null".to_owned());
265 }
266
267 args_pass2.append(
268 &mut vec![
269 "-pass",
270 "2",
271 com_buf
272 .output_file_path
273 .as_os_str()
274 .to_str()
275 .ok_or(Error::WrongPath(
276 "filepath to string returned none, UNICODE shenanigans?".to_owned(),
277 ))?,
278 ]
279 .into_iter()
280 .map(|s| s.to_owned())
281 .collect(),
282 );
283
284 debug!(
285 "ffmpeg {} \n&& ffmpeg {}",
286 args_pass1
287 .clone()
288 .iter()
289 .fold("".to_owned(), |a, b| format!("{} \\\n{}", a, b)),
290 args_pass2
291 .clone()
292 .iter()
293 .fold("".to_owned(), |a, b| format!("{} \\\n{}", a, b))
294 );
295
296 pass1.args(args_pass1);
297 pass2.args(args_pass2);
298
299 com_buf.commands = vec![pass1, pass2];
300 Ok(com_buf)
301 }
302
303 fn run_command_buffer(&self, command: &mut Command) -> Result<(), Error> {
304 command.stdout(Stdio::piped());
305 command.stderr(Stdio::piped());
306 command.stdin(Stdio::null());
307
308 let mut command_handle = command.spawn();
309
310 let buff_reader = BufReader::new(command_handle.as_mut().unwrap().stdout.take().ok_or(
311 Error::Sys("encoder stdout missing - exited early or unavailable".to_owned()),
312 )?);
313
314 for maybe_line in buff_reader.lines() {
315 match maybe_line {
316 Ok(line) => {
317 trace!(line);
318 if line.contains("end") {
319 trace!("LINE CONTAINED END!");
320 break;
321 }
322 }
323 Err(e) => return Err(e.into()),
324 }
325 }
326 Ok(())
327 }
328
329 fn is_run_sucessful(&self, output_file_path: &mut PathBuf) -> Result<(), Error> {
330 match output_file_path.try_exists() {
331 Ok(exists) => {
332 if !exists {
333 return Err(Error::WrongPath(
334 "File doesn't exist according to PathBuf.try_exists(), aiaiai".to_string(),
335 ));
336 }
337 if output_file_path.metadata()?.size() < 1_000 {
338 return Err(Error::Ffmpeg(
339 "File is smaller than 1kb, prolly invalid encode?".to_owned(),
340 ));
341 }
342 }
343 Err(e) => return Err(e.into()),
344 }
345 Ok(())
346 }
347
348 pub fn reencode(&self, file_path: PathBuf) -> Result<PathBuf, Error> {
349 let mut com_buffers = self.create_base_command_buffers(file_path, None, None)?;
350 for com_buffer in com_buffers.commands.iter_mut() {
351 self.run_command_buffer(com_buffer)?;
352 }
353
354 self.is_run_sucessful(&mut com_buffers.output_file_path)?;
355 Ok(com_buffers.output_file_path)
356 }
357
358 pub fn picture_to_video(
359 &self,
360 file_path: PathBuf,
361 duration: Option<u32>,
362 force_resolution: Option<Resolution>,
363 ) -> Result<PathBuf, Error> {
364 let duration = duration
365 .unwrap_or(self.photo_format.default_video_duration)
366 .to_string();
367
368 let mut append_args = vec![];
369 let mut prepend_args = vec![];
370 let aspect_filter = force_resolution.map(|a| {
371 format!(
372 "scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2",
373 a.0, a.1, a.0, a.1
374 )
375 });
376
377 if let Some(aspect) = aspect_filter.as_ref() {
378 append_args.append(&mut vec!["-vf", &aspect])
379 }
380 prepend_args.append(&mut vec!["-t", &duration]);
381
382 let mut com_buffers =
383 self.create_base_command_buffers(file_path, Some(prepend_args), Some(append_args))?;
384 for com_buffer in com_buffers.commands.iter_mut() {
385 self.run_command_buffer(com_buffer)?;
386 }
387
388 self.is_run_sucessful(&mut com_buffers.output_file_path)?;
389 Ok(com_buffers.output_file_path)
390 }
391
392 pub fn trim_video(
393 &self,
394 file_path: PathBuf,
395 start_time: Option<u32>,
396 end_time: Option<u32>,
397 duration: Option<f32>,
398 ) -> Result<PathBuf, Error> {
399 unimplemented!();
400 }
428
429 pub fn surround_video(
430 &self,
431 file_path: PathBuf,
432 append_file_path: PathBuf,
433 prepend_file_path: PathBuf,
434 ) -> Result<PathBuf, Error> {
435 unimplemented!();
436 }
437}
438
439impl Default for VideoOutputFormat {
440 fn default() -> Self {
441 Self {
442 video_kbitrate: 1000,
443 audio_kbitrate: 128,
444 video_codec: VideoCodec::default(),
445 audio_codec: AudioCodec::default(),
446 }
447 }
448}
449
450impl Default for PhotoOutputFormat {
451 fn default() -> Self {
452 Self {
453 quality: 80,
454 codec: PhotoCodec::default(),
455 default_video_duration: 5,
456 }
457 }
458}
459
460fn ms_to_str(ms: u32) -> String {
461 let mut s = ms / 1000;
462 let mut ms = ms - s * 1000;
463 let mut m = s / 60;
464
465 unimplemented!()
468}