Compare commits
No commits in common. "52c89dc95af6814a23324fe98cf42912da9339eb" and "b6bd1be12e8df137c1667de42a9eba879782c18d" have entirely different histories.
52c89dc95a
...
b6bd1be12e
6 changed files with 207 additions and 277 deletions
14
src/iotro.rs
14
src/iotro.rs
|
@ -24,9 +24,7 @@ pub struct Language<'a> {
|
||||||
download_videos: &'a str,
|
download_videos: &'a str,
|
||||||
questions_feedback: &'a str,
|
questions_feedback: &'a str,
|
||||||
// metadata
|
// metadata
|
||||||
pub(crate) from: &'a str,
|
pub(crate) from: &'a str
|
||||||
// questions
|
|
||||||
pub(crate) question: &'a str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const GERMAN: Language<'static> = Language {
|
pub const GERMAN: Language<'static> = Language {
|
||||||
|
@ -60,9 +58,7 @@ pub const GERMAN: Language<'static> = Language {
|
||||||
download_videos: "Videos herunterladen",
|
download_videos: "Videos herunterladen",
|
||||||
questions_feedback: "Fragen, Vorschläge und Feedback",
|
questions_feedback: "Fragen, Vorschläge und Feedback",
|
||||||
|
|
||||||
from: "vom",
|
from: "vom"
|
||||||
|
|
||||||
question: "Frage"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const BRITISH: Language<'static> = Language {
|
pub const BRITISH: Language<'static> = Language {
|
||||||
|
@ -91,7 +87,7 @@ pub const BRITISH: Language<'static> = Language {
|
||||||
3 | 23 => "rd",
|
3 | 23 => "rd",
|
||||||
_ => "th"
|
_ => "th"
|
||||||
};
|
};
|
||||||
format!("{}{th} {month} {:04}", d.day, d.year)
|
format!("{:02}{th} {month} {:04}", d.day, d.year)
|
||||||
},
|
},
|
||||||
|
|
||||||
lecture_from: "Lecture from",
|
lecture_from: "Lecture from",
|
||||||
|
@ -102,9 +98,7 @@ pub const BRITISH: Language<'static> = Language {
|
||||||
download_videos: "Download videos",
|
download_videos: "Download videos",
|
||||||
questions_feedback: "Questions, Suggestions and Feedback",
|
questions_feedback: "Questions, Suggestions and Feedback",
|
||||||
|
|
||||||
from: "from",
|
from: "from"
|
||||||
|
|
||||||
question: "Question"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Default for Language<'static> {
|
impl Default for Language<'static> {
|
||||||
|
|
209
src/main.rs
209
src/main.rs
|
@ -3,27 +3,30 @@
|
||||||
#![forbid(elided_lifetimes_in_paths, unsafe_code)]
|
#![forbid(elided_lifetimes_in_paths, unsafe_code)]
|
||||||
|
|
||||||
mod iotro;
|
mod iotro;
|
||||||
mod preset;
|
|
||||||
mod project;
|
|
||||||
mod question;
|
mod question;
|
||||||
mod render;
|
mod render;
|
||||||
mod time;
|
mod time;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
project::{Project, ProjectLecture, ProjectSource, Resolution},
|
iotro::Language,
|
||||||
render::Renderer,
|
question::Question,
|
||||||
time::{parse_date, parse_time, Time}
|
render::{ffmpeg::FfmpegOutputFormat, Renderer},
|
||||||
|
time::{parse_date, parse_time, Date, Time}
|
||||||
};
|
};
|
||||||
use crate::preset::Preset;
|
|
||||||
use camino::Utf8PathBuf as PathBuf;
|
use camino::Utf8PathBuf as PathBuf;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use console::style;
|
use console::style;
|
||||||
|
use rational::Rational;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
#[cfg(feature = "mem_limit")]
|
#[cfg(feature = "mem_limit")]
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::BTreeSet,
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufRead as _, Write}
|
io::{self, BufRead as _, Write},
|
||||||
|
str::FromStr
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "mem_limit")]
|
#[cfg(feature = "mem_limit")]
|
||||||
|
@ -35,24 +38,34 @@ struct Args {
|
||||||
#[clap(short = 'C', long, default_value = ".")]
|
#[clap(short = 'C', long, default_value = ".")]
|
||||||
directory: PathBuf,
|
directory: PathBuf,
|
||||||
|
|
||||||
/// The preset of the lecture. Can be a toml file or a known course slug.
|
/// The slug of the course, e.g. "23ws-malo2".
|
||||||
#[clap(short, long)]
|
#[clap(short = 'c', long, default_value = "23ws-malo2")]
|
||||||
preset: String,
|
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")]
|
#[cfg(feature = "mem_limit")]
|
||||||
/// The memory limit for external tools like ffmpeg.
|
/// The memory limit for external tools like ffmpeg.
|
||||||
#[clap(short, long, default_value = "12G")]
|
#[clap(short, long, default_value = "12G")]
|
||||||
mem_limit: String,
|
mem_limit: String,
|
||||||
|
|
||||||
/// Transcode the final video clip down to the minimum resolution specified. If not
|
/// Transcode the final video clip down to the minimum resolution specified.
|
||||||
/// specified, the default value from the preset is used.
|
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
transcode: Option<Resolution>,
|
transcode: Option<Resolution>,
|
||||||
|
|
||||||
/// Transcode starts at this resolution, or the source resolution, whichever is lower.
|
/// 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, default_value = "1440p")]
|
||||||
#[clap(short = 'T', long)]
|
transcode_start: Resolution,
|
||||||
transcode_start: Option<Resolution>,
|
|
||||||
|
|
||||||
/// Treat the audio as stereo. By default, only one channel from the input stereo will
|
/// 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.
|
/// be used, assuming either the other channel is backup or the same as the used.
|
||||||
|
@ -60,6 +73,159 @@ struct Args {
|
||||||
stereo: bool
|
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<Self> {
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(default = "Default::default")]
|
||||||
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
|
lang: Language<'static>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct ProjectSource {
|
||||||
|
files: Vec<String>,
|
||||||
|
stereo: bool,
|
||||||
|
|
||||||
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
|
start: Option<Time>,
|
||||||
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
|
end: Option<Time>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
|
||||||
|
fast: Vec<(Time, Time)>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
|
||||||
|
questions: Vec<(Time, Time, String)>,
|
||||||
|
|
||||||
|
metadata: Option<ProjectSourceMetadata>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct ProjectSourceMetadata {
|
||||||
|
/// The duration of the source video.
|
||||||
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
|
source_duration: Time,
|
||||||
|
/// The FPS of the source video.
|
||||||
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
|
source_fps: Rational,
|
||||||
|
/// The time base of the source video.
|
||||||
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
|
source_tbn: Rational,
|
||||||
|
/// The resolution of the source video.
|
||||||
|
source_res: Resolution,
|
||||||
|
/// The sample rate of the source audio.
|
||||||
|
source_sample_rate: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize, Serialize)]
|
||||||
|
struct ProjectProgress {
|
||||||
|
#[serde(default)]
|
||||||
|
preprocessed: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
asked_start_end: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
asked_fast: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
asked_questions: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
rendered_assets: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
rendered: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
transcoded: BTreeSet<Resolution>
|
||||||
|
}
|
||||||
|
|
||||||
fn ask(question: impl Display) -> String {
|
fn ask(question: impl Display) -> String {
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
let mut stdin = io::stdin().lock();
|
let mut stdin = io::stdin().lock();
|
||||||
|
@ -116,8 +282,7 @@ fn main() {
|
||||||
|
|
||||||
// process arguments
|
// process arguments
|
||||||
let directory = args.directory.canonicalize_utf8().unwrap();
|
let directory = args.directory.canonicalize_utf8().unwrap();
|
||||||
let preset = Preset::find(&args.preset).unwrap();
|
let course = args.course;
|
||||||
let course = preset.course;
|
|
||||||
|
|
||||||
// let's see if we need to initialise the project
|
// let's see if we need to initialise the project
|
||||||
let project_path = directory.join("project.toml");
|
let project_path = directory.join("project.toml");
|
||||||
|
@ -162,10 +327,10 @@ fn main() {
|
||||||
let project = Project {
|
let project = Project {
|
||||||
lecture: ProjectLecture {
|
lecture: ProjectLecture {
|
||||||
course,
|
course,
|
||||||
label: preset.label,
|
label: args.label,
|
||||||
docent: preset.docent,
|
docent: args.docent,
|
||||||
date,
|
date,
|
||||||
lang: preset.lang
|
lang: args.lang
|
||||||
},
|
},
|
||||||
source: ProjectSource {
|
source: ProjectSource {
|
||||||
files,
|
files,
|
||||||
|
@ -274,10 +439,10 @@ 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 {
|
||||||
for res in Resolution::values().into_iter().rev() {
|
for res in Resolution::values().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
|
||||||
|| res < lowest_res
|
|| res < lowest_res
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
//! This struct defines presets.
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
iotro::{Language, BRITISH, GERMAN},
|
|
||||||
project::Resolution
|
|
||||||
};
|
|
||||||
use anyhow::bail;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
|
||||||
use std::{fs, io};
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub(crate) struct Preset {
|
|
||||||
// options for the intro slide
|
|
||||||
pub(crate) course: String,
|
|
||||||
pub(crate) label: String,
|
|
||||||
pub(crate) docent: String,
|
|
||||||
|
|
||||||
/// Course language
|
|
||||||
#[serde(default = "Default::default")]
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
pub(crate) lang: Language<'static>,
|
|
||||||
|
|
||||||
// coding options
|
|
||||||
pub(crate) transcode_start: Resolution,
|
|
||||||
pub(crate) transcode: Option<Resolution>
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Preset {
|
|
||||||
pub(crate) 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 => {},
|
|
||||||
Err(err) => return Err(err.into())
|
|
||||||
}
|
|
||||||
Ok(match name {
|
|
||||||
"23ws-malo2" => preset_23ws_malo2(),
|
|
||||||
"24ss-algomod" => preset_24ss_algomod(),
|
|
||||||
"24ss-qc" => preset_24ss_qc(),
|
|
||||||
_ => bail!("Unknown preset {name:?}")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
164
src/project.rs
164
src/project.rs
|
@ -1,164 +0,0 @@
|
||||||
//! Defines the structure of the `project.toml` file.
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
iotro::Language,
|
|
||||||
render::ffmpeg::FfmpegOutputFormat,
|
|
||||||
time::{Date, Time}
|
|
||||||
};
|
|
||||||
use rational::Rational;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
|
||||||
use std::{collections::BTreeSet, 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),+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn bitrate(self) -> u64 {
|
|
||||||
match self {
|
|
||||||
$(Self::$res => $bitrate),+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) 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<Self> {
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub(crate) struct Project {
|
|
||||||
pub(crate) lecture: ProjectLecture,
|
|
||||||
pub(crate) source: ProjectSource,
|
|
||||||
pub(crate) progress: ProjectProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub(crate) struct ProjectLecture {
|
|
||||||
pub(crate) course: String,
|
|
||||||
pub(crate) label: String,
|
|
||||||
pub(crate) docent: String,
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
pub(crate) date: Date,
|
|
||||||
#[serde(default = "Default::default")]
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
pub(crate) lang: Language<'static>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub(crate) struct ProjectSource {
|
|
||||||
pub(crate) files: Vec<String>,
|
|
||||||
pub(crate) stereo: bool,
|
|
||||||
|
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
|
||||||
pub(crate) start: Option<Time>,
|
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
|
||||||
pub(crate) end: Option<Time>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
|
|
||||||
pub(crate) fast: Vec<(Time, Time)>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
|
|
||||||
pub(crate) questions: Vec<(Time, Time, String)>,
|
|
||||||
|
|
||||||
pub(crate) metadata: Option<ProjectSourceMetadata>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub(crate) struct ProjectSourceMetadata {
|
|
||||||
/// The duration of the source video.
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
pub(crate) source_duration: Time,
|
|
||||||
/// The FPS of the source video.
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
pub(crate) source_fps: Rational,
|
|
||||||
/// The time base of the source video.
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
pub(crate) source_tbn: Rational,
|
|
||||||
/// The resolution of the source video.
|
|
||||||
pub(crate) source_res: Resolution,
|
|
||||||
/// The sample rate of the source audio.
|
|
||||||
pub(crate) source_sample_rate: u32
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
|
||||||
pub(crate) struct ProjectProgress {
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) preprocessed: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) asked_start_end: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) asked_fast: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) asked_questions: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) rendered_assets: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) rendered: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) transcoded: BTreeSet<Resolution>
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ use fontconfig::Fontconfig;
|
||||||
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
|
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use svgwriter::{
|
use svgwriter::{
|
||||||
tags::{Group, Path, TSpan, TagWithPresentationAttributes as _, Text},
|
tags::{Group, Path, Rect, TSpan, TagWithPresentationAttributes as _, Text},
|
||||||
Data, Graphic, Transform
|
Data, Graphic, Transform
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ impl Question {
|
||||||
.with_dominant_baseline("middle")
|
.with_dominant_baseline("middle")
|
||||||
.with_text_anchor("middle")
|
.with_text_anchor("middle")
|
||||||
.with_font_weight(600)
|
.with_font_weight(600)
|
||||||
.append(lang.question.to_owned())
|
.append("Question")
|
||||||
);
|
);
|
||||||
g.push(text);
|
g.push(text);
|
||||||
|
|
||||||
|
@ -138,3 +138,15 @@ impl Question {
|
||||||
svg
|
svg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[test]
|
||||||
|
fn question() {
|
||||||
|
let svg = Question::new(
|
||||||
|
Resolution::FullHD,
|
||||||
|
&Language::default(),
|
||||||
|
"Hallo Welt! Dies ist eine sehr kluge Frage aus dem Publikum. Die Frage ist nämlich: Was ist eigentlich die Frage?".into()
|
||||||
|
)
|
||||||
|
.finish();
|
||||||
|
std::fs::write("question.svg", svg.to_string_pretty()).unwrap();
|
||||||
|
}
|
||||||
|
|
|
@ -7,10 +7,10 @@ use self::{
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
iotro::{intro, outro},
|
iotro::{intro, outro},
|
||||||
project::{Project, ProjectLecture, ProjectSourceMetadata, Resolution},
|
|
||||||
question::Question,
|
question::Question,
|
||||||
render::ffmpeg::{Ffmpeg, FfmpegInput},
|
render::ffmpeg::{Ffmpeg, FfmpegInput},
|
||||||
time::{format_date, format_time, Time}
|
time::{format_date, format_time, Time},
|
||||||
|
Project, ProjectLecture, ProjectSourceMetadata, Resolution
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
|
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
|
||||||
|
|
Loading…
Reference in a new issue