use AV1 for 1440p and higher

This commit is contained in:
Dominic 2023-12-19 23:56:04 +01:00
parent c288f55ed0
commit f4adda912a
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
3 changed files with 131 additions and 84 deletions

View file

@ -7,7 +7,7 @@ mod render;
mod time; mod time;
use crate::{ use crate::{
render::Renderer, render::{ffmpeg::FfmpegOutputFormat, Renderer},
time::{parse_date, parse_time, Date, Time} time::{parse_date, parse_time, Date, Time}
}; };
use camino::Utf8PathBuf as PathBuf; use camino::Utf8PathBuf as PathBuf;
@ -46,7 +46,7 @@ struct Args {
} }
macro_rules! resolutions { macro_rules! resolutions {
($($res:ident: $width:literal x $height:literal at $bitrate:literal),+) => { ($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => {
#[allow(non_camel_case_types, clippy::upper_case_acronyms)] #[allow(non_camel_case_types, clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
enum Resolution { enum Resolution {
@ -84,6 +84,12 @@ macro_rules! resolutions {
$(Self::$res => $bitrate),+ $(Self::$res => $bitrate),+
} }
} }
fn format(self) -> FfmpegOutputFormat {
match self {
$(Self::$res => FfmpegOutputFormat::$format),+
}
}
} }
impl FromStr for Resolution { impl FromStr for Resolution {
@ -100,12 +106,12 @@ macro_rules! resolutions {
} }
resolutions! { resolutions! {
nHD: 640 x 360 at 500_000, nHD: 640 x 360 at 500_000 in AvcAac,
HD: 1280 x 720 at 1_000_000, HD: 1280 x 720 at 1_000_000 in AvcAac,
FullHD: 1920 x 1080 at 2_000_000, FullHD: 1920 x 1080 at 2_000_000 in AvcAac,
WQHD: 2560 x 1440 at 3_000_000, WQHD: 2560 x 1440 at 3_000_000 in Av1Opus,
// TODO qsx muss mal sagen wieviel bitrate für 4k // TODO qsx muss mal sagen wieviel bitrate für 4k
UHD: 3840 x 2160 at 4_000_000 UHD: 3840 x 2160 at 4_000_000 in Av1Opus
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -234,7 +240,7 @@ fn main() {
println!("{}", toml::to_string(&project).unwrap()); println!("{}", toml::to_string(&project).unwrap());
let renderer = Renderer::new(&directory, &project).unwrap(); let renderer = Renderer::new(&directory, &project).unwrap();
let recording = renderer.recording_mp4(); let recording = renderer.recording_mkv();
// preprocess the video // preprocess the video
if !project.progress.preprocessed { if !project.progress.preprocessed {
@ -282,7 +288,7 @@ fn main() {
// render the video // render the video
let mut videos = Vec::new(); let mut videos = Vec::new();
videos.push(if project.progress.rendered { videos.push(if project.progress.rendered {
renderer.video_mp4(&project) renderer.video_file_output()
} else { } else {
let video = renderer.render(&mut project).unwrap(); let video = renderer.render(&mut project).unwrap();
project.progress.rendered = true; project.progress.rendered = true;
@ -302,7 +308,7 @@ fn main() {
continue; continue;
} }
if !project.progress.transcoded.contains(&res) { if !project.progress.transcoded.contains(&res) {
videos.push(renderer.rescale(res, &project).unwrap()); videos.push(renderer.rescale(res).unwrap());
project.progress.transcoded.insert(res); project.progress.transcoded.insert(res);
println!("{}", toml::to_string(&project).unwrap()); println!("{}", toml::to_string(&project).unwrap());

View file

@ -41,7 +41,9 @@ impl FfmpegInput {
cmd.arg("-r").arg(fps.to_string()); cmd.arg("-r").arg(fps.to_string());
} }
if let Some(start) = self.start { if let Some(start) = self.start {
cmd.arg("-seek_streams_individually").arg("false"); if self.path.ends_with(".mp4") {
cmd.arg("-seek_streams_individualy").arg("false");
}
cmd.arg("-ss").arg(format_time(start)); cmd.arg("-ss").arg(format_time(start));
} }
if let Some(duration) = self.duration { if let Some(duration) = self.duration {
@ -51,18 +53,35 @@ impl FfmpegInput {
} }
} }
pub(crate) enum FfmpegOutputFormat {
/// AV1 / FLAC
Av1Flac,
/// AV1 / OPUS
Av1Opus,
/// AVC (H.264) / AAC
AvcAac
}
pub(crate) struct FfmpegOutput { pub(crate) struct FfmpegOutput {
pub(crate) format: FfmpegOutputFormat,
pub(crate) audio_bitrate: Option<u64>,
pub(crate) video_bitrate: Option<u64>,
pub(crate) fps: Option<Rational>, pub(crate) fps: Option<Rational>,
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) faststart: bool,
pub(crate) path: PathBuf pub(crate) path: PathBuf
} }
impl FfmpegOutput { impl FfmpegOutput {
pub(crate) fn new(path: PathBuf) -> Self { pub(crate) fn new(format: FfmpegOutputFormat, path: PathBuf) -> Self {
Self { Self {
format,
audio_bitrate: None,
video_bitrate: None,
fps: None, fps: None,
duration: None, duration: None,
time_base: None, time_base: None,
@ -77,7 +96,42 @@ impl FfmpegOutput {
self self
} }
fn append_to_cmd(self, cmd: &mut Command) { fn append_to_cmd(self, cmd: &mut Command, venc: bool, _aenc: bool, vaapi: bool) {
// select codec and bitrate
const QUALITY: &str = "22";
if venc {
let mut vcodec: String = match self.format {
FfmpegOutputFormat::Av1Flac | FfmpegOutputFormat::Av1Opus => "av1".into(),
FfmpegOutputFormat::AvcAac => "h264".into()
};
if vaapi {
vcodec = format!("{vcodec}_vaapi");
}
cmd.arg("-c:v").arg(vcodec);
if let Some(bv) = self.video_bitrate {
cmd.arg("-b:v").arg(bv.to_string());
} else if vaapi {
cmd.arg("-rc_mode").arg("CQP");
cmd.arg("-global_quality").arg(QUALITY);
} else {
cmd.arg("-crf").arg(QUALITY);
}
} 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());
} else {
cmd.arg("-b:a").arg("128k");
}
// other output options
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());
} }
@ -110,7 +164,6 @@ enum FfmpegFilter {
pub(crate) struct Ffmpeg { pub(crate) struct Ffmpeg {
inputs: Vec<FfmpegInput>, inputs: Vec<FfmpegInput>,
filter: FfmpegFilter, filter: FfmpegFilter,
video_bitrate: Option<u64>,
output: FfmpegOutput, output: FfmpegOutput,
filter_idx: usize filter_idx: usize
@ -121,7 +174,6 @@ impl Ffmpeg {
Self { Self {
inputs: Vec::new(), inputs: Vec::new(),
filter: FfmpegFilter::None, filter: FfmpegFilter::None,
video_bitrate: None,
output, output,
filter_idx: 0 filter_idx: 0
@ -182,11 +234,6 @@ impl Ffmpeg {
self self
} }
pub fn set_video_bitrate(&mut self, bitrate: u64) -> &mut Self {
self.video_bitrate = Some(bitrate);
self
}
pub fn run(mut self) -> anyhow::Result<()> { pub fn run(mut self) -> anyhow::Result<()> {
let mut cmd = cmd(); let mut cmd = cmd();
cmd.arg("ffmpeg").arg("-hide_banner").arg("-y"); cmd.arg("ffmpeg").arg("-hide_banner").arg("-y");
@ -256,35 +303,7 @@ impl Ffmpeg {
} }
} }
// append encoding options self.output.append_to_cmd(&mut cmd, venc, aenc, vaapi);
const QUALITY: &str = "22";
if venc {
if vaapi {
cmd.arg("-c:v").arg("h264_vaapi");
if self.video_bitrate.is_none() {
cmd.arg("-rc_mode").arg("CQP");
cmd.arg("-global_quality").arg(QUALITY);
}
} else {
cmd.arg("-c:v").arg("libx264");
if self.video_bitrate.is_none() {
cmd.arg("-crf").arg(QUALITY);
}
}
if self.video_bitrate.is_some() {
cmd.arg("-b:v").arg(self.video_bitrate.unwrap().to_string());
}
} else {
cmd.arg("-c:v").arg("copy");
}
if aenc {
cmd.arg("-c:a").arg("aac");
cmd.arg("-b:a").arg("128000");
} else {
cmd.arg("-c:a").arg("copy");
}
self.output.append_to_cmd(&mut cmd);
let status = cmd.status()?; let status = cmd.status()?;
if status.success() { if status.success() {

View file

@ -1,7 +1,10 @@
pub mod ffmpeg; pub mod ffmpeg;
mod filter; mod filter;
use self::{ffmpeg::FfmpegOutput, filter::Filter}; use self::{
ffmpeg::{FfmpegOutput, FfmpegOutputFormat},
filter::Filter
};
use crate::{ use crate::{
iotro::{intro, outro}, iotro::{intro, outro},
render::ffmpeg::{Ffmpeg, FfmpegInput}, render::ffmpeg::{Ffmpeg, FfmpegInput},
@ -114,19 +117,17 @@ pub(crate) struct Renderer<'a> {
target: PathBuf target: PathBuf
} }
fn svg2mp4( fn svg2mkv(
meta: &ProjectSourceMetadata, meta: &ProjectSourceMetadata,
svg: PathBuf, svg: PathBuf,
mp4: PathBuf, mkv: PathBuf,
duration: Time duration: Time
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut ffmpeg = Ffmpeg::new(FfmpegOutput { let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
fps: None,
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, ..FfmpegOutput::new(FfmpegOutputFormat::Av1Flac, mkv)
path: mp4
}); });
ffmpeg.add_input(FfmpegInput { ffmpeg.add_input(FfmpegInput {
loop_input: true, loop_input: true,
@ -176,8 +177,16 @@ impl<'a> Renderer<'a> {
}) })
} }
pub(crate) fn recording_mp4(&self) -> PathBuf { pub(crate) fn recording_mkv(&self) -> PathBuf {
self.target.join("recording.mp4") self.target.join("recording.mkv")
}
fn intro_mkv(&self) -> PathBuf {
self.target.join("intro.mkv")
}
fn outro_mkv(&self) -> PathBuf {
self.target.join("outro.mkv")
} }
pub(crate) fn preprocess(&self, project: &mut Project) -> anyhow::Result<()> { pub(crate) fn preprocess(&self, project: &mut Project) -> anyhow::Result<()> {
@ -193,8 +202,11 @@ impl<'a> Renderer<'a> {
println!("\x1B[1m ==> Concatenating Video and Normalising Audio ...\x1B[0m"); println!("\x1B[1m ==> Concatenating Video and Normalising Audio ...\x1B[0m");
let source_sample_rate = let source_sample_rate =
ffprobe_audio("stream=sample_rate", &recording_txt)?.parse()?; ffprobe_audio("stream=sample_rate", &recording_txt)?.parse()?;
let recording_mp4 = self.recording_mp4(); let recording_mkv = self.recording_mkv();
let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new(recording_mp4.clone())); let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new(
FfmpegOutputFormat::Av1Flac,
recording_mkv.clone()
));
ffmpeg.add_input(FfmpegInput { ffmpeg.add_input(FfmpegInput {
concat: true, concat: true,
..FfmpegInput::new(recording_txt) ..FfmpegInput::new(recording_txt)
@ -202,8 +214,8 @@ impl<'a> Renderer<'a> {
ffmpeg.enable_loudnorm(); ffmpeg.enable_loudnorm();
ffmpeg.run()?; ffmpeg.run()?;
let width = ffprobe_video("stream=width", &recording_mp4)?.parse()?; let width = ffprobe_video("stream=width", &recording_mkv)?.parse()?;
let height = ffprobe_video("stream=height", &recording_mp4)?.parse()?; let height = ffprobe_video("stream=height", &recording_mkv)?.parse()?;
let source_res = match (width, height) { let source_res = match (width, height) {
(3840, 2160) => Resolution::UHD, (3840, 2160) => Resolution::UHD,
(2560, 1440) => Resolution::WQHD, (2560, 1440) => Resolution::WQHD,
@ -213,9 +225,9 @@ impl<'a> Renderer<'a> {
(width, height) => bail!("Unknown resolution: {width}x{height}") (width, height) => bail!("Unknown resolution: {width}x{height}")
}; };
project.source.metadata = Some(ProjectSourceMetadata { project.source.metadata = Some(ProjectSourceMetadata {
source_duration: ffprobe_video("format=duration", &recording_mp4)?.parse()?, source_duration: ffprobe_video("format=duration", &recording_mkv)?.parse()?,
source_fps: ffprobe_video("stream=r_frame_rate", &recording_mp4)?.parse()?, source_fps: ffprobe_video("stream=r_frame_rate", &recording_mkv)?.parse()?,
source_tbn: ffprobe_video("stream=time_base", &recording_mp4)?.parse()?, source_tbn: ffprobe_video("stream=time_base", &recording_mkv)?.parse()?,
source_res, source_res,
source_sample_rate source_sample_rate
}); });
@ -231,8 +243,8 @@ impl<'a> Renderer<'a> {
.to_string_pretty() .to_string_pretty()
.into_bytes() .into_bytes()
)?; )?;
let intro_mp4 = self.target.join("intro.mp4"); let intro_mkv = self.intro_mkv();
svg2mp4(metadata, intro_svg, intro_mp4, INTRO_LEN)?; svg2mkv(metadata, intro_svg, intro_mkv, INTRO_LEN)?;
// render outro to svg then mp4 // render outro to svg then mp4
let outro_svg = self.target.join("outro.svg"); let outro_svg = self.target.join("outro.svg");
@ -240,8 +252,8 @@ impl<'a> Renderer<'a> {
&outro_svg, &outro_svg,
outro(source_res).to_string_pretty().into_bytes() outro(source_res).to_string_pretty().into_bytes()
)?; )?;
let outro_mp4 = self.target.join("outro.mp4"); let outro_mkv = self.outro_mkv();
svg2mp4(metadata, outro_svg, outro_mp4, OUTRO_LEN)?; svg2mkv(metadata, outro_svg, outro_mkv, OUTRO_LEN)?;
// copy logo then render to png // copy logo then render to png
let logo_svg = self.target.join("logo.svg"); let logo_svg = self.target.join("logo.svg");
@ -271,25 +283,35 @@ impl<'a> Renderer<'a> {
Ok(()) Ok(())
} }
fn video_mp4_res(&self, res: Resolution) -> PathBuf { /// Get the video file for a specific resolution, completely finished.
fn video_file_res(&self, res: Resolution) -> PathBuf {
let extension = match res.format() {
FfmpegOutputFormat::Av1Flac => "mkv",
FfmpegOutputFormat::Av1Opus => "webm",
FfmpegOutputFormat::AvcAac => "mp4"
};
self.target self.target
.join(format!("{}-{}p.mp4", self.slug, res.height())) .join(format!("{}-{}p.{extension}", self.slug, res.height()))
} }
pub(crate) fn video_mp4(&self, project: &Project) -> PathBuf { /// Get the video file directly outputed to further transcode.
self.video_mp4_res(project.source.metadata.as_ref().unwrap().source_res) pub(crate) fn video_file_output(&self) -> PathBuf {
self.target.join(format!("{}.mkv", self.slug))
} }
pub(crate) fn render(&self, project: &mut Project) -> anyhow::Result<PathBuf> { pub(crate) fn render(&self, project: &mut Project) -> anyhow::Result<PathBuf> {
let source_res = project.source.metadata.as_ref().unwrap().source_res; let source_res = project.source.metadata.as_ref().unwrap().source_res;
let output = self.video_mp4(project); let output = self.video_file_output();
let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new(output.clone())); let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
video_bitrate: Some(source_res.bitrate() * 3),
..FfmpegOutput::new(FfmpegOutputFormat::Av1Flac, output.clone())
});
// add all of our inputs // add all of our inputs
let intro = ffmpeg.add_input(FfmpegInput::new(self.target.join("intro.mp4"))); let intro = ffmpeg.add_input(FfmpegInput::new(self.intro_mkv()));
let rec_file = self.target.join("recording.mp4"); let rec_file = self.recording_mkv();
let outro = ffmpeg.add_input(FfmpegInput::new(self.target.join("outro.mp4"))); let outro = ffmpeg.add_input(FfmpegInput::new(self.outro_mkv()));
let logo = ffmpeg.add_input(FfmpegInput::new(self.target.join("logo.png"))); let logo = ffmpeg.add_input(FfmpegInput::new(self.target.join("logo.png")));
let ff = ffmpeg.add_input(FfmpegInput::new(self.target.join("fastforward.png"))); let ff = ffmpeg.add_input(FfmpegInput::new(self.target.join("fastforward.png")));
@ -444,22 +466,22 @@ impl<'a> Renderer<'a> {
// we're done :) // we're done :)
ffmpeg.set_filter_output(overlay); ffmpeg.set_filter_output(overlay);
ffmpeg.set_video_bitrate(source_res.bitrate() * 3);
ffmpeg.run()?; ffmpeg.run()?;
Ok(output) Ok(output)
} }
pub fn rescale(&self, res: Resolution, project: &Project) -> anyhow::Result<PathBuf> { pub fn rescale(&self, res: Resolution) -> anyhow::Result<PathBuf> {
let input = self.video_mp4(project); let input = self.video_file_output();
let output = self.video_mp4_res(res); let output = self.video_file_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 = let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
Ffmpeg::new(FfmpegOutput::new(output.clone()).enable_faststart()); video_bitrate: Some(res.bitrate()),
..FfmpegOutput::new(res.format(), output.clone()).enable_faststart()
});
ffmpeg.add_input(FfmpegInput::new(input)); ffmpeg.add_input(FfmpegInput::new(input));
ffmpeg.rescale_video(res); ffmpeg.rescale_video(res);
ffmpeg.set_video_bitrate(res.bitrate());
ffmpeg.run()?; ffmpeg.run()?;
Ok(output) Ok(output)
} }