From 17ee86afc108f853bed39a74204ca92bf1ac051b Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 28 Apr 2022 17:54:55 +0200 Subject: [PATCH] start with documentations --- src/main.rs | 43 ++++++++++++++++++++++-------------- src/utils/arg_parse.rs | 1 + src/utils/config.rs | 7 ++++++ src/utils/controller.rs | 10 +++++++++ src/utils/generator.rs | 12 ++++++++++ src/utils/json_serializer.rs | 4 ++++ src/utils/json_validate.rs | 8 +++++++ src/utils/logging.rs | 16 ++++++++++++-- src/utils/mod.rs | 41 +++++++++++++++++++++++++++++----- 9 files changed, 118 insertions(+), 24 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0427caf6..cb557d59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,10 @@ extern crate log; extern crate simplelog; use std::{ - {fs, fs::File}, + fs::{self, File}, path::PathBuf, process::exit, thread, - }; use serde::{Deserialize, Serialize}; @@ -32,26 +31,26 @@ struct StatusData { date: String, } -fn main() { - init_config(); - let config = GlobalConfig::global(); - let play_control = PlayerControl::new(); - let playout_stat = PlayoutStatus::new(); - let proc_control = ProcessControl::new(); - - if !PathBuf::from(config.general.stat_file.clone()).exists() { +/// Here we create a status file in temp folder. +/// We need ths for reading/saving program status. +/// For example when we skip a playing file, +/// we save the time difference, so we stay in sync. +/// +/// When file not exists we create it, and when it exists we get its values. +fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) { + if !PathBuf::from(stat_file).exists() { let data = json!({ "time_shift": 0.0, "date": String::new(), }); let json: String = serde_json::to_string(&data).expect("Serialize status data failed"); - fs::write(config.general.stat_file.clone(), &json).expect("Unable to write file"); + fs::write(stat_file, &json).expect("Unable to write file"); } else { let stat_file = File::options() .read(true) .write(false) - .open(&config.general.stat_file) + .open(&stat_file) .expect("Could not open status file"); let data: StatusData = @@ -60,13 +59,24 @@ fn main() { *playout_stat.time_shift.lock().unwrap() = data.time_shift; *playout_stat.date.lock().unwrap() = data.date; } +} + +fn main() { + // Init the config, set process controller, create logging. + init_config(); + let config = GlobalConfig::global(); + let play_control = PlayerControl::new(); + let playout_stat = PlayoutStatus::new(); + let proc_control = ProcessControl::new(); let logging = init_logging(); CombinedLogger::init(logging).unwrap(); validate_ffmpeg(); + status_file(&config.general.stat_file, &playout_stat); if let Some(range) = config.general.generate.clone() { + // run a simple playlist generator and save them to disk generate_playlist(range); exit(0); @@ -77,16 +87,15 @@ fn main() { let proc_ctl = proc_control.clone(); if config.rpc_server.enable { - thread::spawn( move || json_rpc_server( - play_ctl, - play_stat, - proc_ctl, - )); + // If RPC server is enable we also fire up a JSON RPC server. + thread::spawn(move || json_rpc_server(play_ctl, play_stat, proc_ctl)); } if &config.out.mode.to_lowercase() == "hls" { + // write files/playlist to HLS m3u8 playlist write_hls(play_control, playout_stat, proc_control); } else { + // play on desktop or stream to a remote target player(play_control, playout_stat, proc_control); } diff --git a/src/utils/arg_parse.rs b/src/utils/arg_parse.rs index ff612d34..87cfca59 100644 --- a/src/utils/arg_parse.rs +++ b/src/utils/arg_parse.rs @@ -54,6 +54,7 @@ pub struct Args { pub volume: Option, } +/// Get arguments from command line, and return them. pub fn get_args() -> Args { let args = Args::parse(); diff --git a/src/utils/config.rs b/src/utils/config.rs index 07489213..bf8493cb 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -12,6 +12,9 @@ use shlex::split; use crate::utils::{get_args, time_to_sec}; +/// Global Config +/// +/// This we init ones, when ffplayout is starting and use them globally in the hole program. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GlobalConfig { pub general: General, @@ -132,6 +135,7 @@ pub struct Out { } impl GlobalConfig { + /// Read config from YAML file, and set some extra config values. fn new() -> Self { let args = get_args(); let mut config_path = match env::current_exe() { @@ -173,6 +177,7 @@ impl GlobalConfig { config.playlist.length_sec = Some(86400.0); } + // We set the decoder settings here, so we only define them ones. let mut settings: Vec = vec![ "-pix_fmt", "yuv420p", @@ -209,6 +214,8 @@ impl GlobalConfig { config.out.preview_cmd = split(config.out.preview_param.as_str()); config.out.output_cmd = split(config.out.output_param.as_str()); + // Read command line arguments, and override the config with them. + if let Some(gen) = args.generate { config.general.generate = Some(gen); } diff --git a/src/utils/controller.rs b/src/utils/controller.rs index c51b00ba..50add407 100644 --- a/src/utils/controller.rs +++ b/src/utils/controller.rs @@ -12,6 +12,7 @@ use simplelog::*; use crate::utils::Media; +/// Defined process units. pub enum ProcessUnit { Decoder, Encoder, @@ -30,6 +31,10 @@ impl fmt::Display for ProcessUnit { use ProcessUnit::*; +/// Process Controller +/// +/// We save here some global states, about what is running and which processes are alive. +/// This we need for process termination, skipping clip decoder etc. #[derive(Clone)] pub struct ProcessControl { pub decoder_term: Arc>>, @@ -88,6 +93,8 @@ impl ProcessControl { Ok(()) } + /// Wait for process to proper close. + /// This prevents orphaned/zombi processes in system pub fn wait(&mut self, proc: ProcessUnit) -> Result<(), String> { match proc { Decoder => { @@ -116,6 +123,7 @@ impl ProcessControl { Ok(()) } + /// No matter what is running, terminate them all. pub fn kill_all(&mut self) { self.is_terminated.store(true, Ordering::SeqCst); @@ -141,6 +149,7 @@ impl Drop for ProcessControl { } } +/// Global player control, to get infos about current clip etc. #[derive(Clone)] pub struct PlayerControl { pub current_media: Arc>>, @@ -158,6 +167,7 @@ impl PlayerControl { } } +/// Global playout control, for move forward/backward clip, or resetting playlist/state. #[derive(Clone, Debug)] pub struct PlayoutStatus { pub time_shift: Arc>, diff --git a/src/utils/generator.rs b/src/utils/generator.rs index 49e86ed4..8736f72e 100644 --- a/src/utils/generator.rs +++ b/src/utils/generator.rs @@ -1,3 +1,12 @@ +/// Simple Playlist Generator +/// +/// You can call ffplayout[.exe] -g YYYY-mm-dd - YYYY-mm-dd to generate JSON playlists. +/// +/// The generator takes the files from storage, which are set in config. +/// It also respect the shuffle/sort mode. +/// +/// Beside that it is really very basic, without any logic. + use std::{ fs::{create_dir_all, write}, path::Path, @@ -11,6 +20,8 @@ use simplelog::*; use crate::input::Source; use crate::utils::{json_serializer::Playlist, GlobalConfig, Media}; + +/// Generate a vector with dates, from given range. fn get_date_range(date_range: &Vec) -> Vec { let mut range = vec![]; let start; @@ -46,6 +57,7 @@ fn get_date_range(date_range: &Vec) -> Vec { range } +/// Generate playlists pub fn generate_playlist(mut date_range: Vec) { let config = GlobalConfig::global(); let total_length = config.playlist.length_sec.unwrap().clone(); diff --git a/src/utils/json_serializer.rs b/src/utils/json_serializer.rs index f963b1c3..20dfb12e 100644 --- a/src/utils/json_serializer.rs +++ b/src/utils/json_serializer.rs @@ -12,6 +12,7 @@ use crate::utils::{get_date, modified_time, validate_playlist, GlobalConfig, Med pub const DUMMY_LEN: f64 = 60.0; +/// This is our main playlist object, it holds all necessary information for the current day. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Playlist { pub date: String, @@ -44,6 +45,8 @@ impl Playlist { } } +/// Read json playlist file, fills Playlist struct and set some extra values, +/// which we need to process. pub fn read_json( path: Option, is_terminated: Arc, @@ -96,6 +99,7 @@ pub fn read_json( playlist.modified = Some(modi.to_string()); } + // Add extra values to every media clip for (i, item) in playlist.program.iter_mut().enumerate() { item.begin = Some(start_sec); item.index = Some(i); diff --git a/src/utils/json_validate.rs b/src/utils/json_validate.rs index 753df56f..5962cc7e 100644 --- a/src/utils/json_validate.rs +++ b/src/utils/json_validate.rs @@ -4,6 +4,14 @@ use simplelog::*; use crate::utils::{sec_to_time, GlobalConfig, MediaProbe, Playlist}; + +/// Validate a given playlist, to check if: +/// +/// - the source files are existing +/// - file can be read by ffprobe and metadata exists +/// - total playtime fits target length from config +/// +/// This function we run in a thread, to don't block the main function. pub fn validate_playlist(playlist: Playlist, is_terminated: Arc, config: GlobalConfig) { let date = playlist.date; let mut length = config.playlist.length_sec.unwrap(); diff --git a/src/utils/logging.rs b/src/utils/logging.rs index bf83d732..f48384c5 100644 --- a/src/utils/logging.rs +++ b/src/utils/logging.rs @@ -21,6 +21,7 @@ use simplelog::*; use crate::utils::GlobalConfig; +/// send log messages to mail recipient fn send_mail(msg: String) { let config = GlobalConfig::global(); @@ -52,9 +53,10 @@ fn send_mail(msg: String) { } } +/// Basic Mail Queue +/// +/// Check every give seconds for messages and send them. fn mail_queue(messages: Arc>>, interval: u64) { - // check every give seconds for messages and send them - loop { if messages.lock().unwrap().len() > 0 { let msg = messages.lock().unwrap().join("\n"); @@ -67,6 +69,7 @@ fn mail_queue(messages: Arc>>, interval: u64) { } } +/// Self made Mail Log struct, to extend simplelog. pub struct LogMailer { level: LevelFilter, pub config: Config, @@ -121,12 +124,20 @@ impl SharedLogger for LogMailer { } } +/// Workaround to remove color information from log +/// +/// ToDo: maybe in next version from simplelog this is not necessary anymore. fn clean_string(text: &str) -> String { let regex: Regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap(); regex.replace_all(text, "").to_string() } +/// Initialize our logging, to have: +/// +/// - console logger +/// - file logger +/// - mail logger pub fn init_logging() -> Vec> { let config = GlobalConfig::global(); let app_config = config.logging.clone(); @@ -191,6 +202,7 @@ pub fn init_logging() -> Vec> { )); } + // set mail logger only the recipient is set in config if config.mail.recipient.contains("@") && config.mail.recipient.contains(".") { let messages: Arc>> = Arc::new(Mutex::new(Vec::new())); let messages_clone = messages.clone(); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ee26434b..a51ed2fc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -35,6 +35,7 @@ pub use logging::init_logging; use crate::filter::filter_chains; +/// Video clip struct to hold some important states and comments for current media. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Media { #[serde(skip_serializing, skip_deserializing)] @@ -124,6 +125,8 @@ impl Media { } } + +/// We use the ffprobe crate, but we map the metadata to our needs. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MediaProbe { pub format: Option, @@ -185,6 +188,9 @@ impl MediaProbe { } } +/// Write current status to status file in temp folder. +/// +/// The status file is init in main function and mostly modified in RPC server. pub fn write_status(date: &str, shift: f64) { let config = GlobalConfig::global(); let stat_file = config.general.stat_file.clone(); @@ -206,6 +212,7 @@ pub fn write_status(date: &str, shift: f64) { // local.timestamp_millis() as i64 // } +/// Get current time in seconds. pub fn get_sec() -> f64 { let local: DateTime = Local::now(); @@ -213,6 +220,10 @@ pub fn get_sec() -> f64 { + (local.nanosecond() as f64 / 1000000000.0) } +/// Get current date for playlist, but check time with conditions: +/// +/// - When time is before playlist start, get date from yesterday. +/// - When given next_start is over target length (normally a full day), get date from tomorrow. pub fn get_date(seek: bool, start: f64, next_start: f64) -> String { let local: DateTime = Local::now(); @@ -227,6 +238,7 @@ pub fn get_date(seek: bool, start: f64, next_start: f64) -> String { local.format("%Y-%m-%d").to_string() } +/// Get file modification time. pub fn modified_time(path: &str) -> Option> { let metadata = metadata(path).unwrap(); @@ -238,6 +250,7 @@ pub fn modified_time(path: &str) -> Option> { None } +/// Convert a formatted time string to seconds. pub fn time_to_sec(time_str: &str) -> f64 { if ["now", "", "none"].contains(&time_str) || !time_str.contains(":") { return get_sec(); @@ -251,6 +264,7 @@ pub fn time_to_sec(time_str: &str) -> f64 { h * 3600.0 + m * 60.0 + s } +/// Convert floating number (seconds) to a formatted time string. pub fn sec_to_time(sec: f64) -> String { let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64); // Create DateTime from SystemTime @@ -259,6 +273,8 @@ pub fn sec_to_time(sec: f64) -> String { date_time.format("%H:%M:%S%.3f").to_string() } +/// Test if given numbers are close to each other, +/// with a third number for setting the maximum range. pub fn is_close(a: f64, b: f64, to: f64) -> bool { if (a - b).abs() < to { return true; @@ -267,6 +283,10 @@ pub fn is_close(a: f64, b: f64, to: f64) -> bool { false } +/// Get delta between clip start and current time. This value we need to check, +/// if we still in sync. +/// +/// We also get here the global delta between clip start and time when a new playlist should start. pub fn get_delta(begin: &f64) -> (f64, f64) { let config = GlobalConfig::global(); let mut current_time = get_sec(); @@ -299,6 +319,7 @@ pub fn get_delta(begin: &f64) -> (f64, f64) { (current_delta, total_delta) } +/// Check if clip in playlist is in sync with global time. pub fn check_sync(delta: f64) -> bool { let config = GlobalConfig::global(); @@ -310,6 +331,7 @@ pub fn check_sync(delta: f64) -> bool { true } +/// Create a dummy clip as a placeholder for missing video files. pub fn gen_dummy(duration: f64) -> (String, Vec) { let config = GlobalConfig::global(); let color = "#121212"; @@ -334,6 +356,7 @@ pub fn gen_dummy(duration: f64) -> (String, Vec) { (source, cmd) } +/// Set clip seek in and length value. pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec { let mut source_cmd: Vec = vec![]; @@ -353,16 +376,13 @@ pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec, suffix: &str) -> Result<(), Error> { - // read ffmpeg stderr decoder, encoder and server instance - // and log the output - fn format_line(line: String, level: &str) -> String { line.replace(&format!("[{level: >5}] "), "") } - // let buffer = BufReader::new(std_errors); - for line in buffer.lines() { let line = line?; @@ -389,6 +409,7 @@ pub fn stderr_reader(buffer: BufReader, suffix: &str) -> Result<(), Ok(()) } +/// Run program to test if it is in system. fn is_in_system(name: &str) { if let Ok(mut proc) = Command::new(name) .stderr(Stdio::null()) @@ -407,6 +428,8 @@ fn is_in_system(name: &str) { fn ffmpeg_libs_and_filter() -> (Vec, Vec) { let mut libs: Vec = vec![]; let mut filters: Vec = vec![]; + + // filter lines which contains filter let re: Regex = Regex::new(r"^( ?) [TSC.]+").unwrap(); let mut ff_proc = match Command::new("ffmpeg") @@ -425,6 +448,8 @@ fn ffmpeg_libs_and_filter() -> (Vec, Vec) { let err_buffer = BufReader::new(ff_proc.stderr.take().unwrap()); let out_buffer = BufReader::new(ff_proc.stdout.take().unwrap()); + // stderr shows only the ffmpeg configuration + // get codec library's for line in err_buffer.lines() { if let Ok(line) = line { if line.contains("configuration:") { @@ -439,6 +464,8 @@ fn ffmpeg_libs_and_filter() -> (Vec, Vec) { } } + // stdout shows filter help text + // get filters for line in out_buffer.lines() { if let Ok(line) = line { if let Some(_) = re.captures(line.as_str()) { @@ -455,6 +482,10 @@ fn ffmpeg_libs_and_filter() -> (Vec, Vec) { (libs, filters) } + +/// Validate ffmpeg/ffprobe/ffplay. +/// +/// Check if they are in system and has all filters and codecs we need. pub fn validate_ffmpeg() { let config = GlobalConfig::global();