start with documentations

This commit is contained in:
jb-alvarado 2022-04-28 17:54:55 +02:00
parent 03bb49c0b8
commit 17ee86afc1
9 changed files with 118 additions and 24 deletions

View File

@ -2,11 +2,10 @@ extern crate log;
extern crate simplelog; extern crate simplelog;
use std::{ use std::{
{fs, fs::File}, fs::{self, File},
path::PathBuf, path::PathBuf,
process::exit, process::exit,
thread, thread,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -32,26 +31,26 @@ struct StatusData {
date: String, date: String,
} }
fn main() { /// Here we create a status file in temp folder.
init_config(); /// We need ths for reading/saving program status.
let config = GlobalConfig::global(); /// For example when we skip a playing file,
let play_control = PlayerControl::new(); /// we save the time difference, so we stay in sync.
let playout_stat = PlayoutStatus::new(); ///
let proc_control = ProcessControl::new(); /// 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(config.general.stat_file.clone()).exists() { if !PathBuf::from(stat_file).exists() {
let data = json!({ let data = json!({
"time_shift": 0.0, "time_shift": 0.0,
"date": String::new(), "date": String::new(),
}); });
let json: String = serde_json::to_string(&data).expect("Serialize status data failed"); 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 { } else {
let stat_file = File::options() let stat_file = File::options()
.read(true) .read(true)
.write(false) .write(false)
.open(&config.general.stat_file) .open(&stat_file)
.expect("Could not open status file"); .expect("Could not open status file");
let data: StatusData = let data: StatusData =
@ -60,13 +59,24 @@ fn main() {
*playout_stat.time_shift.lock().unwrap() = data.time_shift; *playout_stat.time_shift.lock().unwrap() = data.time_shift;
*playout_stat.date.lock().unwrap() = data.date; *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(); let logging = init_logging();
CombinedLogger::init(logging).unwrap(); CombinedLogger::init(logging).unwrap();
validate_ffmpeg(); validate_ffmpeg();
status_file(&config.general.stat_file, &playout_stat);
if let Some(range) = config.general.generate.clone() { if let Some(range) = config.general.generate.clone() {
// run a simple playlist generator and save them to disk
generate_playlist(range); generate_playlist(range);
exit(0); exit(0);
@ -77,16 +87,15 @@ fn main() {
let proc_ctl = proc_control.clone(); let proc_ctl = proc_control.clone();
if config.rpc_server.enable { if config.rpc_server.enable {
thread::spawn( move || json_rpc_server( // If RPC server is enable we also fire up a JSON RPC server.
play_ctl, thread::spawn(move || json_rpc_server(play_ctl, play_stat, proc_ctl));
play_stat,
proc_ctl,
));
} }
if &config.out.mode.to_lowercase() == "hls" { if &config.out.mode.to_lowercase() == "hls" {
// write files/playlist to HLS m3u8 playlist
write_hls(play_control, playout_stat, proc_control); write_hls(play_control, playout_stat, proc_control);
} else { } else {
// play on desktop or stream to a remote target
player(play_control, playout_stat, proc_control); player(play_control, playout_stat, proc_control);
} }

View File

@ -54,6 +54,7 @@ pub struct Args {
pub volume: Option<f64>, pub volume: Option<f64>,
} }
/// Get arguments from command line, and return them.
pub fn get_args() -> Args { pub fn get_args() -> Args {
let args = Args::parse(); let args = Args::parse();

View File

@ -12,6 +12,9 @@ use shlex::split;
use crate::utils::{get_args, time_to_sec}; 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GlobalConfig { pub struct GlobalConfig {
pub general: General, pub general: General,
@ -132,6 +135,7 @@ pub struct Out {
} }
impl GlobalConfig { impl GlobalConfig {
/// Read config from YAML file, and set some extra config values.
fn new() -> Self { fn new() -> Self {
let args = get_args(); let args = get_args();
let mut config_path = match env::current_exe() { let mut config_path = match env::current_exe() {
@ -173,6 +177,7 @@ impl GlobalConfig {
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
} }
// We set the decoder settings here, so we only define them ones.
let mut settings: Vec<String> = vec![ let mut settings: Vec<String> = vec![
"-pix_fmt", "-pix_fmt",
"yuv420p", "yuv420p",
@ -209,6 +214,8 @@ impl GlobalConfig {
config.out.preview_cmd = split(config.out.preview_param.as_str()); config.out.preview_cmd = split(config.out.preview_param.as_str());
config.out.output_cmd = split(config.out.output_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 { if let Some(gen) = args.generate {
config.general.generate = Some(gen); config.general.generate = Some(gen);
} }

View File

@ -12,6 +12,7 @@ use simplelog::*;
use crate::utils::Media; use crate::utils::Media;
/// Defined process units.
pub enum ProcessUnit { pub enum ProcessUnit {
Decoder, Decoder,
Encoder, Encoder,
@ -30,6 +31,10 @@ impl fmt::Display for ProcessUnit {
use 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)] #[derive(Clone)]
pub struct ProcessControl { pub struct ProcessControl {
pub decoder_term: Arc<Mutex<Option<Child>>>, pub decoder_term: Arc<Mutex<Option<Child>>>,
@ -88,6 +93,8 @@ impl ProcessControl {
Ok(()) Ok(())
} }
/// Wait for process to proper close.
/// This prevents orphaned/zombi processes in system
pub fn wait(&mut self, proc: ProcessUnit) -> Result<(), String> { pub fn wait(&mut self, proc: ProcessUnit) -> Result<(), String> {
match proc { match proc {
Decoder => { Decoder => {
@ -116,6 +123,7 @@ impl ProcessControl {
Ok(()) Ok(())
} }
/// No matter what is running, terminate them all.
pub fn kill_all(&mut self) { pub fn kill_all(&mut self) {
self.is_terminated.store(true, Ordering::SeqCst); 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)] #[derive(Clone)]
pub struct PlayerControl { pub struct PlayerControl {
pub current_media: Arc<Mutex<Option<Media>>>, pub current_media: Arc<Mutex<Option<Media>>>,
@ -158,6 +167,7 @@ impl PlayerControl {
} }
} }
/// Global playout control, for move forward/backward clip, or resetting playlist/state.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PlayoutStatus { pub struct PlayoutStatus {
pub time_shift: Arc<Mutex<f64>>, pub time_shift: Arc<Mutex<f64>>,

View File

@ -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::{ use std::{
fs::{create_dir_all, write}, fs::{create_dir_all, write},
path::Path, path::Path,
@ -11,6 +20,8 @@ use simplelog::*;
use crate::input::Source; use crate::input::Source;
use crate::utils::{json_serializer::Playlist, GlobalConfig, Media}; use crate::utils::{json_serializer::Playlist, GlobalConfig, Media};
/// Generate a vector with dates, from given range.
fn get_date_range(date_range: &Vec<String>) -> Vec<String> { fn get_date_range(date_range: &Vec<String>) -> Vec<String> {
let mut range = vec![]; let mut range = vec![];
let start; let start;
@ -46,6 +57,7 @@ fn get_date_range(date_range: &Vec<String>) -> Vec<String> {
range range
} }
/// Generate playlists
pub fn generate_playlist(mut date_range: Vec<String>) { pub fn generate_playlist(mut date_range: Vec<String>) {
let config = GlobalConfig::global(); let config = GlobalConfig::global();
let total_length = config.playlist.length_sec.unwrap().clone(); let total_length = config.playlist.length_sec.unwrap().clone();

View File

@ -12,6 +12,7 @@ use crate::utils::{get_date, modified_time, validate_playlist, GlobalConfig, Med
pub const DUMMY_LEN: f64 = 60.0; 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Playlist { pub struct Playlist {
pub date: String, 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( pub fn read_json(
path: Option<String>, path: Option<String>,
is_terminated: Arc<AtomicBool>, is_terminated: Arc<AtomicBool>,
@ -96,6 +99,7 @@ pub fn read_json(
playlist.modified = Some(modi.to_string()); playlist.modified = Some(modi.to_string());
} }
// Add extra values to every media clip
for (i, item) in playlist.program.iter_mut().enumerate() { for (i, item) in playlist.program.iter_mut().enumerate() {
item.begin = Some(start_sec); item.begin = Some(start_sec);
item.index = Some(i); item.index = Some(i);

View File

@ -4,6 +4,14 @@ use simplelog::*;
use crate::utils::{sec_to_time, GlobalConfig, MediaProbe, Playlist}; 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<AtomicBool>, config: GlobalConfig) { pub fn validate_playlist(playlist: Playlist, is_terminated: Arc<AtomicBool>, config: GlobalConfig) {
let date = playlist.date; let date = playlist.date;
let mut length = config.playlist.length_sec.unwrap(); let mut length = config.playlist.length_sec.unwrap();

View File

@ -21,6 +21,7 @@ use simplelog::*;
use crate::utils::GlobalConfig; use crate::utils::GlobalConfig;
/// send log messages to mail recipient
fn send_mail(msg: String) { fn send_mail(msg: String) {
let config = GlobalConfig::global(); 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<Mutex<Vec<String>>>, interval: u64) { fn mail_queue(messages: Arc<Mutex<Vec<String>>>, interval: u64) {
// check every give seconds for messages and send them
loop { loop {
if messages.lock().unwrap().len() > 0 { if messages.lock().unwrap().len() > 0 {
let msg = messages.lock().unwrap().join("\n"); let msg = messages.lock().unwrap().join("\n");
@ -67,6 +69,7 @@ fn mail_queue(messages: Arc<Mutex<Vec<String>>>, interval: u64) {
} }
} }
/// Self made Mail Log struct, to extend simplelog.
pub struct LogMailer { pub struct LogMailer {
level: LevelFilter, level: LevelFilter,
pub config: Config, 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 { fn clean_string(text: &str) -> String {
let regex: Regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap(); let regex: Regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap();
regex.replace_all(text, "").to_string() regex.replace_all(text, "").to_string()
} }
/// Initialize our logging, to have:
///
/// - console logger
/// - file logger
/// - mail logger
pub fn init_logging() -> Vec<Box<dyn SharedLogger>> { pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
let config = GlobalConfig::global(); let config = GlobalConfig::global();
let app_config = config.logging.clone(); let app_config = config.logging.clone();
@ -191,6 +202,7 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
)); ));
} }
// set mail logger only the recipient is set in config
if config.mail.recipient.contains("@") && config.mail.recipient.contains(".") { if config.mail.recipient.contains("@") && config.mail.recipient.contains(".") {
let messages: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new())); let messages: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let messages_clone = messages.clone(); let messages_clone = messages.clone();

View File

@ -35,6 +35,7 @@ pub use logging::init_logging;
use crate::filter::filter_chains; use crate::filter::filter_chains;
/// Video clip struct to hold some important states and comments for current media.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Media { pub struct Media {
#[serde(skip_serializing, skip_deserializing)] #[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)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MediaProbe { pub struct MediaProbe {
pub format: Option<Format>, pub format: Option<Format>,
@ -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) { pub fn write_status(date: &str, shift: f64) {
let config = GlobalConfig::global(); let config = GlobalConfig::global();
let stat_file = config.general.stat_file.clone(); 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 // local.timestamp_millis() as i64
// } // }
/// Get current time in seconds.
pub fn get_sec() -> f64 { pub fn get_sec() -> f64 {
let local: DateTime<Local> = Local::now(); let local: DateTime<Local> = Local::now();
@ -213,6 +220,10 @@ pub fn get_sec() -> f64 {
+ (local.nanosecond() as f64 / 1000000000.0) + (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 { pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
let local: DateTime<Local> = Local::now(); let local: DateTime<Local> = 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() local.format("%Y-%m-%d").to_string()
} }
/// Get file modification time.
pub fn modified_time(path: &str) -> Option<DateTime<Local>> { pub fn modified_time(path: &str) -> Option<DateTime<Local>> {
let metadata = metadata(path).unwrap(); let metadata = metadata(path).unwrap();
@ -238,6 +250,7 @@ pub fn modified_time(path: &str) -> Option<DateTime<Local>> {
None None
} }
/// Convert a formatted time string to seconds.
pub fn time_to_sec(time_str: &str) -> f64 { pub fn time_to_sec(time_str: &str) -> f64 {
if ["now", "", "none"].contains(&time_str) || !time_str.contains(":") { if ["now", "", "none"].contains(&time_str) || !time_str.contains(":") {
return get_sec(); return get_sec();
@ -251,6 +264,7 @@ pub fn time_to_sec(time_str: &str) -> f64 {
h * 3600.0 + m * 60.0 + s h * 3600.0 + m * 60.0 + s
} }
/// Convert floating number (seconds) to a formatted time string.
pub fn sec_to_time(sec: f64) -> String { pub fn sec_to_time(sec: f64) -> String {
let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64); let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64);
// Create DateTime from SystemTime // 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() 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 { pub fn is_close(a: f64, b: f64, to: f64) -> bool {
if (a - b).abs() < to { if (a - b).abs() < to {
return true; return true;
@ -267,6 +283,10 @@ pub fn is_close(a: f64, b: f64, to: f64) -> bool {
false 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) { pub fn get_delta(begin: &f64) -> (f64, f64) {
let config = GlobalConfig::global(); let config = GlobalConfig::global();
let mut current_time = get_sec(); let mut current_time = get_sec();
@ -299,6 +319,7 @@ pub fn get_delta(begin: &f64) -> (f64, f64) {
(current_delta, total_delta) (current_delta, total_delta)
} }
/// Check if clip in playlist is in sync with global time.
pub fn check_sync(delta: f64) -> bool { pub fn check_sync(delta: f64) -> bool {
let config = GlobalConfig::global(); let config = GlobalConfig::global();
@ -310,6 +331,7 @@ pub fn check_sync(delta: f64) -> bool {
true true
} }
/// Create a dummy clip as a placeholder for missing video files.
pub fn gen_dummy(duration: f64) -> (String, Vec<String>) { pub fn gen_dummy(duration: f64) -> (String, Vec<String>) {
let config = GlobalConfig::global(); let config = GlobalConfig::global();
let color = "#121212"; let color = "#121212";
@ -334,6 +356,7 @@ pub fn gen_dummy(duration: f64) -> (String, Vec<String>) {
(source, cmd) (source, cmd)
} }
/// Set clip seek in and length value.
pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec<String> { pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec<String> {
let mut source_cmd: Vec<String> = vec![]; let mut source_cmd: Vec<String> = vec![];
@ -353,16 +376,13 @@ pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec<S
source_cmd source_cmd
} }
/// Read ffmpeg stderr decoder, encoder and server instance
/// and log the output.
pub fn stderr_reader(buffer: BufReader<ChildStderr>, suffix: &str) -> Result<(), Error> { pub fn stderr_reader(buffer: BufReader<ChildStderr>, suffix: &str) -> Result<(), Error> {
// read ffmpeg stderr decoder, encoder and server instance
// and log the output
fn format_line(line: String, level: &str) -> String { fn format_line(line: String, level: &str) -> String {
line.replace(&format!("[{level: >5}] "), "") line.replace(&format!("[{level: >5}] "), "")
} }
// let buffer = BufReader::new(std_errors);
for line in buffer.lines() { for line in buffer.lines() {
let line = line?; let line = line?;
@ -389,6 +409,7 @@ pub fn stderr_reader(buffer: BufReader<ChildStderr>, suffix: &str) -> Result<(),
Ok(()) Ok(())
} }
/// Run program to test if it is in system.
fn is_in_system(name: &str) { fn is_in_system(name: &str) {
if let Ok(mut proc) = Command::new(name) if let Ok(mut proc) = Command::new(name)
.stderr(Stdio::null()) .stderr(Stdio::null())
@ -407,6 +428,8 @@ fn is_in_system(name: &str) {
fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) { fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
let mut libs: Vec<String> = vec![]; let mut libs: Vec<String> = vec![];
let mut filters: Vec<String> = vec![]; let mut filters: Vec<String> = vec![];
// filter lines which contains filter
let re: Regex = Regex::new(r"^( ?) [TSC.]+").unwrap(); let re: Regex = Regex::new(r"^( ?) [TSC.]+").unwrap();
let mut ff_proc = match Command::new("ffmpeg") let mut ff_proc = match Command::new("ffmpeg")
@ -425,6 +448,8 @@ fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
let err_buffer = BufReader::new(ff_proc.stderr.take().unwrap()); let err_buffer = BufReader::new(ff_proc.stderr.take().unwrap());
let out_buffer = BufReader::new(ff_proc.stdout.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() { for line in err_buffer.lines() {
if let Ok(line) = line { if let Ok(line) = line {
if line.contains("configuration:") { if line.contains("configuration:") {
@ -439,6 +464,8 @@ fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
} }
} }
// stdout shows filter help text
// get filters
for line in out_buffer.lines() { for line in out_buffer.lines() {
if let Ok(line) = line { if let Ok(line) = line {
if let Some(_) = re.captures(line.as_str()) { if let Some(_) = re.captures(line.as_str()) {
@ -455,6 +482,10 @@ fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
(libs, filters) (libs, filters)
} }
/// Validate ffmpeg/ffprobe/ffplay.
///
/// Check if they are in system and has all filters and codecs we need.
pub fn validate_ffmpeg() { pub fn validate_ffmpeg() {
let config = GlobalConfig::global(); let config = GlobalConfig::global();