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 ,
#[ clap(short, long, default_value = " 2G " ) ]
mem_limit : String
2023-10-22 20:44:59 +00:00
}
2023-10-30 15:05:21 +00:00
#[ allow(non_camel_case_types, clippy::upper_case_acronyms) ]
2023-10-28 21:38:17 +00:00
#[ derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize) ]
enum Resolution {
/// 640x360
nHD ,
/// 1280x720
HD ,
/// 1920x1080
FullHD ,
/// 2560x1440
WQHD ,
/// 3840x2160
UHD
}
impl Resolution {
fn width ( self ) -> usize {
match self {
Self ::nHD = > 640 ,
Self ::HD = > 1280 ,
Self ::FullHD = > 1920 ,
Self ::WQHD = > 2560 ,
Self ::UHD = > 3840
}
}
fn height ( self ) -> usize {
match self {
Self ::nHD = > 360 ,
Self ::HD = > 720 ,
Self ::FullHD = > 1080 ,
Self ::WQHD = > 1440 ,
Self ::UHD = > 2160
}
}
}
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-10-30 20:26:17 +00:00
let video = renderer . render ( & mut project ) . unwrap ( ) ;
println! ( " \x1B [1m ==> DONE :) \x1B [0m Video: {video} " ) ;
2023-10-22 20:44:59 +00:00
}