render_video/src/main.rs

387 lines
10 KiB
Rust
Raw Normal View History

2023-10-30 15:05:21 +00:00
#![allow(clippy::manual_range_contains)]
2023-11-16 11:12:17 +00:00
#![warn(clippy::unreadable_literal, rust_2018_idioms)]
2023-10-22 20:44:59 +00:00
#![forbid(elided_lifetimes_in_paths, unsafe_code)]
2023-10-28 21:38:17 +00:00
mod iotro;
mod render;
2023-10-22 20:44:59 +00:00
mod time;
2023-10-30 19:28:17 +00:00
use crate::{
2023-12-19 22:56:04 +00:00
render::{ffmpeg::FfmpegOutputFormat, Renderer},
2023-10-30 19:28:17 +00:00
time::{parse_date, parse_time, Date, Time}
};
2023-10-22 20:44:59 +00:00
use camino::Utf8PathBuf as PathBuf;
use clap::Parser;
2024-05-17 19:29:22 +00:00
use iotro::Language;
2023-10-28 21:38:17 +00:00
use rational::Rational;
2023-10-22 20:44:59 +00:00
use serde::{Deserialize, Serialize};
2023-10-30 16:32:21 +00:00
use serde_with::{serde_as, DisplayFromStr};
#[cfg(feature = "mem_limit")]
use std::sync::RwLock;
2023-10-22 20:44:59 +00:00
use std::{
2023-10-28 21:38:17 +00:00
collections::BTreeSet,
2023-10-22 20:44:59 +00:00
fmt::Display,
fs,
2023-11-02 20:33:21 +00:00
io::{self, BufRead as _, Write},
str::FromStr
2023-10-22 20:44:59 +00:00
};
#[cfg(feature = "mem_limit")]
2023-11-02 20:33:21 +00:00
static MEM_LIMIT: RwLock<String> = RwLock::new(String::new());
2023-10-22 20:44:59 +00:00
#[derive(Debug, Parser)]
struct Args {
2023-11-14 09:36:12 +00:00
/// The root directory of the project. It should contain the raw video file(s).
2023-10-22 20:44:59 +00:00
#[clap(short = 'C', long, default_value = ".")]
directory: PathBuf,
2023-11-14 09:36:12 +00:00
/// The slug of the course, e.g. "23ws-malo2".
#[clap(short = 'c', long, default_value = "23ws-malo2")]
2023-11-02 20:33:21 +00:00
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,
2024-05-17 19:29:22 +00:00
/// 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")]
2023-11-14 09:36:12 +00:00
/// The memory limit for external tools like ffmpeg.
#[clap(short, long, default_value = "12G")]
2023-11-14 09:36:12 +00:00
mem_limit: String,
/// Transcode the final video clip down to the minimum resolution specified.
#[clap(short, long)]
transcode: Option<Resolution>,
/// Transcode starts at this resolution, or the source resolution, whichever is lower.
#[clap(short = 'T', long, default_value = "1440p")]
transcode_start: Resolution,
/// 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.
#[clap(short, long, default_value = "false")]
stereo: bool
2023-10-22 20:44:59 +00:00
}
2023-11-03 09:02:30 +00:00
macro_rules! resolutions {
2023-12-19 22:56:04 +00:00
($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => {
2023-11-03 09:02:30 +00:00
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
2023-11-14 09:36:12 +00:00
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2023-11-03 09:02:30 +00:00
enum Resolution {
$(
#[doc = concat!(stringify!($width), "x", stringify!($height))]
$res
),+
2023-10-28 21:38:17 +00:00
}
2023-11-03 09:02:30 +00:00
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),+
}
}
2023-11-16 10:49:38 +00:00
fn bitrate(self) -> u64 {
2023-11-03 09:02:30 +00:00
match self {
$(Self::$res => $bitrate),+
}
}
2023-12-19 22:56:04 +00:00
fn format(self) -> FfmpegOutputFormat {
match self {
$(Self::$res => FfmpegOutputFormat::$format),+
}
}
2023-10-28 21:38:17 +00:00
}
2023-11-14 09:36:12 +00:00
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:?}")
})
}
}
2023-10-28 21:38:17 +00:00
}
}
2023-11-03 09:02:30 +00:00
resolutions! {
2023-12-19 22:56:04 +00:00
nHD: 640 x 360 at 500_000 in AvcAac,
HD: 1280 x 720 at 1_000_000 in AvcAac,
2024-05-24 10:01:45 +00:00
FullHD: 1920 x 1080 at 750_000 in Av1Opus,
WQHD: 2560 x 1440 at 1_000_000 in Av1Opus,
2023-11-03 09:02:30 +00:00
// TODO qsx muss mal sagen wieviel bitrate für 4k
2024-05-24 10:01:45 +00:00
UHD: 3840 x 2160 at 2_000_000 in Av1Opus
2023-11-03 09:02:30 +00:00
}
2023-10-22 20:44:59 +00:00
#[derive(Deserialize, Serialize)]
struct Project {
lecture: ProjectLecture,
2023-10-28 21:38:17 +00:00
source: ProjectSource,
progress: ProjectProgress
2023-10-22 20:44:59 +00:00
}
#[serde_as]
#[derive(Deserialize, Serialize)]
struct ProjectLecture {
course: String,
label: String,
docent: String,
2023-10-22 20:44:59 +00:00
#[serde_as(as = "DisplayFromStr")]
2024-05-17 19:29:22 +00:00
date: Date,
2024-05-18 13:21:14 +00:00
#[serde(default = "Default::default")]
2024-05-17 19:29:22 +00:00
#[serde_as(as = "DisplayFromStr")]
lang: Language<'static>
2023-10-22 20:44:59 +00:00
}
#[serde_as]
#[derive(Deserialize, Serialize)]
struct ProjectSource {
files: Vec<String>,
stereo: bool,
2023-10-28 21:38:17 +00:00
2023-10-30 19:28:17 +00:00
#[serde_as(as = "Option<DisplayFromStr>")]
start: Option<Time>,
#[serde_as(as = "Option<DisplayFromStr>")]
end: Option<Time>,
#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
fast: Vec<(Time, Time)>,
2023-10-28 21:38:17 +00:00
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.
2023-10-30 16:32:21 +00:00
#[serde_as(as = "DisplayFromStr")]
2023-10-28 21:38:17 +00:00
source_fps: Rational,
/// The time base of the source video.
2023-10-30 19:28:17 +00:00
#[serde_as(as = "DisplayFromStr")]
source_tbn: Rational,
2023-10-28 21:38:17 +00:00
/// 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 {
preprocessed: bool,
2023-10-30 19:28:17 +00:00
asked_start_end: bool,
asked_fast: bool,
2023-10-28 21:38:17 +00:00
rendered: bool,
transcoded: BTreeSet<Resolution>
2023-10-22 20:44:59 +00:00
}
fn ask(question: impl Display) -> String {
let mut stdout = io::stdout().lock();
let mut stdin = io::stdin().lock();
writeln!(stdout, "{question}").unwrap();
let mut line = String::new();
write!(stdout, "> ").unwrap();
stdout.flush().unwrap();
stdin.read_line(&mut line).unwrap();
line.trim().to_owned()
}
2023-10-22 20:44:59 +00:00
fn ask_time(question: impl Display) -> Time {
let mut stdout = io::stdout().lock();
let mut stdin = io::stdin().lock();
writeln!(stdout, "{question}").unwrap();
let mut line = String::new();
loop {
line.clear();
write!(stdout, "> ").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, "Invalid Input {line:?}: {err}").unwrap()
}
}
}
fn main() {
let args = Args::parse();
#[cfg(feature = "mem_limit")]
{
*(MEM_LIMIT.write().unwrap()) = args.mem_limit;
}
2023-10-22 20:44:59 +00:00
// process arguments
let directory = args.directory.canonicalize_utf8().unwrap();
let course = args.course;
// let's see if we need to initialise the project
let project_path = directory.join("project.toml");
2023-10-30 19:28:17 +00:00
let mut project = if project_path.exists() {
2023-10-22 20:44:59 +00:00
toml::from_slice(&fs::read(&project_path).unwrap()).unwrap()
} else {
let dirname = directory.file_name().unwrap();
let date =
parse_date(dirname).expect("Directory name is not in the expected format");
let mut files = Vec::new();
for entry in directory.read_dir_utf8().unwrap() {
let entry = entry.unwrap();
let name = entry.file_name();
let lower = name.to_ascii_lowercase();
if (lower.ends_with(".mp4")
|| lower.ends_with(".mts")
|| lower.ends_with(".mkv"))
2024-01-09 20:27:35 +00:00
&& !entry.file_type().unwrap().is_dir()
2023-10-22 20:44:59 +00:00
{
files.push(String::from(name));
}
}
files.sort_unstable();
assert!(!files.is_empty());
2024-01-09 20:27:35 +00:00
print!("I found the following source files:");
for f in &files {
print!(" {f}");
}
println!();
files = ask("Which source files would you like to use? (specify multiple files separated by whitespace)")
.split_ascii_whitespace()
.map(String::from)
.collect();
assert!(!files.is_empty());
2023-10-22 20:44:59 +00:00
let project = Project {
lecture: ProjectLecture {
course,
label: args.label,
docent: args.docent,
2024-05-17 19:29:22 +00:00
date,
lang: args.lang
},
2023-10-22 20:44:59 +00:00
source: ProjectSource {
files,
stereo: args.stereo,
2023-10-30 19:28:17 +00:00
start: None,
end: None,
fast: Vec::new(),
2023-10-28 21:38:17 +00:00
metadata: None
},
progress: Default::default()
2023-10-22 20:44:59 +00:00
};
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
project
};
2023-10-30 19:28:17 +00:00
let renderer = Renderer::new(&directory, &project).unwrap();
2023-12-19 22:56:04 +00:00
let recording = renderer.recording_mkv();
2023-10-30 19:28:17 +00:00
// preprocess the video
if !project.progress.preprocessed {
renderer.preprocess(&mut project).unwrap();
project.progress.preprocessed = true;
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
}
// ask the user about start and end times
if !project.progress.asked_start_end {
project.source.start = Some(ask_time(format_args!(
"Please take a look at the file {recording} and tell me the first second you want included"
)));
project.source.end = Some(ask_time(format_args!(
"Please take a look at the file {recording} and tell me the last second you want included"
)));
project.progress.asked_start_end = true;
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
}
// ask the user about fast forward times
if !project.progress.asked_fast {
loop {
let start = ask_time(format_args!(
"Please take a look at the file {recording} and tell me the first second you want fast-forwarded. You may reply with `0` if there are no more fast-forward sections"
));
if start.seconds == 0 && start.micros == 0 {
break;
}
let end = ask_time(format_args!(
"Please tell me the last second you want fast-forwarded"
));
project.source.fast.push((start, end));
}
project.progress.asked_fast = true;
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
}
2023-11-03 09:02:30 +00:00
// render the video
let mut videos = Vec::new();
2023-11-16 11:12:17 +00:00
videos.push(if project.progress.rendered {
2023-12-19 22:56:04 +00:00
renderer.video_file_output()
2023-11-16 11:12:17 +00:00
} else {
2023-11-03 09:02:30 +00:00
let video = renderer.render(&mut project).unwrap();
project.progress.rendered = true;
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
video
});
// rescale the video
2023-11-14 09:36:12 +00:00
if let Some(lowest_res) = args.transcode {
for res in Resolution::values().into_iter().rev() {
if res > project.source.metadata.as_ref().unwrap().source_res
|| res > args.transcode_start
2023-11-14 09:36:12 +00:00
|| res < lowest_res
{
continue;
}
if !project.progress.transcoded.contains(&res) {
2024-05-24 10:17:40 +00:00
videos.push(renderer.rescale(&project.lecture, res).unwrap());
2023-11-14 09:36:12 +00:00
project.progress.transcoded.insert(res);
2023-11-03 09:02:30 +00:00
2023-11-14 09:36:12 +00:00
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes())
.unwrap();
}
2023-11-03 09:02:30 +00:00
}
}
println!("\x1B[1m ==> DONE :)\x1B[0m");
println!(" Videos:");
for v in &videos {
println!(" -> {v}");
}
2023-10-22 20:44:59 +00:00
}