run all transcoding through the ffmpeg helper
This commit is contained in:
parent
2882fb286a
commit
5cc91d712f
2 changed files with 122 additions and 65 deletions
|
@ -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");
|
||||||
}
|
}
|
||||||
|
if self.faststart {
|
||||||
cmd.arg("-movflags").arg("+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,15 +192,26 @@ 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 {
|
||||||
|
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);
|
cmd.arg("-vaapi_device").arg(&vaapi_device);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// append all the inputs
|
// append all the inputs
|
||||||
for i in self.inputs {
|
for i in self.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");
|
||||||
|
if self.video_bitrate.is_none() {
|
||||||
cmd.arg("-rc_mode").arg("CQP");
|
cmd.arg("-rc_mode").arg("CQP");
|
||||||
cmd.arg("-global_quality").arg(QUALITY);
|
cmd.arg("-global_quality").arg(QUALITY);
|
||||||
|
}
|
||||||
} else if venc {
|
} else if venc {
|
||||||
cmd.arg("-c:v").arg("libx264");
|
cmd.arg("-c:v").arg("libx264");
|
||||||
|
if self.video_bitrate.is_none() {
|
||||||
cmd.arg("-crf").arg(QUALITY);
|
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");
|
||||||
|
|
|
@ -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")
|
|
||||||
.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)
|
Ok(output)
|
||||||
} else {
|
|
||||||
bail!("ffmpeg failed with exit code {:?}", status.code())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue