render_video/src/render/ffmpeg.rs

367 lines
8.9 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},
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);
}
}
2024-01-15 17:50:15 +00:00
#[derive(Clone, Copy)]
2023-12-19 22:56:04 +00:00
pub(crate) enum FfmpegOutputFormat {
/// AV1 / FLAC
Av1Flac,
/// AV1 / OPUS
Av1Opus,
/// AVC (H.264) / AAC
AvcAac
}
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>,
pub(crate) fps: Option<Rational>,
pub(crate) duration: Option<Time>,
pub(crate) time_base: Option<Rational>,
pub(crate) fps_mode_vfr: bool,
pub(crate) faststart: bool,
2023-12-19 22:56:04 +00:00
2024-05-24 10:17:40 +00:00
// video container metadata
pub(crate) title: Option<String>,
pub(crate) author: Option<String>,
pub(crate) album: Option<String>,
pub(crate) year: Option<String>,
pub(crate) comment: Option<String>,
pub(crate) language: Option<String>,
pub(crate) path: PathBuf
}
impl FfmpegOutput {
2023-12-19 22:56:04 +00:00
pub(crate) fn new(format: FfmpegOutputFormat, path: PathBuf) -> Self {
Self {
2023-12-19 22:56:04 +00:00
format,
audio_bitrate: None,
video_bitrate: None,
2024-05-24 10:17:40 +00:00
fps: None,
duration: None,
time_base: None,
fps_mode_vfr: false,
faststart: false,
2024-05-24 10:17:40 +00:00
title: None,
author: None,
album: None,
year: None,
comment: None,
language: None,
path
}
}
pub(crate) fn enable_faststart(mut self) -> Self {
2024-01-09 21:01:35 +00:00
// only enable faststart for MP4 containers
if matches!(self.format, FfmpegOutputFormat::AvcAac) {
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
2024-05-24 10:01:45 +00:00
const QUALITY: &str = "18";
2023-12-19 22:56:04 +00:00
if venc {
let vcodec = match (self.format, vaapi) {
(FfmpegOutputFormat::Av1Flac, false)
| (FfmpegOutputFormat::Av1Opus, false) => "libsvtav1",
(FfmpegOutputFormat::Av1Flac, true)
| (FfmpegOutputFormat::Av1Opus, true) => "av1_vaapi",
(FfmpegOutputFormat::AvcAac, false) => "h264",
(FfmpegOutputFormat::AvcAac, true) => "h264_vaapi"
2023-12-19 22:56:04 +00:00
};
cmd.arg("-c:v").arg(vcodec);
2024-05-24 10:01:45 +00:00
if vcodec == "libsvtav1" {
cmd.arg("-svtav1-params").arg("fast-decode=1");
cmd.arg("-preset").arg("8");
}
match self.video_bitrate {
Some(bv) if vcodec != "libsvtav1" => {
cmd.arg("-b:v").arg(bv.to_string());
},
None if vaapi => {
cmd.arg("-rc_mode").arg("CQP");
cmd.arg("-global_quality").arg(QUALITY);
},
_ => {
cmd.arg("-crf").arg(QUALITY);
}
2023-12-19 22:56:04 +00:00
}
} 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());
2024-01-09 21:01:35 +00:00
} else if !matches!(self.format, FfmpegOutputFormat::Av1Flac) {
2023-12-19 22:56:04 +00:00
cmd.arg("-b:a").arg("128k");
}
// other output options
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");
}
if self.faststart {
cmd.arg("-movflags").arg("+faststart");
}
2024-05-24 10:17:40 +00:00
// metadata
macro_rules! add_meta {
($this:ident, $cmd:ident: $($meta:ident),+) => {
$(if let Some(value) = $this.$meta.as_deref() {
$cmd.arg("-metadata").arg(format!("{}={}", stringify!($meta), value));
})+
}
}
add_meta!(self, cmd: title, author, album, year, comment, language);
cmd.arg(self.path);
}
}
enum FfmpegFilter {
None,
Filters {
filters: Vec<Filter>,
output: Cow<'static, str>
},
Loudnorm {
stereo: bool
},
Rescale(Resolution)
}
2023-10-28 21:38:17 +00:00
pub(crate) struct Ffmpeg {
inputs: Vec<FfmpegInput>,
filter: FfmpegFilter,
output: FfmpegOutput,
2023-10-30 15:05:21 +00:00
filter_idx: usize
2023-10-28 21:38:17 +00:00
}
impl Ffmpeg {
pub fn new(output: FfmpegOutput) -> Self {
2023-10-28 21:38:17 +00:00
Self {
inputs: Vec::new(),
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 {
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,
filter_output: T
2023-10-30 16:32:21 +00:00
) -> &mut Self {
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, loudnorm_stereo: bool) -> &mut Self {
match &mut self.filter {
FfmpegFilter::None => {
self.filter = FfmpegFilter::Loudnorm {
stereo: loudnorm_stereo
}
},
FfmpegFilter::Loudnorm { stereo } if *stereo == loudnorm_stereo => {},
_ => 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),
_ => 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
// 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();
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
match self.filter {
FfmpegFilter::None => {},
FfmpegFilter::Filters { filters, output } => {
2023-10-30 16:32:21 +00:00
let mut complex = String::new();
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]");
cmd.arg("-map").arg(channel('a', &output));
},
FfmpegFilter::Loudnorm { stereo: false } => {
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::Loudnorm { stereo: true } => {
cmd.arg("-af")
.arg("loudnorm=print_format=summary,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
}
}