diff --git a/Cargo.lock b/Cargo.lock index 0bea58c6..15c8c4d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,16 +2,88 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ffplayout-engine-rs" version = "0.1.0" dependencies = [ + "chrono", + "clap", + "regex", "serde", "serde_yaml", ] @@ -22,6 +94,21 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "indexmap" version = "1.8.0" @@ -32,12 +119,82 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" + [[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -56,6 +213,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "ryu" version = "1.0.9" @@ -94,6 +268,12 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.86" @@ -105,12 +285,81 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 39230123..0f2604b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,9 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" +regex = "1" +chrono = "0.4" +clap = { version = "3.0.14", features = ["derive"] } [[bin]] name = "ffplayout" diff --git a/ffplayout.yml b/ffplayout.yml index 32efc2e7..b29eb285 100644 --- a/ffplayout.yml +++ b/ffplayout.yml @@ -11,10 +11,138 @@ mail: helptext: Send error messages to email address, like missing playlist; invalid json format; missing clip path. Leave recipient blank, if you don't need this. 'mail_level' can be WARNING or ERROR. - subject: Playout Error - smtp_server: mail.example.org + subject: "Playout Error" + smtp_server: "mail.example.org" smtp_port: 587 - sender_addr: ffplayout@example.org - sender_pass: abc123 - recipient: null - mail_level: ERROR + sender_addr: "ffplayout@example.org" + sender_pass: "abc123" + recipient: + mail_level: "ERROR" + +logging: + helptext: Logging to file, if 'log_to_file' False log to console. 'backup_count' + says how long log files will be saved in days. Path to /var/log/ only if you + run this program as daemon. 'log_level' can be DEBUG, INFO, WARNING, + ERROR. 'ffmpeg_level' can be INFO, WARNING, ERROR. + log_to_file: true + backup_count: 7 + log_path: "/var/log/ffplayout/" + log_level: "DEBUG" + ffmpeg_level: "ERROR" + +processing: + helptext: Set playing mode, like playlist; folder, or you own custom one. + Default processing, for all clips that they get prepared in that way, + so the output is unique. 'aspect' must be a float number. 'logo' is only used + if the path exist. 'logo_scale' scale the logo to target size, leave it blank + when no scaling is needed, format is 'number:number', for example '100:-1' + for proportional scaling. With 'logo_opacity' logo can become transparent. + With 'logo_filter' 'overlay=W-w-12:12' you can modify the logo position. + With 'use_loudnorm' you can activate single pass EBU R128 loudness normalization. + 'loud_*' can adjust the loudnorm filter. 'output_count' sets the outputs for + the filtering, > 1 gives the option to use the same filters for multiple outputs. + This outputs can be taken in 'stream_param', names will be vout2, vout3; + aout2, aout2 etc. + mode: playlist + width: 1024 + height: 576 + aspect: 1.778 + fps: 25 + add_logo: true + logo: "docs/logo.png" + logo_scale: + logo_opacity: 0.7 + logo_filter: "overlay=W-w-12:12" + add_loudnorm: false + loud_i: -18 + loud_tp: -1.5 + loud_lra: 11 + output_count: 1 + +ingest: + helptext: Works not with direct hls output, it always needs full processing! Run a server + for a ingest stream. This stream will override the normal streaming until is done. + There is no authentication, this is up to you. The recommend way is to set address to localhost, stream to a local server with authentication and from there stream to this app. + enable: false + stream_input: [-f, live_flv, -listen, 1, -i, rtmp://localhost:1936/live/stream] + +playlist: + helptext: > + 'path' can be a path to a single file, or a directory. For directory put + only the root folder, for example '/playlists', subdirectories are read by the + script. Subdirectories needs this structure '/playlists/2018/01'. 'day_start' + means at which time the playlist should start, leave day_start blank when playlist + should always start at the begin. 'length' represent the target length from + playlist, when is blank real length will not consider. 'loop true' works with + single playlist file and loops it infinitely. + path: "/playlists" + day_start: "5:59:25" + length: "24:00:00" + loop: false + +storage: + helptext: Play ordered or randomly files from path. 'filler_clip' is for fill + the end to reach 24 hours, it will loop when is necessary. 'extensions' search + only files with this extension. Set 'shuffle' to 'True' to pick files randomly. + path: "/mediaStorage" + filler_clip: "/mediaStorage/filler/filler.mp4" + extensions: + - ".mp4" + - ".mkv" + shuffle: true + +text: + helptext: Overlay text in combination with libzmq for remote text manipulation. + On windows fontfile path need to be like this 'C\:/WINDOWS/fonts/DejaVuSans.ttf'. + In a standard environment the filter drawtext node is Parsed_drawtext_2. + 'over_pre' if True text will be overlay in pre processing. Continue same text + over multiple files is in that mode not possible. 'text_from_filename' activate the + extraction from text of a filename. With 'style' you can define the drawtext + parameters like position, color, etc. Post Text over API will override this. + With 'regex' you can format file names, to get a title from it. + add_text: false + over_pre: false + bind_address: "127.0.0.1:5555" + fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" + text_from_filename: false + style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4" + regex: "^(.*)_" + +out: + helptext: The final playout compression. Set the settings to your needs. + 'mode' has the standard options 'desktop', 'hls', 'live_switch', 'stream'. Self made + outputs can be define, by adding script in output folder with an 'output' function + inside. 'preview' works only in streaming output and creates a separate preview stream. + mode: 'stream' + preview: false + preview_param: + [-s, 512x288, + -c:v, libx264, + -crf, 24, + -x264-params, keyint=50:min-keyint=25:scenecut=-1, + -maxrate, 800k, + -bufsize, 1600k, + -preset, ultrafast, + -tune, zerolatency, + -profile:v, Main, + -level, 3.1, + -c:a, aac, + -ar, 44100, + -b:a, 128k, + -flags, +global_header, + -f, flv, rtmp://preview.local/live/stream] + stream_param: + [-c:v, libx264, + -crf, 23, + -x264-params, keyint=50:min-keyint=25:scenecut=-1, + -maxrate, 1300k, + -bufsize, 2600k, + -preset, faster, + -tune, zerolatency, + -profile:v, Main, + -level, 3.1, + -c:a, aac, + -ar, 44100, + -b:a, 128k, + -flags, +global_header, + -f, flv, rtmp://localhost/live/stream] diff --git a/src/arg_parse.rs b/src/arg_parse.rs new file mode 100644 index 00000000..0780c491 --- /dev/null +++ b/src/arg_parse.rs @@ -0,0 +1,39 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[clap(version, about = "ffplayout, the rust playout solution", long_about = None)] +pub struct Args { + #[clap(short, long, help = "file path to ffplayout.conf")] + pub config: Option, + + #[clap(short, long, help = "play folder content")] + pub folder: Option, + + #[clap(short, long, help = "file path for logfile")] + pub log: Option, + + #[clap(short = 'i', long, help = "loop playlist infinitely")] + pub r#loop: bool, + + #[clap(short, long, + help = "set output mode: desktop, hls, stream")] + pub output: Option, + + #[clap(short, long, help = "path from playlist")] + pub playlist: Option, + + #[clap(short, long, help = "start time in 'hh:mm:ss', 'now' for start with first")] + pub start: Option, + + #[clap(short = 't', long, help = "set length in 'hh:mm:ss', 'none' for no length check")] + pub length: Option, + + #[clap(long, help = "playing mode: folder, playlist, custom...")] + pub play_mode: Option, +} + +pub fn get_args() -> Args { + let args = Args::parse(); + + args +} diff --git a/src/config_reader.rs b/src/config_reader.rs new file mode 100644 index 00000000..a16f9dec --- /dev/null +++ b/src/config_reader.rs @@ -0,0 +1,114 @@ +use serde::{Deserialize, Serialize}; +use serde_yaml::{self}; +use std::path::Path; +// use regex::Regex; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + pub general: General, + pub mail: Mail, + pub logging: Logging, + pub processing: Processing, + pub ingest: Ingest, + pub playlist: Playlist, + pub storage: Storage, + pub text: Text, + pub out: Out, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct General { + pub stop_threshold: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Mail { + pub subject: String, + pub smtp_server: String, + pub smtp_port: u32, + pub sender_addr: String, + pub sender_pass: String, + pub recipient: String, + pub mail_level: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Logging { + pub log_to_file: bool, + pub backup_count: u32, + pub log_path: String, + pub log_level: String, + pub ffmpeg_level: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Processing { + pub mode: String, + pub width: u32, + pub height: u32, + pub aspect: f32, + pub fps: u32, + pub add_logo: bool, + pub logo: String, + pub logo_scale: String, + pub logo_opacity: f32, + pub logo_filter: String, + pub add_loudnorm: bool, + pub loud_i: f32, + pub loud_tp: f32, + pub loud_lra: f32, + pub output_count: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Ingest { + pub enable: bool, + pub stream_input: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Playlist { + pub path: String, + pub day_start: String, + pub length: String, + pub r#loop: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Storage { + pub path: String, + pub filler_clip: String, + pub extensions: Vec, + pub shuffle: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Text { + pub add_text: bool, + pub over_pre: bool, + pub bind_address: String, + pub text_from_filename: bool, + pub style: String, + pub regex: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Out { + pub mode: String, + pub preview: bool, + pub preview_param: Vec, + pub stream_param: Vec, +} + +pub fn read_yaml() -> Config { + let mut config_path: String = "ffplayout.yml".to_string(); + + if Path::new("/etc/ffplayout/ffplayout.yml").exists() { + config_path = "/etc/ffplayout/ffplayout.yml".to_string(); + } + + let f = std::fs::File::open(config_path).expect("Could not open file."); + let config: Config = serde_yaml::from_reader(f).expect("Could not read config file."); + + config +} diff --git a/src/main.rs b/src/main.rs index 3a3ccf23..36afabe5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,17 @@ +mod arg_parse; +mod config_reader; mod utils; fn main() { - let config = utils::read_yaml(); + //println!("{:#?}", utils::Mail()); + let config = config_reader::read_yaml(); + let args = arg_parse::get_args(); println!("{:#?}", config); + println!("{:#?}", args); + println!("{:#?}", args.config.is_some()); + println!("{:#?}", args.config.unwrap()); //println!("{:?}", config.general.stop_threshold); + + println!("{:#?}", utils::get_sec()); + println!("{:#?}", utils::get_timestamp()); } diff --git a/src/utils.rs b/src/utils.rs index 91163c09..5c3cd67e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,33 +1,17 @@ -use serde::{Deserialize, Serialize}; -use serde_yaml::{self}; +use chrono::prelude::*; -#[derive(Debug, Serialize, Deserialize)] -pub struct Config { - pub general: General, - pub mail: Mail, +pub fn get_sec() -> f64 { + let local: DateTime = Local::now(); + + let sec = ( + local.hour() * 3600 + local.minute() * 60 + local.second() + ) as f64 + (local.nanosecond() as f64 / 1000000000.0); + + sec } -#[derive(Debug, Serialize, Deserialize)] -pub struct General { - pub helptext: String, - pub stop_threshold: u32, -} +pub fn get_timestamp() -> i64 { + let local: DateTime = Local::now(); -#[derive(Debug, Serialize, Deserialize)] -pub struct Mail { - pub helptext: String, - pub subject: String, - pub smtp_server: String, - pub smtp_port: u32, - pub sender_addr: String, - pub sender_pass: String, - pub recipient: String, - pub mail_level: String, -} - -pub fn read_yaml() -> Config { - let f = std::fs::File::open("ffplayout.yml").expect("Could not open file."); - let config: Config = serde_yaml::from_reader(f).expect("Could not read values."); - - config + local.timestamp_millis() as i64 }