From d077f5aa09de124bc6f62d26938eb68b6bd63b75 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 21 Apr 2024 21:12:02 +0200 Subject: [PATCH 01/22] add vs code settings --- .gitignore | 1 - .vscode/extensions.json | 7 +++++++ .vscode/settings.json | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index f70d6280..ae60ba83 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,4 @@ ffpapi.1.gz /dist/ /public/ tmp/ -.vscode/ assets/playlist_template.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..f4fb1d4f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "statiolake.vscode-rustfmt", + "tamasfe.even-better-toml", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..766d5939 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "rust-analyzer.cargo.target": null, + "rust-analyzer.checkOnSave": true, + "rust-analyzer.cargo.buildScripts.overrideCommand": null, + "rust-analyzer.rustfmt.overrideCommand": null, + "rust-analyzer.inlayHints.chainingHints.enable": false, + "rust-analyzer.inlayHints.parameterHints.enable": false, + "rust-analyzer.inlayHints.typeHints.enable": false, + "rust-analyzer.diagnostics.disabled": ["unresolved-proc-macro"], + "[dockercompose]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[rust]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "statiolake.vscode-rustfmt" + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} From 586a5a91e6fa9071470c8e945f5b89ece4856ae7 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 21 Apr 2024 21:23:06 +0200 Subject: [PATCH 02/22] catch remote playlist error and get empty playlist --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- lib/src/utils/json_serializer.rs | 10 ++++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60c69ff6..9739e0ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1217,7 +1217,7 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "ffplayout" -version = "0.21.2" +version = "0.21.3" dependencies = [ "chrono", "clap", @@ -1239,7 +1239,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.21.2" +version = "0.21.3" dependencies = [ "actix-files", "actix-multipart", @@ -1278,7 +1278,7 @@ dependencies = [ [[package]] name = "ffplayout-lib" -version = "0.21.2" +version = "0.21.3" dependencies = [ "chrono", "crossbeam-channel", @@ -3458,7 +3458,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.21.2" +version = "0.21.3" dependencies = [ "chrono", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 3c0f8742..15993ce8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"] resolver = "2" [workspace.package] -version = "0.21.2" +version = "0.21.3" license = "GPL-3.0" repository = "https://github.com/ffplayout/ffplayout" authors = ["Jonathan Baecker "] diff --git a/lib/src/utils/json_serializer.rs b/lib/src/utils/json_serializer.rs index a486a7d3..6e620352 100644 --- a/lib/src/utils/json_serializer.rs +++ b/lib/src/utils/json_serializer.rs @@ -126,8 +126,14 @@ pub fn read_json( let headers = resp.headers().clone(); if let Ok(body) = resp.text() { - let mut playlist: JsonPlaylist = - serde_json::from_str(&body).expect("Could't read remote json playlist."); + let mut playlist: JsonPlaylist = match serde_json::from_str(&body) { + Ok(p) => p, + Err(e) => { + error!("Could't read remote json playlist. {e:?}"); + JsonPlaylist::new(date.clone(), start_sec) + } + }; + playlist.path = Some(current_file); playlist.start_sec = Some(start_sec); From 5855eed6915b6eed4db64e0f47249f82a6388e4c Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 21 Apr 2024 21:23:42 +0200 Subject: [PATCH 03/22] update frontend --- ffplayout-frontend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffplayout-frontend b/ffplayout-frontend index 56db578d..52411f61 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 56db578d8cf69aee7fc573828016fa728315bf79 +Subproject commit 52411f61ef25a1b11129a77d26d987f44c6ea543 From 12086904cd3ba118d0816cbf834d7e0a90ea2fa7 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 22 Apr 2024 15:06:00 +0200 Subject: [PATCH 04/22] ignore advanced config in tests, support advanced config path as argument, support advanced config on null output --- ffplayout-api/src/main.rs | 2 +- ffplayout-api/src/utils/channels.rs | 7 +- ffplayout-engine/src/input/ingest.rs | 8 +- ffplayout-engine/src/output/desktop.rs | 8 +- ffplayout-engine/src/output/hls.rs | 14 +- ffplayout-engine/src/output/mod.rs | 17 +- ffplayout-engine/src/output/null.rs | 20 ++- ffplayout-engine/src/output/stream.rs | 8 +- ffplayout-engine/src/utils/arg_parse.rs | 3 + ffplayout-engine/src/utils/mod.rs | 15 +- lib/src/filter/mod.rs | 209 ++++++++++++++++++------ lib/src/filter/v_drawtext.rs | 15 +- lib/src/lib.rs | 8 - lib/src/utils/advanced_config.rs | 19 +-- lib/src/utils/config.rs | 21 ++- lib/src/utils/json_validate.rs | 7 +- tests/src/engine_playlist.rs | 18 +- 17 files changed, 271 insertions(+), 128 deletions(-) diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index 094f7e12..769cdffc 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -65,7 +65,7 @@ async fn validator( #[actix_web::main] async fn main() -> std::io::Result<()> { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.mail.recipient = String::new(); config.logging.log_to_file = false; config.logging.timestamp = false; diff --git a/ffplayout-api/src/utils/channels.rs b/ffplayout-api/src/utils/channels.rs index 0437536b..e3ae52c0 100644 --- a/ffplayout-api/src/utils/channels.rs +++ b/ffplayout-api/src/utils/channels.rs @@ -31,9 +31,10 @@ pub async fn create_channel( Err(_) => rand::thread_rng().gen_range(71..99), }; - let mut config = PlayoutConfig::new(Some(PathBuf::from( - "/usr/share/ffplayout/ffplayout.yml.orig", - ))); + let mut config = PlayoutConfig::new( + Some(PathBuf::from("/usr/share/ffplayout/ffplayout.yml.orig")), + None, + ); config.general.stat_file = format!(".ffp_{channel_name}",); config.logging.path = config.logging.path.join(&channel_name); diff --git a/ffplayout-engine/src/input/ingest.rs b/ffplayout-engine/src/input/ingest.rs index 85ea600d..139272a5 100644 --- a/ffplayout-engine/src/input/ingest.rs +++ b/ffplayout-engine/src/input/ingest.rs @@ -14,7 +14,7 @@ use ffplayout_lib::{ controller::ProcessUnit::*, test_tcp_port, Media, PlayoutConfig, ProcessControl, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, }, - vec_strings, ADVANCED_CONFIG, + vec_strings, }; fn server_monitor( @@ -64,7 +64,11 @@ pub fn ingest_server( dummy_media.unit = Ingest; dummy_media.add_filter(&config, &None); - if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.ingest.input_cmd { + if let Some(ingest_input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.ingest.input_cmd.clone()) + { server_cmd.append(&mut ingest_input_cmd.clone()); } diff --git a/ffplayout-engine/src/output/desktop.rs b/ffplayout-engine/src/output/desktop.rs index 6fe59bdd..b76054d2 100644 --- a/ffplayout-engine/src/output/desktop.rs +++ b/ffplayout-engine/src/output/desktop.rs @@ -2,7 +2,7 @@ use std::process::{self, Command, Stdio}; use simplelog::*; -use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings, ADVANCED_CONFIG}; +use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings}; /// Desktop Output /// @@ -12,7 +12,11 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format]; - if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd { + if let Some(encoder_input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.encoder.input_cmd.clone()) + { enc_cmd.append(&mut encoder_input_cmd.clone()); } diff --git a/ffplayout-engine/src/output/hls.rs b/ffplayout-engine/src/output/hls.rs index 7c6e118b..413dde97 100644 --- a/ffplayout-engine/src/output/hls.rs +++ b/ffplayout-engine/src/output/hls.rs @@ -34,7 +34,7 @@ use ffplayout_lib::{ controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media, PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl, }, - vec_strings, ADVANCED_CONFIG, + vec_strings, }; /// Ingest Server for HLS @@ -50,7 +50,11 @@ fn ingest_to_hls_server( let mut dummy_media = Media::new(0, "Live Stream", false); dummy_media.unit = Ingest; - if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.ingest.input_cmd { + if let Some(ingest_input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.ingest.input_cmd.clone()) + { server_prefix.append(&mut ingest_input_cmd.clone()); } @@ -203,7 +207,11 @@ pub fn write_hls( let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format]; - if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd { + if let Some(encoder_input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.encoder.input_cmd.clone()) + { enc_prefix.append(&mut encoder_input_cmd.clone()); } diff --git a/ffplayout-engine/src/output/mod.rs b/ffplayout-engine/src/output/mod.rs index d8fa0723..81429f74 100644 --- a/ffplayout-engine/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -19,14 +19,11 @@ pub use hls::write_hls; use crate::input::{ingest_server, source_generator}; use crate::utils::task_runner; -use ffplayout_lib::vec_strings; -use ffplayout_lib::{ - utils::{ - sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus, - ProcessControl, ProcessUnit::*, - }, - ADVANCED_CONFIG, +use ffplayout_lib::utils::{ + sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus, + ProcessControl, ProcessUnit::*, }; +use ffplayout_lib::vec_strings; /// Player /// @@ -147,7 +144,11 @@ pub fn player( let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format]; - if let Some(decoder_input_cmd) = &ADVANCED_CONFIG.decoder.input_cmd { + if let Some(decoder_input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.decoder.input_cmd.clone()) + { dec_cmd.append(&mut decoder_input_cmd.clone()); } diff --git a/ffplayout-engine/src/output/null.rs b/ffplayout-engine/src/output/null.rs index 9ca04396..7dfaefba 100644 --- a/ffplayout-engine/src/output/null.rs +++ b/ffplayout-engine/src/output/null.rs @@ -16,15 +16,17 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { media.unit = Encoder; media.add_filter(config, &None); - let enc_prefix = vec_strings![ - "-hide_banner", - "-nostats", - "-v", - log_format, - "-re", - "-i", - "pipe:0" - ]; + let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", log_format]; + + if let Some(input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.encoder.input_cmd.clone()) + { + enc_prefix.append(&mut input_cmd.clone()); + } + + enc_prefix.append(&mut vec_strings!["-re", "-i", "pipe:0"]); let enc_cmd = prepare_output_cmd(config, enc_prefix, &media.filter); diff --git a/ffplayout-engine/src/output/stream.rs b/ffplayout-engine/src/output/stream.rs index a9c1e66e..dda5060d 100644 --- a/ffplayout-engine/src/output/stream.rs +++ b/ffplayout-engine/src/output/stream.rs @@ -5,7 +5,7 @@ use simplelog::*; use crate::utils::prepare_output_cmd; use ffplayout_lib::{ utils::{Media, PlayoutConfig, ProcessUnit::*}, - vec_strings, ADVANCED_CONFIG, + vec_strings, }; /// Streaming Output @@ -18,7 +18,11 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", log_format]; - if let Some(input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd { + if let Some(input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.encoder.input_cmd.clone()) + { enc_prefix.append(&mut input_cmd.clone()); } diff --git a/ffplayout-engine/src/utils/arg_parse.rs b/ffplayout-engine/src/utils/arg_parse.rs index 0aa43f09..975e39dd 100644 --- a/ffplayout-engine/src/utils/arg_parse.rs +++ b/ffplayout-engine/src/utils/arg_parse.rs @@ -11,6 +11,9 @@ use ffplayout_lib::utils::{OutputMode, ProcessMode}; \n ffplayout (ARGS) [OPTIONS]\n\n Pass channel name only in multi channel environment!", long_about = None)] pub struct Args { + #[clap(long, help = "File path to advanced.yml")] + pub advanced_config: Option, + #[clap(index = 1, value_parser, help = "Channel name")] pub channel: Option, diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index dfed8bfc..5a464d72 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -1,4 +1,5 @@ use std::{ + env, fs::File, path::{Path, PathBuf}, }; @@ -37,7 +38,19 @@ pub fn get_config(args: Args) -> Result { None => args.config, }; - let mut config = PlayoutConfig::new(cfg_path); + let mut adv_config_path = PathBuf::from("/etc/ffplayout/advanced.yml"); + + if let Some(adv_path) = args.advanced_config { + adv_config_path = adv_path; + } else if !adv_config_path.is_file() { + if Path::new("./assets/advanced.yml").is_file() { + adv_config_path = PathBuf::from("./assets/advanced.yml") + } else if let Some(p) = env::current_exe().ok().as_ref().and_then(|op| op.parent()) { + adv_config_path = p.join("advanced.yml") + }; + } + + let mut config = PlayoutConfig::new(cfg_path, Some(adv_config_path)); if let Some(gen) = args.generate { config.general.generate = Some(gen); diff --git a/lib/src/filter/mod.rs b/lib/src/filter/mod.rs index e56dc501..78d8298c 100644 --- a/lib/src/filter/mod.rs +++ b/lib/src/filter/mod.rs @@ -14,7 +14,6 @@ use crate::utils::{ controller::ProcessUnit::*, custom_format, fps_calc, is_close, Media, OutputMode::*, PlayoutConfig, }; -use crate::ADVANCED_CONFIG; use super::vec_strings; @@ -179,15 +178,19 @@ impl Filters { impl Default for Filters { fn default() -> Self { - Self::new(PlayoutConfig::new(None), 0) + Self::new(PlayoutConfig::new(None, None), 0) } } -fn deinterlace(field_order: &Option, chain: &mut Filters) { +fn deinterlace(field_order: &Option, chain: &mut Filters, config: &PlayoutConfig) { if let Some(order) = field_order { if order != "progressive" { - let deinterlace = match &ADVANCED_CONFIG.decoder.filters.deinterlace { - Some(deinterlace) => deinterlace.clone(), + let deinterlace = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.deinterlace.clone()) + { + Some(deinterlace) => deinterlace, None => "yadif=0:-1:0".to_string(), }; @@ -202,14 +205,22 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl if let (Some(w), Some(h)) = (v_stream.width, v_stream.height) { if w > config.processing.width && aspect > config.processing.aspect { - scale = match &ADVANCED_CONFIG.decoder.filters.pad_scale_w { + scale = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.pad_scale_w.clone()) + { Some(pad_scale_w) => { custom_format(&format!("{pad_scale_w},"), &[&config.processing.width]) } None => format!("scale={}:-1,", config.processing.width), }; } else if h > config.processing.height && aspect < config.processing.aspect { - scale = match &ADVANCED_CONFIG.decoder.filters.pad_scale_h { + scale = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.pad_scale_h.clone()) + { Some(pad_scale_h) => { custom_format(&format!("{pad_scale_h},"), &[&config.processing.width]) } @@ -218,7 +229,11 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl } } - let pad = match &ADVANCED_CONFIG.decoder.filters.pad_video { + let pad = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.pad_video.clone()) + { Some(pad_video) => custom_format( &format!("{scale}{pad_video}"), &[ @@ -238,8 +253,12 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) { if fps != config.processing.fps { - let fps_filter = match &ADVANCED_CONFIG.decoder.filters.fps { - Some(fps) => custom_format(fps, &[&config.processing.fps]), + let fps_filter = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.fps.clone()) + { + Some(fps) => custom_format(&fps, &[&config.processing.fps]), None => format!("fps={}", config.processing.fps), }; @@ -257,9 +276,13 @@ fn scale( // width: i64, height: i64 if let (Some(w), Some(h)) = (width, height) { if w != config.processing.width || h != config.processing.height { - let scale = match &ADVANCED_CONFIG.decoder.filters.scale { + let scale = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.scale.clone()) + { Some(scale) => custom_format( - scale, + &scale, &[&config.processing.width, &config.processing.height], ), None => format!( @@ -274,17 +297,25 @@ fn scale( } if !is_close(aspect, config.processing.aspect, 0.03) { - let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar { - Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]), + let dar = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.set_dar.clone()) + { + Some(set_dar) => custom_format(&set_dar, &[&config.processing.aspect]), None => format!("setdar=dar={}", config.processing.aspect), }; chain.add_filter(&dar, 0, Video); } } else { - let scale = match &ADVANCED_CONFIG.decoder.filters.scale { + let scale = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.scale.clone()) + { Some(scale) => custom_format( - scale, + &scale, &[&config.processing.width, &config.processing.height], ), None => format!( @@ -294,8 +325,12 @@ fn scale( }; chain.add_filter(&scale, 0, Video); - let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar { - Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]), + let dar = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.set_dar.clone()) + { + Some(set_dar) => custom_format(&set_dar, &[&config.processing.aspect]), None => format!("setdar=dar={}", config.processing.aspect), }; @@ -303,7 +338,13 @@ fn scale( } } -fn fade(node: &mut Media, chain: &mut Filters, nr: i32, filter_type: FilterType) { +fn fade( + node: &mut Media, + chain: &mut Filters, + nr: i32, + filter_type: FilterType, + config: &PlayoutConfig, +) { let mut t = ""; let mut fade_audio = false; @@ -319,11 +360,19 @@ fn fade(node: &mut Media, chain: &mut Filters, nr: i32, filter_type: FilterType) let mut fade_in = format!("{t}fade=in:st=0:d=0.5"); if t == "a" { - if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.afade_in { - fade_in = custom_format(fade, &[t]); + if let Some(fade) = config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.afade_in.clone()) + { + fade_in = custom_format(&fade, &[t]); } - } else if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.fade_in { - fade_in = custom_format(fade, &[t]); + } else if let Some(fade) = config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.fade_in.clone()) + { + fade_in = custom_format(&fade, &[t]); }; chain.add_filter(&fade_in, nr, filter_type); @@ -333,11 +382,20 @@ fn fade(node: &mut Media, chain: &mut Filters, nr: i32, filter_type: FilterType) let mut fade_out = format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0)); if t == "a" { - if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.afade_out { - fade_out = custom_format(fade, &[t]); + if let Some(fade) = config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.afade_out.clone()) + { + fade_out = custom_format(&fade, &[node.out - node.seek - 1.0]); } - } else if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.fade_out { - fade_out = custom_format(fade, &[t]); + } else if let Some(fade) = config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.fade_out.clone()) + .clone() + { + fade_out = custom_format(&fade, &[node.out - node.seek - 1.0]); }; chain.add_filter(&fade_out, nr, filter_type); @@ -360,7 +418,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { ); if node.last_ad { - match &ADVANCED_CONFIG.decoder.filters.overlay_logo_fade_in { + match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.overlay_logo_fade_in.clone()) + { Some(fade_in) => logo_chain.push_str(&format!(",{fade_in}")), None => logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1"), }; @@ -369,7 +431,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { if node.next_ad { let length = node.out - node.seek - 1.0; - match &ADVANCED_CONFIG.decoder.filters.overlay_logo_fade_out { + match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.overlay_logo_fade_out.clone()) + { Some(fade_out) => { logo_chain.push_str(&custom_format(&format!(",{fade_out}"), &[length])) } @@ -378,7 +444,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { } if !config.processing.logo_scale.is_empty() { - match &ADVANCED_CONFIG.decoder.filters.overlay_logo_scale { + match &config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.overlay_logo_scale.clone()) + { Some(logo_scale) => logo_chain.push_str(&custom_format( &format!(",{logo_scale}"), &[&config.processing.logo_scale], @@ -387,13 +457,20 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { } } - match &ADVANCED_CONFIG.decoder.filters.overlay_logo { + match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.overlay_logo.clone()) + { Some(overlay) => { if !overlay.starts_with(',') { logo_chain.push(','); } - logo_chain.push_str(&custom_format(overlay, &[&config.processing.logo_position])) + logo_chain.push_str(&custom_format( + &overlay, + &[&config.processing.logo_position], + )) } None => logo_chain.push_str(&format!( "[l];[v][l]overlay={}:shortest=1", @@ -405,7 +482,7 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { } } -fn extend_video(node: &mut Media, chain: &mut Filters) { +fn extend_video(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { if let Some(video_duration) = node .probe .as_ref() @@ -416,8 +493,12 @@ fn extend_video(node: &mut Media, chain: &mut Filters) { if node.out - node.seek > video_duration - node.seek + 0.1 && node.duration >= node.out { let duration = (node.out - node.seek) - (video_duration - node.seek); - let tpad = match &ADVANCED_CONFIG.decoder.filters.tpad { - Some(pad) => custom_format(pad, &[duration]), + let tpad = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.tpad.clone()) + { + Some(pad) => custom_format(&pad, &[duration]), None => format!("tpad=stop_mode=add:stop_duration={duration}"), }; @@ -442,9 +523,13 @@ fn add_text( } } -fn add_audio(node: &Media, chain: &mut Filters, nr: i32) { - let audio = match &ADVANCED_CONFIG.decoder.filters.aevalsrc { - Some(aevalsrc) => custom_format(aevalsrc, &[node.out - node.seek]), +fn add_audio(node: &Media, chain: &mut Filters, nr: i32, config: &PlayoutConfig) { + let audio = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.aevalsrc.clone()) + { + Some(aevalsrc) => custom_format(&aevalsrc, &[node.out - node.seek]), None => format!( "aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000", node.out - node.seek @@ -454,7 +539,7 @@ fn add_audio(node: &Media, chain: &mut Filters, nr: i32) { chain.add_filter(&audio, nr, Audio); } -fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) { +fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32, config: &PlayoutConfig) { if !Path::new(&node.audio).is_file() { if let Some(audio_duration) = node .probe @@ -465,8 +550,12 @@ fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) { { if node.out - node.seek > audio_duration - node.seek + 0.1 && node.duration >= node.out { - let apad = match &ADVANCED_CONFIG.decoder.filters.apad { - Some(apad) => custom_format(apad, &[node.out - node.seek]), + let apad = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.apad.clone()) + { + Some(apad) => custom_format(&apad, &[node.out - node.seek]), None => format!("apad=whole_dur={}", node.out - node.seek), }; @@ -478,8 +567,12 @@ fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) { fn audio_volume(chain: &mut Filters, config: &PlayoutConfig, nr: i32) { if config.processing.volume != 1.0 { - let volume = match &ADVANCED_CONFIG.decoder.filters.volume { - Some(volume) => custom_format(volume, &[config.processing.volume]), + let volume = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.volume.clone()) + { + Some(volume) => custom_format(&volume, &[config.processing.volume]), None => format!("volume={}", config.processing.volume), }; @@ -500,7 +593,13 @@ fn aspect_calc(aspect_string: &Option, config: &PlayoutConfig) -> f64 { source_aspect } -pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: FilterType) { +pub fn split_filter( + chain: &mut Filters, + count: usize, + nr: i32, + filter_type: FilterType, + config: &PlayoutConfig, +) { if count > 1 { let out_link = match filter_type { Audio => &mut chain.audio_out_link, @@ -514,8 +613,12 @@ pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: Fil } } - let split = match &ADVANCED_CONFIG.decoder.filters.split { - Some(split) => custom_format(split, &[count.to_string(), out_link.join("")]), + let split = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.split.clone()) + { + Some(split) => custom_format(&split, &[count.to_string(), out_link.join("")]), None => format!("split={count}{}", out_link.join("")), }; @@ -582,7 +685,7 @@ pub fn filter_chains( if let Some(f) = config.out.output_filter.clone() { process_output_filters(config, &mut filters, &f) } else if config.out.output_count > 1 && !config.processing.audio_only { - split_filter(&mut filters, config.out.output_count, 0, Video); + split_filter(&mut filters, config.out.output_count, 0, Video, config); } return filters; @@ -598,7 +701,7 @@ pub fn filter_chains( let aspect = aspect_calc(&v_stream.display_aspect_ratio, config); let frame_per_sec = fps_calc(&v_stream.r_frame_rate, 1.0); - deinterlace(&v_stream.field_order, &mut filters); + deinterlace(&v_stream.field_order, &mut filters, config); pad(aspect, &mut filters, v_stream, config); fps(frame_per_sec, &mut filters, config); scale( @@ -610,14 +713,14 @@ pub fn filter_chains( ); } - extend_video(node, &mut filters); + extend_video(node, &mut filters, config); } else { fps(0.0, &mut filters, config); scale(None, None, 1.0, &mut filters, config); } add_text(node, &mut filters, config, filter_chain); - fade(node, &mut filters, 0, Video); + fade(node, &mut filters, 0, Video, config); overlay(node, &mut filters, config); } @@ -653,7 +756,7 @@ pub fn filter_chains( .is_some() || Path::new(&node.audio).is_file() { - extend_audio(node, &mut filters, i); + extend_audio(node, &mut filters, i, config); } else if node.unit == Decoder { if !node.source.contains("color=c=") { warn!( @@ -662,14 +765,14 @@ pub fn filter_chains( ); } - add_audio(node, &mut filters, i); + add_audio(node, &mut filters, i, config); } // add at least anull filter, for correct filter construction, // is important for split filter in HLS mode filters.add_filter("anull", i, Audio); - fade(node, &mut filters, i, Audio); + fade(node, &mut filters, i, Audio, config); audio_volume(&mut filters, config, i); custom(&proc_af, &mut filters, i, Audio); diff --git a/lib/src/filter/v_drawtext.rs b/lib/src/filter/v_drawtext.rs index 1b655de8..8da18173 100644 --- a/lib/src/filter/v_drawtext.rs +++ b/lib/src/filter/v_drawtext.rs @@ -7,7 +7,6 @@ use std::{ use regex::Regex; use crate::utils::{controller::ProcessUnit::*, custom_format, Media, PlayoutConfig}; -use crate::ADVANCED_CONFIG; pub fn filter_node( config: &PlayoutConfig, @@ -45,7 +44,11 @@ pub fn filter_node( .replace('%', "\\\\\\%") .replace(':', "\\:"); - filter = match &ADVANCED_CONFIG.decoder.filters.drawtext_from_file { + filter = match &config + .advanced + .clone() + .and_then(|a| a.decoder.filters.drawtext_from_file) + { Some(drawtext) => custom_format(drawtext, &[&escaped_text, &config.text.style, &font]), None => format!("drawtext=text='{escaped_text}':{}{font}", config.text.style), }; @@ -58,8 +61,12 @@ pub fn filter_node( } } - filter = match &ADVANCED_CONFIG.decoder.filters.drawtext_from_zmq { - Some(drawtext) => custom_format(drawtext, &[&socket.replace(':', "\\:"), &filter_cmd]), + filter = match config + .advanced + .as_ref() + .and_then(|a| a.decoder.filters.drawtext_from_zmq.clone()) + { + Some(drawtext) => custom_format(&drawtext, &[&socket.replace(':', "\\:"), &filter_cmd]), None => format!( "zmq=b=tcp\\\\://'{}',drawtext@dyntext={filter_cmd}", socket.replace(':', "\\:") diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a3b6350f..c259a337 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,16 +1,8 @@ -use std::sync::Arc; - extern crate log; extern crate simplelog; -use lazy_static::lazy_static; - pub mod filter; pub mod macros; pub mod utils; use utils::advanced_config::AdvancedConfig; - -lazy_static! { - pub static ref ADVANCED_CONFIG: Arc = Arc::new(AdvancedConfig::new()); -} diff --git a/lib/src/utils/advanced_config.rs b/lib/src/utils/advanced_config.rs index 575fdd2f..f60481c0 100644 --- a/lib/src/utils/advanced_config.rs +++ b/lib/src/utils/advanced_config.rs @@ -1,8 +1,4 @@ -use std::{ - env, - fs::File, - path::{Path, PathBuf}, -}; +use std::{fs::File, path::PathBuf}; use serde::{Deserialize, Serialize}; use shlex::split; @@ -67,19 +63,10 @@ pub struct Filters { } impl AdvancedConfig { - pub fn new() -> Self { + pub fn new(cfg_path: PathBuf) -> Self { let mut config: AdvancedConfig = Default::default(); - let mut config_path = PathBuf::from("/etc/ffplayout/advanced.yml"); - if !config_path.is_file() { - if Path::new("./assets/advanced.yml").is_file() { - config_path = PathBuf::from("./assets/advanced.yml") - } else if let Some(p) = env::current_exe().ok().as_ref().and_then(|op| op.parent()) { - config_path = p.join("advanced.yml") - }; - } - - if let Ok(f) = File::open(&config_path) { + if let Ok(f) = File::open(&cfg_path) { config = match serde_yaml::from_reader(f) { Ok(yaml) => yaml, Err(_) => AdvancedConfig::default(), diff --git a/lib/src/utils/config.rs b/lib/src/utils/config.rs index 52fa0f10..f7a587ce 100644 --- a/lib/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -11,7 +11,7 @@ use log::LevelFilter; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use shlex::split; -use crate::ADVANCED_CONFIG; +use crate::AdvancedConfig; use super::vec_strings; use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*}; @@ -145,6 +145,8 @@ pub struct Source { /// This we init ones, when ffplayout is starting and use them globally in the hole program. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PlayoutConfig { + #[serde(default, skip_serializing, skip_deserializing)] + pub advanced: Option, pub general: General, pub rpc_server: RpcServer, pub mail: Mail, @@ -362,7 +364,7 @@ fn default_channels() -> u8 { impl PlayoutConfig { /// Read config from YAML file, and set some extra config values. - pub fn new(cfg_path: Option) -> Self { + pub fn new(cfg_path: Option, advanced_path: Option) -> Self { let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); if let Some(cfg) = cfg_path { @@ -389,6 +391,11 @@ impl PlayoutConfig { let mut config: PlayoutConfig = serde_yaml::from_reader(f).expect("Could not read config file."); + + if let Some(adv_path) = advanced_path { + config.advanced = Some(AdvancedConfig::new(adv_path)) + } + config.general.generate = None; config.general.config_path = config_path.to_string_lossy().to_string(); @@ -430,12 +437,16 @@ impl PlayoutConfig { } let mut process_cmd = vec_strings![]; + let advanced_output_cmd = config + .advanced + .as_ref() + .and_then(|a| a.decoder.output_cmd.clone()); if config.processing.audio_only { process_cmd.append(&mut vec_strings!["-vn"]); } else if config.processing.copy_video { process_cmd.append(&mut vec_strings!["-c:v", "copy"]); - } else if let Some(decoder_cmd) = &ADVANCED_CONFIG.decoder.output_cmd { + } else if let Some(decoder_cmd) = &advanced_output_cmd { process_cmd.append(&mut decoder_cmd.clone()); } else { let bitrate = format!( @@ -470,7 +481,7 @@ impl PlayoutConfig { if config.processing.copy_audio { process_cmd.append(&mut vec_strings!["-c:a", "copy"]); - } else if ADVANCED_CONFIG.decoder.output_cmd.is_none() { + } else if advanced_output_cmd.is_none() { process_cmd.append(&mut pre_audio_codec( &config.processing.custom_filter, &config.ingest.custom_filter, @@ -530,7 +541,7 @@ impl PlayoutConfig { impl Default for PlayoutConfig { fn default() -> Self { - Self::new(None) + Self::new(None, None) } } diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index d66d27e3..f98bdacf 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -17,7 +17,6 @@ use crate::utils::{ JsonPlaylist, Media, OutputMode::Null, PlayerControl, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT, }; -use crate::ADVANCED_CONFIG; /// Validate a single media file. /// @@ -38,7 +37,11 @@ fn check_media( let mut process_length = 0.1; - if let Some(decoder_input_cmd) = &ADVANCED_CONFIG.decoder.input_cmd { + if let Some(decoder_input_cmd) = config + .advanced + .as_ref() + .and_then(|a| a.decoder.input_cmd.clone()) + { dec_cmd.append(&mut decoder_input_cmd.clone()); } diff --git a/tests/src/engine_playlist.rs b/tests/src/engine_playlist.rs index 94a0d796..4530b001 100644 --- a/tests/src/engine_playlist.rs +++ b/tests/src/engine_playlist.rs @@ -20,7 +20,7 @@ fn timed_stop(sec: u64, proc_ctl: ProcessControl) { #[test] #[ignore] fn test_gen_source() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; @@ -76,7 +76,7 @@ fn test_gen_source() { 100, ); - assert_eq!(valid_media.out, 1.9); + assert_eq!(valid_media.out, 1.2); let mut no_valid_source_with_probe = Media::new(0, "assets/media_mix/av_snc.mp4", true); no_valid_source_with_probe.duration = 30.0; @@ -97,7 +97,7 @@ fn test_gen_source() { #[serial] #[ignore] fn playlist_missing() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; @@ -140,7 +140,7 @@ fn playlist_missing() { #[serial] #[ignore] fn playlist_next_missing() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; @@ -183,7 +183,7 @@ fn playlist_next_missing() { #[serial] #[ignore] fn playlist_to_short() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; @@ -226,7 +226,7 @@ fn playlist_to_short() { #[serial] #[ignore] fn playlist_init_after_list_end() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; @@ -269,7 +269,7 @@ fn playlist_init_after_list_end() { #[serial] #[ignore] fn playlist_change_at_midnight() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; @@ -312,7 +312,7 @@ fn playlist_change_at_midnight() { #[serial] #[ignore] fn playlist_change_before_midnight() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; @@ -355,7 +355,7 @@ fn playlist_change_before_midnight() { #[serial] #[ignore] fn playlist_change_at_six() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.skip_validation = true; config.mail.recipient = "".into(); config.processing.mode = Playlist; From 6efa9e2cf9a8cd2a2fd44bcf73520201d6f1c10f Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 22 Apr 2024 15:06:53 +0200 Subject: [PATCH 05/22] fix: don't skip last clip from playlist --- ffplayout-api/src/db/models.rs | 1 + ffplayout-engine/src/input/playlist.rs | 18 +++++------------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/ffplayout-api/src/db/models.rs b/ffplayout-api/src/db/models.rs index de1993ce..dcacc1df 100644 --- a/ffplayout-api/src/db/models.rs +++ b/ffplayout-api/src/db/models.rs @@ -42,6 +42,7 @@ impl LoginUser { Self { id, username } } } + #[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)] pub struct TextPreset { #[sqlx(default)] diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index dc68251b..4426f292 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -112,32 +112,24 @@ impl CurrentProgram { let (delta, total_delta) = get_delta(&self.config, &time_in_seconds()); let mut next = false; - let duration = if self.current_node.duration >= self.current_node.out { - self.current_node.duration - } else { - // maybe out is longer to be able to loop - self.current_node.out - }; - trace!( "delta: {delta}, total_delta: {total_delta}, current index: {}", self.current_node.index.unwrap_or_default() ); - let mut next_start = - self.current_node.begin.unwrap_or_default() - self.start_sec + duration + delta; + let mut clip_start = self.current_node.begin.unwrap_or_default() - self.start_sec; if self.player_control.current_index.load(Ordering::SeqCst) == self.player_control.current_list.lock().unwrap().len() - 1 { - next_start += self.config.general.stop_threshold; + clip_start += self.config.general.stop_threshold; } - trace!("next_start: {next_start}, end_sec: {}", self.end_sec); + trace!("clip_start: {clip_start}, end_sec: {}", self.end_sec); // Check if we over the target length or we are close to it, if so we load the next playlist. if !self.config.playlist.infinit - && (next_start >= self.end_sec + && (clip_start >= self.end_sec || is_close(total_delta, 0.0, 2.0) || is_close(total_delta, self.end_sec, 2.0)) { @@ -788,7 +780,7 @@ fn handle_list_end( player_control: &PlayerControl, last_index: usize, ) -> Media { - debug!("Playlist end"); + debug!("Last clip from day"); let mut out = if node.seek > 0.0 { node.seek + total_delta From a1d1be5fc1bb780eba3215ed4445008bcc4508bd Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 22 Apr 2024 15:10:26 +0200 Subject: [PATCH 06/22] add missing value --- Cargo.lock | 41 ++++++++++++++++++--------- lib/src/utils/advanced_config.rs | 2 +- tests/src/engine_cmd.rs | 48 ++++++++++++++++---------------- tests/src/engine_generator.rs | 6 ++-- tests/src/lib_utils.rs | 2 +- 5 files changed, 57 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9739e0ba..a840f695 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,7 +519,7 @@ dependencies = [ "futures-lite 2.3.0", "parking", "polling 3.6.0", - "rustix 0.38.32", + "rustix 0.38.33", "slab", "tracing", "windows-sys 0.52.0", @@ -2519,7 +2519,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.32", + "rustix 0.38.33", "tracing", "windows-sys 0.52.0", ] @@ -2804,9 +2804,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" dependencies = [ "bitflags 2.5.0", "errno", @@ -2896,12 +2896,27 @@ dependencies = [ "regex", ] +[[package]] +name = "scc" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96560eea317a9cc4e0bb1f6a2c93c09a19b8c4fc5cb3fcc0ec1c094cd783e2" +dependencies = [ + "sdd", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "semver" version = "1.0.22" @@ -2975,23 +2990,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" +checksum = "adb86f9315df5df6a70eae0cc22395a44e544a0d8897586820770a35ede74449" dependencies = [ - "dashmap", "futures", - "lazy_static", "log", + "once_cell", "parking_lot", + "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" +checksum = "a9bb72430492e9549b0c4596725c0f82729bff861c45aa8099c0a8e67fc3b721" dependencies = [ "proc-macro2", "quote", @@ -3034,9 +3049,9 @@ checksum = "b2a4eed4c5ae38438470ab8e0108bb751012f786f44ff585cfd837c9a5fe426f" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3443,7 +3458,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.2", - "rustix 0.38.32", + "rustix 0.38.33", "windows-sys 0.52.0", ] diff --git a/lib/src/utils/advanced_config.rs b/lib/src/utils/advanced_config.rs index f60481c0..ad81991c 100644 --- a/lib/src/utils/advanced_config.rs +++ b/lib/src/utils/advanced_config.rs @@ -66,7 +66,7 @@ impl AdvancedConfig { pub fn new(cfg_path: PathBuf) -> Self { let mut config: AdvancedConfig = Default::default(); - if let Ok(f) = File::open(&cfg_path) { + if let Ok(f) = File::open(cfg_path) { config = match serde_yaml::from_reader(f) { Ok(yaml) => yaml, Err(_) => AdvancedConfig::default(), diff --git a/tests/src/engine_cmd.rs b/tests/src/engine_cmd.rs index 07d25576..582fa38f 100644 --- a/tests/src/engine_cmd.rs +++ b/tests/src/engine_cmd.rs @@ -8,7 +8,7 @@ use ffplayout_lib::{ #[test] fn video_audio_input() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = Stream; @@ -37,7 +37,7 @@ fn video_audio_input() { #[test] fn video_audio_custom_filter1_input() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = Stream; @@ -64,7 +64,7 @@ fn video_audio_custom_filter1_input() { #[test] fn video_audio_custom_filter2_input() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = Stream; @@ -93,7 +93,7 @@ fn video_audio_custom_filter2_input() { #[test] fn video_audio_custom_filter3_input() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = Stream; @@ -121,7 +121,7 @@ fn video_audio_custom_filter3_input() { #[test] fn dual_audio_aevalsrc_input() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = Stream; @@ -149,7 +149,7 @@ fn dual_audio_aevalsrc_input() { #[test] fn dual_audio_input() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = Stream; @@ -176,7 +176,7 @@ fn dual_audio_input() { #[test] fn video_separate_audio_input() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = Stream; @@ -213,7 +213,7 @@ fn video_separate_audio_input() { #[test] fn video_audio_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.out.output_cmd = Some(vec_strings![ @@ -272,7 +272,7 @@ fn video_audio_stream() { #[test] fn video_audio_filter1_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = false; @@ -347,7 +347,7 @@ fn video_audio_filter1_stream() { #[test] fn video_audio_filter2_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -430,7 +430,7 @@ fn video_audio_filter2_stream() { #[test] fn video_audio_filter3_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -516,7 +516,7 @@ fn video_audio_filter3_stream() { #[test] fn video_audio_filter4_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -602,7 +602,7 @@ fn video_audio_filter4_stream() { #[test] fn video_dual_audio_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -673,7 +673,7 @@ fn video_dual_audio_stream() { #[test] fn video_dual_audio_filter_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -753,7 +753,7 @@ fn video_dual_audio_filter_stream() { #[test] fn video_audio_multi_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.out.output_cmd = Some(vec_strings![ @@ -842,7 +842,7 @@ fn video_audio_multi_stream() { #[test] fn video_dual_audio_multi_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -956,7 +956,7 @@ fn video_dual_audio_multi_stream() { #[test] fn video_audio_text_multi_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -1069,7 +1069,7 @@ fn video_audio_text_multi_stream() { #[test] fn video_dual_audio_multi_filter_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -1198,7 +1198,7 @@ fn video_dual_audio_multi_filter_stream() { #[test] fn video_audio_text_filter_stream() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 1; @@ -1320,7 +1320,7 @@ fn video_audio_text_filter_stream() { #[test] fn video_audio_hls() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = HLS; @@ -1407,7 +1407,7 @@ fn video_audio_hls() { #[test] fn video_audio_sub_meta_hls() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = HLS; @@ -1502,7 +1502,7 @@ fn video_audio_sub_meta_hls() { #[test] fn video_multi_audio_hls() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = HLS; @@ -1592,7 +1592,7 @@ fn video_multi_audio_hls() { #[test] fn multi_video_audio_hls() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = HLS; @@ -1707,7 +1707,7 @@ fn multi_video_audio_hls() { #[test] fn multi_video_multi_audio_hls() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); let player_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); config.out.mode = HLS; diff --git a/tests/src/engine_generator.rs b/tests/src/engine_generator.rs index dbd53859..64a83e6e 100644 --- a/tests/src/engine_generator.rs +++ b/tests/src/engine_generator.rs @@ -53,7 +53,7 @@ fn test_ordered_list() { #[test] #[ignore] fn test_filler_list() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.storage.filler = "assets/".into(); let f_list = filler_list(&config, 2440.0); @@ -64,7 +64,7 @@ fn test_filler_list() { #[test] #[ignore] fn test_generate_playlist_from_folder() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.generate = Some(vec!["2023-09-11".to_string()]); config.processing.mode = Playlist; config.logging.log_to_file = false; @@ -98,7 +98,7 @@ fn test_generate_playlist_from_folder() { #[test] #[ignore] fn test_generate_playlist_from_template() { - let mut config = PlayoutConfig::new(None); + let mut config = PlayoutConfig::new(None, None); config.general.generate = Some(vec!["2023-09-12".to_string()]); config.general.template = Some(Template { sources: vec![ diff --git a/tests/src/lib_utils.rs b/tests/src/lib_utils.rs index 2e97442e..139d5521 100644 --- a/tests/src/lib_utils.rs +++ b/tests/src/lib_utils.rs @@ -40,7 +40,7 @@ fn get_date_tomorrow() { #[test] fn test_delta() { - let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None); config.mail.recipient = "".into(); config.processing.mode = Playlist; config.playlist.day_start = "00:00:00".into(); From b6f9a2545fe6d9b9f3cbe0a2add22d09a9c43dff Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 23 Apr 2024 22:02:48 +0200 Subject: [PATCH 07/22] add uuid based auth example --- Cargo.lock | 1 + ffplayout-api/Cargo.toml | 1 + ffplayout-api/examples/uuid_auth.rs | 80 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 ffplayout-api/examples/uuid_auth.rs diff --git a/Cargo.lock b/Cargo.lock index a840f695..60da8ec6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,7 @@ dependencies = [ "static-files", "sysinfo", "tokio", + "uuid", ] [[package]] diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index 50978ed2..e6658834 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -46,6 +46,7 @@ static-files = "0.2" sysinfo ={ version = "0.30", features = ["linux-netdevs"] } sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } tokio = { version = "1.29", features = ["full"] } +uuid = "1.8" [build-dependencies] static-files = "0.2" diff --git a/ffplayout-api/examples/uuid_auth.rs b/ffplayout-api/examples/uuid_auth.rs new file mode 100644 index 00000000..13739d20 --- /dev/null +++ b/ffplayout-api/examples/uuid_auth.rs @@ -0,0 +1,80 @@ +/// Example for a simple auth mechanism in SSE. +/// +/// get new UUID: curl -X GET http://127.0.0.1:8080/generate +/// use UUID: curl --header "UUID: f2f8c29b-712a-48c5-8919-b535d3a05a3a" -X GET http://127.0.0.1:8080/check +/// +use std::{collections::HashSet, sync::Mutex, time::Duration, time::SystemTime}; + +use actix_web::{middleware::Logger, web, App, HttpRequest, HttpResponse, HttpServer}; +use simplelog::*; +use uuid::Uuid; + +use ffplayout_lib::utils::{init_logging, PlayoutConfig}; + +#[derive(Debug, Eq, Hash, PartialEq)] +struct UuidData { + uuid: Uuid, + expiration_time: SystemTime, +} + +struct AppState { + uuids: Mutex>, +} + +fn prune_uuids(uuids: &mut HashSet) { + uuids.retain(|entry| entry.expiration_time > SystemTime::now()); +} + +async fn generate_uuid(data: web::Data) -> HttpResponse { + let uuid = Uuid::new_v4(); + let expiration_time = SystemTime::now() + Duration::from_secs(30); // 24 * 3600 -> for 24 hours + let mut uuids = data.uuids.lock().unwrap(); + + prune_uuids(&mut uuids); + + uuids.insert(UuidData { + uuid, + expiration_time, + }); + + HttpResponse::Ok().body(uuid.to_string()) +} + +async fn check_uuid(data: web::Data, req: HttpRequest) -> HttpResponse { + let uuid = req.headers().get("uuid").unwrap().to_str().unwrap(); + let uuid_from_client = Uuid::parse_str(uuid).unwrap(); + let mut uuids = data.uuids.lock().unwrap(); + + prune_uuids(&mut uuids); + + match uuids.iter().find(|entry| entry.uuid == uuid_from_client) { + Some(_) => HttpResponse::Ok().body("UUID is valid"), + None => HttpResponse::Unauthorized().body("Invalid or expired UUID"), + } +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let mut config = PlayoutConfig::new(None, None); + config.mail.recipient = String::new(); + config.logging.log_to_file = false; + config.logging.timestamp = false; + + let logging = init_logging(&config, None, None); + CombinedLogger::init(logging).unwrap(); + + let state = web::Data::new(AppState { + uuids: Mutex::new(HashSet::new()), + }); + + HttpServer::new(move || { + App::new() + .app_data(state.clone()) + .wrap(Logger::default()) + .route("/generate", web::get().to(generate_uuid)) + .route("/check", web::get().to(check_uuid)) + }) + .bind("127.0.0.1:8080")? + .run() + .await +} From 1fbfda2e85eb026e977362fbd845b785cedc7e64 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 24 Apr 2024 09:58:57 +0200 Subject: [PATCH 08/22] work on sse --- Cargo.lock | 104 +++++++++++++++++++++++++++++ ffplayout-api/Cargo.toml | 3 + ffplayout-api/examples/index.html | 28 ++++++++ ffplayout-api/examples/sse.rs | 57 ++++++++++++++++ ffplayout-api/src/api/routes.rs | 4 +- ffplayout-api/src/lib.rs | 21 ++++++ ffplayout-api/src/main.rs | 46 +++++-------- ffplayout-api/src/sse/broadcast.rs | 89 ++++++++++++++++++++++++ ffplayout-api/src/sse/mod.rs | 48 +++++++++++++ ffplayout-api/src/sse/routes.rs | 54 +++++++++++++++ ffplayout-api/src/utils/errors.rs | 12 +++- 11 files changed, 433 insertions(+), 33 deletions(-) create mode 100644 ffplayout-api/examples/index.html create mode 100644 ffplayout-api/examples/sse.rs create mode 100644 ffplayout-api/src/lib.rs create mode 100644 ffplayout-api/src/sse/broadcast.rs create mode 100644 ffplayout-api/src/sse/mod.rs create mode 100644 ffplayout-api/src/sse/routes.rs diff --git a/Cargo.lock b/Cargo.lock index 60da8ec6..8cd55bb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,6 +267,55 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-web-lab" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7675c1a84eec1b179c844cdea8488e3e409d8e4984026e92fa96c87dd86f33c6" +dependencies = [ + "actix-http", + "actix-router", + "actix-service", + "actix-utils", + "actix-web", + "actix-web-lab-derive", + "ahash", + "arc-swap", + "async-trait", + "bytes", + "bytestring", + "csv", + "derive_more", + "futures-core", + "futures-util", + "http 0.2.12", + "impl-more", + "itertools", + "local-channel", + "mediatype", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_html_form", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "actix-web-lab-derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa0b287c8de4a76b691f29dbb5451e8dd5b79d777eaf87350c9b0cbfdb5e968" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "actix-web-static-files" version = "4.0.1" @@ -406,6 +455,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "argon2" version = "0.5.3" @@ -981,6 +1036,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "darling" version = "0.20.8" @@ -1246,6 +1322,7 @@ dependencies = [ "actix-web", "actix-web-grants", "actix-web-httpauth", + "actix-web-lab", "actix-web-static-files", "argon2", "chrono", @@ -1259,6 +1336,7 @@ dependencies = [ "lexical-sort", "local-ip-address", "once_cell", + "parking_lot", "path-clean", "rand", "regex", @@ -1274,6 +1352,7 @@ dependencies = [ "static-files", "sysinfo", "tokio", + "tokio-stream", "uuid", ] @@ -1822,6 +1901,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-more" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" + [[package]] name = "indexmap" version = "2.2.6" @@ -2097,6 +2182,12 @@ dependencies = [ "digest", ] +[[package]] +name = "mediatype" +version = "0.19.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878cd8d1b3c8c8ae4b2ba0a36652b7cf192f618a599a7fbdfa25cffd4ea72dd" + [[package]] name = "memchr" version = "2.7.2" @@ -2944,6 +3035,19 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "serde_html_form" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_json" version = "1.0.116" diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index e6658834..10328234 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -19,6 +19,7 @@ actix-multipart = "0.6" actix-web = "4" actix-web-grants = "4" actix-web-httpauth = "0.8" +actix-web-lab = "0.20" actix-web-static-files = "4.0" argon2 = "0.5" chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } @@ -31,6 +32,7 @@ lazy_static = "1.4" lexical-sort = "0.3" local-ip-address = "0.6" once_cell = "1.18" +parking_lot = "0.12" path-clean = "1.0" rand = "0.8" regex = "1" @@ -46,6 +48,7 @@ static-files = "0.2" sysinfo ={ version = "0.30", features = ["linux-netdevs"] } sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } tokio = { version = "1.29", features = ["full"] } +tokio-stream = "0.1" uuid = "1.8" [build-dependencies] diff --git a/ffplayout-api/examples/index.html b/ffplayout-api/examples/index.html new file mode 100644 index 00000000..ef87ab39 --- /dev/null +++ b/ffplayout-api/examples/index.html @@ -0,0 +1,28 @@ + + + + + + + Server-sent events + + + +
+ + + diff --git a/ffplayout-api/examples/sse.rs b/ffplayout-api/examples/sse.rs new file mode 100644 index 00000000..573bdce8 --- /dev/null +++ b/ffplayout-api/examples/sse.rs @@ -0,0 +1,57 @@ +/// https://github.com/actix/examples/tree/master/server-sent-events +/// +use std::{io, sync::Arc}; + +use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; +use actix_web_lab::{extract::Path, respond::Html}; + +use simplelog::*; + +use ffplayout_api::sse::broadcast::Broadcaster; + +use ffplayout_lib::utils::{init_logging, PlayoutConfig}; + +#[actix_web::main] +async fn main() -> io::Result<()> { + let mut config = PlayoutConfig::new(None, None); + config.mail.recipient = String::new(); + config.logging.log_to_file = false; + config.logging.timestamp = false; + + let logging = init_logging(&config, None, None); + CombinedLogger::init(logging).unwrap(); + + let data = Broadcaster::create(); + + HttpServer::new(move || { + App::new() + .app_data(web::Data::from(Arc::clone(&data))) + .service(index) + .service(event_stream) + .service(broadcast_msg) + .wrap(Logger::default()) + }) + .bind(("127.0.0.1", 8080))? + .workers(2) + .run() + .await +} + +#[get("/")] +async fn index() -> impl Responder { + Html(include_str!("index.html").to_owned()) +} + +#[get("/events")] +async fn event_stream(broadcaster: web::Data) -> impl Responder { + broadcaster.new_client().await +} + +#[post("/broadcast/{msg}")] +async fn broadcast_msg( + broadcaster: web::Data, + Path((msg,)): Path<(String,)>, +) -> impl Responder { + broadcaster.broadcast(&msg).await; + HttpResponse::Ok().body("msg sent") +} diff --git a/ffplayout-api/src/api/routes.rs b/ffplayout-api/src/api/routes.rs index 91fbea4a..570d6fcd 100644 --- a/ffplayout-api/src/api/routes.rs +++ b/ffplayout-api/src/api/routes.rs @@ -246,7 +246,7 @@ async fn get_user( /// ``` #[get("/user/{name}")] #[protect("Role::Admin", ty = "Role")] -async fn get_user_by_name( +async fn get_by_name( pool: web::Data>, name: web::Path, ) -> Result { @@ -326,7 +326,7 @@ async fn update_user( return Err(ServiceError::InternalServerError); } - Err(ServiceError::Unauthorized) + Err(ServiceError::Unauthorized("No Permission".to_string())) } /// **Add User** diff --git a/ffplayout-api/src/lib.rs b/ffplayout-api/src/lib.rs new file mode 100644 index 00000000..3809055f --- /dev/null +++ b/ffplayout-api/src/lib.rs @@ -0,0 +1,21 @@ +use std::sync::{Arc, Mutex}; + +use clap::Parser; +use lazy_static::lazy_static; +use sysinfo::{Disks, Networks, System}; + +pub mod api; +pub mod db; +pub mod sse; +pub mod utils; + +use utils::args_parse::Args; + +lazy_static! { + pub static ref ARGS: Args = Args::parse(); + pub static ref DISKS: Arc> = + Arc::new(Mutex::new(Disks::new_with_refreshed_list())); + pub static ref NETWORKS: Arc> = + Arc::new(Mutex::new(Networks::new_with_refreshed_list())); + pub static ref SYS: Arc> = Arc::new(Mutex::new(System::new_all())); +} diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index 769cdffc..89fb70b5 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -1,8 +1,4 @@ -use std::{ - env, - process::exit, - sync::{Arc, Mutex}, -}; +use std::{collections::HashSet, env, process::exit, sync::Mutex}; use actix_files::Files; use actix_web::{ @@ -14,37 +10,25 @@ use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthent #[cfg(all(not(debug_assertions), feature = "embed_frontend"))] use actix_web_static_files::ResourceFiles; -use clap::Parser; -use lazy_static::lazy_static; use path_clean::PathClean; use simplelog::*; -use sysinfo::{Disks, Networks, System}; -pub mod api; -pub mod db; -pub mod utils; - -use api::{auth, routes::*}; -use db::{db_pool, models::LoginUser}; -use utils::{args_parse::Args, control::ProcessControl, db_path, init_config, run_args}; +use ffplayout_api::{ + api::{auth, routes::*}, + db::{db_pool, models::LoginUser}, + sse::{routes::*, AuthState}, + utils::{control::ProcessControl, db_path, init_config, run_args}, + ARGS, +}; #[cfg(any(debug_assertions, not(feature = "embed_frontend")))] -use utils::public_path; +use ffplayout_api::utils::public_path; use ffplayout_lib::utils::{init_logging, PlayoutConfig}; #[cfg(all(not(debug_assertions), feature = "embed_frontend"))] include!(concat!(env!("OUT_DIR"), "/generated.rs")); -lazy_static! { - pub static ref ARGS: Args = Args::parse(); - pub static ref DISKS: Arc> = - Arc::new(Mutex::new(Disks::new_with_refreshed_list())); - pub static ref NETWORKS: Arc> = - Arc::new(Mutex::new(Networks::new_with_refreshed_list())); - pub static ref SYS: Arc> = Arc::new(Mutex::new(System::new_all())); -} - async fn validator( req: ServiceRequest, credentials: BearerAuth, @@ -95,6 +79,9 @@ async fn main() -> std::io::Result<()> { let addr = ip_port[0]; let port = ip_port[1].parse::().unwrap(); let engine_process = web::Data::new(ProcessControl::new()); + let auth_state = web::Data::new(AuthState { + uuids: Mutex::new(HashSet::new()), + }); info!("running ffplayout API, listen on http://{conn}"); @@ -109,14 +96,15 @@ async fn main() -> std::io::Result<()> { let mut web_app = App::new() .app_data(db_pool) .app_data(engine_process.clone()) + .app_data(auth_state.clone()) .wrap(logger) .service(login) .service( web::scope("/api") - .wrap(auth) + .wrap(auth.clone()) .service(add_user) .service(get_user) - .service(get_user_by_name) + .service(get_by_name) .service(get_users) .service(remove_user) .service(get_playout_config) @@ -149,8 +137,10 @@ async fn main() -> std::io::Result<()> { .service(save_file) .service(import_playlist) .service(get_program) - .service(get_system_stat), + .service(get_system_stat) + .service(generate_uuid), ) + .service(web::scope("/data").service(validate_uuid)) .service(get_file); if let Some(public) = &ARGS.public { diff --git a/ffplayout-api/src/sse/broadcast.rs b/ffplayout-api/src/sse/broadcast.rs new file mode 100644 index 00000000..25f216f2 --- /dev/null +++ b/ffplayout-api/src/sse/broadcast.rs @@ -0,0 +1,89 @@ +use std::{sync::Arc, time::Duration}; + +use actix_web::rt::time::interval; +use actix_web_lab::{ + sse::{self, Sse}, + util::InfallibleStream, +}; +use futures_util::future; +use parking_lot::Mutex; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +pub struct Broadcaster { + inner: Mutex, +} + +#[derive(Debug, Clone, Default)] +struct BroadcasterInner { + clients: Vec>, +} + +impl Broadcaster { + /// Constructs new broadcaster and spawns ping loop. + pub fn create() -> Arc { + let this = Arc::new(Broadcaster { + inner: Mutex::new(BroadcasterInner::default()), + }); + + Broadcaster::spawn_ping(Arc::clone(&this)); + + this + } + + /// Pings clients every 10 seconds to see if they are alive and remove them from the broadcast + /// list if not. + fn spawn_ping(this: Arc) { + actix_web::rt::spawn(async move { + let mut interval = interval(Duration::from_secs(10)); + + loop { + interval.tick().await; + this.remove_stale_clients().await; + } + }); + } + + /// Removes all non-responsive clients from broadcast list. + async fn remove_stale_clients(&self) { + let clients = self.inner.lock().clients.clone(); + + let mut ok_clients = Vec::new(); + + for client in clients { + if client + .send(sse::Event::Comment("ping".into())) + .await + .is_ok() + { + ok_clients.push(client.clone()); + } + } + + self.inner.lock().clients = ok_clients; + } + + /// Registers client with broadcaster, returning an SSE response body. + pub async fn new_client(&self) -> Sse>> { + let (tx, rx) = mpsc::channel(10); + + tx.send(sse::Data::new("connected").into()).await.unwrap(); + + self.inner.lock().clients.push(tx); + + Sse::from_infallible_receiver(rx) + } + + /// Broadcasts `msg` to all clients. + pub async fn broadcast(&self, msg: &str) { + let clients = self.inner.lock().clients.clone(); + + let send_futures = clients + .iter() + .map(|client| client.send(sse::Data::new(msg).into())); + + // try to send to all clients, ignoring failures + // disconnected clients will get swept up by `remove_stale_clients` + let _ = future::join_all(send_futures).await; + } +} diff --git a/ffplayout-api/src/sse/mod.rs b/ffplayout-api/src/sse/mod.rs new file mode 100644 index 00000000..d908d058 --- /dev/null +++ b/ffplayout-api/src/sse/mod.rs @@ -0,0 +1,48 @@ +use std::{ + collections::HashSet, + sync::Mutex, + time::{Duration, SystemTime}, +}; + +use uuid::Uuid; + +use crate::utils::errors::ServiceError; + +pub mod broadcast; +pub mod routes; + +#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy)] +pub struct UuidData { + pub uuid: Uuid, + pub expiration: SystemTime, +} + +impl UuidData { + pub fn new() -> Self { + Self { + uuid: Uuid::new_v4(), + expiration: SystemTime::now() + Duration::from_secs(12 * 3600), // 12 hours + } + } +} + +pub struct AuthState { + pub uuids: Mutex>, +} + +pub fn prune_uuids(uuids: &mut HashSet) { + uuids.retain(|entry| entry.expiration > SystemTime::now()); +} + +pub fn check_uuid(uuids: &mut HashSet, uuid: &str) -> Result<&'static str, ServiceError> { + let client_uuid = Uuid::parse_str(uuid)?; + + prune_uuids(uuids); + + match uuids.iter().find(|entry| entry.uuid == client_uuid) { + Some(_) => Ok("UUID is valid"), + None => Err(ServiceError::Unauthorized( + "Invalid or expired UUID".to_string(), + )), + } +} diff --git a/ffplayout-api/src/sse/routes.rs b/ffplayout-api/src/sse/routes.rs new file mode 100644 index 00000000..59cda62c --- /dev/null +++ b/ffplayout-api/src/sse/routes.rs @@ -0,0 +1,54 @@ +use actix_web::{get, post, web, Responder}; +use actix_web_grants::proc_macro::protect; +use serde::{Deserialize, Serialize}; + +use super::{check_uuid, prune_uuids, AuthState, UuidData}; +use crate::utils::{errors::ServiceError, Role}; + +#[derive(Deserialize, Serialize)] +struct User { + uuid: String, +} + +impl User { + fn new(uuid: String) -> Self { + Self { uuid } + } +} + +/// **Get generated UUID** +/// +/// ```BASH +/// curl -X GET 'http://127.0.0.1:8787/api/generate-uuid' -H 'Authorization: Bearer ' +/// ``` +#[post("/generate-uuid")] +#[protect(any("Role::Admin", "Role::User"), ty = "Role")] +async fn generate_uuid(data: web::Data) -> Result { + let mut uuids = data.uuids.lock().map_err(|e| e.to_string())?; + let new_uuid = UuidData::new(); + let user_auth = User::new(new_uuid.uuid.to_string()); + + prune_uuids(&mut uuids); + + uuids.insert(new_uuid); + + Ok(web::Json(user_auth)) +} + +/// **Validate UUID** +/// +/// ```BASH +/// curl -X GET 'http://127.0.0.1:8787/data/validate?uuid=f2f8c29b-712a-48c5-8919-b535d3a05a3a' +/// ``` +#[get("/validate")] +async fn validate_uuid( + data: web::Data, + user: web::Query, +) -> Result { + let mut uuids = data.uuids.lock().map_err(|e| e.to_string())?; + + match check_uuid(&mut uuids, user.uuid.as_str()) { + Ok(s) => Ok(web::Json(s)), + Err(e) => Err(e), + } +} diff --git a/ffplayout-api/src/utils/errors.rs b/ffplayout-api/src/utils/errors.rs index 8aa5a628..72def236 100644 --- a/ffplayout-api/src/utils/errors.rs +++ b/ffplayout-api/src/utils/errors.rs @@ -12,8 +12,8 @@ pub enum ServiceError { #[display(fmt = "Conflict: {_0}")] Conflict(String), - #[display(fmt = "Unauthorized")] - Unauthorized, + #[display(fmt = "Unauthorized: {_0}")] + Unauthorized(String), #[display(fmt = "NoContent: {_0}")] NoContent(String), @@ -31,7 +31,7 @@ impl ResponseError for ServiceError { } ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message), - ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"), + ServiceError::Unauthorized(ref message) => HttpResponse::Unauthorized().json(message), ServiceError::NoContent(ref message) => HttpResponse::NoContent().json(message), ServiceError::ServiceUnavailable(ref message) => { HttpResponse::ServiceUnavailable().json(message) @@ -87,3 +87,9 @@ impl From for ServiceError { ServiceError::BadRequest(err.to_string()) } } + +impl From for ServiceError { + fn from(err: uuid::Error) -> ServiceError { + ServiceError::BadRequest(err.to_string()) + } +} From d0244da05e1c5868901e0c13dcdcb9ba1c1bfb59 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 25 Apr 2024 12:41:26 +0200 Subject: [PATCH 09/22] send system status over sse --- .vscode/settings.json | 7 ++- ffplayout-api/examples/index.html | 57 ++++++++++++---------- ffplayout-api/examples/sse.rs | 4 +- ffplayout-api/src/main.rs | 17 +++++-- ffplayout-api/src/sse/broadcast.rs | 78 +++++++++++++++++++++++++++--- ffplayout-api/src/sse/mod.rs | 3 +- ffplayout-api/src/sse/routes.rs | 38 +++++++++++++-- ffplayout-api/src/utils/system.rs | 6 +++ 8 files changed, 168 insertions(+), 42 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 766d5939..629bf9a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,10 @@ }, "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "cSpell.words": [ + "actix", + "tokio", + "uuids" + ] } diff --git a/ffplayout-api/examples/index.html b/ffplayout-api/examples/index.html index ef87ab39..a9c6d4e6 100644 --- a/ffplayout-api/examples/index.html +++ b/ffplayout-api/examples/index.html @@ -1,28 +1,35 @@ - - - - - Server-sent events - - - -
- - + + + + + Server-sent events + + + +
+
+ + diff --git a/ffplayout-api/examples/sse.rs b/ffplayout-api/examples/sse.rs index 573bdce8..033df68f 100644 --- a/ffplayout-api/examples/sse.rs +++ b/ffplayout-api/examples/sse.rs @@ -44,7 +44,9 @@ async fn index() -> impl Responder { #[get("/events")] async fn event_stream(broadcaster: web::Data) -> impl Responder { - broadcaster.new_client().await + broadcaster + .new_client(1, PlayoutConfig::default(), "ping".to_string()) + .await } #[post("/broadcast/{msg}")] diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index 89fb70b5..981c460f 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -1,4 +1,9 @@ -use std::{collections::HashSet, env, process::exit, sync::Mutex}; +use std::{ + collections::HashSet, + env, + process::exit, + sync::{Arc, Mutex}, +}; use actix_files::Files; use actix_web::{ @@ -16,7 +21,7 @@ use simplelog::*; use ffplayout_api::{ api::{auth, routes::*}, db::{db_pool, models::LoginUser}, - sse::{routes::*, AuthState}, + sse::{broadcast::Broadcaster, routes::*, AuthState}, utils::{control::ProcessControl, db_path, init_config, run_args}, ARGS, }; @@ -82,6 +87,7 @@ async fn main() -> std::io::Result<()> { let auth_state = web::Data::new(AuthState { uuids: Mutex::new(HashSet::new()), }); + let broadcast_data = Broadcaster::create(); info!("running ffplayout API, listen on http://{conn}"); @@ -97,6 +103,7 @@ async fn main() -> std::io::Result<()> { .app_data(db_pool) .app_data(engine_process.clone()) .app_data(auth_state.clone()) + .app_data(web::Data::from(Arc::clone(&broadcast_data))) .wrap(logger) .service(login) .service( @@ -140,7 +147,11 @@ async fn main() -> std::io::Result<()> { .service(get_system_stat) .service(generate_uuid), ) - .service(web::scope("/data").service(validate_uuid)) + .service( + web::scope("/data") + .service(validate_uuid) + .service(event_stream), + ) .service(get_file); if let Some(public) = &ARGS.public { diff --git a/ffplayout-api/src/sse/broadcast.rs b/ffplayout-api/src/sse/broadcast.rs index 25f216f2..7286f638 100644 --- a/ffplayout-api/src/sse/broadcast.rs +++ b/ffplayout-api/src/sse/broadcast.rs @@ -1,22 +1,50 @@ use std::{sync::Arc, time::Duration}; -use actix_web::rt::time::interval; +use actix_web::{rt::time::interval, web}; use actix_web_lab::{ sse::{self, Sse}, util::InfallibleStream, }; + +use ffplayout_lib::utils::PlayoutConfig; use futures_util::future; use parking_lot::Mutex; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; +use crate::utils::system; + +#[derive(Debug, Clone)] +struct Client { + _channel: i32, + config: PlayoutConfig, + endpoint: String, + sender: mpsc::Sender, +} + +impl Client { + fn new( + _channel: i32, + config: PlayoutConfig, + endpoint: String, + sender: mpsc::Sender, + ) -> Self { + Self { + _channel, + config, + endpoint, + sender, + } + } +} + pub struct Broadcaster { inner: Mutex, } #[derive(Debug, Clone, Default)] struct BroadcasterInner { - clients: Vec>, + clients: Vec, } impl Broadcaster { @@ -35,11 +63,24 @@ impl Broadcaster { /// list if not. fn spawn_ping(this: Arc) { actix_web::rt::spawn(async move { - let mut interval = interval(Duration::from_secs(10)); + let mut interval = interval(Duration::from_secs(1)); + let mut counter = 0; loop { interval.tick().await; - this.remove_stale_clients().await; + + if counter % 10 == 0 { + this.remove_stale_clients().await; + } + + if counter % 30 == 0 { + // TODO: implement playout status + this.broadcast("ping").await; + } + + this.broadcast_system().await; + + counter = (counter + 1) % 61; } }); } @@ -52,6 +93,7 @@ impl Broadcaster { for client in clients { if client + .sender .send(sse::Event::Comment("ping".into())) .await .is_ok() @@ -64,12 +106,20 @@ impl Broadcaster { } /// Registers client with broadcaster, returning an SSE response body. - pub async fn new_client(&self) -> Sse>> { + pub async fn new_client( + &self, + channel: i32, + config: PlayoutConfig, + endpoint: String, + ) -> Sse>> { let (tx, rx) = mpsc::channel(10); tx.send(sse::Data::new("connected").into()).await.unwrap(); - self.inner.lock().clients.push(tx); + self.inner + .lock() + .clients + .push(Client::new(channel, config, endpoint, tx)); Sse::from_infallible_receiver(rx) } @@ -80,10 +130,24 @@ impl Broadcaster { let send_futures = clients .iter() - .map(|client| client.send(sse::Data::new(msg).into())); + .map(|client| client.sender.send(sse::Data::new(msg).into())); // try to send to all clients, ignoring failures // disconnected clients will get swept up by `remove_stale_clients` let _ = future::join_all(send_futures).await; } + + /// Broadcasts `msg` to all clients. + pub async fn broadcast_system(&self) { + let clients = self.inner.lock().clients.clone(); + + for client in clients { + if &client.endpoint == "system" { + if let Ok(stat) = web::block(move || system::stat(client.config.clone())).await { + let stat_string = stat.to_string(); + let _ = client.sender.send(sse::Data::new(stat_string).into()).await; + }; + } + } + } } diff --git a/ffplayout-api/src/sse/mod.rs b/ffplayout-api/src/sse/mod.rs index d908d058..e2545c8b 100644 --- a/ffplayout-api/src/sse/mod.rs +++ b/ffplayout-api/src/sse/mod.rs @@ -21,7 +21,7 @@ impl UuidData { pub fn new() -> Self { Self { uuid: Uuid::new_v4(), - expiration: SystemTime::now() + Duration::from_secs(12 * 3600), // 12 hours + expiration: SystemTime::now() + Duration::from_secs(2 * 3600), // 2 hours } } } @@ -30,6 +30,7 @@ pub struct AuthState { pub uuids: Mutex>, } +/// Remove all UUIDs from HashSet which are older the expiration time. pub fn prune_uuids(uuids: &mut HashSet) { uuids.retain(|entry| entry.expiration > SystemTime::now()); } diff --git a/ffplayout-api/src/sse/routes.rs b/ffplayout-api/src/sse/routes.rs index 59cda62c..665ca9b1 100644 --- a/ffplayout-api/src/sse/routes.rs +++ b/ffplayout-api/src/sse/routes.rs @@ -1,18 +1,22 @@ use actix_web::{get, post, web, Responder}; use actix_web_grants::proc_macro::protect; use serde::{Deserialize, Serialize}; +use sqlx::{Pool, Sqlite}; use super::{check_uuid, prune_uuids, AuthState, UuidData}; -use crate::utils::{errors::ServiceError, Role}; +use crate::sse::broadcast::Broadcaster; +use crate::utils::{errors::ServiceError, playout_config, Role}; #[derive(Deserialize, Serialize)] struct User { + #[serde(default, skip_serializing)] + endpoint: String, uuid: String, } impl User { - fn new(uuid: String) -> Self { - Self { uuid } + fn new(endpoint: String, uuid: String) -> Self { + Self { endpoint, uuid } } } @@ -26,7 +30,7 @@ impl User { async fn generate_uuid(data: web::Data) -> Result { let mut uuids = data.uuids.lock().map_err(|e| e.to_string())?; let new_uuid = UuidData::new(); - let user_auth = User::new(new_uuid.uuid.to_string()); + let user_auth = User::new(String::new(), new_uuid.uuid.to_string()); prune_uuids(&mut uuids); @@ -52,3 +56,29 @@ async fn validate_uuid( Err(e) => Err(e), } } + +/// **Connect to event handler** +/// +/// ```BASH +/// curl -X GET 'http://127.0.0.1:8787/data/event/1?endpoint=system&uuid=f2f8c29b-712a-48c5-8919-b535d3a05a3a' +/// ``` +#[get("/event/{channel}")] +async fn event_stream( + pool: web::Data>, + broadcaster: web::Data, + data: web::Data, + id: web::Path, + user: web::Query, +) -> Result { + let mut uuids = data.uuids.lock().map_err(|e| e.to_string())?; + + if let Err(e) = check_uuid(&mut uuids, user.uuid.as_str()) { + return Err(e); + } + + let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; + + Ok(broadcaster + .new_client(*id, config, user.endpoint.clone()) + .await) +} diff --git a/ffplayout-api/src/utils/system.rs b/ffplayout-api/src/utils/system.rs index 9cd47e15..3feb2e47 100644 --- a/ffplayout-api/src/utils/system.rs +++ b/ffplayout-api/src/utils/system.rs @@ -71,6 +71,12 @@ pub struct SystemStat { pub system: MySystem, } +impl SystemStat { + pub fn to_string(&self) -> String { + serde_json::to_string(&self).unwrap() + } +} + pub fn stat(config: PlayoutConfig) -> SystemStat { let mut disks = DISKS.lock().unwrap(); let mut networks = NETWORKS.lock().unwrap(); From 09d6e279d038491dee5a6af2a6a4929bb135bba4 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 25 Apr 2024 13:00:13 +0200 Subject: [PATCH 10/22] fix clippy errors --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- ffplayout-api/src/main.rs | 8 ++------ ffplayout-api/src/sse/mod.rs | 8 +++++++- ffplayout-api/src/sse/routes.rs | 10 ++++------ ffplayout-api/src/utils/system.rs | 8 ++++---- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8cd55bb8..ac231a67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1293,7 +1293,7 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "ffplayout" -version = "0.21.3" +version = "0.22.0" dependencies = [ "chrono", "clap", @@ -1315,7 +1315,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.21.3" +version = "0.22.0" dependencies = [ "actix-files", "actix-multipart", @@ -1358,7 +1358,7 @@ dependencies = [ [[package]] name = "ffplayout-lib" -version = "0.21.3" +version = "0.22.0" dependencies = [ "chrono", "crossbeam-channel", @@ -3578,7 +3578,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.21.3" +version = "0.22.0" dependencies = [ "chrono", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 15993ce8..00dbb1c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"] resolver = "2" [workspace.package] -version = "0.21.3" +version = "0.22.0" license = "GPL-3.0" repository = "https://github.com/ffplayout/ffplayout" authors = ["Jonathan Baecker "] diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index 981c460f..c3e52737 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -1,9 +1,4 @@ -use std::{ - collections::HashSet, - env, - process::exit, - sync::{Arc, Mutex}, -}; +use std::{collections::HashSet, env, process::exit, sync::Arc}; use actix_files::Files; use actix_web::{ @@ -17,6 +12,7 @@ use actix_web_static_files::ResourceFiles; use path_clean::PathClean; use simplelog::*; +use tokio::sync::Mutex; use ffplayout_api::{ api::{auth, routes::*}, diff --git a/ffplayout-api/src/sse/mod.rs b/ffplayout-api/src/sse/mod.rs index e2545c8b..834573ea 100644 --- a/ffplayout-api/src/sse/mod.rs +++ b/ffplayout-api/src/sse/mod.rs @@ -1,9 +1,9 @@ use std::{ collections::HashSet, - sync::Mutex, time::{Duration, SystemTime}, }; +use tokio::sync::Mutex; use uuid::Uuid; use crate::utils::errors::ServiceError; @@ -26,6 +26,12 @@ impl UuidData { } } +impl Default for UuidData { + fn default() -> Self { + Self::new() + } +} + pub struct AuthState { pub uuids: Mutex>, } diff --git a/ffplayout-api/src/sse/routes.rs b/ffplayout-api/src/sse/routes.rs index 665ca9b1..a33bf02b 100644 --- a/ffplayout-api/src/sse/routes.rs +++ b/ffplayout-api/src/sse/routes.rs @@ -28,7 +28,7 @@ impl User { #[post("/generate-uuid")] #[protect(any("Role::Admin", "Role::User"), ty = "Role")] async fn generate_uuid(data: web::Data) -> Result { - let mut uuids = data.uuids.lock().map_err(|e| e.to_string())?; + let mut uuids = data.uuids.lock().await; let new_uuid = UuidData::new(); let user_auth = User::new(String::new(), new_uuid.uuid.to_string()); @@ -49,7 +49,7 @@ async fn validate_uuid( data: web::Data, user: web::Query, ) -> Result { - let mut uuids = data.uuids.lock().map_err(|e| e.to_string())?; + let mut uuids = data.uuids.lock().await; match check_uuid(&mut uuids, user.uuid.as_str()) { Ok(s) => Ok(web::Json(s)), @@ -70,11 +70,9 @@ async fn event_stream( id: web::Path, user: web::Query, ) -> Result { - let mut uuids = data.uuids.lock().map_err(|e| e.to_string())?; + let mut uuids = data.uuids.lock().await; - if let Err(e) = check_uuid(&mut uuids, user.uuid.as_str()) { - return Err(e); - } + check_uuid(&mut uuids, user.uuid.as_str())?; let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; diff --git a/ffplayout-api/src/utils/system.rs b/ffplayout-api/src/utils/system.rs index 3feb2e47..f52f35e2 100644 --- a/ffplayout-api/src/utils/system.rs +++ b/ffplayout-api/src/utils/system.rs @@ -1,4 +1,4 @@ -// use std::cmp; +use std::fmt; use local_ip_address::list_afinet_netifas; use serde::Serialize; @@ -71,9 +71,9 @@ pub struct SystemStat { pub system: MySystem, } -impl SystemStat { - pub fn to_string(&self) -> String { - serde_json::to_string(&self).unwrap() +impl fmt::Display for SystemStat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", serde_json::to_string(self).unwrap()) } } From 7dcb8fce65a376baccb0c7dfbef7906d10b7b0c4 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 09:37:52 +0200 Subject: [PATCH 11/22] ignore module in logging --- lib/src/utils/logging.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/utils/logging.rs b/lib/src/utils/logging.rs index ffef82e0..92523b9d 100644 --- a/lib/src/utils/logging.rs +++ b/lib/src/utils/logging.rs @@ -191,6 +191,7 @@ pub fn init_logging( .add_filter_ignore_str("reqwest") .add_filter_ignore_str("rpc") .add_filter_ignore_str("rustls") + .add_filter_ignore_str("serial_test") .add_filter_ignore_str("sqlx") .add_filter_ignore_str("tiny_http") .set_level_padding(LevelPadding::Left) From aeb5a1b5eab161ab78cb118bfd58449a89c65399 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 09:42:33 +0200 Subject: [PATCH 12/22] switch back to playlist change behavior from v0.20 but get right index, fix #619 --- ffplayout-engine/src/input/playlist.rs | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index 4426f292..48e91b28 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -112,24 +112,36 @@ impl CurrentProgram { let (delta, total_delta) = get_delta(&self.config, &time_in_seconds()); let mut next = false; - trace!( - "delta: {delta}, total_delta: {total_delta}, current index: {}", - self.current_node.index.unwrap_or_default() - ); + let duration = if self.current_node.duration >= self.current_node.out { + self.current_node.duration + } else { + // maybe out is longer to be able to loop + self.current_node.out + }; - let mut clip_start = self.current_node.begin.unwrap_or_default() - self.start_sec; + let node_index = self.current_node.index.unwrap_or_default(); - if self.player_control.current_index.load(Ordering::SeqCst) - == self.player_control.current_list.lock().unwrap().len() - 1 + trace!("delta: {delta}, total_delta: {total_delta}, current index: {node_index}",); + + let mut next_start = + self.current_node.begin.unwrap_or_default() - self.start_sec + duration + delta; + + if node_index > 0 + && node_index == self.player_control.current_list.lock().unwrap().len() - 1 { - clip_start += self.config.general.stop_threshold; + println!("add threshold!"); + next_start += self.config.general.stop_threshold; } - trace!("clip_start: {clip_start}, end_sec: {}", self.end_sec); + trace!( + "next_start: {next_start} | end_sec: {} | source {}", + self.end_sec, + self.current_node.source + ); // Check if we over the target length or we are close to it, if so we load the next playlist. if !self.config.playlist.infinit - && (clip_start >= self.end_sec + && (next_start >= self.end_sec || is_close(total_delta, 0.0, 2.0) || is_close(total_delta, self.end_sec, 2.0)) { From 07f8445c529ec956b592182ca3c28526c0bdd61c Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 09:47:04 +0200 Subject: [PATCH 13/22] cleanup --- ffplayout-engine/src/input/playlist.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index 48e91b28..7256d8db 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -129,7 +129,6 @@ impl CurrentProgram { if node_index > 0 && node_index == self.player_control.current_list.lock().unwrap().len() - 1 { - println!("add threshold!"); next_start += self.config.general.stop_threshold; } From bc89a163064f3ede42b6f36e6eb2f9cb2f5bdd6f Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 09:48:15 +0200 Subject: [PATCH 14/22] update version --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a840f695..158182cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1217,7 +1217,7 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "ffplayout" -version = "0.21.3" +version = "0.21.4" dependencies = [ "chrono", "clap", @@ -1239,7 +1239,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.21.3" +version = "0.21.4" dependencies = [ "actix-files", "actix-multipart", @@ -1278,7 +1278,7 @@ dependencies = [ [[package]] name = "ffplayout-lib" -version = "0.21.3" +version = "0.21.4" dependencies = [ "chrono", "crossbeam-channel", @@ -3473,7 +3473,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.21.3" +version = "0.21.4" dependencies = [ "chrono", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 15993ce8..c73282ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"] resolver = "2" [workspace.package] -version = "0.21.3" +version = "0.21.4" license = "GPL-3.0" repository = "https://github.com/ffplayout/ffplayout" authors = ["Jonathan Baecker "] From 35f527bf9b85de0f90d4e0bccfd2d5ce21885cb3 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 11:56:01 +0200 Subject: [PATCH 15/22] switch to cross-rs toolchain --- Cross.toml | 26 ++++++++++++++ docs/developer.md | 89 ++++++++++------------------------------------- scripts/build.sh | 8 ++--- 3 files changed, 48 insertions(+), 75 deletions(-) create mode 100644 Cross.toml diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 00000000..f5325d03 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,26 @@ +[target.x86_64-unknown-linux-musl] +pre-build = [ + """apt-get update && \ + apt-get --assume-yes install curl && \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get --assume-yes install nodejs + """ +] + +[target.aarch64-unknown-linux-gnu] +pre-build = [ + """apt-get update && \ + apt-get --assume-yes install curl && \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get --assume-yes install nodejs + """ +] + +[target.x86_64-pc-windows-gnu] +pre-build = [ + """apt-get update && \ + apt-get --assume-yes install curl && \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get --assume-yes install nodejs + """ +] diff --git a/docs/developer.md b/docs/developer.md index 9728c85f..f19e1673 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,24 +2,6 @@ For compiling use always the news Rust version, the best is to install it from [rustup](https://rustup.rs/). -### Cross Compile - -For cross compiling on fedora linux, you need to install some extra packages: - -- mingw compiler: -``` -dnf install mingw64-filesystem mingw64-binutils mingw64-gcc{,-c++} mingw64-crt mingw64-headers mingw64-pkg-config mingw64-hamlib mingw64-libpng mingw64-libusbx mingw64-portaudio mingw64-fltk mingw64-libgnurx mingw64-gettext mingw64-winpthreads-static intltool -``` - -- rust tools: -``` -rustup target add x86_64-pc-windows-gnu -``` - -[Cross](https://github.com/cross-rs/cross#dependencies) could be an option to. - -To build, run: `cargo build --release --target=x86_64-pc-windows-gnu` - ### Static Linking Running `cargo build` ends up in a binary which depend on **libc.so**. But you can compile also the binary totally static: @@ -33,6 +15,20 @@ Compile with: `cargo build --release --target=x86_64-unknown-linux-musl`. This release should run on any Linux distro. +**Note: You can also create a static version with Cross Toolchain. For this, follow the next steps.** + +### Cross Compile + +For cross compiling install docker or podman and latest [cross-rs](https://github.com/cross-rs/cross): + +``` +cargo install cross --git https://github.com/cross-rs/cross +``` + +To build for windows, run: `cross build --release --target x86_64-pc-windows-gnu`\ +To build for linux aarch64: `cross build --release --target aarch64-unknown-linux-gnu` +Etc. + ### Compile from Linux for macOS Add toolchain: @@ -70,72 +66,23 @@ CC="aarch64-apple-darwin20.4-clang -arch arm64e" cargo build --release --target= # for x86_64 CC="o64-clang" cargo build --release --target=x86_64-apple-darwin ``` - -### Compile for armv7 Linux - -Add toolchain: - -```Bash -rustup target add armv7-unknown-linux-gnueabihf -``` - -Add cross compiler: - -```Bash -dnf copr enable lantw44/arm-linux-gnueabihf-toolchain - -dnf install arm-linux-gnueabihf-{binutils,gcc,glibc} -``` - -Add target to `~/.cargo/config`: - -```Bash -[target.armv7-unknown-linux-gnueabihf] -linker = "arm-linux-gnueabihf-gcc" -rustflags = [ "-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc" ] -``` - -### Compile for aarch64 Linux - -Add toolchain: - -```Bash -rustup target add aarch64-unknown-linux-gnu -``` - -Add cross compiler: - -```Bash -dnf copr enable lantw44/aarch64-linux-gnu-toolchain - -dnf install aarch64-linux-gnu-{binutils,gcc,glibc} -``` - -Add target to `~/.cargo/config`: - -```Bash -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" -rustflags = [ "-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc" ] -``` - ### Create debian DEB and RHEL RPM packages install: - `cargo install cargo-deb` - `cargo install cargo-generate-rpm` -And run with: +Compile to your target system with cargo or cross, and run: ```Bash # for debian based systems: -cargo deb --target=x86_64-unknown-linux-musl +cargo deb --no-build --target=x86_64-unknown-linux-musl # for armhf -cargo deb --target=armv7-unknown-linux-gnueabihf --variant=armhf -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml +cargo deb --no-build --target=armv7-unknown-linux-gnueabihf --variant=armhf -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml # for arm64 -cargo deb --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml +cargo deb --no-build --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml # for rhel based systems: cargo generate-rpm --target=x86_64-unknown-linux-musl diff --git a/scripts/build.sh b/scripts/build.sh index 769c09cc..b96b481d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -32,7 +32,7 @@ for target in "${targets[@]}"; do rm -f "ffplayout-v${version}_${target}.zip" fi - cargo build --release --target=$target + cross build --release --target=$target cp ./target/${target}/release/ffpapi.exe . cp ./target/${target}/release/ffplayout.exe . @@ -61,7 +61,7 @@ for target in "${targets[@]}"; do rm -f "ffplayout-v${version}_${target}.tar.gz" fi - cargo build --release --target=$target + cross build --release --target=$target cp ./target/${target}/release/ffpapi . cp ./target/${target}/release/ffplayout . @@ -73,10 +73,10 @@ for target in "${targets[@]}"; do done if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "x86_64-unknown-linux-musl" ]]; then - cargo deb --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_amd64.deb + cargo deb --no-build --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_amd64.deb cargo generate-rpm --payload-compress none --target=x86_64-unknown-linux-musl -p ffplayout-engine -o ffplayout-${version}-1.x86_64.rpm fi if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "aarch64-unknown-linux-gnu" ]]; then - cargo deb --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_arm64.deb + cargo deb --no-build --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_arm64.deb fi From 82bcd3d08640ef90e119ab0bb7ede285925602ce Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 13:54:33 +0200 Subject: [PATCH 16/22] add macos cross build --- Cross.toml | 45 ++++++++++++++++++++++++++++++--------------- docs/developer.md | 35 ++++++++--------------------------- scripts/build.sh | 18 ------------------ 3 files changed, 38 insertions(+), 60 deletions(-) diff --git a/Cross.toml b/Cross.toml index f5325d03..1718f18b 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,26 +1,41 @@ [target.x86_64-unknown-linux-musl] pre-build = [ - """apt-get update && \ - apt-get --assume-yes install curl && \ - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ - apt-get --assume-yes install nodejs - """ + "apt-get update", + "apt-get --assume-yes install curl", + "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -", + "apt-get --assume-yes install nodejs" ] [target.aarch64-unknown-linux-gnu] pre-build = [ - """apt-get update && \ - apt-get --assume-yes install curl && \ - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ - apt-get --assume-yes install nodejs - """ + "apt-get update", + "apt-get --assume-yes install curl", + "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -", + "apt-get --assume-yes install nodejs" ] [target.x86_64-pc-windows-gnu] pre-build = [ - """apt-get update && \ - apt-get --assume-yes install curl && \ - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ - apt-get --assume-yes install nodejs - """ + "apt-get update", + "apt-get --assume-yes install curl", + "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -", + "apt-get --assume-yes install nodejs" +] + +[target.x86_64-apple-darwin] +image = "ghcr.io/cross-rs/x86_64-apple-darwin-cross:local" +pre-build = [ + "apt-get update", + "apt-get --assume-yes install curl", + "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -", + "apt-get --assume-yes install nodejs" +] + +[target.aarch64-apple-darwin] +image = "ghcr.io/cross-rs/aarch64-apple-darwin-cross:local" +pre-build = [ + "apt-get update", + "apt-get --assume-yes install curl", + "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -", + "apt-get --assume-yes install nodejs" ] diff --git a/docs/developer.md b/docs/developer.md index f19e1673..ea62268c 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -31,40 +31,21 @@ Etc. ### Compile from Linux for macOS -Add toolchain: +Follow [cross-toolchains](https://github.com/cross-rs/cross-toolchains) instruction to add macOS support to cross. -```Bash -# for arm64 -rustup target add aarch64-apple-darwin +I created my image with: -# for x86_64 -rustup target add x86_64-apple-darwin +``` +cargo build-docker-image x86_64-apple-darwin-cross \ + --build-arg 'MACOS_SDK_URL=https://github.com/joseluisq/macosx-sdks/releases/download/12.3/MacOSX12.3.sdk.tar.xz' ``` -Add linker and ar settings to `~/.cargo/config`: +Build then with: -```Bash -[target.x86_64-apple-darwin] -linker = "x86_64-apple-darwin20.4-clang" -ar = "x86_64-apple-darwin20.4-ar" - -[target.aarch64-apple-darwin] -linker = "aarch64-apple-darwin20.4-clang" -ar = "aarch64-apple-darwin20.4-ar" +``` +cross build --release --target aarch64-apple-darwin ``` -Follow this guide: [rust-cross-compile-linux-to-macos](https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html) - -Or setup [osxcross](https://github.com/tpoechtrager/osxcross) correctly. - -Add **osxcross/target/bin** to your **PATH** and run cargo with: - -```Bash -# for arm64 -CC="aarch64-apple-darwin20.4-clang -arch arm64e" cargo build --release --target=aarch64-apple-darwin - -# for x86_64 -CC="o64-clang" cargo build --release --target=x86_64-apple-darwin ``` ### Create debian DEB and RHEL RPM packages diff --git a/scripts/build.sh b/scripts/build.sh index b96b481d..e5a09ac7 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -38,24 +38,6 @@ for target in "${targets[@]}"; do cp ./target/${target}/release/ffplayout.exe . zip -r "ffplayout-v${version}_${target}.zip" assets docker docs LICENSE README.md CHANGELOG.md ffplayout.exe ffpapi.exe -x *.db -x *.db-shm -x *.db-wal -x '11-ffplayout' -x *.service rm -f ffplayout.exe ffpapi.exe - elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then - if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then - rm -f "ffplayout-v${version}_${target}.tar.gz" - fi - c_cc="x86_64-apple-darwin20.4-clang" - c_cxx="x86_64-apple-darwin20.4-clang++" - - if [[ $target == "aarch64-apple-darwin" ]]; then - c_cc="aarch64-apple-darwin20.4-clang" - c_cxx="aarch64-apple-darwin20.4-clang++" - fi - - CC="$c_cc" CXX="$c_cxx" cargo build --release --target=$target - - cp ./target/${target}/release/ffpapi . - cp ./target/${target}/release/ffplayout . - tar -czvf "ffplayout-v${version}_${target}.tar.gz" --exclude='*.db' --exclude='*.db-shm' --exclude='*.db-wal' --exclude='11-ffplayout' --exclude='*.service' assets docker docs LICENSE README.md CHANGELOG.md ffplayout ffpapi - rm -f ffplayout ffpapi else if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then rm -f "ffplayout-v${version}_${target}.tar.gz" From f73085176a6efc8cd13225c9783fb08f669d286c Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 14:32:08 +0200 Subject: [PATCH 17/22] switch to cross-rs --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 629bf9a2..d89a1f00 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,6 +19,7 @@ }, "cSpell.words": [ "actix", + "rsplit", "tokio", "uuids" ] From 47b88d90ca46a87424e9ee5c8caa1248097828d4 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Apr 2024 14:43:55 +0200 Subject: [PATCH 18/22] reduce trace log --- ffplayout-engine/src/input/playlist.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index 7256d8db..6d5dcda2 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -121,8 +121,6 @@ impl CurrentProgram { let node_index = self.current_node.index.unwrap_or_default(); - trace!("delta: {delta}, total_delta: {total_delta}, current index: {node_index}",); - let mut next_start = self.current_node.begin.unwrap_or_default() - self.start_sec + duration + delta; @@ -133,7 +131,7 @@ impl CurrentProgram { } trace!( - "next_start: {next_start} | end_sec: {} | source {}", + "delta: {delta} | total_delta: {total_delta}, index: {node_index} \nnext_start: {next_start} | end_sec: {} | source {}", self.end_sec, self.current_node.source ); From 1d8307015f91137759a9e35b10c75be6a48c9f61 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 28 Apr 2024 21:41:17 +0200 Subject: [PATCH 19/22] playout status --- ffplayout-api/src/api/routes.rs | 31 +++++++++++++++++----- ffplayout-api/src/sse/broadcast.rs | 40 ++++++++++++++++++----------- ffplayout-api/src/utils/channels.rs | 9 ++++--- ffplayout-api/src/utils/control.rs | 30 +++++++++------------- 4 files changed, 68 insertions(+), 42 deletions(-) diff --git a/ffplayout-api/src/api/routes.rs b/ffplayout-api/src/api/routes.rs index 570d6fcd..586877e2 100644 --- a/ffplayout-api/src/api/routes.rs +++ b/ffplayout-api/src/api/routes.rs @@ -651,7 +651,9 @@ pub async fn send_text_message( id: web::Path, data: web::Json>, ) -> Result { - match send_message(&pool.into_inner(), *id, data.into_inner()).await { + let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; + + match send_message(&config, data.into_inner()).await { Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } @@ -674,7 +676,9 @@ pub async fn control_playout( id: web::Path, control: web::Json, ) -> Result { - match control_state(&pool.into_inner(), *id, &control.control).await { + let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; + + match control_state(&config, &control.control).await { Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } @@ -716,7 +720,9 @@ pub async fn media_current( pool: web::Data>, id: web::Path, ) -> Result { - match media_info(&pool.into_inner(), *id, "current".into()).await { + let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; + + match media_info(&config, "current".into()).await { Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } @@ -733,7 +739,9 @@ pub async fn media_next( pool: web::Data>, id: web::Path, ) -> Result { - match media_info(&pool.into_inner(), *id, "next".into()).await { + let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; + + match media_info(&config, "next".into()).await { Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } @@ -751,7 +759,9 @@ pub async fn media_last( pool: web::Data>, id: web::Path, ) -> Result { - match media_info(&pool.into_inner(), *id, "last".into()).await { + let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; + + match media_info(&config, "last".into()).await { Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } @@ -778,7 +788,16 @@ pub async fn process_control( proc: web::Json, engine_process: web::Data, ) -> Result { - control_service(&pool.into_inner(), *id, &proc.command, Some(engine_process)).await + let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; + + control_service( + &pool.into_inner(), + &config, + *id, + &proc.command, + Some(engine_process), + ) + .await } /// #### ffplayout Playlist Operations diff --git a/ffplayout-api/src/sse/broadcast.rs b/ffplayout-api/src/sse/broadcast.rs index 7286f638..3492e6ef 100644 --- a/ffplayout-api/src/sse/broadcast.rs +++ b/ffplayout-api/src/sse/broadcast.rs @@ -7,12 +7,11 @@ use actix_web_lab::{ }; use ffplayout_lib::utils::PlayoutConfig; -use futures_util::future; use parking_lot::Mutex; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; -use crate::utils::system; +use crate::utils::{control::media_info, system}; #[derive(Debug, Clone)] struct Client { @@ -73,9 +72,8 @@ impl Broadcaster { this.remove_stale_clients().await; } - if counter % 30 == 0 { - // TODO: implement playout status - this.broadcast("ping").await; + if counter % 5 == 0 { + this.broadcast_playout().await; } this.broadcast_system().await; @@ -124,20 +122,32 @@ impl Broadcaster { Sse::from_infallible_receiver(rx) } - /// Broadcasts `msg` to all clients. - pub async fn broadcast(&self, msg: &str) { + /// Broadcasts playout status to clients. + pub async fn broadcast_playout(&self) { let clients = self.inner.lock().clients.clone(); - let send_futures = clients - .iter() - .map(|client| client.sender.send(sse::Data::new(msg).into())); - - // try to send to all clients, ignoring failures - // disconnected clients will get swept up by `remove_stale_clients` - let _ = future::join_all(send_futures).await; + for client in clients.iter().filter(|client| client.endpoint == "playout") { + match media_info(&client.config, "current".into()).await { + Ok(res) => { + let _ = client + .sender + .send( + sse::Data::new(res.text().await.unwrap_or_else(|_| "Success".into())) + .into(), + ) + .await; + } + Err(_) => { + let _ = client + .sender + .send(sse::Data::new("not running").into()) + .await; + } + }; + } } - /// Broadcasts `msg` to all clients. + /// Broadcasts system status to clients. pub async fn broadcast_system(&self) { let clients = self.inner.lock().clients.clone(); diff --git a/ffplayout-api/src/utils/channels.rs b/ffplayout-api/src/utils/channels.rs index e3ae52c0..3d289c3e 100644 --- a/ffplayout-api/src/utils/channels.rs +++ b/ffplayout-api/src/utils/channels.rs @@ -12,6 +12,7 @@ use crate::utils::{ use ffplayout_lib::utils::PlayoutConfig; use crate::db::{handles, models::Channel}; +use crate::utils::playout_config; pub async fn create_channel( conn: &Pool, @@ -51,15 +52,17 @@ pub async fn create_channel( serde_yaml::to_writer(file, &config).unwrap(); let new_channel = handles::insert_channel(conn, target_channel).await?; - control_service(conn, new_channel.id, &ServiceCmd::Enable, None).await?; + control_service(conn, &config, new_channel.id, &ServiceCmd::Enable, None).await?; Ok(new_channel) } pub async fn delete_channel(conn: &Pool, id: i32) -> Result<(), ServiceError> { let channel = handles::select_channel(conn, &id).await?; - control_service(conn, channel.id, &ServiceCmd::Stop, None).await?; - control_service(conn, channel.id, &ServiceCmd::Disable, None).await?; + let (config, _) = playout_config(conn, &id).await?; + + control_service(conn, &config, channel.id, &ServiceCmd::Stop, None).await?; + control_service(conn, &config, channel.id, &ServiceCmd::Disable, None).await?; if let Err(e) = fs::remove_file(channel.config_path) { error!("{e}"); diff --git a/ffplayout-api/src/utils/control.rs b/ffplayout-api/src/utils/control.rs index cef70a3d..19e62d31 100644 --- a/ffplayout-api/src/utils/control.rs +++ b/ffplayout-api/src/utils/control.rs @@ -15,8 +15,8 @@ use tokio::{ }; use crate::db::handles::select_channel; -use crate::utils::{errors::ServiceError, playout_config}; -use ffplayout_lib::vec_strings; +use crate::utils::errors::ServiceError; +use ffplayout_lib::{utils::PlayoutConfig, vec_strings}; #[derive(Debug, Deserialize, Serialize, Clone)] struct TextParams { @@ -241,11 +241,10 @@ impl SystemD { } } -async fn post_request(conn: &Pool, id: i32, obj: T) -> Result +async fn post_request(config: &PlayoutConfig, obj: T) -> Result where T: Serialize, { - let (config, _) = playout_config(conn, &id).await?; let url = format!("http://{}", config.rpc_server.address); let client = Client::new(); @@ -262,8 +261,7 @@ where } pub async fn send_message( - conn: &Pool, - id: i32, + config: &PlayoutConfig, message: HashMap, ) -> Result { let json_obj = TextParams { @@ -271,33 +269,29 @@ pub async fn send_message( message, }; - post_request(conn, id, json_obj).await + post_request(config, json_obj).await } pub async fn control_state( - conn: &Pool, - id: i32, + config: &PlayoutConfig, command: &str, ) -> Result { let json_obj = ControlParams { control: command.to_owned(), }; - post_request(conn, id, json_obj).await + post_request(config, json_obj).await } -pub async fn media_info( - conn: &Pool, - id: i32, - command: String, -) -> Result { +pub async fn media_info(config: &PlayoutConfig, command: String) -> Result { let json_obj = MediaParams { media: command }; - post_request(conn, id, json_obj).await + post_request(config, json_obj).await } pub async fn control_service( conn: &Pool, + config: &PlayoutConfig, id: i32, command: &ServiceCmd, engine: Option>, @@ -307,14 +301,14 @@ pub async fn control_service( match command { ServiceCmd::Start => en.start().await, ServiceCmd::Stop => { - if control_state(conn, id, "stop_all").await.is_ok() { + if control_state(config, "stop_all").await.is_ok() { en.stop().await } else { Err(ServiceError::NoContent("Nothing to stop".to_string())) } } ServiceCmd::Restart => { - if control_state(conn, id, "stop_all").await.is_ok() { + if control_state(config, "stop_all").await.is_ok() { en.restart().await } else { Err(ServiceError::NoContent("Nothing to restart".to_string())) From c4ea14dc94b37c675bfb7d7c9543a7db94ac3dd2 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 29 Apr 2024 09:31:35 +0200 Subject: [PATCH 20/22] add sse playout status, reduce current media object --- README.md | 12 +++--- ffplayout-api/examples/index.html | 35 ------------------ ffplayout-api/examples/sse.rs | 59 ------------------------------ ffplayout-api/src/api/routes.rs | 20 ++++------ ffplayout-api/src/sse/broadcast.rs | 5 +-- ffplayout-engine/src/utils/mod.rs | 22 +++++------ 6 files changed, 23 insertions(+), 130 deletions(-) delete mode 100644 ffplayout-api/examples/index.html delete mode 100644 ffplayout-api/examples/sse.rs diff --git a/README.md b/README.md index f1633b4f..4be5d9be 100644 --- a/README.md +++ b/README.md @@ -176,19 +176,17 @@ Output from `{"media":"current"}` show: ```JSON { - "current_media": { + "media": { "category": "", "duration": 154.2, "out": 154.2, - "seek": 0.0, + "in": 0.0, "source": "/opt/tv-media/clip.mp4" }, "index": 39, - "play_mode": "playlist", - "played_sec": 67.80771999300123, - "remaining_sec": 86.39228000699876, - "start_sec": 24713.631999999998, - "start_time": "06:51:53.631" + "mode": "playlist", + "ingest": false, + "played": 67.80771999300123, } ``` diff --git a/ffplayout-api/examples/index.html b/ffplayout-api/examples/index.html deleted file mode 100644 index a9c6d4e6..00000000 --- a/ffplayout-api/examples/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - Server-sent events - - - -
-
- - - diff --git a/ffplayout-api/examples/sse.rs b/ffplayout-api/examples/sse.rs deleted file mode 100644 index 033df68f..00000000 --- a/ffplayout-api/examples/sse.rs +++ /dev/null @@ -1,59 +0,0 @@ -/// https://github.com/actix/examples/tree/master/server-sent-events -/// -use std::{io, sync::Arc}; - -use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; -use actix_web_lab::{extract::Path, respond::Html}; - -use simplelog::*; - -use ffplayout_api::sse::broadcast::Broadcaster; - -use ffplayout_lib::utils::{init_logging, PlayoutConfig}; - -#[actix_web::main] -async fn main() -> io::Result<()> { - let mut config = PlayoutConfig::new(None, None); - config.mail.recipient = String::new(); - config.logging.log_to_file = false; - config.logging.timestamp = false; - - let logging = init_logging(&config, None, None); - CombinedLogger::init(logging).unwrap(); - - let data = Broadcaster::create(); - - HttpServer::new(move || { - App::new() - .app_data(web::Data::from(Arc::clone(&data))) - .service(index) - .service(event_stream) - .service(broadcast_msg) - .wrap(Logger::default()) - }) - .bind(("127.0.0.1", 8080))? - .workers(2) - .run() - .await -} - -#[get("/")] -async fn index() -> impl Responder { - Html(include_str!("index.html").to_owned()) -} - -#[get("/events")] -async fn event_stream(broadcaster: web::Data) -> impl Responder { - broadcaster - .new_client(1, PlayoutConfig::default(), "ping".to_string()) - .await -} - -#[post("/broadcast/{msg}")] -async fn broadcast_msg( - broadcaster: web::Data, - Path((msg,)): Path<(String,)>, -) -> impl Responder { - broadcaster.broadcast(&msg).await; - HttpResponse::Ok().body("msg sent") -} diff --git a/ffplayout-api/src/api/routes.rs b/ffplayout-api/src/api/routes.rs index 586877e2..17806f3f 100644 --- a/ffplayout-api/src/api/routes.rs +++ b/ffplayout-api/src/api/routes.rs @@ -694,25 +694,19 @@ pub async fn control_playout( /// **Response:** /// /// ```JSON -/// { -/// "jsonrpc": "2.0", -/// "result": { -/// "current_media": { +/// { +/// "media": { /// "category": "", /// "duration": 154.2, /// "out": 154.2, -/// "seek": 0.0, +/// "in": 0.0, /// "source": "/opt/tv-media/clip.mp4" /// }, /// "index": 39, -/// "play_mode": "playlist", -/// "played_sec": 67.80771999300123, -/// "remaining_sec": 86.39228000699876, -/// "start_sec": 24713.631999999998, -/// "start_time": "06:51:53.631" -/// }, -/// "id": 1 -/// } +/// "ingest": false, +/// "mode": "playlist", +/// "played": 67.808 +/// } /// ``` #[get("/control/{id}/media/current")] #[protect(any("Role::Admin", "Role::User"), ty = "Role")] diff --git a/ffplayout-api/src/sse/broadcast.rs b/ffplayout-api/src/sse/broadcast.rs index 3492e6ef..5ec9e853 100644 --- a/ffplayout-api/src/sse/broadcast.rs +++ b/ffplayout-api/src/sse/broadcast.rs @@ -72,10 +72,7 @@ impl Broadcaster { this.remove_stale_clients().await; } - if counter % 5 == 0 { - this.broadcast_playout().await; - } - + this.broadcast_playout().await; this.broadcast_system().await; counter = (counter + 1) % 61; diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index 5a464d72..2fd02b3a 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -15,8 +15,8 @@ pub use arg_parse::Args; use ffplayout_lib::{ filter::Filters, utils::{ - config::Template, errors::ProcError, parse_log_level_filter, sec_to_time, time_in_seconds, - time_to_sec, Media, OutputMode::*, PlayoutConfig, PlayoutStatus, ProcessMode::*, + config::Template, errors::ProcError, parse_log_level_filter, time_in_seconds, time_to_sec, + Media, OutputMode::*, PlayoutConfig, PlayoutStatus, ProcessMode::*, }, vec_strings, }; @@ -252,7 +252,7 @@ pub fn prepare_output_cmd( /// map media struct to json object pub fn get_media_map(media: Media) -> Value { json!({ - "seek": media.seek, + "in": media.seek, "out": media.out, "duration": media.duration, "category": media.category, @@ -272,21 +272,19 @@ pub fn get_data_map( let shift = *playout_stat.time_shift.lock().unwrap(); let begin = media.begin.unwrap_or(0.0) - shift; - data_map.insert("play_mode".to_string(), json!(config.processing.mode)); - data_map.insert("ingest_runs".to_string(), json!(server_is_running)); + data_map.insert("mode".to_string(), json!(config.processing.mode)); + data_map.insert("ingest".to_string(), json!(server_is_running)); data_map.insert("index".to_string(), json!(media.index)); - data_map.insert("start_sec".to_string(), json!(begin)); if begin > 0.0 { let played_time = current_time - begin; - let remaining_time = media.out - played_time; - - data_map.insert("start_time".to_string(), json!(sec_to_time(begin))); - data_map.insert("played_sec".to_string(), json!(played_time)); - data_map.insert("remaining_sec".to_string(), json!(remaining_time)); + data_map.insert( + "played".to_string(), + json!((played_time * 1000.0).round() / 1000.0), + ); } - data_map.insert("current_media".to_string(), get_media_map(media)); + data_map.insert("media".to_string(), get_media_map(media)); data_map } From 1d22cda65c4f95aa959dff835dc4ee549d618454 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 29 Apr 2024 11:05:51 +0200 Subject: [PATCH 21/22] add nginx location for sse, update frontend --- assets/ffplayout.conf | 12 +++++++++++- ffplayout-frontend | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/assets/ffplayout.conf b/assets/ffplayout.conf index 3cbea865..a9f13a00 100644 --- a/assets/ffplayout.conf +++ b/assets/ffplayout.conf @@ -22,7 +22,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 36000s; - proxy_connect_timeout 36000s; + proxy_connect_timeout 36000s; proxy_send_timeout 36000s; proxy_buffer_size 128k; proxy_buffers 4 256k; @@ -31,6 +31,16 @@ server { proxy_pass http://127.0.0.1:8787; } + location /data { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_http_version 1.1; + proxy_pass http://127.0.0.1:8787/data; + } + location /live/ { alias /usr/share/ffplayout/public/live/; } diff --git a/ffplayout-frontend b/ffplayout-frontend index 52411f61..c4aa6f49 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 52411f61ef25a1b11129a77d26d987f44c6ea543 +Subproject commit c4aa6f495fd58be368a4266cb426e2c646e1424a From 772572172d21de112c8f566e5d109f2f2741311b Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 30 Apr 2024 09:12:33 +0200 Subject: [PATCH 22/22] add shift, rename played to elapsed --- ffplayout-engine/src/utils/mod.rs | 22 +++++++++++----------- ffplayout-frontend | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index 2fd02b3a..678e1d8e 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -271,19 +271,19 @@ pub fn get_data_map( let current_time = time_in_seconds(); let shift = *playout_stat.time_shift.lock().unwrap(); let begin = media.begin.unwrap_or(0.0) - shift; + let played_time = current_time - begin; - data_map.insert("mode".to_string(), json!(config.processing.mode)); - data_map.insert("ingest".to_string(), json!(server_is_running)); data_map.insert("index".to_string(), json!(media.index)); - - if begin > 0.0 { - let played_time = current_time - begin; - data_map.insert( - "played".to_string(), - json!((played_time * 1000.0).round() / 1000.0), - ); - } - + data_map.insert("ingest".to_string(), json!(server_is_running)); + data_map.insert("mode".to_string(), json!(config.processing.mode)); + data_map.insert( + "shift".to_string(), + json!((shift * 1000.0).round() / 1000.0), + ); + data_map.insert( + "elapsed".to_string(), + json!((played_time * 1000.0).round() / 1000.0), + ); data_map.insert("media".to_string(), get_media_map(media)); data_map diff --git a/ffplayout-frontend b/ffplayout-frontend index c4aa6f49..6111c268 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit c4aa6f495fd58be368a4266cb426e2c646e1424a +Subproject commit 6111c2686d14b3bf33a4c0b29c85672f7e4f4399