2023-10-30 15:05:21 +00:00
#![ allow(clippy::manual_range_contains) ]
2023-10-22 20:44:59 +00:00
#![ warn(rust_2018_idioms) ]
#![ 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 ::{
render ::Renderer ,
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 } ,
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 {
#[ clap(short = 'C', long, default_value = " . " ) ]
directory : PathBuf ,
#[ clap(short = 'c', long, default_value = " 23ws-malo " ) ]
2023-11-02 20:33:21 +00:00
course : String ,
2023-11-02 21:40:49 +00:00
#[ clap(short, long, default_value = " 8G " ) ]
2023-11-02 20:33:21 +00:00
mem_limit : String
2023-10-22 20:44:59 +00:00
}
2023-11-03 09:02:30 +00:00
macro_rules ! resolutions {
( $( $res :ident : $width :literal x $height :literal at $bitrate :literal ) , + ) = > {
#[ allow(non_camel_case_types, clippy::upper_case_acronyms) ]
#[ derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize) ]
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 ) , +
}
}
fn bitrate ( self ) -> & 'static str {
match self {
$( Self ::$res = > $bitrate ) , +
}
}
2023-10-28 21:38:17 +00:00
}
}
}
2023-11-03 09:02:30 +00:00
resolutions! {
nHD : 640 x 360 at " 500k " ,
HD : 1280 x 720 at " 1M " ,
FullHD : 1920 x 1080 at " 2M " ,
WQHD : 2560 x 1440 at " 3M " ,
// TODO qsx muss mal sagen wieviel bitrate für 4k
UHD : 3840 x 2160 at " 4M "
}
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 ,
#[ serde_as(as = " DisplayFromStr " ) ]
date : Date
}
#[ serde_as ]
#[ derive(Deserialize, Serialize) ]
struct ProjectSource {
files : Vec < String > ,
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_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 ( ) ;
if ( lower . ends_with ( " .mp4 " ) | | lower . ends_with ( " .mts " ) )
& & entry . file_type ( ) . unwrap ( ) . is_file ( )
{
files . push ( String ::from ( name ) ) ;
}
}
files . sort_unstable ( ) ;
assert! ( ! files . is_empty ( ) ) ;
println! ( " I found the following source files: {files:?} " ) ;
let project = Project {
lecture : ProjectLecture { course , date } ,
source : ProjectSource {
files ,
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
} ;
println! ( " {} " , toml ::to_string ( & project ) . unwrap ( ) ) ;
2023-10-30 19:28:17 +00:00
let renderer = Renderer ::new ( & directory , & project ) . unwrap ( ) ;
let recording = renderer . recording_mp4 ( ) ;
// preprocess the video
if ! project . progress . preprocessed {
renderer . preprocess ( & mut project ) . unwrap ( ) ;
project . progress . preprocessed = true ;
println! ( " {} " , toml ::to_string ( & project ) . unwrap ( ) ) ;
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 ;
println! ( " {} " , toml ::to_string ( & project ) . unwrap ( ) ) ;
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 ;
println! ( " {} " , toml ::to_string ( & project ) . unwrap ( ) ) ;
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 ( ) ;
videos . push ( if ! project . progress . rendered {
let video = renderer . render ( & mut project ) . unwrap ( ) ;
project . progress . rendered = true ;
println! ( " {} " , toml ::to_string ( & project ) . unwrap ( ) ) ;
fs ::write ( & project_path , toml ::to_string ( & project ) . unwrap ( ) . as_bytes ( ) ) . unwrap ( ) ;
video
} else {
renderer . video_mp4 ( & project )
} ) ;
// rescale the video
for res in Resolution ::values ( ) {
if res > = project . source . metadata . as_ref ( ) . unwrap ( ) . source_res {
continue ;
}
if ! project . progress . transcoded . contains ( & res ) {
videos . push ( renderer . rescale ( res , & project ) . unwrap ( ) ) ;
project . progress . transcoded . insert ( res ) ;
println! ( " {} " , toml ::to_string ( & project ) . unwrap ( ) ) ;
fs ::write ( & project_path , toml ::to_string ( & project ) . unwrap ( ) . as_bytes ( ) )
. unwrap ( ) ;
}
}
println! ( " \x1B [1m ==> DONE :) \x1B [0m " ) ;
println! ( " Videos: " ) ;
for v in & videos {
println! ( " -> {v} " ) ;
}
2023-10-22 20:44:59 +00:00
}