diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 2b4a8f4..0000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -**/target/ -23*/ -24*/ diff --git a/.forgejo/workflows/webhook.yml b/.forgejo/workflows/webhook.yml deleted file mode 100644 index 6a5ecc0..0000000 --- a/.forgejo/workflows/webhook.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Trigger quay.io Webhook - -on: - push: - branches: [main] - -jobs: - run: - runs-on: alpine-latest - steps: - - run: | - apk add ca-certificates curl - curl -D - --fail-with-body -X POST -H 'Content-Type: application/json' --data '{ - "commit": "${{github.sha}}", - "ref": "${{github.ref}}", - "default_branch": "main" - }' 'https://$token:${{secrets.quay_token}}@quay.io/webhooks/push/trigger/f21fe844-3a4b-43b0-a92f-7871d7d7ea68' - shell: ash -eo pipefail {0} diff --git a/230101/project.toml b/230101/project.toml index 814e1c2..d90df0c 100644 --- a/230101/project.toml +++ b/230101/project.toml @@ -7,13 +7,9 @@ date = "230101" [source] files = ["C01.mp4", "C02.mp4", "C03.mp4"] stereo = false -start = "1" +start = "2" end = "12" -fast = [["6", "8"], ["10", "11"]] -questions = [ - ["1.5", "3", "Hallo liebes Publikum. Ich habe leider meine Frage vergessen. Bitte entschuldigt die Störung."], - ["3.5", "5", "Ah jetzt weiß ich es wieder. Meine Frage war: Was war meine Frage?"] -] +fast = [["5", "7"], ["9", "11"]] [source.metadata] source_duration = "12.53000" @@ -26,6 +22,5 @@ source_sample_rate = 48000 preprocessed = false asked_start_end = true asked_fast = true -asked_questions = true rendered = false transcoded = [] diff --git a/Cargo.toml b/Cargo.toml index e405c72..3a03999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,10 @@ license = "EPL-2.0" [dependencies] anyhow = "1.0" camino = "1.1" -console = "0.15" clap = { version = "4.4", features = ["derive"] } -fontconfig = "0.8" -harfbuzz_rs = "2.0" indexmap = "2.2" rational = "1.5" serde = { version = "1.0.188", features = ["derive"] } serde_with = "3.4" svgwriter = "0.1" toml = { package = "basic-toml", version = "0.1.4" } - -[features] -default = ["mem_limit"] -mem_limit = [] -vaapi = [] diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1ec20c0..0000000 --- a/Dockerfile +++ /dev/null @@ -1,64 +0,0 @@ -FROM alpine:3.20 - -ARG ffmpeg_ver=7.0 - -RUN mkdir -p /usr/local/src/render_video -COPY LICENSE /usr/local/src/render_video/LICENSE -COPY Cargo.toml /usr/local/src/render_video/Cargo.toml -COPY src /usr/local/src/render_video/src -COPY assets /usr/local/src/render_video/assets - -RUN apk add --no-cache \ - dav1d fontconfig freetype harfbuzz librsvg libva lilv-libs opus svt-av1 x264-libs x265 \ - font-noto inkscape libarchive-tools libgcc \ - && apk add --no-cache --virtual .build-deps \ - build-base cargo pkgconf \ - dav1d-dev fontconfig-dev freetype-dev harfbuzz-dev librsvg-dev libva-dev lilv-dev opus-dev svt-av1-dev x264-dev x265-dev \ - # build the render_video project - && cargo install --path /usr/local/src/render_video --root /usr/local --no-default-features \ - && rm -rf ~/.cargo \ - # we install ffmpeg ourselves to get libsvtav1 support which is not part of the alpine package \ - && wget -q https://ffmpeg.org/releases/ffmpeg-${ffmpeg_ver}.tar.bz2 \ - && tar xfa ffmpeg-${ffmpeg_ver}.tar.bz2 \ - && cd ffmpeg-${ffmpeg_ver} \ - && ./configure \ - --prefix=/usr/local \ - --disable-asm \ - --disable-librtmp \ - --disable-lzma \ - --disable-static \ - --enable-avfilter \ - --enable-gpl \ - --enable-libdav1d \ - --enable-libfontconfig \ - --enable-libfreetype \ - --enable-libharfbuzz \ - --enable-libopus \ - --enable-librsvg \ - --enable-libsvtav1 \ - --enable-libx264 \ - --enable-libx265 \ - --enable-lto=auto \ - --enable-lv2 \ - --enable-pic \ - --enable-postproc \ - --enable-pthreads \ - --enable-shared \ - --enable-vaapi \ - --enable-version3 \ - --optflags="-O3" \ - && make -j$(nproc) install \ - && apk del --no-cache .build-deps \ - && cd .. \ - && rm -r ffmpeg-${ffmpeg_ver} ffmpeg-${ffmpeg_ver}.tar.bz2 \ - # we need Arial Black for the VideoAG logo \ - && wget -q https://www.freedesktop.org/software/fontconfig/webfonts/webfonts.tar.gz \ - && tar xfa webfonts.tar.gz \ - && cd msfonts \ - && for file in *.exe; do bsdtar xf "$file"; done \ - && install -Dm644 -t /usr/share/fonts/msfonts/ *.ttf *.TTF \ - && install -Dm644 -t /usr/share/licenses/msfonts/ Licen.TXT \ - && cd .. \ - && rm -r msfonts webfonts.tar.gz - -ENTRYPOINT ["/usr/local/bin/render_video"] diff --git a/README.md b/README.md deleted file mode 100644 index d457a56..0000000 --- a/README.md +++ /dev/null @@ -1,17 +0,0 @@ -**ACHTUNG!** This repository might be mirrored at different places, but the main repository is and remains at [msrd0.dev/msrd0/render_video](https://msrd0.dev/msrd0/render_video). Please redirect all issues and pull requests there. - -# render_video - -This "script" is an extremely fancy wrapper around ffmpeg to cut/render videos for the [VideoAG](https://video.fsmpi.rwth-aachen.de) of the [Fachschaft I/1 der RWTH Aachen University](https://fsmpi.rwth-aachen.de). - -You can find a ready-to-use docker image at [`quay.io/msrd0/render_video`](https://quay.io/msrd0/render_video). - -## Features - - - **Extract a single audio channel from stereo recording.** We use that with one of our cameras that supports plugging a lavalier microphone (mono source) into one channel of the stereo recording, and using the camera microphone (mono source) for the other channel of the stereo recording. - - **Cut away before/after the lecture.** We don't hit the start record button the exact time that the lecture starts, and don't hit the stop button exactly when the lecture ends, so we need to cut away those unwanted bits. - - **Fast-forward through _Tafelwischpausen_.** Sometimes docents still use blackboards and need to wipe those, which can be fast-forwarded by this tool. - - **Overlay questions from the audience.** Sometimes people in the audience have questions, and those are usually poorly understood on the lavalier microphones. Therefore you can subtitle these using the other microphones in the room that don't make it into the final video and have those overlayed. - - **Add intro and outro.** We add intro and outro slides at the start/end at all lectures, which this tool can do for you. - - **Add our logo watermark.** We add a logo watermark in the bottom right corner of all videos, which this tool can do for you. - - **Rescale to lower resolutions.** We usually published videos at different resolutions, and this tool can rescale your video for all resolutions you want. diff --git a/assets/fastforward.svg b/assets/fastforward.svg index 6d0ea91..5243967 100644 --- a/assets/fastforward.svg +++ b/assets/fastforward.svg @@ -1,25 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/assets/logo.svg b/assets/logo.svg index 9bbdd78..b14083d 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,36 +1,199 @@ + + - - - - + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="299.99982" + height="300.00003" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.46" + sodipodi:docbase="C:\Eigene Dateien\Video AG" + sodipodi:docname="logo.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + version="1.0" + inkscape:export-filename="Q:\video AG\fs-pub-video\folien\logo-1024.png" + inkscape:export-xdpi="307.20001" + inkscape:export-ydpi="307.20001"> + + + + + - + - - - - - - - - - - - - - V - - V - - - - ideo - - - AG - + + + + + image/svg+xml + + + + + + + + V + + ideo + AG + V diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 9320c6f..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! 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 e37045e..683eb10 100644 --- a/src/iotro.rs +++ b/src/iotro.rs @@ -1,35 +1,28 @@ //! A module for writing intros and outros -use crate::{ - project::{ProjectLecture, Resolution}, - time::Date -}; +use crate::{time::Date, ProjectLecture, Resolution}; use anyhow::anyhow; use std::{ fmt::{self, Debug, Display, Formatter}, str::FromStr }; use svgwriter::{ - tags::{Group, Rect, TagWithPresentationAttributes as _, Text}, + tags::{Group, Rect, TagWithPresentationAttributes, Text}, Graphic }; #[derive(Clone)] pub struct Language<'a> { - pub(crate) lang: &'a str, - pub(crate) format_date_long: fn(Date) -> String, + lang: &'a str, + format_date_long: fn(Date) -> String, // intro lecture_from: &'a str, - pub(crate) video_created_by_us: &'a str, + video_created_by_us: &'a str, // outro video_created_by: &'a str, our_website: &'a str, download_videos: &'a str, - questions_feedback: &'a str, - // metadata - pub(crate) from: &'a str, - // questions - pub(crate) question: &'a str + questions_feedback: &'a str } pub const GERMAN: Language<'static> = Language { @@ -61,11 +54,7 @@ pub const GERMAN: Language<'static> = Language { video_created_by: "Video erstellt von der", our_website: "Website der Fachschaft", download_videos: "Videos herunterladen", - questions_feedback: "Fragen, Vorschläge und Feedback", - - from: "vom", - - question: "Frage" + questions_feedback: "Fragen, Vorschläge und Feedback" }; pub const BRITISH: Language<'static> = Language { @@ -94,28 +83,17 @@ pub const BRITISH: Language<'static> = Language { 3 | 23 => "rd", _ => "th" }; - format!("{}{th} {month} {:04}", d.day, d.year) + format!("{:02}{th} {month} {:04}", d.day, d.year) }, lecture_from: "Lecture from", video_created_by_us: "Video created by the Video AG, Fachschaft I/1", - video_created_by: "Video created by the", our_website: "The Fachschaft's website", download_videos: "Download videos", - questions_feedback: "Questions, Suggestions and Feedback", - - from: "from", - - question: "Question" + questions_feedback: "Questions, Suggestions and Feedback" }; -impl Default for Language<'static> { - fn default() -> Self { - GERMAN - } -} - impl FromStr for Language<'static> { type Err = anyhow::Error; @@ -143,14 +121,14 @@ impl Debug for Language<'_> { } #[repr(u16)] -pub(crate) enum FontSize { +enum FontSize { Huge = 72, Large = 56, Big = 44 } #[repr(u16)] -pub(crate) enum FontWeight { +enum FontWeight { Normal = 400, SemiBold = 500, Bold = 700 diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 998054a..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![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 7d0a56b..b21fbdd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,17 +2,30 @@ #![warn(clippy::unreadable_literal, rust_2018_idioms)] #![forbid(elided_lifetimes_in_paths, unsafe_code)] +mod iotro; +mod render; +mod time; + +use crate::{ + render::{ffmpeg::FfmpegOutputFormat, Renderer}, + time::{parse_date, parse_time, Date, Time} +}; use camino::Utf8PathBuf as PathBuf; use clap::Parser; -use console::style; -use render_video::{ - cli::{ask, ask_time}, - preset::Preset, - project::{Project, ProjectLecture, ProjectSource, Resolution}, - render::Renderer, - time::parse_date +use iotro::Language; +use rational::Rational; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::{ + collections::BTreeSet, + fmt::Display, + fs, + io::{self, BufRead as _, Write}, + str::FromStr, + sync::RwLock }; -use std::{collections::BTreeSet, fs}; + +static MEM_LIMIT: RwLock = RwLock::new(String::new()); #[derive(Debug, Parser)] struct Args { @@ -20,24 +33,33 @@ struct Args { #[clap(short = 'C', long, default_value = ".")] directory: PathBuf, - /// The preset of the lecture. Can be a toml file or a known course slug. - #[clap(short, long)] - preset: String, + /// The slug of the course, e.g. "23ws-malo2". + #[clap(short = 'c', long, default_value = "23ws-malo2")] + course: String, + + /// The label of the course, e.g. "Mathematische Logik II". + #[clap(short, long, default_value = "Mathematische Logik II")] + label: String, + + /// The docent of the course, e.g. "Prof. E. Grädel". + #[clap(short, long, default_value = "Prof. E. Grädel")] + docent: String, + + /// The language of the lecture. Used for the intro and outro frame. + #[clap(short = 'L', long, default_value = "de")] + lang: Language<'static>, - #[cfg(feature = "mem_limit")] /// The memory limit for external tools like ffmpeg. #[clap(short, long, default_value = "12G")] mem_limit: String, - /// Transcode the final video clip down to the minimum resolution specified. If not - /// specified, the default value from the preset is used. + /// Transcode the final video clip down to the minimum resolution specified. #[clap(short, long)] transcode: Option, /// Transcode starts at this resolution, or the source resolution, whichever is lower. - /// If not specified, the default value from the preset is used. - #[clap(short = 'T', long)] - transcode_start: Option, + #[clap(short = 'T', long, default_value = "1440p")] + transcode_start: Resolution, /// Treat the audio as stereo. By default, only one channel from the input stereo will /// be used, assuming either the other channel is backup or the same as the used. @@ -45,18 +67,175 @@ struct Args { stereo: bool } +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)] + enum Resolution { + $( + #[doc = concat!(stringify!($width), "x", stringify!($height))] + $res + ),+ + } + + const NUM_RESOLUTIONS: usize = { + let mut num = 0; + $(num += 1; stringify!($res);)+ + num + }; + + impl Resolution { + fn values() -> [Self; NUM_RESOLUTIONS] { + [$(Self::$res),+] + } + + fn width(self) -> usize { + match self { + $(Self::$res => $width),+ + } + } + + fn height(self) -> usize { + match self { + $(Self::$res => $height),+ + } + } + + fn bitrate(self) -> u64 { + match self { + $(Self::$res => $bitrate),+ + } + } + + fn format(self) -> FfmpegOutputFormat { + match self { + $(Self::$res => FfmpegOutputFormat::$format),+ + } + } + } + + 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:?}") + }) + } + } + } +} + +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 2_000_000 in Av1Opus, + 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 in Av1Opus +} + +#[derive(Deserialize, Serialize)] +struct Project { + lecture: ProjectLecture, + source: ProjectSource, + progress: ProjectProgress +} + +#[serde_as] +#[derive(Deserialize, Serialize)] +struct ProjectLecture { + course: String, + label: String, + docent: String, + #[serde_as(as = "DisplayFromStr")] + date: Date, + #[serde_as(as = "DisplayFromStr")] + lang: Language<'static> +} + +#[serde_as] +#[derive(Deserialize, Serialize)] +struct ProjectSource { + files: Vec, + stereo: bool, + + #[serde_as(as = "Option")] + start: Option