2023-10-30 15:05:21 +00:00
|
|
|
use super::{cmd, filter::Filter};
|
2023-10-30 16:32:21 +00:00
|
|
|
use crate::{
|
|
|
|
render::filter::channel,
|
2023-11-16 08:33:58 +00:00
|
|
|
time::{format_time, Time},
|
|
|
|
Resolution
|
2023-10-30 16:32:21 +00:00
|
|
|
};
|
|
|
|
use anyhow::bail;
|
2023-10-30 19:28:17 +00:00
|
|
|
use camino::Utf8PathBuf as PathBuf;
|
2023-10-28 21:38:17 +00:00
|
|
|
use rational::Rational;
|
2023-10-30 19:28:17 +00:00
|
|
|
use std::{borrow::Cow, fmt::Write as _, process::Command};
|
2023-10-28 21:38:17 +00:00
|
|
|
|
|
|
|
pub(crate) struct FfmpegInput {
|
2023-10-29 21:58:46 +00:00
|
|
|
pub(crate) concat: bool,
|
2023-10-28 21:38:17 +00:00
|
|
|
pub(crate) loop_input: bool,
|
|
|
|
pub(crate) fps: Option<Rational>,
|
|
|
|
pub(crate) start: Option<Time>,
|
|
|
|
pub(crate) duration: Option<Time>,
|
|
|
|
pub(crate) path: PathBuf
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FfmpegInput {
|
|
|
|
pub(crate) fn new(path: PathBuf) -> Self {
|
|
|
|
Self {
|
2023-10-29 21:58:46 +00:00
|
|
|
concat: false,
|
2023-10-28 21:38:17 +00:00
|
|
|
loop_input: false,
|
|
|
|
fps: None,
|
|
|
|
start: None,
|
|
|
|
duration: None,
|
|
|
|
path
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn append_to_cmd(self, cmd: &mut Command) {
|
2023-10-29 21:58:46 +00:00
|
|
|
if self.concat {
|
|
|
|
cmd.arg("-f").arg("concat").arg("-safe").arg("0");
|
|
|
|
}
|
2023-10-28 21:38:17 +00:00
|
|
|
if self.loop_input {
|
|
|
|
cmd.arg("-loop").arg("1");
|
|
|
|
}
|
|
|
|
if let Some(fps) = self.fps {
|
|
|
|
cmd.arg("-r").arg(fps.to_string());
|
|
|
|
}
|
|
|
|
if let Some(start) = self.start {
|
2023-12-19 22:56:04 +00:00
|
|
|
if self.path.ends_with(".mp4") {
|
|
|
|
cmd.arg("-seek_streams_individualy").arg("false");
|
|
|
|
}
|
2023-10-28 21:38:17 +00:00
|
|
|
cmd.arg("-ss").arg(format_time(start));
|
|
|
|
}
|
|
|
|
if let Some(duration) = self.duration {
|
|
|
|
cmd.arg("-t").arg(format_time(duration));
|
|
|
|
}
|
|
|
|
cmd.arg("-i").arg(self.path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-19 22:56:04 +00:00
|
|
|
pub(crate) enum FfmpegOutputFormat {
|
|
|
|
/// AV1 / FLAC
|
|
|
|
Av1Flac,
|
|
|
|
/// AV1 / OPUS
|
|
|
|
Av1Opus,
|
|
|
|
/// AVC (H.264) / AAC
|
|
|
|
AvcAac
|
|
|
|
}
|
|
|
|
|
2023-11-15 14:44:58 +00:00
|
|
|
pub(crate) struct FfmpegOutput {
|
2023-12-19 22:56:04 +00:00
|
|
|
pub(crate) format: FfmpegOutputFormat,
|
|
|
|
pub(crate) audio_bitrate: Option<u64>,
|
|
|
|
pub(crate) video_bitrate: Option<u64>,
|
|
|
|
|
2023-11-15 14:44:58 +00:00
|
|
|
pub(crate) fps: Option<Rational>,
|
|
|
|
pub(crate) duration: Option<Time>,
|
|
|
|
pub(crate) time_base: Option<Rational>,
|
|
|
|
pub(crate) fps_mode_vfr: bool,
|
2023-11-16 08:33:58 +00:00
|
|
|
pub(crate) faststart: bool,
|
2023-12-19 22:56:04 +00:00
|
|
|
|
2023-11-15 14:44:58 +00:00
|
|
|
pub(crate) path: PathBuf
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FfmpegOutput {
|
2023-12-19 22:56:04 +00:00
|
|
|
pub(crate) fn new(format: FfmpegOutputFormat, path: PathBuf) -> Self {
|
2023-11-15 14:44:58 +00:00
|
|
|
Self {
|
2023-12-19 22:56:04 +00:00
|
|
|
format,
|
|
|
|
audio_bitrate: None,
|
|
|
|
video_bitrate: None,
|
2023-11-15 14:44:58 +00:00
|
|
|
fps: None,
|
|
|
|
duration: None,
|
|
|
|
time_base: None,
|
|
|
|
fps_mode_vfr: false,
|
2023-11-16 08:33:58 +00:00
|
|
|
faststart: false,
|
2023-11-15 14:44:58 +00:00
|
|
|
path
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-16 08:33:58 +00:00
|
|
|
pub(crate) fn enable_faststart(mut self) -> Self {
|
|
|
|
self.faststart = true;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2023-12-19 22:56:04 +00:00
|
|
|
fn append_to_cmd(self, cmd: &mut Command, venc: bool, _aenc: bool, vaapi: bool) {
|
|
|
|
// select codec and bitrate
|
|
|
|
const QUALITY: &str = "22";
|
|
|
|
if venc {
|
|
|
|
let mut vcodec: String = match self.format {
|
|
|
|
FfmpegOutputFormat::Av1Flac | FfmpegOutputFormat::Av1Opus => "av1".into(),
|
|
|
|
FfmpegOutputFormat::AvcAac => "h264".into()
|
|
|
|
};
|
|
|
|
if vaapi {
|
|
|
|
vcodec = format!("{vcodec}_vaapi");
|
|
|
|
}
|
|
|
|
cmd.arg("-c:v").arg(vcodec);
|
|
|
|
|
|
|
|
if let Some(bv) = self.video_bitrate {
|
|
|
|
cmd.arg("-b:v").arg(bv.to_string());
|
|
|
|
} else if vaapi {
|
|
|
|
cmd.arg("-rc_mode").arg("CQP");
|
|
|
|
cmd.arg("-global_quality").arg(QUALITY);
|
|
|
|
} else {
|
|
|
|
cmd.arg("-crf").arg(QUALITY);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
cmd.arg("-c:v").arg("copy");
|
|
|
|
}
|
|
|
|
cmd.arg("-c:a").arg(match self.format {
|
|
|
|
FfmpegOutputFormat::Av1Flac => "flac",
|
|
|
|
FfmpegOutputFormat::Av1Opus => "libopus",
|
|
|
|
FfmpegOutputFormat::AvcAac => "aac"
|
|
|
|
});
|
|
|
|
if let Some(ba) = self.audio_bitrate {
|
|
|
|
cmd.arg("-b:a").arg(ba.to_string());
|
|
|
|
} else {
|
|
|
|
cmd.arg("-b:a").arg("128k");
|
|
|
|
}
|
|
|
|
|
|
|
|
// other output options
|
2023-11-15 14:44:58 +00:00
|
|
|
if let Some(fps) = self.fps {
|
|
|
|
cmd.arg("-r").arg(fps.to_string());
|
|
|
|
}
|
|
|
|
if let Some(duration) = self.duration {
|
|
|
|
cmd.arg("-t").arg(format_time(duration));
|
|
|
|
}
|
|
|
|
if let Some(time_base) = self.time_base {
|
|
|
|
cmd.arg("-enc_time_base").arg(time_base.to_string());
|
|
|
|
}
|
|
|
|
if self.fps_mode_vfr {
|
|
|
|
cmd.arg("-fps_mode").arg("vfr");
|
|
|
|
}
|
2023-11-16 08:33:58 +00:00
|
|
|
if self.faststart {
|
|
|
|
cmd.arg("-movflags").arg("+faststart");
|
|
|
|
}
|
2023-11-15 14:44:58 +00:00
|
|
|
cmd.arg(self.path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-16 08:33:58 +00:00
|
|
|
enum FfmpegFilter {
|
|
|
|
None,
|
|
|
|
Filters {
|
|
|
|
filters: Vec<Filter>,
|
|
|
|
output: Cow<'static, str>
|
|
|
|
},
|
|
|
|
Loudnorm,
|
|
|
|
Rescale(Resolution)
|
|
|
|
}
|
|
|
|
|
2023-10-28 21:38:17 +00:00
|
|
|
pub(crate) struct Ffmpeg {
|
|
|
|
inputs: Vec<FfmpegInput>,
|
2023-11-16 08:33:58 +00:00
|
|
|
filter: FfmpegFilter,
|
2023-11-15 14:44:58 +00:00
|
|
|
output: FfmpegOutput,
|
2023-10-30 15:05:21 +00:00
|
|
|
|
|
|
|
filter_idx: usize
|
2023-10-28 21:38:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Ffmpeg {
|
2023-11-15 14:44:58 +00:00
|
|
|
pub fn new(output: FfmpegOutput) -> Self {
|
2023-10-28 21:38:17 +00:00
|
|
|
Self {
|
|
|
|
inputs: Vec::new(),
|
2023-11-16 08:33:58 +00:00
|
|
|
filter: FfmpegFilter::None,
|
2023-10-30 15:05:21 +00:00
|
|
|
output,
|
|
|
|
|
|
|
|
filter_idx: 0
|
2023-10-28 21:38:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-30 20:26:17 +00:00
|
|
|
pub fn add_input(&mut self, input: FfmpegInput) -> String {
|
2023-10-30 16:32:21 +00:00
|
|
|
self.inputs.push(input);
|
2023-10-30 20:26:17 +00:00
|
|
|
(self.inputs.len() - 1).to_string()
|
2023-10-30 16:32:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_filter(&mut self, filter: Filter) -> &mut Self {
|
2023-11-16 08:33:58 +00:00
|
|
|
match &mut self.filter {
|
|
|
|
FfmpegFilter::None => {
|
|
|
|
self.filter = FfmpegFilter::Filters {
|
|
|
|
filters: vec![filter],
|
|
|
|
output: "0".into()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
FfmpegFilter::Filters { filters, .. } => filters.push(filter),
|
|
|
|
_ => panic!("An incompatible type of filter has been set before")
|
|
|
|
}
|
2023-10-30 16:32:21 +00:00
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_filter_output<T: Into<Cow<'static, str>>>(
|
|
|
|
&mut self,
|
2023-11-16 08:33:58 +00:00
|
|
|
filter_output: T
|
2023-10-30 16:32:21 +00:00
|
|
|
) -> &mut Self {
|
2023-11-16 08:33:58 +00:00
|
|
|
match &mut self.filter {
|
|
|
|
FfmpegFilter::None => {
|
|
|
|
self.filter = FfmpegFilter::Filters {
|
|
|
|
filters: vec![],
|
|
|
|
output: filter_output.into()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
FfmpegFilter::Filters { output, .. } => *output = filter_output.into(),
|
|
|
|
_ => panic!("An incompatible type of filter has been set before")
|
|
|
|
}
|
2023-10-30 16:32:21 +00:00
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn enable_loudnorm(&mut self) -> &mut Self {
|
2023-11-16 08:33:58 +00:00
|
|
|
match &mut self.filter {
|
|
|
|
FfmpegFilter::None => self.filter = FfmpegFilter::Loudnorm,
|
|
|
|
FfmpegFilter::Loudnorm => {},
|
|
|
|
_ => panic!("An incompatible type of filter has been set before")
|
|
|
|
}
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn rescale_video(&mut self, res: Resolution) -> &mut Self {
|
|
|
|
match &mut self.filter {
|
|
|
|
FfmpegFilter::None => self.filter = FfmpegFilter::Rescale(res),
|
|
|
|
FfmpegFilter::Loudnorm => {},
|
|
|
|
_ => panic!("An incompatible type of filter has been set before")
|
|
|
|
}
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2023-10-30 16:32:21 +00:00
|
|
|
pub fn run(mut self) -> anyhow::Result<()> {
|
2023-10-28 21:38:17 +00:00
|
|
|
let mut cmd = cmd();
|
2023-10-30 19:28:17 +00:00
|
|
|
cmd.arg("ffmpeg").arg("-hide_banner").arg("-y");
|
2023-10-28 21:38:17 +00:00
|
|
|
|
2023-10-29 21:58:46 +00:00
|
|
|
// determine whether the video need to be re-encoded
|
2023-11-16 08:33:58 +00:00
|
|
|
// vdec is only true if the video should be decoded on hardware
|
|
|
|
let (vdec, venc, aenc) = match &self.filter {
|
|
|
|
FfmpegFilter::None => (false, false, false),
|
|
|
|
FfmpegFilter::Filters { .. } => (false, true, true),
|
|
|
|
FfmpegFilter::Loudnorm => (false, false, true),
|
|
|
|
FfmpegFilter::Rescale(_) => (true, true, false)
|
|
|
|
};
|
2023-10-29 21:58:46 +00:00
|
|
|
|
2023-10-28 21:38:17 +00:00
|
|
|
// initialise a vaapi device if one exists
|
|
|
|
let vaapi_device: PathBuf = "/dev/dri/renderD128".into();
|
2023-11-16 08:33:58 +00:00
|
|
|
let vaapi = vaapi_device.exists();
|
|
|
|
if vaapi && venc {
|
|
|
|
if vdec {
|
|
|
|
cmd.arg("-hwaccel").arg("vaapi");
|
|
|
|
cmd.arg("-hwaccel_device").arg(vaapi_device);
|
|
|
|
cmd.arg("-hwaccel_output_format").arg("vaapi");
|
|
|
|
} else {
|
|
|
|
cmd.arg("-vaapi_device").arg(&vaapi_device);
|
|
|
|
}
|
2023-10-28 21:38:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// append all the inputs
|
|
|
|
for i in self.inputs {
|
|
|
|
i.append_to_cmd(&mut cmd);
|
|
|
|
}
|
|
|
|
|
|
|
|
// always try to synchronise audio
|
|
|
|
cmd.arg("-async").arg("1");
|
|
|
|
|
2023-10-30 19:28:17 +00:00
|
|
|
// apply filters
|
2023-11-16 08:33:58 +00:00
|
|
|
match self.filter {
|
|
|
|
FfmpegFilter::None => {},
|
|
|
|
FfmpegFilter::Filters { filters, output } => {
|
2023-10-30 16:32:21 +00:00
|
|
|
let mut complex = String::new();
|
2023-11-16 08:33:58 +00:00
|
|
|
for filter in filters {
|
2023-11-16 11:12:17 +00:00
|
|
|
filter
|
|
|
|
.append_to_complex_filter(&mut complex, &mut self.filter_idx)?;
|
2023-10-30 16:32:21 +00:00
|
|
|
}
|
2023-10-30 19:28:17 +00:00
|
|
|
if vaapi {
|
2023-11-16 11:12:17 +00:00
|
|
|
write!(complex, "{}format=nv12,hwupload[v]", channel('v', &output))?;
|
2023-10-30 19:28:17 +00:00
|
|
|
} else {
|
2023-11-16 11:12:17 +00:00
|
|
|
write!(complex, "{}null[v]", channel('v', &output))?;
|
2023-10-30 19:28:17 +00:00
|
|
|
}
|
2023-10-30 16:32:21 +00:00
|
|
|
cmd.arg("-filter_complex").arg(complex);
|
2023-10-30 19:28:17 +00:00
|
|
|
cmd.arg("-map").arg("[v]");
|
2023-11-16 08:33:58 +00:00
|
|
|
cmd.arg("-map").arg(channel('a', &output));
|
|
|
|
},
|
|
|
|
FfmpegFilter::Loudnorm => {
|
|
|
|
cmd.arg("-af").arg(concat!(
|
|
|
|
"pan=mono|c0=FR,",
|
|
|
|
"loudnorm=dual_mono=true:print_format=summary,",
|
|
|
|
"pan=stereo|c0=c0|c1=c0,",
|
|
|
|
"aformat=sample_rates=48000"
|
|
|
|
));
|
|
|
|
},
|
|
|
|
FfmpegFilter::Rescale(res) => {
|
|
|
|
cmd.arg("-vf").arg(if vaapi {
|
|
|
|
format!("scale_vaapi=w={}:h={}", res.width(), res.height())
|
|
|
|
} else {
|
|
|
|
format!("scale=w={}:h={}", res.width(), res.height())
|
|
|
|
});
|
2023-10-30 16:32:21 +00:00
|
|
|
}
|
|
|
|
}
|
2023-10-29 21:58:46 +00:00
|
|
|
|
2023-12-19 22:56:04 +00:00
|
|
|
self.output.append_to_cmd(&mut cmd, venc, aenc, vaapi);
|
2023-10-30 19:28:17 +00:00
|
|
|
|
2023-10-30 16:32:21 +00:00
|
|
|
let status = cmd.status()?;
|
|
|
|
if status.success() {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
bail!("ffmpeg failed with exit code {:?}", status.code())
|
|
|
|
}
|
2023-10-28 21:38:17 +00:00
|
|
|
}
|
|
|
|
}
|