render_video/src/render/ffmpeg.rs

191 lines
4.3 KiB
Rust
Raw Normal View History

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,
time::{format_time, Time}
};
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-11-02 10:07:35 +00:00
cmd.arg("-seek_streams_individually").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);
}
}
pub(crate) struct Ffmpeg {
inputs: Vec<FfmpegInput>,
2023-10-29 21:58:46 +00:00
filters: Vec<Filter>,
2023-10-30 16:32:21 +00:00
filters_output: Cow<'static, str>,
loudnorm: bool,
2023-10-30 19:28:17 +00:00
duration: Option<Time>,
2023-10-30 15:05:21 +00:00
output: PathBuf,
filter_idx: usize
2023-10-28 21:38:17 +00:00
}
impl Ffmpeg {
pub fn new(output: PathBuf) -> Self {
Self {
inputs: Vec::new(),
2023-10-29 21:58:46 +00:00
filters: Vec::new(),
2023-10-30 16:32:21 +00:00
filters_output: "0".into(),
loudnorm: false,
2023-10-30 19:28:17 +00:00
duration: 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 {
assert!(!self.loudnorm);
self.filters.push(filter);
self
}
pub fn set_filter_output<T: Into<Cow<'static, str>>>(
&mut self,
output: T
) -> &mut Self {
self.filters_output = output.into();
self
}
pub fn enable_loudnorm(&mut self) -> &mut Self {
assert!(self.filters.is_empty());
self.loudnorm = true;
self
}
2023-10-30 19:28:17 +00:00
pub fn set_duration(&mut self, duration: Time) -> &mut Self {
self.duration = Some(duration);
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-10-30 19:28:17 +00:00
let venc = !self.filters.is_empty();
let aenc = !self.filters.is_empty() || self.loudnorm;
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-10-29 21:58:46 +00:00
let vaapi = venc && vaapi_device.exists();
2023-10-28 21:38:17 +00:00
if vaapi {
cmd.arg("-vaapi_device").arg(&vaapi_device);
}
// 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-10-30 16:32:21 +00:00
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) => {
let mut complex = String::new();
for filter in f {
filter.append_to_complex_filter(&mut complex, &mut self.filter_idx);
}
2023-10-30 19:28:17 +00:00
if vaapi {
write!(
complex,
"{}format=nv12,hwupload[v]",
channel('v', &self.filters_output)
);
} else {
write!(complex, "{}null[v]", channel('v', &self.filters_output));
}
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-10-30 16:32:21 +00:00
cmd.arg("-map").arg(channel('a', &self.filters_output));
}
}
2023-10-29 21:58:46 +00:00
// append encoding options
if vaapi {
cmd.arg("-c:v").arg("h264_vaapi");
cmd.arg("-rc_mode").arg("CQP");
2023-11-02 21:40:49 +00:00
cmd.arg("-global_quality").arg("24");
2023-10-29 21:58:46 +00:00
} else if venc {
cmd.arg("-c:v").arg("libx264");
cmd.arg("-crf").arg("22");
} else {
cmd.arg("-c:v").arg("copy");
}
2023-10-30 15:05:21 +00:00
if aenc {
cmd.arg("-c:a").arg("aac");
cmd.arg("-b:a").arg("128000");
} else {
cmd.arg("-c:a").arg("copy");
}
2023-10-29 21:58:46 +00:00
2023-10-30 19:28:17 +00:00
if let Some(duration) = self.duration {
cmd.arg("-t").arg(format_time(duration));
}
2023-11-15 13:13:57 +00:00
cmd.arg("-movflags").arg("+faststart");
2023-10-30 16:32:21 +00:00
cmd.arg(&self.output);
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
}
}