Skip to main content

simple_ffmpeg_edits/
lib.rs

1/*
2* TODO:
3    - turn picture into a video (append prepend logo)
4        - force aspect ratio, fit within
5    - merge two videos together (append prepend logo)
6    - trim video from either start end
7    - convert images
8*/
9
10mod 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        /*
401                let mut append_args = vec![];
402                if let Some(ms) = start_time {
403                    let ts = ms_to_tsm(ms);
404                    let from_time = time_format::strftime_ms_local("%H:%M:%S.{ms}", ts)?;
405                    append_args.append(&mut vec!["-ss".to_owned(), from_time])
406                }
407                if let Some(ms) = end_time
408                    && let Some(duration) = duration
409                {
410                    let ts = ms_to_tsm(duration as u32 - ms);
411                    let to_time = time_format::strftime_ms_local("%H:%M:%S.{ms}", ts)?;
412                    append_args.append(&mut vec!["-to".to_owned(), to_time])
413                }
414                // let mut append_args = start_time.map(|e|);
415
416                let append_args_b = append_args.iter().map(|s| s.as_str()).collect::<Vec<_>>();
417
418                let mut com_buffers =
419                    self.create_base_command_buffers(file_path, None, Some(append_args_b))?;
420                for com_buffer in com_buffers.commands.iter_mut() {
421                    self.run_command_buffer(com_buffer)?;
422                }
423
424                self.is_run_sucessful(&mut com_buffers.output_file_path)?;
425                Ok(com_buffers.output_file_path)
426        */
427    }
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    // let ts = TimeStampMs::new(s as i64, m as u16);
466    // debug!("sec:{}, ms:{}", ts.seconds, ts.milliseconds);
467    unimplemented!()
468}