Compare commits
5 commits
b6bd1be12e
...
52c89dc95a
Author | SHA1 | Date | |
---|---|---|---|
52c89dc95a | |||
4aefb5a647 | |||
14daa1c9f9 | |||
78609dec9a | |||
1dfe835587 |
6 changed files with 277 additions and 207 deletions
14
src/iotro.rs
14
src/iotro.rs
|
@ -24,7 +24,9 @@ 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 {
|
||||||
|
@ -58,7 +60,9 @@ 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 {
|
||||||
|
@ -87,7 +91,7 @@ pub const BRITISH: Language<'static> = Language {
|
||||||
3 | 23 => "rd",
|
3 | 23 => "rd",
|
||||||
_ => "th"
|
_ => "th"
|
||||||
};
|
};
|
||||||
format!("{:02}{th} {month} {:04}", d.day, d.year)
|
format!("{}{th} {month} {:04}", d.day, d.year)
|
||||||
},
|
},
|
||||||
|
|
||||||
lecture_from: "Lecture from",
|
lecture_from: "Lecture from",
|
||||||
|
@ -98,7 +102,9 @@ 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,30 +3,27 @@
|
||||||
#![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::{
|
||||||
iotro::Language,
|
project::{Project, ProjectLecture, ProjectSource, Resolution},
|
||||||
question::Question,
|
render::Renderer,
|
||||||
render::{ffmpeg::FfmpegOutputFormat, Renderer},
|
time::{parse_date, parse_time, Time}
|
||||||
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")]
|
||||||
|
@ -38,34 +35,24 @@ struct Args {
|
||||||
#[clap(short = 'C', long, default_value = ".")]
|
#[clap(short = 'C', long, default_value = ".")]
|
||||||
directory: PathBuf,
|
directory: PathBuf,
|
||||||
|
|
||||||
/// The slug of the course, e.g. "23ws-malo2".
|
/// The preset of the lecture. Can be a toml file or a known course slug.
|
||||||
#[clap(short = 'c', long, default_value = "23ws-malo2")]
|
#[clap(short, long)]
|
||||||
course: String,
|
preset: 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.
|
/// Transcode the final video clip down to the minimum resolution specified. If not
|
||||||
|
/// 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.
|
||||||
#[clap(short = 'T', long, default_value = "1440p")]
|
/// If not specified, the default value from the preset is used.
|
||||||
transcode_start: Resolution,
|
#[clap(short = 'T', long)]
|
||||||
|
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.
|
||||||
|
@ -73,159 +60,6 @@ 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();
|
||||||
|
@ -282,7 +116,8 @@ fn main() {
|
||||||
|
|
||||||
// process arguments
|
// process arguments
|
||||||
let directory = args.directory.canonicalize_utf8().unwrap();
|
let directory = args.directory.canonicalize_utf8().unwrap();
|
||||||
let course = args.course;
|
let preset = Preset::find(&args.preset).unwrap();
|
||||||
|
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");
|
||||||
|
@ -327,10 +162,10 @@ fn main() {
|
||||||
let project = Project {
|
let project = Project {
|
||||||
lecture: ProjectLecture {
|
lecture: ProjectLecture {
|
||||||
course,
|
course,
|
||||||
label: args.label,
|
label: preset.label,
|
||||||
docent: args.docent,
|
docent: preset.docent,
|
||||||
date,
|
date,
|
||||||
lang: args.lang
|
lang: preset.lang
|
||||||
},
|
},
|
||||||
source: ProjectSource {
|
source: ProjectSource {
|
||||||
files,
|
files,
|
||||||
|
@ -439,10 +274,10 @@ fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// rescale the video
|
// rescale the video
|
||||||
if let Some(lowest_res) = args.transcode {
|
if let Some(lowest_res) = args.transcode.or(preset.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
|
|| res > args.transcode_start.unwrap_or(preset.transcode_start)
|
||||||
|| res < lowest_res
|
|| res < lowest_res
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
77
src/preset.rs
Normal file
77
src/preset.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
//! 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
Normal file
164
src/project.rs
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
//! 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, Rect, TSpan, TagWithPresentationAttributes as _, Text},
|
tags::{Group, Path, 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("Question")
|
.append(lang.question.to_owned())
|
||||||
);
|
);
|
||||||
g.push(text);
|
g.push(text);
|
||||||
|
|
||||||
|
@ -138,15 +138,3 @@ 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