start with documentations
This commit is contained in:
parent
03bb49c0b8
commit
17ee86afc1
43
src/main.rs
43
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);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>>,
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user