From 680ea8f4e57108ed007e2cd1e9392c207e1cc0f5 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 23 Jun 2024 15:53:45 +0000 Subject: [PATCH] Refactor the code into a binary and library (#1) Reviewed-on: https://msrd0.dev/msrd0/render_video/pulls/1 Co-authored-by: Dominic Co-committed-by: Dominic --- src/cli.rs | 54 +++++++++ src/iotro.rs | 9 +- src/lib.rs | 17 +++ src/main.rs | 79 ++----------- src/preset.rs | 38 ++++--- src/project.rs | 257 +++++++++++++++++++++++++++---------------- src/question.rs | 6 +- src/render/ffmpeg.rs | 8 +- src/render/mod.rs | 49 ++++----- 9 files changed, 297 insertions(+), 220 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/lib.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..9320c6f --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,54 @@ +//! This module contains helper functions for implementing CLI/TUI. + +use crate::time::{parse_time, Time}; +use console::style; +use std::{ + fmt::Display, + io::{self, BufRead as _, Write as _} +}; + +pub fn ask(question: impl Display) -> String { + let mut stdout = io::stdout().lock(); + let mut stdin = io::stdin().lock(); + + write!( + stdout, + "{} {} ", + style(question).bold().magenta(), + style(">").cyan() + ) + .unwrap(); + stdout.flush().unwrap(); + let mut line = String::new(); + stdin.read_line(&mut line).unwrap(); + line.trim().to_owned() +} + +pub fn ask_time(question: impl Display + Copy) -> Time { + let mut stdout = io::stdout().lock(); + let mut stdin = io::stdin().lock(); + + let mut line = String::new(); + loop { + line.clear(); + write!( + stdout, + "{} {} ", + style(question).bold().magenta(), + style(">").cyan() + ) + .unwrap(); + stdout.flush().unwrap(); + stdin.read_line(&mut line).unwrap(); + let line = line.trim(); + match parse_time(line) { + Ok(time) => return time, + Err(err) => writeln!( + stdout, + "{} {line:?}: {err}", + style("Invalid Input").bold().red() + ) + .unwrap() + } + } +} diff --git a/src/iotro.rs b/src/iotro.rs index a4eeccb..cdbd1b2 100644 --- a/src/iotro.rs +++ b/src/iotro.rs @@ -1,6 +1,9 @@ //! A module for writing intros and outros -use crate::{time::Date, ProjectLecture, Resolution}; +use crate::{ + project::{ProjectLecture, Resolution}, + time::Date +}; use anyhow::anyhow; use std::{ fmt::{self, Debug, Display, Formatter}, @@ -188,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/lib.rs b/src/lib.rs new file mode 100644 index 0000000..998054a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,17 @@ +#![allow(clippy::manual_range_contains)] +#![warn(clippy::unreadable_literal, rust_2018_idioms)] +#![forbid(elided_lifetimes_in_paths, unsafe_code)] + +pub mod cli; +pub mod iotro; +pub mod preset; +pub mod project; +pub mod question; +pub mod render; +pub mod time; + +#[cfg(feature = "mem_limit")] +use std::sync::RwLock; + +#[cfg(feature = "mem_limit")] +pub static MEM_LIMIT: RwLock = RwLock::new(String::new()); diff --git a/src/main.rs b/src/main.rs index 67d9ce0..53df6ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,32 +2,17 @@ #![warn(clippy::unreadable_literal, rust_2018_idioms)] #![forbid(elided_lifetimes_in_paths, unsafe_code)] -mod iotro; -mod preset; -mod project; -mod question; -mod render; -mod time; - -use self::{ - project::{Project, ProjectLecture, ProjectSource, Resolution}, - render::Renderer, - time::{parse_date, parse_time, Time} -}; -use crate::preset::Preset; use camino::Utf8PathBuf as PathBuf; use clap::Parser; use console::style; -#[cfg(feature = "mem_limit")] -use std::sync::RwLock; -use std::{ - fmt::Display, - fs, - io::{self, BufRead as _, Write} +use render_video::{ + cli::{ask, ask_time}, + preset::Preset, + project::{Project, ProjectLecture, ProjectSource, Resolution}, + render::Renderer, + time::parse_date }; - -#[cfg(feature = "mem_limit")] -static MEM_LIMIT: RwLock = RwLock::new(String::new()); +use std::fs; #[derive(Debug, Parser)] struct Args { @@ -60,58 +45,12 @@ struct Args { stereo: bool } -fn ask(question: impl Display) -> String { - let mut stdout = io::stdout().lock(); - let mut stdin = io::stdin().lock(); - - write!( - stdout, - "{} {} ", - style(question).bold().magenta(), - style(">").cyan() - ) - .unwrap(); - stdout.flush().unwrap(); - let mut line = String::new(); - stdin.read_line(&mut line).unwrap(); - line.trim().to_owned() -} - -fn ask_time(question: impl Display + Copy) -> Time { - let mut stdout = io::stdout().lock(); - let mut stdin = io::stdin().lock(); - - let mut line = String::new(); - loop { - line.clear(); - write!( - stdout, - "{} {} ", - style(question).bold().magenta(), - style(">").cyan() - ) - .unwrap(); - stdout.flush().unwrap(); - stdin.read_line(&mut line).unwrap(); - let line = line.trim(); - match parse_time(line) { - Ok(time) => return time, - Err(err) => writeln!( - stdout, - "{} {line:?}: {err}", - style("Invalid Input").bold().red() - ) - .unwrap() - } - } -} - fn main() { let args = Args::parse(); #[cfg(feature = "mem_limit")] { - *(MEM_LIMIT.write().unwrap()) = args.mem_limit; + *(render_video::MEM_LIMIT.write().unwrap()) = args.mem_limit; } // process arguments @@ -276,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 85f37c6..507401a 100644 --- a/src/preset.rs +++ b/src/preset.rs @@ -11,57 +11,59 @@ use std::{fs, io}; #[serde_as] #[derive(Deserialize, Serialize)] -pub(crate) struct Preset { +pub struct Preset { // options for the intro slide - pub(crate) course: String, - pub(crate) label: String, - pub(crate) docent: String, + pub course: String, + pub label: String, + pub docent: String, /// Course language - #[serde(default = "Default::default")] + #[serde(default)] #[serde_as(as = "DisplayFromStr")] - pub(crate) lang: Language<'static>, + pub lang: Language<'static>, // coding options - pub(crate) transcode_start: Resolution, - pub(crate) transcode: Option + #[serde_as(as = "DisplayFromStr")] + pub transcode_start: Resolution, + #[serde_as(as = "Option")] + pub transcode: Option } -fn preset_23ws_malo2() -> Preset { +pub fn preset_23ws_malo2() -> Preset { Preset { course: "23ws-malo2".into(), 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()) } } -fn preset_24ss_algomod() -> Preset { +pub fn preset_24ss_algomod() -> Preset { Preset { course: "24ss-algomod".into(), 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()) } } -fn preset_24ss_qc() -> Preset { +pub fn preset_24ss_qc() -> Preset { Preset { course: "24ss-qc".into(), 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()) } } impl Preset { - pub(crate) fn find(name: &str) -> anyhow::Result { + pub fn find(name: &str) -> anyhow::Result { match fs::read(name) { Ok(buf) => return Ok(toml::from_slice(&buf)?), Err(err) if err.kind() == io::ErrorKind::NotFound => {}, diff --git a/src/project.rs b/src/project.rs index 7dbda1f..a16da81 100644 --- a/src/project.rs +++ b/src/project.rs @@ -8,157 +8,226 @@ 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(crate) 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(crate) fn values() -> [Self; NUM_RESOLUTIONS] { - [$(Self::$res),+] - } - - pub(crate) fn width(self) -> usize { - match self { - $(Self::$res => $width),+ - } - } - - pub(crate) fn height(self) -> usize { - match self { - $(Self::$res => $height),+ - } - } - - pub(crate) fn bitrate(self) -> u64 { - match self { - $(Self::$res => $bitrate),+ - } - } - - pub(crate) 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)] -pub(crate) struct Project { - pub(crate) lecture: ProjectLecture, - pub(crate) source: ProjectSource, - pub(crate) progress: ProjectProgress +pub struct Project { + pub lecture: ProjectLecture, + pub source: ProjectSource, + pub progress: ProjectProgress } #[serde_as] #[derive(Deserialize, Serialize)] -pub(crate) struct ProjectLecture { - pub(crate) course: String, - pub(crate) label: String, - pub(crate) docent: String, +pub struct ProjectLecture { + pub course: String, + pub label: String, + pub docent: String, #[serde_as(as = "DisplayFromStr")] - pub(crate) date: Date, + pub date: Date, #[serde(default = "Default::default")] #[serde_as(as = "DisplayFromStr")] - pub(crate) lang: Language<'static> + pub lang: Language<'static> } #[serde_as] #[derive(Deserialize, Serialize)] -pub(crate) struct ProjectSource { - pub(crate) files: Vec, - pub(crate) stereo: bool, +pub struct ProjectSource { + pub files: Vec, + pub stereo: bool, #[serde_as(as = "Option")] - pub(crate) start: Option