rework Resolution struct
All checks were successful
Trigger quay.io Webhook / run (push) Successful in 6s

the `source_res` field in the project.toml files will not be compatible
but I don't care
This commit is contained in:
Dominic 2024-06-23 17:48:36 +02:00
parent ba9c6ede3e
commit cb2d48e774
Signed by: msrd0
GPG key ID: AAF7C8430CA3345D
7 changed files with 161 additions and 97 deletions

View file

@ -191,8 +191,8 @@ impl Iotro {
fn finish(self) -> Graphic { fn finish(self) -> Graphic {
let mut svg = Graphic::new(); let mut svg = Graphic::new();
svg.set_width(self.res.width()); svg.set_width(self.res.width);
svg.set_height(self.res.height()); svg.set_height(self.res.height);
svg.set_view_box("0 0 1920 1080"); svg.set_view_box("0 0 1920 1080");
svg.push( svg.push(
Rect::new() Rect::new()

View file

@ -215,7 +215,7 @@ fn main() {
// rescale the video // rescale the video
if let Some(lowest_res) = args.transcode.or(preset.transcode) { 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 if res > project.source.metadata.as_ref().unwrap().source_res
|| res > args.transcode_start.unwrap_or(preset.transcode_start) || res > args.transcode_start.unwrap_or(preset.transcode_start)
|| res < lowest_res || res < lowest_res

View file

@ -18,12 +18,14 @@ pub struct Preset {
pub docent: String, pub docent: String,
/// Course language /// Course language
#[serde(default = "Default::default")] #[serde(default)]
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
pub lang: Language<'static>, pub lang: Language<'static>,
// coding options // coding options
#[serde_as(as = "DisplayFromStr")]
pub transcode_start: Resolution, pub transcode_start: Resolution,
#[serde_as(as = "Option<DisplayFromStr>")]
pub transcode: Option<Resolution> pub transcode: Option<Resolution>
} }
@ -33,8 +35,8 @@ pub fn preset_23ws_malo2() -> Preset {
label: "Mathematische Logik II".into(), label: "Mathematische Logik II".into(),
docent: "Prof. E. Grädel".into(), docent: "Prof. E. Grädel".into(),
lang: GERMAN, lang: GERMAN,
transcode_start: Resolution::WQHD, transcode_start: "1440p".parse().unwrap(),
transcode: Some(Resolution::nHD) transcode: Some("360p".parse().unwrap())
} }
} }
@ -44,8 +46,8 @@ pub fn preset_24ss_algomod() -> Preset {
label: "Algorithmische Modelltheorie".into(), label: "Algorithmische Modelltheorie".into(),
docent: "Prof. E. Grädel".into(), docent: "Prof. E. Grädel".into(),
lang: GERMAN, lang: GERMAN,
transcode_start: Resolution::WQHD, transcode_start: "1440p".parse().unwrap(),
transcode: Some(Resolution::HD) transcode: Some("720p".parse().unwrap())
} }
} }
@ -55,8 +57,8 @@ pub fn preset_24ss_qc() -> Preset {
label: "Introduction to Quantum Computing".into(), label: "Introduction to Quantum Computing".into(),
docent: "Prof. D. Unruh".into(), docent: "Prof. D. Unruh".into(),
lang: BRITISH, lang: BRITISH,
transcode_start: Resolution::WQHD, transcode_start: "1440p".parse().unwrap(),
transcode: Some(Resolution::HD) transcode: Some("720p".parse().unwrap())
} }
} }

View file

@ -8,52 +8,83 @@ use crate::{
use rational::Rational; use rational::Rational;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr};
use std::{collections::BTreeSet, str::FromStr}; use std::{
cmp,
macro_rules! resolutions { collections::BTreeSet,
($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => { fmt::{self, Display, Formatter},
#[allow(non_camel_case_types, clippy::upper_case_acronyms)] str::FromStr
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Resolution {
$(
#[doc = concat!(stringify!($width), "x", stringify!($height))]
$res
),+
}
const NUM_RESOLUTIONS: usize = {
let mut num = 0;
$(num += 1; stringify!($res);)+
num
}; };
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct Resolution {
pub width: u32,
pub height: u32
}
impl Resolution { impl Resolution {
pub fn values() -> [Self; NUM_RESOLUTIONS] { pub(crate) fn bitrate(self) -> u64 {
[$(Self::$res),+] // 640 * 360: 500k
if self.width <= 640 {
500_000
} }
// 1280 * 720: 1M
pub fn width(self) -> usize { else if self.width <= 1280 {
match self { 1_000_000
$(Self::$res => $width),+ }
// 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 fn height(self) -> usize { pub(crate) fn default_codec(self) -> FfmpegOutputFormat {
match self { if self.width <= 1920 {
$(Self::$res => $height),+ FfmpegOutputFormat::Av1Opus
} else {
FfmpegOutputFormat::AvcAac
} }
} }
pub fn bitrate(self) -> u64 { pub const STANDARD_RESOLUTIONS: [Self; 5] = [
match self { Self {
$(Self::$res => $bitrate),+ width: 640,
height: 360
},
Self {
width: 1280,
height: 720
},
Self {
width: 1920,
height: 1080
},
Self {
width: 2560,
height: 1440
},
Self {
width: 3840,
height: 2160
} }
];
} }
pub fn format(self) -> FfmpegOutputFormat { impl Display for Resolution {
match self { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
$(Self::$res => FfmpegOutputFormat::$format),+ write!(f, "{}p", self.height)
}
} }
} }
@ -61,22 +92,58 @@ macro_rules! resolutions {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> { fn from_str(s: &str) -> anyhow::Result<Self> {
Ok(match s { Ok(match s.to_lowercase().as_str() {
$(concat!(stringify!($height), "p") => Self::$res,)+ "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:?}") _ => 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))
} }
} }
resolutions! { impl Eq for Resolution {}
nHD: 640 x 360 at 500_000 in AvcAac,
HD: 1280 x 720 at 1_000_000 in AvcAac, impl PartialOrd for Resolution {
FullHD: 1920 x 1080 at 750_000 in Av1Opus, fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
WQHD: 2560 x 1440 at 1_000_000 in Av1Opus, Some(self.cmp(other))
// TODO qsx muss mal sagen wieviel bitrate für 4k }
UHD: 3840 x 2160 at 2_000_000 in Av1Opus }
impl PartialEq for Resolution {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == cmp::Ordering::Equal
}
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -139,6 +206,7 @@ pub struct ProjectSourceMetadata {
pub source_sample_rate: u32 pub source_sample_rate: u32
} }
#[serde_as]
#[derive(Default, Deserialize, Serialize)] #[derive(Default, Deserialize, Serialize)]
pub struct ProjectProgress { pub struct ProjectProgress {
#[serde(default)] #[serde(default)]
@ -159,6 +227,7 @@ pub struct ProjectProgress {
#[serde(default)] #[serde(default)]
pub rendered: bool, pub rendered: bool,
#[serde_as(as = "BTreeSet<DisplayFromStr>")]
#[serde(default)] #[serde(default)]
pub transcoded: BTreeSet<Resolution> pub transcoded: BTreeSet<Resolution>
} }

View file

@ -131,8 +131,8 @@ impl Question {
pub(crate) fn finish(self) -> Graphic { pub(crate) fn finish(self) -> Graphic {
let mut svg = Graphic::new(); let mut svg = Graphic::new();
svg.set_width(self.res.width()); svg.set_width(self.res.width);
svg.set_height(self.res.height()); svg.set_height(self.res.height);
svg.set_view_box("0 0 1920 1080"); svg.set_view_box("0 0 1920 1080");
svg.push(self.g); svg.push(self.g);
svg svg

View file

@ -347,9 +347,9 @@ impl Ffmpeg {
}, },
FfmpegFilter::Rescale(res) => { FfmpegFilter::Rescale(res) => {
cmd.arg("-vf").arg(if vaapi { 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 { } else {
format!("scale=w={}:h={}", res.width(), res.height()) format!("scale=w={}:h={}", res.width, res.height)
}); });
} }
} }

View file

@ -41,8 +41,8 @@ const QUESTION_FADE_LEN: Time = Time {
}; };
const FF_MULTIPLIER: usize = 8; const FF_MULTIPLIER: usize = 8;
// logo sizes at full hd, will be scaled to source resolution // logo sizes at full hd, will be scaled to source resolution
const FF_LOGO_SIZE: usize = 128; const FF_LOGO_SIZE: u32 = 128;
const LOGO_SIZE: usize = 96; const LOGO_SIZE: u32 = 96;
fn cmd() -> Command { fn cmd() -> Command {
#[cfg(feature = "mem_limit")] #[cfg(feature = "mem_limit")]
@ -157,7 +157,7 @@ fn svg2mkv(
ffmpeg.run() 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(); let mut cmd = cmd();
cmd.arg("inkscape") cmd.arg("inkscape")
.arg("-w") .arg("-w")
@ -263,14 +263,7 @@ impl<'a> Renderer<'a> {
let width = ffprobe_video("stream=width", &recording_mkv)?.parse()?; let width = ffprobe_video("stream=width", &recording_mkv)?.parse()?;
let height = ffprobe_video("stream=height", &recording_mkv)?.parse()?; let height = ffprobe_video("stream=height", &recording_mkv)?.parse()?;
let source_res = match (width, height) { let source_res = Resolution { 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}")
};
project.source.metadata = Some(ProjectSourceMetadata { project.source.metadata = Some(ProjectSourceMetadata {
source_duration: ffprobe_video("format=duration", &recording_mkv)?.parse()?, source_duration: ffprobe_video("format=duration", &recording_mkv)?.parse()?,
source_fps: ffprobe_video("stream=r_frame_rate", &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")) include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/logo.svg"))
)?; )?;
let logo_png = self.target.join("logo.png"); 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)?; svg2png(&logo_svg, &logo_png, logo_size, logo_size)?;
// copy fastforward then render to png // copy fastforward then render to png
@ -335,7 +328,7 @@ impl<'a> Renderer<'a> {
)) ))
)?; )?;
let fastforward_png = self.target.join("fastforward.png"); 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( svg2png(
&fastforward_svg, &fastforward_svg,
&fastforward_png, &fastforward_png,
@ -355,8 +348,8 @@ impl<'a> Renderer<'a> {
svg2png( svg2png(
&q_svg, &q_svg,
&q_png, &q_png,
metadata.source_res.width(), metadata.source_res.width,
metadata.source_res.height() metadata.source_res.height
)?; )?;
} }
@ -365,13 +358,13 @@ impl<'a> Renderer<'a> {
/// Get the video file for a specific resolution, completely finished. /// Get the video file for a specific resolution, completely finished.
fn video_file_res(&self, res: Resolution) -> PathBuf { fn video_file_res(&self, res: Resolution) -> PathBuf {
let extension = match res.format() { let extension = match res.default_codec() {
FfmpegOutputFormat::Av1Flac => "mkv", FfmpegOutputFormat::Av1Flac => "mkv",
FfmpegOutputFormat::Av1Opus => "webm", FfmpegOutputFormat::Av1Opus => "webm",
FfmpegOutputFormat::AvcAac => "mp4" FfmpegOutputFormat::AvcAac => "mp4"
}; };
self.target 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. /// Get the video file directly outputed to further transcode.
@ -627,8 +620,8 @@ impl<'a> Renderer<'a> {
output: logoalpha.into() output: logoalpha.into()
}); });
let overlay = "overlay"; let overlay = "overlay";
let overlay_off_x = 130 * source_res.width() / 3840; let overlay_off_x = 130 * source_res.width / 3840;
let overlay_off_y = 65 * source_res.height() / 2160; let overlay_off_y = 65 * source_res.height / 2160;
ffmpeg.add_filter(Filter::Overlay { ffmpeg.add_filter(Filter::Overlay {
video_input: concat.into(), video_input: concat.into(),
overlay_input: logoalpha.into(), overlay_input: logoalpha.into(),
@ -657,7 +650,7 @@ impl<'a> Renderer<'a> {
println!( println!(
" {} {}", " {} {}",
style("==>").bold().cyan(), 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 { let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
@ -675,7 +668,7 @@ impl<'a> Renderer<'a> {
comment: Some(lecture.lang.video_created_by_us.into()), comment: Some(lecture.lang.video_created_by_us.into()),
language: Some(lecture.lang.lang.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.add_input(FfmpegInput::new(input));
ffmpeg.rescale_video(res); ffmpeg.rescale_video(res);