n-mb/src/encoder.rs
2024-06-24 21:34:31 -04:00

488 lines
14 KiB
Rust

use anyhow::{bail, Context};
use pbr::{Pipe, ProgressBar};
use std::{path::PathBuf, process::Stdio};
use tokio::{
io::{BufReader, Lines},
process::{Child, ChildStdout, Command},
};
use crate::VideoCodec;
const MAX_OPUS_BITRATE: f32 = 256.; //kbits
const MIN_OPUS_BITRATE: f32 = 50.; //kbits
pub struct FFMPEGCommand {
pub file_name: String,
pub command: (Command, Option<Command>),
pub target_size: u16,
pub resolution: Option<(u32, u32)>,
pub duration: Option<f32>,
pub media_type: MediaType,
pub exec_handle: Option<Child>,
pub buff_reader: Option<Lines<BufReader<ChildStdout>>>,
pub status: EncodingStatus,
pub passed_pass_1: bool,
pub progressed_time: f32,
pub progress_bar: Option<ProgressBar<Pipe>>,
}
struct MediaData {
resolution: Option<(u16, u16)>,
duration: f32,
old_kbit_rate: Option<u32>,
}
impl FFMPEGCommand {
pub async fn new(
media_type: MediaType,
path: &PathBuf,
size: u16,
codec: VideoCodec,
) -> anyhow::Result<Self> {
match media_type {
MediaType::Video => Self::create_video(path, size, codec).await,
MediaType::Audio => Self::create_audio(path, size).await,
MediaType::Image => Self::create_image(path, size),
MediaType::AnimatedImage => Self::create_animated_image(path),
}
}
async fn create_audio(path: &PathBuf, size: u16) -> anyhow::Result<Self> {
let ffprobe_out = parse_ffprobe(path).await?;
let duration = ffprobe_out.duration;
let max_kbit_rate = match ffprobe_out.old_kbit_rate {
None => MAX_OPUS_BITRATE,
Some(r) => {
if (r as f32) < MAX_OPUS_BITRATE {
r as f32
} else {
MAX_OPUS_BITRATE
}
}
};
let bitrate = (size as f32 * 1000. / duration) * 0.85;
let bitrate = bitrate.clamp(MIN_OPUS_BITRATE, max_kbit_rate) as u16;
/*
println!(
"{} * {} ~= {} (actually is {})",
duration,
bitrate,
size * 1000,
duration * bitrate as f32
);
*/
let mut new_path = path.clone();
new_path.set_extension("ogg");
let mut command = Command::new("ffmpeg");
command.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]);
command.args([
"-y",
"-i",
path.as_os_str()
.to_str()
.expect("Path dissapeared on unwrap"),
"-c:a",
"libopus",
"-b:a",
format!("{}k", bitrate).as_str(),
new_path
.as_os_str()
.to_str()
.expect("Path dissapeared on unwrap"),
]);
Ok(FFMPEGCommand {
file_name: path.file_name().unwrap().to_str().unwrap().to_owned(),
resolution: None,
duration: Some(duration),
command: (command, None),
media_type: MediaType::Audio,
target_size: size,
status: EncodingStatus::NotStarted,
exec_handle: None,
buff_reader: None,
progress_bar: None,
passed_pass_1: false,
progressed_time: 0.,
})
}
async fn create_video(path: &PathBuf, size: u16, codec: VideoCodec) -> anyhow::Result<Self> {
let ffprobe_out = parse_ffprobe(path).await?;
let duration = ffprobe_out.duration;
let resolution = ffprobe_out.resolution.context("Missing resolution")?;
let mut overflown_audio_bitrate = None;
let mut audio_bitrate = size as f32 * 180. / duration;
let mut video_bitrate = size as f32 * 780. / duration;
if audio_bitrate < MIN_OPUS_BITRATE {
overflown_audio_bitrate = Some(audio_bitrate - MIN_OPUS_BITRATE);
audio_bitrate = MIN_OPUS_BITRATE;
}
if audio_bitrate > MAX_OPUS_BITRATE {
overflown_audio_bitrate = Some(audio_bitrate - MAX_OPUS_BITRATE);
audio_bitrate = MAX_OPUS_BITRATE;
}
if let Some(overflow) = overflown_audio_bitrate {
/*
println!(
"-b:v:{}\n-b:a:{} (ovw: {})\nsum:{}/{}",
video_bitrate,
audio_bitrate,
overflow,
video_bitrate + audio_bitrate,
size
);*/
video_bitrate += overflow;
}
let mut height = resolution.1;
if resolution.1 >= 1080 && duration > 150. {
height = 1080
}
if resolution.1 >= 720 && duration > 600. {
height = 720
}
if resolution.1 >= 480 && duration > 900. {
height = 480
}
let old_path_str = path.as_os_str().to_str().context("missing or bad path")?;
let mut new_path = path.clone();
let scale_arg = format!("scale=-1:{height}");
let bitrate_arg = format!("{}k", video_bitrate as u16);
let minrate_arg = format!("{}k", (video_bitrate * 0.5) as u16);
let maxrate_arg = format!("{}k", (video_bitrate * 1.45) as u16);
let ba_arg = format!("{}k", audio_bitrate as u16);
let mut passlogfile = path.clone();
passlogfile.set_extension("");
let mut command = Command::new("ffmpeg");
let mut command2 = Command::new("ffmpeg");
command.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]);
command2.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]);
let video_codec;
let audio_codec;
match codec {
VideoCodec::WEBM => {
video_codec = "libvpx-vp9";
audio_codec = "libopus";
new_path.set_extension("webm");
new_path.set_file_name(
"minified_".to_owned() + new_path.file_name().unwrap().to_str().unwrap(),
)
}
VideoCodec::HEVC => {
video_codec = "libx265";
audio_codec = "aac";
new_path.set_extension("mp4");
new_path.set_file_name(
"minified_".to_owned() + new_path.file_name().unwrap().to_str().unwrap(),
)
}
};
/*
println!(
"{} * ({}+{}) ~= {} (actually is {})",
duration,
video_bitrate,
audio_bitrate,
size,
(duration * ((video_bitrate + audio_bitrate) / 1000.)) as f32
);
*/
let pass = [
"-y",
"-i",
old_path_str,
"-vcodec",
video_codec,
"-acodec",
audio_codec,
"-vf",
&scale_arg,
"-deadline",
"good",
"-quality",
"good",
"-cpu-used",
"0",
"-undershoot-pct",
"0",
"-overshoot-pct",
"0",
"-b:v",
&bitrate_arg,
"-minrate",
&minrate_arg,
"-maxrate",
&maxrate_arg,
"-b:a",
&ba_arg,
"-row-mt",
"1",
"-tile-rows",
"2",
"-tile-columns",
"4",
"-threads",
"16",
"-auto-alt-ref",
"6",
"-qmax",
"60",
"-qmin",
"1",
"-g",
"240",
"-passlogfile",
passlogfile
.as_os_str()
.to_str()
.context("missing or bad path")?,
];
command.args(pass);
command.args([
"-pass",
"1",
"-f",
new_path.extension().unwrap().to_str().unwrap(),
]);
if cfg!(windows) {
command.arg("NUL");
} else {
command.arg("/dev/null");
}
command2.args(pass);
command2.args([
"-pass",
"2",
new_path
.as_os_str()
.to_str()
.context("missing or bad path")?,
]);
// dbg!(&command);
// dbg!(&command2);
Ok(FFMPEGCommand {
file_name: path.file_name().unwrap().to_str().unwrap().to_owned(),
resolution: None,
duration: Some(duration),
command: (command, Some(command2)),
media_type: MediaType::Video,
target_size: size,
buff_reader: None,
exec_handle: None,
status: EncodingStatus::InProgress,
passed_pass_1: false,
progressed_time: 0.,
progress_bar: None,
})
}
fn create_image(path: &PathBuf, size: u16) -> anyhow::Result<Self> {
let mut new_path = path.clone();
new_path.set_extension("webp");
let mut command = Command::new("ffmpeg");
command.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]);
command.args([
"-y",
"-i",
path.as_os_str()
.to_str()
.expect("Path dissapeared on unwrap"),
"-qscale",
"90",
"-compression_level",
"6",
new_path
.as_os_str()
.to_str()
.expect("Path dissapeared on unwrap"),
]);
Ok(FFMPEGCommand {
file_name: path.file_name().unwrap().to_str().unwrap().to_owned(),
resolution: None,
duration: None,
command: (command, None),
media_type: MediaType::Image,
target_size: size,
status: EncodingStatus::InProgress,
exec_handle: None,
buff_reader: None,
passed_pass_1: false,
progressed_time: 0.,
progress_bar: None,
})
}
fn create_animated_image(_path: &PathBuf) -> anyhow::Result<Self> {
bail!("")
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum MediaType {
Video,
Audio,
Image,
AnimatedImage,
}
#[derive(PartialEq, Eq, Debug)]
pub enum EncodingStatus {
Finished,
Failed,
InProgress,
NotStarted,
}
async fn parse_ffprobe(path: &PathBuf) -> anyhow::Result<MediaData> {
let ffprobe = Command::new("ffprobe")
.args([
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height,duration,bit_rate",
"-of",
"csv=s=,:p=0",
])
.arg(path)
.stderr(Stdio::piped())
.output()
.await?;
ffprobe
.status
.exit_ok()
.context("Failed to run ffprobe. Make sure ffprobe is installed and file exists")?;
let text = std::str::from_utf8(&ffprobe.stdout)?;
let mem = text.split(',').collect::<Vec<_>>();
let width = mem.first().and_then(|v| v.parse::<u16>().ok());
let height = mem.get(1).and_then(|v| v.parse::<u16>().ok());
let ffprobe = Command::new("ffprobe")
.args([
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"format=duration,bit_rate",
"-of",
"csv=s=,:p=0",
])
.arg(path)
.stderr(Stdio::piped())
.output()
.await?;
ffprobe
.status
.exit_ok()
.context("Failed to run ffprobe. Make sure ffprobe is installed and file exists")?;
let text = std::str::from_utf8(&ffprobe.stdout)?;
let mem = text.split(',').collect::<Vec<_>>();
let duration = mem
.get(0)
.and_then(|v| v.parse::<f32>().ok())
.context("missing duration")?;
let old_kbit_rate = mem
.get(1)
.and_then(|v| v.parse::<u32>().ok().map(|v| v / 1000));
let resolution = width.zip(height);
// if let Ok(dur) = parse_duration(text) {
// duration = dur
// } else {
// bail!("FFProbe missing duration in media. Is file corrupted or non-existent?")
// }
// let old_kbit_rate = parse_bitrate(text).ok();
//
// let mut resolution = None;
// if text.contains("Stream") {
// resolution = parse_resolution(text).ok();
// }
//
Ok(MediaData {
duration,
resolution,
old_kbit_rate,
})
}
fn parse_duration(text: &str) -> anyhow::Result<f32> {
let text = text[text.find("Duration").unwrap()..].to_owned();
let dur_text = text[text
.find(':')
.context("something wrong with the ffprobe output")?
+ 2
..text
.find(',')
.context("something wrong with the ffprobe output")?]
.to_owned();
let durs_text: Vec<&str> = dur_text.split(':').collect();
let mut durs_text_iter = durs_text.into_iter();
let h = durs_text_iter
.next()
.context("something wrong with the ffprobe output")?
.parse::<f32>()?;
let m = durs_text_iter
.next()
.context("something wrong with the ffprobe output")?
.parse::<f32>()?;
let s = durs_text_iter
.next()
.context("something wrong with the ffprobe output")?
.parse::<f32>()?;
Ok(h * 60. * 60. + m * 60. + s)
}
// fn parse_bitrate(text: &str) -> anyhow::Result<u16> {
// let text = text[text.find("bitrate").unwrap()..].to_owned();
// let bitrate_text = text[9..text.find('/').unwrap() - 2].to_owned();
//
// Ok(bitrate_text.parse::<u16>()?)
// }
//
// fn parse_resolution(text: &str) -> anyhow::Result<(u16, u16)> {
// let text = text[text.find("Stream").unwrap()..].to_owned();
// let sar_i = text
// .find("[SAR ")
// .context("something wrong with the ffprobe output")?
// - 1;
//
// let rb_b4_sar_i = text[..sar_i]
// .rfind(',')
// .context("something wrong with the ffprobe output")?
// + 1;
//
// let res_text = text[rb_b4_sar_i..sar_i].to_owned();
// let res_text = res_text.trim().to_owned();
//
// let width = res_text[..res_text
// .find('x')
// .context("something wrong with ffprobe output")?]
// .to_owned()
// .parse::<u16>()?;
//
// let height = res_text[res_text
// .find('x')
// .context("something wrong with ffprobe output")?
// + 1..]
// .to_owned()
// .parse::<u16>()?;
//
// Ok((width, height))
// }