diff --git a/src/main.rs b/src/main.rs index dcf9acd48786fc637e441412fe2bfbd3cb737c33..a6715b699b1fb612732db500fe3bf3312cb7f694 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ mod render; mod time; use crate::{ - render::Renderer, + render::{ffmpeg::FfmpegOutputFormat, Renderer}, time::{parse_date, parse_time, Date, Time} }; use camino::Utf8PathBuf as PathBuf; @@ -46,7 +46,7 @@ struct Args { } 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)] #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] enum Resolution { @@ -84,6 +84,12 @@ macro_rules! resolutions { $(Self::$res => $bitrate),+ } } + + fn format(self) -> FfmpegOutputFormat { + match self { + $(Self::$res => FfmpegOutputFormat::$format),+ + } + } } impl FromStr for Resolution { @@ -100,12 +106,12 @@ macro_rules! resolutions { } resolutions! { - nHD: 640 x 360 at 500_000, - HD: 1280 x 720 at 1_000_000, - FullHD: 1920 x 1080 at 2_000_000, - WQHD: 2560 x 1440 at 3_000_000, + nHD: 640 x 360 at 500_000 in AvcAac, + HD: 1280 x 720 at 1_000_000 in AvcAac, + FullHD: 1920 x 1080 at 2_000_000 in AvcAac, + WQHD: 2560 x 1440 at 3_000_000 in Av1Opus, // 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)] @@ -234,7 +240,7 @@ fn main() { println!("{}", toml::to_string(&project).unwrap()); let renderer = Renderer::new(&directory, &project).unwrap(); - let recording = renderer.recording_mp4(); + let recording = renderer.recording_mkv(); // preprocess the video if !project.progress.preprocessed { @@ -282,7 +288,7 @@ fn main() { // render the video let mut videos = Vec::new(); videos.push(if project.progress.rendered { - renderer.video_mp4(&project) + renderer.video_file_output() } else { let video = renderer.render(&mut project).unwrap(); project.progress.rendered = true; @@ -302,7 +308,7 @@ fn main() { continue; } if !project.progress.transcoded.contains(&res) { - videos.push(renderer.rescale(res, &project).unwrap()); + videos.push(renderer.rescale(res).unwrap()); project.progress.transcoded.insert(res); println!("{}", toml::to_string(&project).unwrap()); diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 2e2d174597b7c17e19b2d6e1de3acceb2d42c361..543ad7d8e00b7913729ea685d448dc2222cb737c 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -41,7 +41,9 @@ impl FfmpegInput { cmd.arg("-r").arg(fps.to_string()); } 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)); } 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) 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, + pub(crate) path: PathBuf } impl FfmpegOutput { - pub(crate) fn new(path: PathBuf) -> Self { + pub(crate) fn new(format: FfmpegOutputFormat, path: PathBuf) -> Self { Self { + format, + audio_bitrate: None, + video_bitrate: None, fps: None, duration: None, time_base: None, @@ -77,7 +96,42 @@ impl FfmpegOutput { 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 { cmd.arg("-r").arg(fps.to_string()); } @@ -110,7 +164,6 @@ enum FfmpegFilter { pub(crate) struct Ffmpeg { inputs: Vec<FfmpegInput>, filter: FfmpegFilter, - video_bitrate: Option<u64>, output: FfmpegOutput, filter_idx: usize @@ -121,7 +174,6 @@ impl Ffmpeg { Self { inputs: Vec::new(), filter: FfmpegFilter::None, - video_bitrate: None, output, filter_idx: 0 @@ -182,11 +234,6 @@ impl Ffmpeg { 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<()> { let mut cmd = cmd(); cmd.arg("ffmpeg").arg("-hide_banner").arg("-y"); @@ -256,35 +303,7 @@ impl Ffmpeg { } } - // append encoding options - 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); + self.output.append_to_cmd(&mut cmd, venc, aenc, vaapi); let status = cmd.status()?; if status.success() { diff --git a/src/render/mod.rs b/src/render/mod.rs index 85ab9b28ca54c88275f439c299bcd598e23a7e27..e2576e245815bec94b66e890d8e43f3687f6ced0 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,7 +1,10 @@ pub mod ffmpeg; mod filter; -use self::{ffmpeg::FfmpegOutput, filter::Filter}; +use self::{ + ffmpeg::{FfmpegOutput, FfmpegOutputFormat}, + filter::Filter +}; use crate::{ iotro::{intro, outro}, render::ffmpeg::{Ffmpeg, FfmpegInput}, @@ -114,19 +117,17 @@ pub(crate) struct Renderer<'a> { target: PathBuf } -fn svg2mp4( +fn svg2mkv( meta: &ProjectSourceMetadata, svg: PathBuf, - mp4: PathBuf, + mkv: PathBuf, duration: Time ) -> anyhow::Result<()> { let mut ffmpeg = Ffmpeg::new(FfmpegOutput { - fps: None, duration: Some(duration), time_base: Some(meta.source_tbn), fps_mode_vfr: true, - faststart: false, - path: mp4 + ..FfmpegOutput::new(FfmpegOutputFormat::Av1Flac, mkv) }); ffmpeg.add_input(FfmpegInput { loop_input: true, @@ -176,8 +177,16 @@ impl<'a> Renderer<'a> { }) } - pub(crate) fn recording_mp4(&self) -> PathBuf { - self.target.join("recording.mp4") + pub(crate) fn recording_mkv(&self) -> PathBuf { + 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<()> { @@ -193,8 +202,11 @@ impl<'a> Renderer<'a> { println!("\x1B[1m ==> Concatenating Video and Normalising Audio ...\x1B[0m"); let source_sample_rate = ffprobe_audio("stream=sample_rate", &recording_txt)?.parse()?; - let recording_mp4 = self.recording_mp4(); - let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new(recording_mp4.clone())); + let recording_mkv = self.recording_mkv(); + let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new( + FfmpegOutputFormat::Av1Flac, + recording_mkv.clone() + )); ffmpeg.add_input(FfmpegInput { concat: true, ..FfmpegInput::new(recording_txt) @@ -202,8 +214,8 @@ impl<'a> Renderer<'a> { ffmpeg.enable_loudnorm(); ffmpeg.run()?; - let width = ffprobe_video("stream=width", &recording_mp4)?.parse()?; - let height = ffprobe_video("stream=height", &recording_mp4)?.parse()?; + let width = ffprobe_video("stream=width", &recording_mkv)?.parse()?; + let height = ffprobe_video("stream=height", &recording_mkv)?.parse()?; let source_res = match (width, height) { (3840, 2160) => Resolution::UHD, (2560, 1440) => Resolution::WQHD, @@ -213,9 +225,9 @@ impl<'a> Renderer<'a> { (width, height) => bail!("Unknown resolution: {width}x{height}") }; project.source.metadata = Some(ProjectSourceMetadata { - source_duration: ffprobe_video("format=duration", &recording_mp4)?.parse()?, - source_fps: ffprobe_video("stream=r_frame_rate", &recording_mp4)?.parse()?, - source_tbn: ffprobe_video("stream=time_base", &recording_mp4)?.parse()?, + source_duration: ffprobe_video("format=duration", &recording_mkv)?.parse()?, + source_fps: ffprobe_video("stream=r_frame_rate", &recording_mkv)?.parse()?, + source_tbn: ffprobe_video("stream=time_base", &recording_mkv)?.parse()?, source_res, source_sample_rate }); @@ -231,8 +243,8 @@ impl<'a> Renderer<'a> { .to_string_pretty() .into_bytes() )?; - let intro_mp4 = self.target.join("intro.mp4"); - svg2mp4(metadata, intro_svg, intro_mp4, INTRO_LEN)?; + let intro_mkv = self.intro_mkv(); + svg2mkv(metadata, intro_svg, intro_mkv, INTRO_LEN)?; // render outro to svg then mp4 let outro_svg = self.target.join("outro.svg"); @@ -240,8 +252,8 @@ impl<'a> Renderer<'a> { &outro_svg, outro(source_res).to_string_pretty().into_bytes() )?; - let outro_mp4 = self.target.join("outro.mp4"); - svg2mp4(metadata, outro_svg, outro_mp4, OUTRO_LEN)?; + let outro_mkv = self.outro_mkv(); + svg2mkv(metadata, outro_svg, outro_mkv, OUTRO_LEN)?; // copy logo then render to png let logo_svg = self.target.join("logo.svg"); @@ -271,25 +283,35 @@ impl<'a> Renderer<'a> { 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 - .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 { - self.video_mp4_res(project.source.metadata.as_ref().unwrap().source_res) + /// Get the video file directly outputed to further transcode. + 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> { let source_res = project.source.metadata.as_ref().unwrap().source_res; - let output = self.video_mp4(project); - let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new(output.clone())); + let output = self.video_file_output(); + let mut ffmpeg = Ffmpeg::new(FfmpegOutput { + video_bitrate: Some(source_res.bitrate() * 3), + ..FfmpegOutput::new(FfmpegOutputFormat::Av1Flac, output.clone()) + }); // add all of our inputs - let intro = ffmpeg.add_input(FfmpegInput::new(self.target.join("intro.mp4"))); - let rec_file = self.target.join("recording.mp4"); - let outro = ffmpeg.add_input(FfmpegInput::new(self.target.join("outro.mp4"))); + let intro = ffmpeg.add_input(FfmpegInput::new(self.intro_mkv())); + let rec_file = self.recording_mkv(); + let outro = ffmpeg.add_input(FfmpegInput::new(self.outro_mkv())); let logo = ffmpeg.add_input(FfmpegInput::new(self.target.join("logo.png"))); let ff = ffmpeg.add_input(FfmpegInput::new(self.target.join("fastforward.png"))); @@ -444,22 +466,22 @@ impl<'a> Renderer<'a> { // we're done :) ffmpeg.set_filter_output(overlay); - ffmpeg.set_video_bitrate(source_res.bitrate() * 3); ffmpeg.run()?; Ok(output) } - pub fn rescale(&self, res: Resolution, project: &Project) -> anyhow::Result<PathBuf> { - let input = self.video_mp4(project); - let output = self.video_mp4_res(res); + pub fn rescale(&self, res: Resolution) -> anyhow::Result<PathBuf> { + let input = self.video_file_output(); + let output = self.video_file_res(res); println!("\x1B[1m ==> Rescaling to {}p\x1B[0m", res.height()); - let mut ffmpeg = - Ffmpeg::new(FfmpegOutput::new(output.clone()).enable_faststart()); + let mut ffmpeg = Ffmpeg::new(FfmpegOutput { + video_bitrate: Some(res.bitrate()), + ..FfmpegOutput::new(res.format(), output.clone()).enable_faststart() + }); ffmpeg.add_input(FfmpegInput::new(input)); ffmpeg.rescale_video(res); - ffmpeg.set_video_bitrate(res.bitrate()); ffmpeg.run()?; Ok(output) }