diff --git a/src/iotro.rs b/src/iotro.rs index e37045e..cdbd1b2 100644 --- a/src/iotro.rs +++ b/src/iotro.rs @@ -191,8 +191,8 @@ impl Iotro { fn finish(self) -> Graphic { let mut svg = Graphic::new(); - svg.set_width(self.res.width()); - svg.set_height(self.res.height()); + svg.set_width(self.res.width); + svg.set_height(self.res.height); svg.set_view_box("0 0 1920 1080"); svg.push( Rect::new() diff --git a/src/main.rs b/src/main.rs index 9f895df..53df6ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -215,7 +215,7 @@ fn main() { // rescale the video if let Some(lowest_res) = args.transcode.or(preset.transcode) { - for res in Resolution::values().into_iter().rev() { + for res in Resolution::STANDARD_RESOLUTIONS.into_iter().rev() { if res > project.source.metadata.as_ref().unwrap().source_res || res > args.transcode_start.unwrap_or(preset.transcode_start) || res < lowest_res diff --git a/src/preset.rs b/src/preset.rs index fc19763..507401a 100644 --- a/src/preset.rs +++ b/src/preset.rs @@ -18,12 +18,14 @@ pub struct Preset { pub docent: String, /// Course language - #[serde(default = "Default::default")] + #[serde(default)] #[serde_as(as = "DisplayFromStr")] pub lang: Language<'static>, // coding options + #[serde_as(as = "DisplayFromStr")] pub transcode_start: Resolution, + #[serde_as(as = "Option")] pub transcode: Option } @@ -33,8 +35,8 @@ pub fn preset_23ws_malo2() -> Preset { label: "Mathematische Logik II".into(), docent: "Prof. E. Grädel".into(), lang: GERMAN, - transcode_start: Resolution::WQHD, - transcode: Some(Resolution::nHD) + transcode_start: "1440p".parse().unwrap(), + transcode: Some("360p".parse().unwrap()) } } @@ -44,8 +46,8 @@ pub fn preset_24ss_algomod() -> Preset { label: "Algorithmische Modelltheorie".into(), docent: "Prof. E. Grädel".into(), lang: GERMAN, - transcode_start: Resolution::WQHD, - transcode: Some(Resolution::HD) + transcode_start: "1440p".parse().unwrap(), + transcode: Some("720p".parse().unwrap()) } } @@ -55,8 +57,8 @@ pub fn preset_24ss_qc() -> Preset { label: "Introduction to Quantum Computing".into(), docent: "Prof. D. Unruh".into(), lang: BRITISH, - transcode_start: Resolution::WQHD, - transcode: Some(Resolution::HD) + transcode_start: "1440p".parse().unwrap(), + transcode: Some("720p".parse().unwrap()) } } diff --git a/src/project.rs b/src/project.rs index 6ea1bdd..a16da81 100644 --- a/src/project.rs +++ b/src/project.rs @@ -8,75 +8,142 @@ use crate::{ use rational::Rational; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; -use std::{collections::BTreeSet, str::FromStr}; +use std::{ + cmp, + collections::BTreeSet, + fmt::{self, Display, Formatter}, + str::FromStr +}; -macro_rules! resolutions { - ($($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)] - pub enum Resolution { - $( - #[doc = concat!(stringify!($width), "x", stringify!($height))] - $res - ),+ +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct Resolution { + pub width: u32, + pub height: u32 +} + +impl Resolution { + pub(crate) fn bitrate(self) -> u64 { + // 640 * 360: 500k + if self.width <= 640 { + 500_000 } - - const NUM_RESOLUTIONS: usize = { - let mut num = 0; - $(num += 1; stringify!($res);)+ - num - }; - - impl Resolution { - pub fn values() -> [Self; NUM_RESOLUTIONS] { - [$(Self::$res),+] - } - - pub fn width(self) -> usize { - match self { - $(Self::$res => $width),+ - } - } - - pub fn height(self) -> usize { - match self { - $(Self::$res => $height),+ - } - } - - pub fn bitrate(self) -> u64 { - match self { - $(Self::$res => $bitrate),+ - } - } - - pub fn format(self) -> FfmpegOutputFormat { - match self { - $(Self::$res => FfmpegOutputFormat::$format),+ - } - } + // 1280 * 720: 1M + else if self.width <= 1280 { + 1_000_000 } - - impl FromStr for Resolution { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - Ok(match s { - $(concat!(stringify!($height), "p") => Self::$res,)+ - _ => anyhow::bail!("Unknown Resolution: {s:?}") - }) - } + // 1920 * 1080: 2M + else if self.width <= 1920 { + 2_000_000 } + // 2560 * 1440: 3M + else if self.width <= 2560 { + 3_000_000 + } + // 3840 * 2160: 4M + // TODO die bitrate von 4M ist absolut an den haaren herbeigezogen + else if self.width <= 3840 { + 4_000_000 + } + // we'll cap everything else at 5M for no apparent reason + else { + 5_000_000 + } + } + + pub(crate) fn default_codec(self) -> FfmpegOutputFormat { + if self.width <= 1920 { + FfmpegOutputFormat::Av1Opus + } else { + FfmpegOutputFormat::AvcAac + } + } + + pub const STANDARD_RESOLUTIONS: [Self; 5] = [ + Self { + width: 640, + height: 360 + }, + Self { + width: 1280, + height: 720 + }, + Self { + width: 1920, + height: 1080 + }, + Self { + width: 2560, + height: 1440 + }, + Self { + width: 3840, + height: 2160 + } + ]; +} + +impl Display for Resolution { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}p", self.height) } } -resolutions! { - nHD: 640 x 360 at 500_000 in AvcAac, - HD: 1280 x 720 at 1_000_000 in AvcAac, - FullHD: 1920 x 1080 at 750_000 in Av1Opus, - WQHD: 2560 x 1440 at 1_000_000 in Av1Opus, - // TODO qsx muss mal sagen wieviel bitrate für 4k - UHD: 3840 x 2160 at 2_000_000 in Av1Opus +impl FromStr for Resolution { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + Ok(match s.to_lowercase().as_str() { + "360p" | "nhd" => Self { + width: 640, + height: 360 + }, + "540p" | "qhd" => Self { + width: 960, + height: 540 + }, + "720p" | "hd" => Self { + width: 1280, + height: 720 + }, + "900p" | "hd+" => Self { + width: 1600, + height: 900 + }, + "1080p" | "fhd" | "fullhd" => Self { + width: 1920, + height: 1080 + }, + "1440p" | "wqhd" => Self { + width: 2560, + height: 1440 + }, + "2160p" | "4k" | "uhd" => Self { + width: 3840, + height: 2160 + }, + _ => anyhow::bail!("Unknown Resolution: {s:?}") + }) + } +} + +impl Ord for Resolution { + fn cmp(&self, other: &Self) -> cmp::Ordering { + (self.width * self.height).cmp(&(other.width * other.height)) + } +} + +impl Eq for Resolution {} + +impl PartialOrd for Resolution { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Resolution { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == cmp::Ordering::Equal + } } #[derive(Deserialize, Serialize)] @@ -139,6 +206,7 @@ pub struct ProjectSourceMetadata { pub source_sample_rate: u32 } +#[serde_as] #[derive(Default, Deserialize, Serialize)] pub struct ProjectProgress { #[serde(default)] @@ -159,6 +227,7 @@ pub struct ProjectProgress { #[serde(default)] pub rendered: bool, + #[serde_as(as = "BTreeSet")] #[serde(default)] pub transcoded: BTreeSet } diff --git a/src/question.rs b/src/question.rs index 6bf0da6..0f61a1c 100644 --- a/src/question.rs +++ b/src/question.rs @@ -131,8 +131,8 @@ impl Question { pub(crate) fn finish(self) -> Graphic { let mut svg = Graphic::new(); - svg.set_width(self.res.width()); - svg.set_height(self.res.height()); + svg.set_width(self.res.width); + svg.set_height(self.res.height); svg.set_view_box("0 0 1920 1080"); svg.push(self.g); svg diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 9cd27e7..e619168 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -347,9 +347,9 @@ impl Ffmpeg { }, FfmpegFilter::Rescale(res) => { cmd.arg("-vf").arg(if vaapi { - format!("scale_vaapi=w={}:h={}", res.width(), res.height()) + format!("scale_vaapi=w={}:h={}", res.width, res.height) } else { - format!("scale=w={}:h={}", res.width(), res.height()) + format!("scale=w={}:h={}", res.width, res.height) }); } } diff --git a/src/render/mod.rs b/src/render/mod.rs index 83f99dc..1e361cc 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -41,8 +41,8 @@ const QUESTION_FADE_LEN: Time = Time { }; const FF_MULTIPLIER: usize = 8; // logo sizes at full hd, will be scaled to source resolution -const FF_LOGO_SIZE: usize = 128; -const LOGO_SIZE: usize = 96; +const FF_LOGO_SIZE: u32 = 128; +const LOGO_SIZE: u32 = 96; fn cmd() -> Command { #[cfg(feature = "mem_limit")] @@ -157,7 +157,7 @@ fn svg2mkv( ffmpeg.run() } -fn svg2png(svg: &Path, png: &Path, width: usize, height: usize) -> anyhow::Result<()> { +fn svg2png(svg: &Path, png: &Path, width: u32, height: u32) -> anyhow::Result<()> { let mut cmd = cmd(); cmd.arg("inkscape") .arg("-w") @@ -263,14 +263,7 @@ impl<'a> Renderer<'a> { 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, - (1920, 1080) => Resolution::FullHD, - (1280, 720) => Resolution::HD, - (640, 360) => Resolution::nHD, - (width, height) => bail!("Unknown resolution: {width}x{height}") - }; + let source_res = Resolution { width, height }; project.source.metadata = Some(ProjectSourceMetadata { source_duration: ffprobe_video("format=duration", &recording_mkv)?.parse()?, source_fps: ffprobe_video("stream=r_frame_rate", &recording_mkv)?.parse()?, @@ -322,7 +315,7 @@ impl<'a> Renderer<'a> { include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/logo.svg")) )?; let logo_png = self.target.join("logo.png"); - let logo_size = LOGO_SIZE * metadata.source_res.width() / 1920; + let logo_size = LOGO_SIZE * metadata.source_res.width / 1920; svg2png(&logo_svg, &logo_png, logo_size, logo_size)?; // copy fastforward then render to png @@ -335,7 +328,7 @@ impl<'a> Renderer<'a> { )) )?; let fastforward_png = self.target.join("fastforward.png"); - let ff_logo_size = FF_LOGO_SIZE * metadata.source_res.width() / 1920; + let ff_logo_size = FF_LOGO_SIZE * metadata.source_res.width / 1920; svg2png( &fastforward_svg, &fastforward_png, @@ -355,8 +348,8 @@ impl<'a> Renderer<'a> { svg2png( &q_svg, &q_png, - metadata.source_res.width(), - metadata.source_res.height() + metadata.source_res.width, + metadata.source_res.height )?; } @@ -365,13 +358,13 @@ impl<'a> Renderer<'a> { /// Get the video file for a specific resolution, completely finished. fn video_file_res(&self, res: Resolution) -> PathBuf { - let extension = match res.format() { + let extension = match res.default_codec() { FfmpegOutputFormat::Av1Flac => "mkv", FfmpegOutputFormat::Av1Opus => "webm", FfmpegOutputFormat::AvcAac => "mp4" }; self.target - .join(format!("{}-{}p.{extension}", self.slug, res.height())) + .join(format!("{}-{}p.{extension}", self.slug, res.height)) } /// Get the video file directly outputed to further transcode. @@ -627,8 +620,8 @@ impl<'a> Renderer<'a> { output: logoalpha.into() }); let overlay = "overlay"; - let overlay_off_x = 130 * source_res.width() / 3840; - let overlay_off_y = 65 * source_res.height() / 2160; + let overlay_off_x = 130 * source_res.width / 3840; + let overlay_off_y = 65 * source_res.height / 2160; ffmpeg.add_filter(Filter::Overlay { video_input: concat.into(), overlay_input: logoalpha.into(), @@ -657,7 +650,7 @@ impl<'a> Renderer<'a> { println!( " {} {}", style("==>").bold().cyan(), - style(format!("Rescaling to {}p", res.height())).bold() + style(format!("Rescaling to {}p", res.height)).bold() ); let mut ffmpeg = Ffmpeg::new(FfmpegOutput { @@ -675,7 +668,7 @@ impl<'a> Renderer<'a> { comment: Some(lecture.lang.video_created_by_us.into()), language: Some(lecture.lang.lang.into()), - ..FfmpegOutput::new(res.format(), output.clone()).enable_faststart() + ..FfmpegOutput::new(res.default_codec(), output.clone()).enable_faststart() }); ffmpeg.add_input(FfmpegInput::new(input)); ffmpeg.rescale_video(res);