run all transcoding through the ffmpeg helper

This commit is contained in:
Dominic 2023-11-16 09:33:58 +01:00
parent 2882fb286a
commit 5cc91d712f
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
2 changed files with 122 additions and 65 deletions

View file

@ -1,7 +1,8 @@
use super::{cmd, filter::Filter}; use super::{cmd, filter::Filter};
use crate::{ use crate::{
render::filter::channel, render::filter::channel,
time::{format_time, Time} time::{format_time, Time},
Resolution
}; };
use anyhow::bail; use anyhow::bail;
use camino::Utf8PathBuf as PathBuf; use camino::Utf8PathBuf as PathBuf;
@ -55,6 +56,7 @@ pub(crate) struct FfmpegOutput {
pub(crate) duration: Option<Time>, pub(crate) duration: Option<Time>,
pub(crate) time_base: Option<Rational>, pub(crate) time_base: Option<Rational>,
pub(crate) fps_mode_vfr: bool, pub(crate) fps_mode_vfr: bool,
pub(crate) faststart: bool,
pub(crate) path: PathBuf pub(crate) path: PathBuf
} }
@ -65,10 +67,16 @@ impl FfmpegOutput {
duration: None, duration: None,
time_base: None, time_base: None,
fps_mode_vfr: false, fps_mode_vfr: false,
faststart: false,
path path
} }
} }
pub(crate) fn enable_faststart(mut self) -> Self {
self.faststart = true;
self
}
fn append_to_cmd(self, cmd: &mut Command) { fn append_to_cmd(self, cmd: &mut Command) {
if let Some(fps) = self.fps { if let Some(fps) = self.fps {
cmd.arg("-r").arg(fps.to_string()); cmd.arg("-r").arg(fps.to_string());
@ -82,16 +90,27 @@ impl FfmpegOutput {
if self.fps_mode_vfr { if self.fps_mode_vfr {
cmd.arg("-fps_mode").arg("vfr"); cmd.arg("-fps_mode").arg("vfr");
} }
cmd.arg("-movflags").arg("+faststart"); if self.faststart {
cmd.arg("-movflags").arg("+faststart");
}
cmd.arg(self.path); cmd.arg(self.path);
} }
} }
enum FfmpegFilter {
None,
Filters {
filters: Vec<Filter>,
output: Cow<'static, str>
},
Loudnorm,
Rescale(Resolution)
}
pub(crate) struct Ffmpeg { pub(crate) struct Ffmpeg {
inputs: Vec<FfmpegInput>, inputs: Vec<FfmpegInput>,
filters: Vec<Filter>, filter: FfmpegFilter,
filters_output: Cow<'static, str>, video_bitrate: Option<&'static str>,
loudnorm: bool,
output: FfmpegOutput, output: FfmpegOutput,
filter_idx: usize filter_idx: usize
@ -101,9 +120,8 @@ impl Ffmpeg {
pub fn new(output: FfmpegOutput) -> Self { pub fn new(output: FfmpegOutput) -> Self {
Self { Self {
inputs: Vec::new(), inputs: Vec::new(),
filters: Vec::new(), filter: FfmpegFilter::None,
filters_output: "0".into(), video_bitrate: None,
loudnorm: false,
output, output,
filter_idx: 0 filter_idx: 0
@ -116,22 +134,56 @@ impl Ffmpeg {
} }
pub fn add_filter(&mut self, filter: Filter) -> &mut Self { pub fn add_filter(&mut self, filter: Filter) -> &mut Self {
assert!(!self.loudnorm); match &mut self.filter {
self.filters.push(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")
}
self self
} }
pub fn set_filter_output<T: Into<Cow<'static, str>>>( pub fn set_filter_output<T: Into<Cow<'static, str>>>(
&mut self, &mut self,
output: T filter_output: T
) -> &mut Self { ) -> &mut Self {
self.filters_output = output.into(); 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")
}
self self
} }
pub fn enable_loudnorm(&mut self) -> &mut Self { pub fn enable_loudnorm(&mut self) -> &mut Self {
assert!(self.filters.is_empty()); match &mut self.filter {
self.loudnorm = true; 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
}
pub fn set_video_bitrate(&mut self, bitrate: &'static str) -> &mut Self {
self.video_bitrate = Some(bitrate);
self self
} }
@ -140,14 +192,25 @@ impl Ffmpeg {
cmd.arg("ffmpeg").arg("-hide_banner").arg("-y"); cmd.arg("ffmpeg").arg("-hide_banner").arg("-y");
// determine whether the video need to be re-encoded // determine whether the video need to be re-encoded
let venc = !self.filters.is_empty(); // vdec is only true if the video should be decoded on hardware
let aenc = !self.filters.is_empty() || self.loudnorm; 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)
};
// initialise a vaapi device if one exists // initialise a vaapi device if one exists
let vaapi_device: PathBuf = "/dev/dri/renderD128".into(); let vaapi_device: PathBuf = "/dev/dri/renderD128".into();
let vaapi = venc && vaapi_device.exists(); let vaapi = vaapi_device.exists();
if vaapi { if vaapi && venc {
cmd.arg("-vaapi_device").arg(&vaapi_device); 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);
}
} }
// append all the inputs // append all the inputs
@ -159,30 +222,36 @@ impl Ffmpeg {
cmd.arg("-async").arg("1"); cmd.arg("-async").arg("1");
// apply filters // apply filters
match (self.loudnorm, self.filters) { match self.filter {
(true, f) if f.is_empty() => { FfmpegFilter::None => {},
cmd.arg("-af").arg("pan=mono|c0=FR,loudnorm,pan=stereo|c0=c0|c1=c0,aformat=sample_rates=48000"); FfmpegFilter::Filters { filters, output } => {
},
(true, _) => panic!("Filters and loudnorm at the same time is not supported"),
(false, f) if f.is_empty() => {},
(false, f) => {
let mut complex = String::new(); let mut complex = String::new();
for filter in f { for filter in filters {
filter.append_to_complex_filter(&mut complex, &mut self.filter_idx); filter.append_to_complex_filter(&mut complex, &mut self.filter_idx);
} }
if vaapi { if vaapi {
write!( write!(complex, "{}format=nv12,hwupload[v]", channel('v', &output));
complex,
"{}format=nv12,hwupload[v]",
channel('v', &self.filters_output)
);
} else { } else {
write!(complex, "{}null[v]", channel('v', &self.filters_output)); write!(complex, "{}null[v]", channel('v', &output));
} }
cmd.arg("-filter_complex").arg(complex); cmd.arg("-filter_complex").arg(complex);
cmd.arg("-map").arg("[v]"); cmd.arg("-map").arg("[v]");
cmd.arg("-map").arg(channel('a', &self.filters_output)); 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())
});
} }
} }
@ -190,14 +259,21 @@ impl Ffmpeg {
const QUALITY: &str = "24"; const QUALITY: &str = "24";
if vaapi { if vaapi {
cmd.arg("-c:v").arg("h264_vaapi"); cmd.arg("-c:v").arg("h264_vaapi");
cmd.arg("-rc_mode").arg("CQP"); if self.video_bitrate.is_none() {
cmd.arg("-global_quality").arg(QUALITY); cmd.arg("-rc_mode").arg("CQP");
cmd.arg("-global_quality").arg(QUALITY);
}
} else if venc { } else if venc {
cmd.arg("-c:v").arg("libx264"); cmd.arg("-c:v").arg("libx264");
cmd.arg("-crf").arg(QUALITY); if self.video_bitrate.is_none() {
cmd.arg("-crf").arg(QUALITY);
}
} else { } else {
cmd.arg("-c:v").arg("copy"); cmd.arg("-c:v").arg("copy");
} }
if venc && self.video_bitrate.is_some() {
cmd.arg("-b:v").arg(self.video_bitrate.unwrap());
}
if aenc { if aenc {
cmd.arg("-c:a").arg("aac"); cmd.arg("-c:a").arg("aac");
cmd.arg("-b:a").arg("128000"); cmd.arg("-b:a").arg("128000");

View file

@ -128,6 +128,7 @@ fn svg2mp4(
duration: Some(duration), duration: Some(duration),
time_base: Some(meta.source_tbn), time_base: Some(meta.source_tbn),
fps_mode_vfr: true, fps_mode_vfr: true,
faststart: false,
path: mp4 path: mp4
}); });
ffmpeg.add_input(FfmpegInput { ffmpeg.add_input(FfmpegInput {
@ -452,32 +453,12 @@ impl<'a> Renderer<'a> {
let output = self.video_mp4_res(res); let output = self.video_mp4_res(res);
println!("\x1B[1m ==> Rescaling to {}p\x1B[0m", res.height()); println!("\x1B[1m ==> Rescaling to {}p\x1B[0m", res.height());
let mut ffmpeg = cmd(); let mut ffmpeg =
ffmpeg.arg("ffmpeg").arg("-hide_banner"); Ffmpeg::new(FfmpegOutput::new(output.clone()).enable_faststart());
// TODO do we just always want hwaccel? ffmpeg.add_input(FfmpegInput::new(input));
ffmpeg ffmpeg.rescale_video(res);
.arg("-hwaccel") ffmpeg.set_video_bitrate(res.bitrate());
.arg("vaapi") ffmpeg.run()?;
.arg("-hwaccel_device") Ok(output)
.arg("/dev/dri/renderD128")
.arg("-hwaccel_output_format")
.arg("vaapi");
ffmpeg.arg("-i").arg(input);
ffmpeg.arg("-vf").arg(format!(
"scale_vaapi=w={}:h={}",
res.width(),
res.height()
));
ffmpeg.arg("-c:a").arg("copy").arg("-c:v").arg("h264_vaapi");
ffmpeg.arg("-b:v").arg(res.bitrate());
ffmpeg.arg("-movflags").arg("+faststart");
ffmpeg.arg(&output);
let status = ffmpeg.status()?;
if status.success() {
Ok(output)
} else {
bail!("ffmpeg failed with exit code {:?}", status.code())
}
} }
} }