diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..9320c6f0ee10cf5efb80f282916bbc21e406c97c --- /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 a4eeccbc83ac437b2fdd2c6ecd814b1b5dce5af8..cdbd1b28ad707fbedd313f3ceaa12a01be343162 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 0000000000000000000000000000000000000000..998054a2cbf3ced77f748995e2a249d82cc0acc6 --- /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<String> = RwLock::new(String::new()); diff --git a/src/main.rs b/src/main.rs index 67d9ce08c3c9ceb251d7314e88d2907f787940e3..53df6eaec6b923c258ce5fbdd3b74fa5b8881bee 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<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 diff --git a/src/preset.rs b/src/preset.rs index 85f37c604e32091f1748552fdab65c88b44784d3..507401a219da7c2e59dffba1dce3abb5cf7c266c 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<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 => {}, diff --git a/src/project.rs b/src/project.rs index 7dbda1fc23f182b73dda6455b63fe28065c38448..a16da81faaff76e77571212637c81e731db0663d 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}; - -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 - ),+ - } +use std::{ + cmp, + collections::BTreeSet, + fmt::{self, Display, Formatter}, + str::FromStr +}; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct Resolution { + pub width: u32, + pub height: u32 +} - 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 Resolution { + pub(crate) fn bitrate(self) -> u64 { + // 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 + } + } - impl FromStr for Resolution { - type Err = anyhow::Error; + pub(crate) fn default_codec(self) -> FfmpegOutputFormat { + if self.width <= 1920 { + FfmpegOutputFormat::Av1Opus + } else { + FfmpegOutputFormat::AvcAac + } + } - fn from_str(s: &str) -> anyhow::Result<Self> { - Ok(match s { - $(concat!(stringify!($height), "p") => Self::$res,)+ - _ => anyhow::bail!("Unknown Resolution: {s:?}") - }) - } + 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<Self> { + 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<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> } diff --git a/src/question.rs b/src/question.rs index 53908f9360e050debdb8ecf9ebdad78d5180afaf..0f61a1c96eb55134d5db277863f9e99bf85c39f0 100644 --- a/src/question.rs +++ b/src/question.rs @@ -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 diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 9cc0ab7fa751a8d93c3a8e01f757ae38f4eb51bf..e619168ecc1881c0b70d4be654f6bb28da437399 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -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) }); } } diff --git a/src/render/mod.rs b/src/render/mod.rs index 2fa4c15c1f8ed55beee9839da4660f7e3de077e5..1e361cc04db534165e8512f008e33f2a116731b6 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -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);