diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 63657cd05d1f996f1082c126a4ff98d5be9a229e..614ad1930fb608054edbea254797519e7c8e9dd4 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -1,7 +1,8 @@ use super::{cmd, filter::Filter}; use crate::{ render::filter::channel, - time::{format_time, Time} + time::{format_time, Time}, + Resolution }; use anyhow::bail; use camino::Utf8PathBuf as PathBuf; @@ -55,6 +56,7 @@ pub(crate) struct FfmpegOutput { pub(crate) duration: Option<Time>, pub(crate) time_base: Option<Rational>, pub(crate) fps_mode_vfr: bool, + pub(crate) faststart: bool, pub(crate) path: PathBuf } @@ -65,10 +67,16 @@ impl FfmpegOutput { duration: None, time_base: None, fps_mode_vfr: false, + faststart: false, path } } + pub(crate) fn enable_faststart(mut self) -> Self { + self.faststart = true; + self + } + fn append_to_cmd(self, cmd: &mut Command) { if let Some(fps) = self.fps { cmd.arg("-r").arg(fps.to_string()); @@ -82,16 +90,27 @@ impl FfmpegOutput { if self.fps_mode_vfr { cmd.arg("-fps_mode").arg("vfr"); } - cmd.arg("-movflags").arg("+faststart"); + if self.faststart { + cmd.arg("-movflags").arg("+faststart"); + } cmd.arg(self.path); } } +enum FfmpegFilter { + None, + Filters { + filters: Vec<Filter>, + output: Cow<'static, str> + }, + Loudnorm, + Rescale(Resolution) +} + pub(crate) struct Ffmpeg { inputs: Vec<FfmpegInput>, - filters: Vec<Filter>, - filters_output: Cow<'static, str>, - loudnorm: bool, + filter: FfmpegFilter, + video_bitrate: Option<&'static str>, output: FfmpegOutput, filter_idx: usize @@ -101,9 +120,8 @@ impl Ffmpeg { pub fn new(output: FfmpegOutput) -> Self { Self { inputs: Vec::new(), - filters: Vec::new(), - filters_output: "0".into(), - loudnorm: false, + filter: FfmpegFilter::None, + video_bitrate: None, output, filter_idx: 0 @@ -116,22 +134,56 @@ impl Ffmpeg { } pub fn add_filter(&mut self, filter: Filter) -> &mut Self { - assert!(!self.loudnorm); - self.filters.push(filter); + 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") + } self } pub fn set_filter_output<T: Into<Cow<'static, str>>>( &mut self, - output: T + filter_output: T ) -> &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 } pub fn enable_loudnorm(&mut self) -> &mut Self { - assert!(self.filters.is_empty()); - self.loudnorm = true; + 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 + } + + pub fn set_video_bitrate(&mut self, bitrate: &'static str) -> &mut Self { + self.video_bitrate = Some(bitrate); self } @@ -140,14 +192,25 @@ impl Ffmpeg { cmd.arg("ffmpeg").arg("-hide_banner").arg("-y"); // determine whether the video need to be re-encoded - let venc = !self.filters.is_empty(); - let aenc = !self.filters.is_empty() || self.loudnorm; + // 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) + }; // initialise a vaapi device if one exists let vaapi_device: PathBuf = "/dev/dri/renderD128".into(); - let vaapi = venc && vaapi_device.exists(); - if vaapi { - cmd.arg("-vaapi_device").arg(&vaapi_device); + 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); + } } // append all the inputs @@ -159,30 +222,36 @@ impl Ffmpeg { cmd.arg("-async").arg("1"); // apply filters - match (self.loudnorm, self.filters) { - (true, f) if f.is_empty() => { - cmd.arg("-af").arg("pan=mono|c0=FR,loudnorm,pan=stereo|c0=c0|c1=c0,aformat=sample_rates=48000"); - }, - (true, _) => panic!("Filters and loudnorm at the same time is not supported"), - - (false, f) if f.is_empty() => {}, - (false, f) => { + match self.filter { + FfmpegFilter::None => {}, + FfmpegFilter::Filters { filters, output } => { let mut complex = String::new(); - for filter in f { + for filter in filters { filter.append_to_complex_filter(&mut complex, &mut self.filter_idx); } if vaapi { - write!( - complex, - "{}format=nv12,hwupload[v]", - channel('v', &self.filters_output) - ); + write!(complex, "{}format=nv12,hwupload[v]", channel('v', &output)); } 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("-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"; if vaapi { cmd.arg("-c:v").arg("h264_vaapi"); - cmd.arg("-rc_mode").arg("CQP"); - cmd.arg("-global_quality").arg(QUALITY); + if self.video_bitrate.is_none() { + cmd.arg("-rc_mode").arg("CQP"); + cmd.arg("-global_quality").arg(QUALITY); + } } else if venc { cmd.arg("-c:v").arg("libx264"); - cmd.arg("-crf").arg(QUALITY); + if self.video_bitrate.is_none() { + cmd.arg("-crf").arg(QUALITY); + } } else { cmd.arg("-c:v").arg("copy"); } + if venc && self.video_bitrate.is_some() { + cmd.arg("-b:v").arg(self.video_bitrate.unwrap()); + } if aenc { cmd.arg("-c:a").arg("aac"); cmd.arg("-b:a").arg("128000"); diff --git a/src/render/mod.rs b/src/render/mod.rs index a4fafece156c9e39c53264351d23ad868e4d64e4..3b9257c9d4ede39df92cbffdac808aea016c2ab7 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -128,6 +128,7 @@ fn svg2mp4( duration: Some(duration), time_base: Some(meta.source_tbn), fps_mode_vfr: true, + faststart: false, path: mp4 }); ffmpeg.add_input(FfmpegInput { @@ -452,32 +453,12 @@ impl<'a> Renderer<'a> { let output = self.video_mp4_res(res); println!("\x1B[1m ==> Rescaling to {}p\x1B[0m", res.height()); - let mut ffmpeg = cmd(); - ffmpeg.arg("ffmpeg").arg("-hide_banner"); - // TODO do we just always want hwaccel? - ffmpeg - .arg("-hwaccel") - .arg("vaapi") - .arg("-hwaccel_device") - .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()) - } + let mut ffmpeg = + Ffmpeg::new(FfmpegOutput::new(output.clone()).enable_faststart()); + ffmpeg.add_input(FfmpegInput::new(input)); + ffmpeg.rescale_video(res); + ffmpeg.set_video_bitrate(res.bitrate()); + ffmpeg.run()?; + Ok(output) } }