Refactor the code into a binary and library #1
9 changed files with 297 additions and 220 deletions
54
src/cli.rs
Normal file
54
src/cli.rs
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
17
src/lib.rs
Normal file
17
src/lib.rs
Normal file
|
@ -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<String> = RwLock::new(String::new());
|
79
src/main.rs
79
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<String> = 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
|
||||
|
|
|
@ -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<Resolution>
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub transcode_start: Resolution,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
pub transcode: Option<Resolution>
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
pub fn find(name: &str) -> anyhow::Result<Self> {
|
||||
match fs::read(name) {
|
||||
Ok(buf) => return Ok(toml::from_slice(&buf)?),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {},
|
||||
|
|
235
src/project.rs
235
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
|
||||
),+
|
||||
}
|
||||
|
||||
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),+
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct Resolution {
|
||||
pub width: u32,
|
||||
pub height: u32
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
pub(crate) fn bitrate(self) -> u64 {
|
||||
match self {
|
||||
$(Self::$res => $bitrate),+
|
||||
// 640 * 360: 500k
|
||||
if self.width <= 640 {
|
||||
500_000
|
||||
}
|
||||
// 1280 * 720: 1M
|
||||
else if self.width <= 1280 {
|
||||
1_000_000
|
||||
}
|
||||
// 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 format(self) -> FfmpegOutputFormat {
|
||||
match self {
|
||||
$(Self::$res => FfmpegOutputFormat::$format),+
|
||||
}
|
||||
pub(crate) fn default_codec(self) -> FfmpegOutputFormat {
|
||||
if self.width <= 1920 {
|
||||
FfmpegOutputFormat::Av1Opus
|
||||
} else {
|
||||
FfmpegOutputFormat::AvcAac
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Resolution {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Resolution {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||
Ok(match s {
|
||||
$(concat!(stringify!($height), "p") => Self::$res,)+
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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 Eq for Resolution {}
|
||||
|
||||
impl PartialOrd for Resolution {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
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<String>,
|
||||
pub(crate) stereo: bool,
|
||||
pub struct ProjectSource {
|
||||
pub files: Vec<String>,
|
||||
pub stereo: bool,
|
||||
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
pub(crate) start: Option<Time>,
|
||||
pub start: Option<Time>,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
pub(crate) end: Option<Time>,
|
||||
pub end: Option<Time>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
|
||||
pub(crate) fast: Vec<(Time, Time)>,
|
||||
pub fast: Vec<(Time, Time)>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
|
||||
pub(crate) questions: Vec<(Time, Time, String)>,
|
||||
pub questions: Vec<(Time, Time, String)>,
|
||||
|
||||
pub(crate) metadata: Option<ProjectSourceMetadata>
|
||||
pub metadata: Option<ProjectSourceMetadata>
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct ProjectSourceMetadata {
|
||||
pub struct ProjectSourceMetadata {
|
||||
/// The duration of the source video.
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub(crate) source_duration: Time,
|
||||
pub source_duration: Time,
|
||||
/// The FPS of the source video.
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub(crate) source_fps: Rational,
|
||||
pub source_fps: Rational,
|
||||
/// The time base of the source video.
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub(crate) source_tbn: Rational,
|
||||
pub source_tbn: Rational,
|
||||
/// The resolution of the source video.
|
||||
pub(crate) source_res: Resolution,
|
||||
pub source_res: Resolution,
|
||||
/// The sample rate of the source audio.
|
||||
pub(crate) source_sample_rate: u32
|
||||
pub source_sample_rate: u32
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
pub(crate) struct ProjectProgress {
|
||||
pub struct ProjectProgress {
|
||||
#[serde(default)]
|
||||
pub(crate) preprocessed: bool,
|
||||
pub preprocessed: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) asked_start_end: bool,
|
||||
pub asked_start_end: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) asked_fast: bool,
|
||||
pub asked_fast: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) asked_questions: bool,
|
||||
pub asked_questions: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) rendered_assets: bool,
|
||||
pub rendered_assets: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) rendered: bool,
|
||||
pub rendered: bool,
|
||||
|
||||
#[serde_as(as = "BTreeSet<DisplayFromStr>")]
|
||||
#[serde(default)]
|
||||
pub(crate) transcoded: BTreeSet<Resolution>
|
||||
pub transcoded: BTreeSet<Resolution>
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{iotro::Language, Resolution};
|
||||
use crate::{iotro::Language, project::Resolution};
|
||||
use fontconfig::Fontconfig;
|
||||
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
|
||||
use std::sync::OnceLock;
|
||||
|
@ -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
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use super::{cmd, filter::Filter};
|
||||
use crate::{
|
||||
project::Resolution,
|
||||
render::filter::channel,
|
||||
time::{format_time, Time},
|
||||
Resolution
|
||||
time::{format_time, Time}
|
||||
};
|
||||
use anyhow::bail;
|
||||
use camino::Utf8PathBuf as PathBuf;
|
||||
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
@ -119,7 +119,7 @@ fn ffprobe_audio(query: &str, concat_input: &Path) -> anyhow::Result<String> {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) struct Renderer<'a> {
|
||||
pub struct Renderer<'a> {
|
||||
/// The directory with all the sources.
|
||||
directory: &'a Path,
|
||||
|
||||
|
@ -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")
|
||||
|
@ -175,7 +175,7 @@ fn svg2png(svg: &Path, png: &Path, width: usize, height: usize) -> anyhow::Resul
|
|||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn new(directory: &'a Path, project: &Project) -> anyhow::Result<Self> {
|
||||
pub fn new(directory: &'a Path, project: &Project) -> anyhow::Result<Self> {
|
||||
let slug = format!(
|
||||
"{}-{}",
|
||||
project.lecture.course,
|
||||
|
@ -210,7 +210,7 @@ impl<'a> Renderer<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn recording_mkv(&self) -> PathBuf {
|
||||
pub fn recording_mkv(&self) -> PathBuf {
|
||||
self.target.join("recording.mkv")
|
||||
}
|
||||
|
||||
|
@ -230,7 +230,7 @@ impl<'a> Renderer<'a> {
|
|||
self.target.join(format!("question{q_idx}.png"))
|
||||
}
|
||||
|
||||
pub(crate) fn preprocess(&self, project: &mut Project) -> anyhow::Result<()> {
|
||||
pub fn preprocess(&self, project: &mut Project) -> anyhow::Result<()> {
|
||||
assert!(!project.progress.preprocessed);
|
||||
|
||||
let recording_txt = self.target.join("recording.txt");
|
||||
|
@ -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()?,
|
||||
|
@ -283,7 +276,7 @@ impl<'a> Renderer<'a> {
|
|||
}
|
||||
|
||||
/// Prepare assets like intro, outro and questions.
|
||||
pub(crate) fn render_assets(&self, project: &Project) -> anyhow::Result<()> {
|
||||
pub fn render_assets(&self, project: &Project) -> anyhow::Result<()> {
|
||||
let metadata = project.source.metadata.as_ref().unwrap();
|
||||
|
||||
println!();
|
||||
|
@ -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,21 +358,21 @@ 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.
|
||||
pub(crate) fn video_file_output(&self) -> PathBuf {
|
||||
pub fn video_file_output(&self) -> PathBuf {
|
||||
self.target.join(format!("{}.mkv", self.slug))
|
||||
}
|
||||
|
||||
pub(crate) fn render(&self, project: &mut Project) -> anyhow::Result<PathBuf> {
|
||||
pub fn render(&self, project: &mut Project) -> anyhow::Result<PathBuf> {
|
||||
let source_res = project.source.metadata.as_ref().unwrap().source_res;
|
||||
|
||||
let output = self.video_file_output();
|
||||
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue