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;
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);
}

View File

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

View File

@ -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<String> = 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);
}

View File

@ -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<Mutex<Option<Child>>>,
@ -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<Mutex<Option<Media>>>,
@ -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<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::{
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<String>) -> Vec<String> {
let mut range = vec![];
let start;
@ -46,6 +57,7 @@ fn get_date_range(date_range: &Vec<String>) -> Vec<String> {
range
}
/// Generate playlists
pub fn generate_playlist(mut date_range: Vec<String>) {
let config = GlobalConfig::global();
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;
/// 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<String>,
is_terminated: Arc<AtomicBool>,
@ -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);

View File

@ -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<AtomicBool>, config: GlobalConfig) {
let date = playlist.date;
let mut length = config.playlist.length_sec.unwrap();

View File

@ -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<Mutex<Vec<String>>>, 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<Mutex<Vec<String>>>, 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<Box<dyn SharedLogger>> {
let config = GlobalConfig::global();
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(".") {
let messages: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let messages_clone = messages.clone();

View File

@ -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<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) {
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> = 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> = 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<DateTime<Local>> {
let metadata = metadata(path).unwrap();
@ -238,6 +250,7 @@ pub fn modified_time(path: &str) -> Option<DateTime<Local>> {
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<String>) {
let config = GlobalConfig::global();
let color = "#121212";
@ -334,6 +356,7 @@ pub fn gen_dummy(duration: f64) -> (String, Vec<String>) {
(source, cmd)
}
/// Set clip seek in and length value.
pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec<String> {
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
}
/// Read ffmpeg stderr decoder, encoder and server instance
/// and log the output.
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 {
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<ChildStderr>, 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<String>, Vec<String>) {
let mut libs: Vec<String> = vec![];
let mut filters: Vec<String> = 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<String>, Vec<String>) {
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<String>, Vec<String>) {
}
}
// 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<String>, Vec<String>) {
(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();