mod probe;
#[cfg(test)]
mod test;
use std::{
io::{BufRead, BufReader},
os::unix::{fs::MetadataExt, process::CommandExt},
path::PathBuf,
process::{Command, Stdio},
str::Utf8Error,
};
use tracing::{debug, error, trace};
use crate::probe::{AspectRatio, Resolution};
pub struct PhotoOutputFormat {
pub codec: PhotoCodec,
pub quality: u8,
pub default_video_duration: u32,
}
pub struct VideoOutputFormat {
pub video_codec: VideoCodec,
pub audio_codec: AudioCodec,
pub video_kbitrate: u16,
pub audio_kbitrate: u8,
}
#[derive(Default)]
pub enum VideoCodec {
#[default]
AV1,
}
#[derive(Default)]
pub enum AudioCodec {
#[default]
OPUS,
}
#[derive(Default)]
pub enum PhotoCodec {
#[default]
WEBP,
}
impl std::fmt::Display for AudioCodec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "libopus")
}
}
impl std::fmt::Display for VideoCodec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "libsvtav1")
}
}
impl PhotoCodec {
pub fn to_container_str(&self) -> &str {
match &self {
Self::WEBP => "webp",
}
}
}
impl VideoCodec {
pub fn to_container_str(&self) -> &str {
match &self {
Self::AV1 => "mp4",
}
}
}
#[derive(Default)]
pub struct Encoder {
pub video_format: VideoOutputFormat,
pub photo_format: PhotoOutputFormat,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("File not supported or does not exist, {0}")]
WrongPath(String),
#[error("Spawning ffmpeg and listening to stdin returned error, is ffmpeg installed? {0}")]
Sys(String),
#[error("Listening to process lines threw error, wha..? {0}")]
IO(#[from] std::io::Error),
#[error("Something went wrong during conversion of file, {0}")]
Ffmpeg(String),
#[error("Something went wrong during probing metadata of file, {0}")]
Ffprobe(String),
#[error("String conversion error, {0}")]
Utf8(#[from] Utf8Error),
}
pub struct EncoderCommandBuffer {
pub input_file_path: PathBuf,
pub output_file_path: PathBuf,
pub pass1_logfile: PathBuf,
pub vcodec_str: String,
pub acodec_str: String,
pub vcodec_rate_str: String,
pub acodec_rate_str: String,
pub commands: Vec<Command>,
}
impl Encoder {
pub fn new(
video_format: Option<VideoOutputFormat>,
photo_format: Option<PhotoOutputFormat>,
) -> Self {
Self {
video_format: video_format.unwrap_or(VideoOutputFormat::default()),
photo_format: photo_format.unwrap_or(PhotoOutputFormat::default()),
}
}
fn create_common_command_args<'a>(
&self,
com_buffer: &'a mut EncoderCommandBuffer,
mut prepend_args: Option<Vec<String>>,
mut append_args: Option<Vec<String>>,
) -> Result<Vec<String>, Error> {
let mut common_args: Vec<String> =
vec!["-progress", "-", "-nostats", "-stats_period", "50ms", "-y"]
.into_iter()
.map(|s| s.to_owned())
.collect();
if let Some(prepend) = prepend_args.as_mut() {
common_args.append(prepend)
}
common_args.append(
&mut vec![
"-i",
com_buffer.input_file_path.to_str().ok_or(Error::WrongPath(
"filepath to string returned none, UNICODE shenanigans?".to_owned(),
))?,
"-vcodec",
&com_buffer.vcodec_str,
"-acodec",
&com_buffer.acodec_str,
"-cpu-used",
"0",
"-threads",
"16",
"-b:v",
&com_buffer.vcodec_rate_str,
"-b:a",
&com_buffer.acodec_rate_str,
"-passlogfile",
&com_buffer
.pass1_logfile
.as_os_str()
.to_str()
.ok_or(Error::WrongPath(
"filepath to string returned none, UNICODE shenanigans?".to_owned(),
))?,
]
.into_iter()
.map(|s| s.to_owned())
.collect(),
);
common_args.append(&mut match self.video_format.video_codec {
VideoCodec::AV1 => vec!["-preset", "8", "-svtav1-params", "rc=1:scd=1"]
.into_iter()
.map(|s| s.to_owned())
.collect(),
});
if let Some(append) = append_args.as_mut() {
common_args.append(append)
}
Ok(common_args)
}
fn create_base_command_buffers<'a>(
&self,
input_file_path: PathBuf,
prepend_args: Option<Vec<&'a str>>,
append_args: Option<Vec<&'a str>>,
) -> Result<EncoderCommandBuffer, Error> {
match input_file_path.try_exists() {
Ok(exists) => {
if !exists {
return Err(Error::WrongPath(
"File doesn't exist according to PathBuf.try_exists(), aiaiai".to_string(),
));
}
}
Err(e) => return Err(e.into()),
}
let mut output_file_path = input_file_path.clone();
let mut pass1 = Command::new("ffmpeg");
let mut pass2 = Command::new("ffmpeg");
let mut pass1_logfile = input_file_path.clone();
pass1_logfile.set_extension("");
output_file_path.set_extension(self.video_format.video_codec.to_container_str());
output_file_path.set_file_name(
"encoded_".to_owned() + output_file_path.file_name().unwrap().to_str().unwrap(),
);
let mut args_pass1 = vec![];
let mut args_pass2 = vec![];
let vcodec_str = self.video_format.video_codec.to_string();
let acodec_str = self.video_format.audio_codec.to_string();
let vcodec_rate_str = format!("{}k", self.video_format.video_kbitrate);
let acodec_rate_str = format!("{}k", self.video_format.audio_kbitrate);
let mut com_buf = EncoderCommandBuffer {
input_file_path,
output_file_path,
pass1_logfile,
vcodec_str,
acodec_str,
vcodec_rate_str,
acodec_rate_str,
commands: vec![],
};
let prepend_args = prepend_args.map(|v| v.into_iter().map(|s| s.to_owned()).collect());
let append_args = append_args.map(|v| v.into_iter().map(|s| s.to_owned()).collect());
let mut common_args =
self.create_common_command_args(&mut com_buf, prepend_args, append_args)?;
args_pass1.append(common_args.clone().as_mut());
args_pass2.append(&mut common_args);
args_pass1.append(
&mut vec![
"-pass",
"1",
"-f",
self.video_format.video_codec.to_container_str(),
]
.into_iter()
.map(|s| s.to_owned())
.collect(),
);
if cfg!(windows) {
args_pass1.push("NUL".to_owned());
} else {
args_pass1.push("/dev/null".to_owned());
}
args_pass2.append(
&mut vec![
"-pass",
"2",
com_buf
.output_file_path
.as_os_str()
.to_str()
.ok_or(Error::WrongPath(
"filepath to string returned none, UNICODE shenanigans?".to_owned(),
))?,
]
.into_iter()
.map(|s| s.to_owned())
.collect(),
);
debug!(
"ffmpeg {} \n&& ffmpeg {}",
args_pass1
.clone()
.iter()
.fold("".to_owned(), |a, b| format!("{} \\\n{}", a, b)),
args_pass2
.clone()
.iter()
.fold("".to_owned(), |a, b| format!("{} \\\n{}", a, b))
);
pass1.args(args_pass1);
pass2.args(args_pass2);
com_buf.commands = vec![pass1, pass2];
Ok(com_buf)
}
fn run_command_buffer(&self, command: &mut Command) -> Result<(), Error> {
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
command.stdin(Stdio::null());
let mut command_handle = command.spawn();
let buff_reader = BufReader::new(command_handle.as_mut().unwrap().stdout.take().ok_or(
Error::Sys("encoder stdout missing - exited early or unavailable".to_owned()),
)?);
for maybe_line in buff_reader.lines() {
match maybe_line {
Ok(line) => {
trace!(line);
if line.contains("end") {
trace!("LINE CONTAINED END!");
break;
}
}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
fn is_run_sucessful(&self, output_file_path: &mut PathBuf) -> Result<(), Error> {
match output_file_path.try_exists() {
Ok(exists) => {
if !exists {
return Err(Error::WrongPath(
"File doesn't exist according to PathBuf.try_exists(), aiaiai".to_string(),
));
}
if output_file_path.metadata()?.size() < 1_000 {
return Err(Error::Ffmpeg(
"File is smaller than 1kb, prolly invalid encode?".to_owned(),
));
}
}
Err(e) => return Err(e.into()),
}
Ok(())
}
pub fn reencode(&self, file_path: PathBuf) -> Result<PathBuf, Error> {
let mut com_buffers = self.create_base_command_buffers(file_path, None, None)?;
for com_buffer in com_buffers.commands.iter_mut() {
self.run_command_buffer(com_buffer)?;
}
self.is_run_sucessful(&mut com_buffers.output_file_path)?;
Ok(com_buffers.output_file_path)
}
pub fn picture_to_video(
&self,
file_path: PathBuf,
duration: Option<u32>,
force_resolution: Option<Resolution>,
) -> Result<PathBuf, Error> {
let duration = duration
.unwrap_or(self.photo_format.default_video_duration)
.to_string();
let mut append_args = vec![];
let mut prepend_args = vec![];
let aspect_filter = force_resolution.map(|a| {
format!(
"scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2",
a.0, a.1, a.0, a.1
)
});
if let Some(aspect) = aspect_filter.as_ref() {
append_args.append(&mut vec!["-vf", &aspect])
}
prepend_args.append(&mut vec!["-t", &duration]);
let mut com_buffers =
self.create_base_command_buffers(file_path, Some(prepend_args), Some(append_args))?;
for com_buffer in com_buffers.commands.iter_mut() {
self.run_command_buffer(com_buffer)?;
}
self.is_run_sucessful(&mut com_buffers.output_file_path)?;
Ok(com_buffers.output_file_path)
}
pub fn trim_video(
&self,
file_path: PathBuf,
start_time: Option<u32>,
end_time: Option<u32>,
duration: Option<f32>,
) -> Result<PathBuf, Error> {
unimplemented!();
}
pub fn surround_video(
&self,
file_path: PathBuf,
append_file_path: PathBuf,
prepend_file_path: PathBuf,
) -> Result<PathBuf, Error> {
unimplemented!();
}
}
impl Default for VideoOutputFormat {
fn default() -> Self {
Self {
video_kbitrate: 1000,
audio_kbitrate: 128,
video_codec: VideoCodec::default(),
audio_codec: AudioCodec::default(),
}
}
}
impl Default for PhotoOutputFormat {
fn default() -> Self {
Self {
quality: 80,
codec: PhotoCodec::default(),
default_video_duration: 5,
}
}
}
fn ms_to_str(ms: u32) -> String {
let mut s = ms / 1000;
let mut ms = ms - s * 1000;
let mut m = s / 60;
unimplemented!()
}