Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
5808bff395 | |||
330515d6b4 | |||
7662150b89 | |||
b6fb0fa184 | |||
6e56452f78 | |||
680ea8f4e5 | |||
13c03559d0 | |||
f9129b2351 | |||
9a58e39bf8 | |||
2fdb653496 | |||
cbdf55335a |
10 changed files with 331 additions and 251 deletions
|
@ -2,6 +2,7 @@ name: Trigger quay.io Webhook
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run:
|
run:
|
||||||
|
|
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
|
//! A module for writing intros and outros
|
||||||
|
|
||||||
use crate::{time::Date, ProjectLecture, Resolution};
|
use crate::{
|
||||||
|
project::{ProjectLecture, Resolution},
|
||||||
|
time::Date
|
||||||
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Debug, Display, Formatter},
|
fmt::{self, Debug, Display, Formatter},
|
||||||
|
|
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());
|
84
src/main.rs
84
src/main.rs
|
@ -2,32 +2,17 @@
|
||||||
#![warn(clippy::unreadable_literal, rust_2018_idioms)]
|
#![warn(clippy::unreadable_literal, rust_2018_idioms)]
|
||||||
#![forbid(elided_lifetimes_in_paths, unsafe_code)]
|
#![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 camino::Utf8PathBuf as PathBuf;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use console::style;
|
use console::style;
|
||||||
#[cfg(feature = "mem_limit")]
|
use render_video::{
|
||||||
use std::sync::RwLock;
|
cli::{ask, ask_time},
|
||||||
use std::{
|
preset::Preset,
|
||||||
fmt::Display,
|
project::{Project, ProjectLecture, ProjectSource, Resolution},
|
||||||
fs,
|
render::Renderer,
|
||||||
io::{self, BufRead as _, Write}
|
time::parse_date
|
||||||
};
|
};
|
||||||
|
use std::fs;
|
||||||
#[cfg(feature = "mem_limit")]
|
|
||||||
static MEM_LIMIT: RwLock<String> = RwLock::new(String::new());
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
@ -60,58 +45,12 @@ struct Args {
|
||||||
stereo: bool
|
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() {
|
fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
#[cfg(feature = "mem_limit")]
|
#[cfg(feature = "mem_limit")]
|
||||||
{
|
{
|
||||||
*(MEM_LIMIT.write().unwrap()) = args.mem_limit;
|
*(render_video::MEM_LIMIT.write().unwrap()) = args.mem_limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// process arguments
|
// process arguments
|
||||||
|
@ -135,7 +74,8 @@ fn main() {
|
||||||
let lower = name.to_ascii_lowercase();
|
let lower = name.to_ascii_lowercase();
|
||||||
if (lower.ends_with(".mp4")
|
if (lower.ends_with(".mp4")
|
||||||
|| lower.ends_with(".mts")
|
|| lower.ends_with(".mts")
|
||||||
|| lower.ends_with(".mkv"))
|
|| lower.ends_with(".mkv")
|
||||||
|
|| lower.ends_with(".mov"))
|
||||||
&& !entry.file_type().unwrap().is_dir()
|
&& !entry.file_type().unwrap().is_dir()
|
||||||
{
|
{
|
||||||
files.push(String::from(name));
|
files.push(String::from(name));
|
||||||
|
@ -182,7 +122,7 @@ fn main() {
|
||||||
project
|
project
|
||||||
};
|
};
|
||||||
|
|
||||||
let renderer = Renderer::new(&directory, &project).unwrap();
|
let mut renderer = Renderer::new(&directory, &project).unwrap();
|
||||||
let recording = renderer.recording_mkv();
|
let recording = renderer.recording_mkv();
|
||||||
|
|
||||||
// preprocess the video
|
// preprocess the video
|
||||||
|
@ -275,7 +215,7 @@ 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.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
|
if res > project.source.metadata.as_ref().unwrap().source_res
|
||||||
|| res > args.transcode_start.unwrap_or(preset.transcode_start)
|
|| res > args.transcode_start.unwrap_or(preset.transcode_start)
|
||||||
|| res < lowest_res
|
|| res < lowest_res
|
||||||
|
|
|
@ -11,57 +11,59 @@ use std::{fs, io};
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub(crate) struct Preset {
|
pub struct Preset {
|
||||||
// options for the intro slide
|
// options for the intro slide
|
||||||
pub(crate) course: String,
|
pub course: String,
|
||||||
pub(crate) label: String,
|
pub label: String,
|
||||||
pub(crate) docent: String,
|
pub docent: String,
|
||||||
|
|
||||||
/// Course language
|
/// Course language
|
||||||
#[serde(default = "Default::default")]
|
#[serde(default)]
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub(crate) lang: Language<'static>,
|
pub lang: Language<'static>,
|
||||||
|
|
||||||
// coding options
|
// coding options
|
||||||
pub(crate) transcode_start: Resolution,
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub(crate) transcode: Option<Resolution>
|
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 {
|
Preset {
|
||||||
course: "23ws-malo2".into(),
|
course: "23ws-malo2".into(),
|
||||||
label: "Mathematische Logik II".into(),
|
label: "Mathematische Logik II".into(),
|
||||||
docent: "Prof. E. Grädel".into(),
|
docent: "Prof. E. Grädel".into(),
|
||||||
lang: GERMAN,
|
lang: GERMAN,
|
||||||
transcode_start: Resolution::WQHD,
|
transcode_start: "1440p".parse().unwrap(),
|
||||||
transcode: Some(Resolution::nHD)
|
transcode: Some("360p".parse().unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preset_24ss_algomod() -> Preset {
|
pub fn preset_24ss_algomod() -> Preset {
|
||||||
Preset {
|
Preset {
|
||||||
course: "24ss-algomod".into(),
|
course: "24ss-algomod".into(),
|
||||||
label: "Algorithmische Modelltheorie".into(),
|
label: "Algorithmische Modelltheorie".into(),
|
||||||
docent: "Prof. E. Grädel".into(),
|
docent: "Prof. E. Grädel".into(),
|
||||||
lang: GERMAN,
|
lang: GERMAN,
|
||||||
transcode_start: Resolution::WQHD,
|
transcode_start: "1440p".parse().unwrap(),
|
||||||
transcode: Some(Resolution::HD)
|
transcode: Some("720p".parse().unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preset_24ss_qc() -> Preset {
|
pub fn preset_24ss_qc() -> Preset {
|
||||||
Preset {
|
Preset {
|
||||||
course: "24ss-qc".into(),
|
course: "24ss-qc".into(),
|
||||||
label: "Introduction to Quantum Computing".into(),
|
label: "Introduction to Quantum Computing".into(),
|
||||||
docent: "Prof. D. Unruh".into(),
|
docent: "Prof. D. Unruh".into(),
|
||||||
lang: BRITISH,
|
lang: BRITISH,
|
||||||
transcode_start: Resolution::WQHD,
|
transcode_start: "1440p".parse().unwrap(),
|
||||||
transcode: Some(Resolution::HD)
|
transcode: Some("720p".parse().unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preset {
|
impl Preset {
|
||||||
pub(crate) fn find(name: &str) -> anyhow::Result<Self> {
|
pub fn find(name: &str) -> anyhow::Result<Self> {
|
||||||
match fs::read(name) {
|
match fs::read(name) {
|
||||||
Ok(buf) => return Ok(toml::from_slice(&buf)?),
|
Ok(buf) => return Ok(toml::from_slice(&buf)?),
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {},
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {},
|
||||||
|
|
208
src/project.rs
208
src/project.rs
|
@ -8,157 +8,199 @@ use crate::{
|
||||||
use rational::Rational;
|
use rational::Rational;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
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 {
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||||
($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => {
|
pub struct Resolution(u32, u32);
|
||||||
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
impl Resolution {
|
||||||
pub(crate) enum Resolution {
|
pub fn new(width: u32, height: u32) -> Self {
|
||||||
$(
|
Self(width, height)
|
||||||
#[doc = concat!(stringify!($width), "x", stringify!($height))]
|
|
||||||
$res
|
|
||||||
),+
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NUM_RESOLUTIONS: usize = {
|
pub fn width(self) -> u32 {
|
||||||
let mut num = 0;
|
self.0
|
||||||
$(num += 1; stringify!($res);)+
|
|
||||||
num
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Resolution {
|
|
||||||
pub(crate) fn values() -> [Self; NUM_RESOLUTIONS] {
|
|
||||||
[$(Self::$res),+]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn width(self) -> usize {
|
pub fn height(self) -> u32 {
|
||||||
match self {
|
self.1
|
||||||
$(Self::$res => $width),+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn height(self) -> usize {
|
|
||||||
match self {
|
|
||||||
$(Self::$res => $height),+
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn bitrate(self) -> u64 {
|
pub(crate) fn bitrate(self) -> u64 {
|
||||||
match self {
|
// 640 * 360: 500k
|
||||||
$(Self::$res => $bitrate),+
|
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 {
|
pub(crate) fn default_codec(self) -> FfmpegOutputFormat {
|
||||||
match self {
|
if self.width() > 1920 {
|
||||||
$(Self::$res => FfmpegOutputFormat::$format),+
|
FfmpegOutputFormat::Av1Opus
|
||||||
}
|
} else {
|
||||||
|
FfmpegOutputFormat::AvcAac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Resolution {
|
pub const STANDARD_RESOLUTIONS: [Self; 5] = [
|
||||||
|
Self(640, 360),
|
||||||
|
Self(1280, 720),
|
||||||
|
Self(1920, 1080),
|
||||||
|
Self(2560, 1440),
|
||||||
|
Self(3840, 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;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> anyhow::Result<Self> {
|
fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||||
Ok(match s {
|
Ok(match s.to_lowercase().as_str() {
|
||||||
$(concat!(stringify!($height), "p") => Self::$res,)+
|
"360p" | "nhd" => Self(640, 360),
|
||||||
|
"540p" | "qhd" => Self(960, 540),
|
||||||
|
"720p" | "hd" => Self(1280, 720),
|
||||||
|
"900p" | "hd+" => Self(1600, 900),
|
||||||
|
"1080p" | "fhd" | "fullhd" => Self(1920, 1080),
|
||||||
|
"1440p" | "wqhd" => Self(2560, 1440),
|
||||||
|
"2160p" | "4k" | "uhd" => Self(3840, 2160),
|
||||||
_ => anyhow::bail!("Unknown Resolution: {s:?}")
|
_ => anyhow::bail!("Unknown Resolution: {s:?}")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Ord for Resolution {
|
||||||
|
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||||
|
(self.0 * self.1).cmp(&(other.0 * other.1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolutions! {
|
impl Eq for Resolution {}
|
||||||
nHD: 640 x 360 at 500_000 in AvcAac,
|
|
||||||
HD: 1280 x 720 at 1_000_000 in AvcAac,
|
impl PartialOrd for Resolution {
|
||||||
FullHD: 1920 x 1080 at 750_000 in Av1Opus,
|
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||||
WQHD: 2560 x 1440 at 1_000_000 in Av1Opus,
|
Some(self.cmp(other))
|
||||||
// TODO qsx muss mal sagen wieviel bitrate für 4k
|
}
|
||||||
UHD: 3840 x 2160 at 2_000_000 in Av1Opus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
impl PartialEq for Resolution {
|
||||||
pub(crate) struct Project {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
pub(crate) lecture: ProjectLecture,
|
self.cmp(other) == cmp::Ordering::Equal
|
||||||
pub(crate) source: ProjectSource,
|
}
|
||||||
pub(crate) progress: ProjectProgress
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Project {
|
||||||
|
pub lecture: ProjectLecture,
|
||||||
|
pub source: ProjectSource,
|
||||||
|
pub progress: ProjectProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub(crate) struct ProjectLecture {
|
pub struct ProjectLecture {
|
||||||
pub(crate) course: String,
|
pub course: String,
|
||||||
pub(crate) label: String,
|
pub label: String,
|
||||||
pub(crate) docent: String,
|
pub docent: String,
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub(crate) date: Date,
|
pub date: Date,
|
||||||
#[serde(default = "Default::default")]
|
#[serde(default = "Default::default")]
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub(crate) lang: Language<'static>
|
pub lang: Language<'static>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub(crate) struct ProjectSource {
|
pub struct ProjectSource {
|
||||||
pub(crate) files: Vec<String>,
|
pub files: Vec<String>,
|
||||||
pub(crate) stereo: bool,
|
pub stereo: bool,
|
||||||
|
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub(crate) start: Option<Time>,
|
pub start: Option<Time>,
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub(crate) end: Option<Time>,
|
pub end: Option<Time>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
|
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
|
||||||
pub(crate) fast: Vec<(Time, Time)>,
|
pub fast: Vec<(Time, Time)>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
|
#[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]
|
#[serde_as]
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub(crate) struct ProjectSourceMetadata {
|
pub struct ProjectSourceMetadata {
|
||||||
/// The duration of the source video.
|
/// The duration of the source video.
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub(crate) source_duration: Time,
|
pub source_duration: Time,
|
||||||
/// The FPS of the source video.
|
/// The FPS of the source video.
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub(crate) source_fps: Rational,
|
pub source_fps: Rational,
|
||||||
/// The time base of the source video.
|
/// The time base of the source video.
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub(crate) source_tbn: Rational,
|
pub source_tbn: Rational,
|
||||||
/// The resolution of the source video.
|
/// The resolution of the source video.
|
||||||
pub(crate) source_res: Resolution,
|
pub source_res: Resolution,
|
||||||
/// The sample rate of the source audio.
|
/// The sample rate of the source audio.
|
||||||
pub(crate) source_sample_rate: u32
|
pub source_sample_rate: u32
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
#[serde_as]
|
||||||
pub(crate) struct ProjectProgress {
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
|
pub struct ProjectProgress {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) preprocessed: bool,
|
pub preprocessed: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) asked_start_end: bool,
|
pub asked_start_end: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) asked_fast: bool,
|
pub asked_fast: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) asked_questions: bool,
|
pub asked_questions: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) rendered_assets: bool,
|
pub rendered_assets: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) rendered: bool,
|
pub rendered: bool,
|
||||||
|
|
||||||
|
#[serde_as(as = "BTreeSet<DisplayFromStr>")]
|
||||||
#[serde(default)]
|
#[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 fontconfig::Fontconfig;
|
||||||
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
|
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use super::{cmd, filter::Filter};
|
use super::{cmd, filter::Filter};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
project::Resolution,
|
||||||
render::filter::channel,
|
render::filter::channel,
|
||||||
time::{format_time, Time},
|
time::{format_time, Time}
|
||||||
Resolution
|
|
||||||
};
|
};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use camino::Utf8PathBuf as PathBuf;
|
use camino::Utf8PathBuf as PathBuf;
|
||||||
|
@ -41,8 +41,8 @@ impl FfmpegInput {
|
||||||
cmd.arg("-r").arg(fps.to_string());
|
cmd.arg("-r").arg(fps.to_string());
|
||||||
}
|
}
|
||||||
if let Some(start) = self.start {
|
if let Some(start) = self.start {
|
||||||
if self.path.ends_with(".mp4") {
|
if self.path.ends_with(".mp4") || self.path.ends_with(".mov") {
|
||||||
cmd.arg("-seek_streams_individualy").arg("false");
|
cmd.arg("-seek_streams_individually").arg("false");
|
||||||
}
|
}
|
||||||
cmd.arg("-ss").arg(format_time(start));
|
cmd.arg("-ss").arg(format_time(start));
|
||||||
}
|
}
|
||||||
|
@ -59,12 +59,30 @@ pub(crate) enum FfmpegOutputFormat {
|
||||||
Av1Flac,
|
Av1Flac,
|
||||||
/// AV1 / OPUS
|
/// AV1 / OPUS
|
||||||
Av1Opus,
|
Av1Opus,
|
||||||
|
/// AVC (H.264) / FLAC
|
||||||
|
AvcFlac,
|
||||||
/// AVC (H.264) / AAC
|
/// AVC (H.264) / AAC
|
||||||
AvcAac
|
AvcAac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FfmpegOutputFormat {
|
||||||
|
pub(crate) fn with_flac_audio(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Av1Flac | Self::AvcFlac => self,
|
||||||
|
Self::Av1Opus => Self::Av1Flac,
|
||||||
|
Self::AvcAac => Self::AvcFlac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum FfmpegOutputQuality {
|
||||||
|
Default,
|
||||||
|
VisuallyLossless
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct FfmpegOutput {
|
pub(crate) struct FfmpegOutput {
|
||||||
pub(crate) format: FfmpegOutputFormat,
|
pub(crate) format: FfmpegOutputFormat,
|
||||||
|
pub(crate) quality: FfmpegOutputQuality,
|
||||||
pub(crate) audio_bitrate: Option<u64>,
|
pub(crate) audio_bitrate: Option<u64>,
|
||||||
pub(crate) video_bitrate: Option<u64>,
|
pub(crate) video_bitrate: Option<u64>,
|
||||||
|
|
||||||
|
@ -89,6 +107,7 @@ impl FfmpegOutput {
|
||||||
pub(crate) fn new(format: FfmpegOutputFormat, path: PathBuf) -> Self {
|
pub(crate) fn new(format: FfmpegOutputFormat, path: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
format,
|
format,
|
||||||
|
quality: FfmpegOutputQuality::Default,
|
||||||
audio_bitrate: None,
|
audio_bitrate: None,
|
||||||
video_bitrate: None,
|
video_bitrate: None,
|
||||||
|
|
||||||
|
@ -118,41 +137,48 @@ impl FfmpegOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_to_cmd(self, cmd: &mut Command, venc: bool, _aenc: bool, vaapi: bool) {
|
fn append_to_cmd(self, cmd: &mut Command, venc: bool, _aenc: bool, vaapi: bool) {
|
||||||
// select codec and bitrate
|
// select codec and bitrate/crf
|
||||||
const QUALITY: &str = "18";
|
|
||||||
if venc {
|
if venc {
|
||||||
let vcodec = match (self.format, vaapi) {
|
let vcodec = match (self.format, vaapi) {
|
||||||
(FfmpegOutputFormat::Av1Flac, false)
|
(FfmpegOutputFormat::Av1Flac, false)
|
||||||
| (FfmpegOutputFormat::Av1Opus, false) => "libsvtav1",
|
| (FfmpegOutputFormat::Av1Opus, false) => "libsvtav1",
|
||||||
(FfmpegOutputFormat::Av1Flac, true)
|
(FfmpegOutputFormat::Av1Flac, true)
|
||||||
| (FfmpegOutputFormat::Av1Opus, true) => "av1_vaapi",
|
| (FfmpegOutputFormat::Av1Opus, true) => "av1_vaapi",
|
||||||
(FfmpegOutputFormat::AvcAac, false) => "h264",
|
(FfmpegOutputFormat::AvcAac, false)
|
||||||
(FfmpegOutputFormat::AvcAac, true) => "h264_vaapi"
|
| (FfmpegOutputFormat::AvcFlac, false) => "h264",
|
||||||
|
(FfmpegOutputFormat::AvcAac, true)
|
||||||
|
| (FfmpegOutputFormat::AvcFlac, true) => "h264_vaapi"
|
||||||
};
|
};
|
||||||
cmd.arg("-c:v").arg(vcodec);
|
cmd.arg("-c:v").arg(vcodec);
|
||||||
|
|
||||||
if vcodec == "libsvtav1" {
|
if vcodec == "libsvtav1" {
|
||||||
cmd.arg("-svtav1-params").arg("fast-decode=1");
|
cmd.arg("-svtav1-params").arg("fast-decode=1");
|
||||||
cmd.arg("-preset").arg("8");
|
cmd.arg("-preset").arg("7");
|
||||||
|
cmd.arg("-crf").arg(match self.quality {
|
||||||
|
FfmpegOutputQuality::Default => "28",
|
||||||
|
FfmpegOutputQuality::VisuallyLossless => "18"
|
||||||
|
});
|
||||||
|
} else if vcodec == "h264" {
|
||||||
|
match self.quality {
|
||||||
|
FfmpegOutputQuality::Default => {
|
||||||
|
cmd.arg("-preset").arg("slow");
|
||||||
|
cmd.arg("-crf").arg("21");
|
||||||
|
},
|
||||||
|
FfmpegOutputQuality::VisuallyLossless => {
|
||||||
|
// the quality is not impacted by speed, only the bitrate, and
|
||||||
|
// for this setting we don't really care about bitrate
|
||||||
|
cmd.arg("-preset").arg("veryfast");
|
||||||
|
cmd.arg("-crf").arg("17");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
match self.video_bitrate {
|
} else if let Some(bv) = self.video_bitrate {
|
||||||
Some(bv) if vcodec != "libsvtav1" => {
|
|
||||||
cmd.arg("-b:v").arg(bv.to_string());
|
cmd.arg("-b:v").arg(bv.to_string());
|
||||||
},
|
|
||||||
None if vaapi => {
|
|
||||||
cmd.arg("-rc_mode").arg("CQP");
|
|
||||||
cmd.arg("-global_quality").arg(QUALITY);
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
cmd.arg("-crf").arg(QUALITY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cmd.arg("-c:v").arg("copy");
|
cmd.arg("-c:v").arg("copy");
|
||||||
}
|
}
|
||||||
cmd.arg("-c:a").arg(match self.format {
|
cmd.arg("-c:a").arg(match self.format {
|
||||||
FfmpegOutputFormat::Av1Flac => "flac",
|
FfmpegOutputFormat::Av1Flac | FfmpegOutputFormat::AvcFlac => "flac",
|
||||||
FfmpegOutputFormat::Av1Opus => "libopus",
|
FfmpegOutputFormat::Av1Opus => "libopus",
|
||||||
FfmpegOutputFormat::AvcAac => "aac"
|
FfmpegOutputFormat::AvcAac => "aac"
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,7 @@ use crate::{
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
|
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
|
||||||
use console::style;
|
use console::style;
|
||||||
|
use ffmpeg::FfmpegOutputQuality;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
|
@ -41,8 +42,8 @@ const QUESTION_FADE_LEN: Time = Time {
|
||||||
};
|
};
|
||||||
const FF_MULTIPLIER: usize = 8;
|
const FF_MULTIPLIER: usize = 8;
|
||||||
// logo sizes at full hd, will be scaled to source resolution
|
// logo sizes at full hd, will be scaled to source resolution
|
||||||
const FF_LOGO_SIZE: usize = 128;
|
const FF_LOGO_SIZE: u32 = 128;
|
||||||
const LOGO_SIZE: usize = 96;
|
const LOGO_SIZE: u32 = 96;
|
||||||
|
|
||||||
fn cmd() -> Command {
|
fn cmd() -> Command {
|
||||||
#[cfg(feature = "mem_limit")]
|
#[cfg(feature = "mem_limit")]
|
||||||
|
@ -119,7 +120,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.
|
/// The directory with all the sources.
|
||||||
directory: &'a Path,
|
directory: &'a Path,
|
||||||
|
|
||||||
|
@ -139,6 +140,7 @@ fn svg2mkv(
|
||||||
duration: Time
|
duration: Time
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
|
let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
|
||||||
|
quality: FfmpegOutputQuality::VisuallyLossless,
|
||||||
duration: Some(duration),
|
duration: Some(duration),
|
||||||
time_base: Some(meta.source_tbn),
|
time_base: Some(meta.source_tbn),
|
||||||
fps_mode_vfr: true,
|
fps_mode_vfr: true,
|
||||||
|
@ -157,7 +159,7 @@ fn svg2mkv(
|
||||||
ffmpeg.run()
|
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();
|
let mut cmd = cmd();
|
||||||
cmd.arg("inkscape")
|
cmd.arg("inkscape")
|
||||||
.arg("-w")
|
.arg("-w")
|
||||||
|
@ -175,7 +177,7 @@ fn svg2png(svg: &Path, png: &Path, width: usize, height: usize) -> anyhow::Resul
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Renderer<'a> {
|
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!(
|
let slug = format!(
|
||||||
"{}-{}",
|
"{}-{}",
|
||||||
project.lecture.course,
|
project.lecture.course,
|
||||||
|
@ -184,23 +186,21 @@ impl<'a> Renderer<'a> {
|
||||||
let target = directory.join(&slug);
|
let target = directory.join(&slug);
|
||||||
fs::create_dir_all(&target)?;
|
fs::create_dir_all(&target)?;
|
||||||
|
|
||||||
let first: PathBuf = directory.join(
|
// Ensure we have at least one input file.
|
||||||
project
|
project
|
||||||
.source
|
.source
|
||||||
.files
|
.files
|
||||||
.first()
|
.first()
|
||||||
.context("No source files present")?
|
.context("No source files present")?;
|
||||||
);
|
|
||||||
let height: u32 = ffprobe_video("stream=height", &first)?
|
// In case we don't have a resolution yet, we'll asign this after preprocessing.
|
||||||
.split('\n')
|
let format = project
|
||||||
.next()
|
.source
|
||||||
.unwrap()
|
.metadata
|
||||||
.parse()?;
|
.as_ref()
|
||||||
let format = if height <= 1080 {
|
.map(|meta| meta.source_res.default_codec())
|
||||||
FfmpegOutputFormat::AvcAac
|
.unwrap_or(FfmpegOutputFormat::Av1Flac)
|
||||||
} else {
|
.with_flac_audio();
|
||||||
FfmpegOutputFormat::Av1Flac
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
directory,
|
directory,
|
||||||
|
@ -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")
|
self.target.join("recording.mkv")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ impl<'a> Renderer<'a> {
|
||||||
self.target.join(format!("question{q_idx}.png"))
|
self.target.join(format!("question{q_idx}.png"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn preprocess(&self, project: &mut Project) -> anyhow::Result<()> {
|
pub fn preprocess(&mut self, project: &mut Project) -> anyhow::Result<()> {
|
||||||
assert!(!project.progress.preprocessed);
|
assert!(!project.progress.preprocessed);
|
||||||
|
|
||||||
let recording_txt = self.target.join("recording.txt");
|
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 width = ffprobe_video("stream=width", &recording_mkv)?.parse()?;
|
||||||
let height = ffprobe_video("stream=height", &recording_mkv)?.parse()?;
|
let height = ffprobe_video("stream=height", &recording_mkv)?.parse()?;
|
||||||
let source_res = match (width, height) {
|
let source_res = Resolution::new(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}")
|
|
||||||
};
|
|
||||||
project.source.metadata = Some(ProjectSourceMetadata {
|
project.source.metadata = Some(ProjectSourceMetadata {
|
||||||
source_duration: ffprobe_video("format=duration", &recording_mkv)?.parse()?,
|
source_duration: ffprobe_video("format=duration", &recording_mkv)?.parse()?,
|
||||||
source_fps: ffprobe_video("stream=r_frame_rate", &recording_mkv)?.parse()?,
|
source_fps: ffprobe_video("stream=r_frame_rate", &recording_mkv)?.parse()?,
|
||||||
|
@ -278,12 +271,13 @@ impl<'a> Renderer<'a> {
|
||||||
source_res,
|
source_res,
|
||||||
source_sample_rate
|
source_sample_rate
|
||||||
});
|
});
|
||||||
|
self.format = source_res.default_codec().with_flac_audio();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepare assets like intro, outro and questions.
|
/// 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();
|
let metadata = project.source.metadata.as_ref().unwrap();
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
@ -365,8 +359,8 @@ impl<'a> Renderer<'a> {
|
||||||
|
|
||||||
/// Get the video file for a specific resolution, completely finished.
|
/// Get the video file for a specific resolution, completely finished.
|
||||||
fn video_file_res(&self, res: Resolution) -> PathBuf {
|
fn video_file_res(&self, res: Resolution) -> PathBuf {
|
||||||
let extension = match res.format() {
|
let extension = match res.default_codec() {
|
||||||
FfmpegOutputFormat::Av1Flac => "mkv",
|
FfmpegOutputFormat::Av1Flac | FfmpegOutputFormat::AvcFlac => "mkv",
|
||||||
FfmpegOutputFormat::Av1Opus => "webm",
|
FfmpegOutputFormat::Av1Opus => "webm",
|
||||||
FfmpegOutputFormat::AvcAac => "mp4"
|
FfmpegOutputFormat::AvcAac => "mp4"
|
||||||
};
|
};
|
||||||
|
@ -375,15 +369,16 @@ impl<'a> Renderer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the video file directly outputed to further transcode.
|
/// 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))
|
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 source_res = project.source.metadata.as_ref().unwrap().source_res;
|
||||||
|
|
||||||
let output = self.video_file_output();
|
let output = self.video_file_output();
|
||||||
let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
|
let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
|
||||||
|
quality: FfmpegOutputQuality::VisuallyLossless,
|
||||||
video_bitrate: Some(source_res.bitrate() * 3),
|
video_bitrate: Some(source_res.bitrate() * 3),
|
||||||
..FfmpegOutput::new(self.format, output.clone())
|
..FfmpegOutput::new(self.format, output.clone())
|
||||||
});
|
});
|
||||||
|
@ -675,7 +670,7 @@ impl<'a> Renderer<'a> {
|
||||||
comment: Some(lecture.lang.video_created_by_us.into()),
|
comment: Some(lecture.lang.video_created_by_us.into()),
|
||||||
language: Some(lecture.lang.lang.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.add_input(FfmpegInput::new(input));
|
||||||
ffmpeg.rescale_video(res);
|
ffmpeg.rescale_video(res);
|
||||||
|
|
Loading…
Add table
Reference in a new issue