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 ;
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 } ;
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 } ,
2023-11-14 09:36:12 +00:00
str ::FromStr ,
2023-11-02 20:33:21 +00:00
sync ::RwLock
2023-10-22 20:44:59 +00:00
} ;
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 ,
2024-04-10 10:55:00 +00:00
/// 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 ,
2023-11-14 09:36:12 +00:00
/// The memory limit for external tools like ffmpeg.
2024-01-06 17:55:27 +00:00
#[ 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) ]
2024-01-06 17:55:27 +00:00
transcode : Option < Resolution > ,
2024-01-15 23:24:12 +00:00
/// Transcode starts at this resolution, or the source resolution, whichever is lower.
#[ clap(short = 'T', long, default_value = " 1440p " ) ]
transcode_start : Resolution ,
2024-01-06 17:55:27 +00:00
/// 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 ,
FullHD : 1920 x 1080 at 2_000_000 in AvcAac ,
WQHD : 2560 x 1440 at 3_000_000 in Av1Opus ,
2023-11-03 09:02:30 +00:00
// TODO qsx muss mal sagen wieviel bitrate für 4k
2023-12-19 22:56:04 +00:00
UHD : 3840 x 2160 at 4_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 ,
2024-04-10 10:55:00 +00:00
label : String ,
docent : String ,
2023-10-22 20:44:59 +00:00
#[ serde_as(as = " DisplayFromStr " ) ]
date : Date
}
#[ serde_as ]
#[ derive(Deserialize, Serialize) ]
struct ProjectSource {
files : Vec < String > ,
2024-01-06 17:55:27 +00:00
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
}
2024-01-06 17:55:27 +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 ( ) ;
2023-11-02 20:33:21 +00:00
* ( 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 ( ) ;
2024-01-06 17:55:27 +00:00
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-06 17:55:27 +00:00
2024-01-09 20:27:35 +00:00
print! ( " I found the following source files: " ) ;
for f in & files {
print! ( " {f} " ) ;
}
println! ( ) ;
2024-01-06 17:55:27 +00:00
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 {
2024-04-10 10:55:00 +00:00
lecture : ProjectLecture {
course ,
label : args . label ,
docent : args . docent ,
date
} ,
2023-10-22 20:44:59 +00:00
source : ProjectSource {
files ,
2024-01-06 17:55:27 +00:00
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 ( ) {
2024-01-15 23:24:12 +00:00
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 ) {
2023-12-19 22:56:04 +00:00
videos . push ( renderer . rescale ( 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
}