diff --git a/Cargo.lock b/Cargo.lock index 231e4920..dc2acce2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,7 +485,7 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.22", + "rustix 0.37.23", "slab", "socket2", "waker-fn", @@ -535,9 +535,9 @@ checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.69" +version = "0.1.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", @@ -745,9 +745,9 @@ checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" [[package]] name = "clap" -version = "4.3.10" +version = "4.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a" +checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d" dependencies = [ "clap_builder", "clap_derive", @@ -756,9 +756,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.10" +version = "4.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d" +checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b" dependencies = [ "anstream", "anstyle", @@ -840,9 +840,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -1452,9 +1452,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -1693,7 +1693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" dependencies = [ "hermit-abi", - "rustix 0.38.2", + "rustix 0.38.3", "windows-sys 0.48.0", ] @@ -2392,9 +2392,21 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.4" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56" dependencies = [ "aho-corasick", "memchr", @@ -2403,9 +2415,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" [[package]] name = "relative-path" @@ -2516,9 +2528,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.22" +version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8818fa822adcc98b18fedbb3632a6a33213c070556b5aa7c4c8cc21cff565c4c" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", "errno", @@ -2530,9 +2542,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.2" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" dependencies = [ "bitflags 2.3.3", "errno", @@ -2632,9 +2644,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" dependencies = [ "itoa", "ryu", @@ -2773,9 +2785,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" @@ -3071,7 +3083,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix 0.37.22", + "rustix 0.37.23", "windows-sys 0.48.0", ] @@ -3111,18 +3123,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" dependencies = [ "proc-macro2", "quote", diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index 38ce245d..5b90b14d 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -250,13 +250,19 @@ impl CurrentProgram { if !self.playout_stat.list_init.load(Ordering::SeqCst) { let time_sec = self.get_current_time(); let index = self.index.fetch_add(1, Ordering::SeqCst); + let nodes = self.nodes.lock().unwrap(); + let last_index = nodes.len() - 1; // de-instance node to preserve original values in list - let mut node_clone = self.nodes.lock().unwrap()[index].clone(); + let mut node_clone = nodes[index].clone(); node_clone.seek = time_sec - node_clone.begin.unwrap(); - self.current_node = - handle_list_init(&self.config, node_clone, &self.playout_stat.chain); + self.current_node = handle_list_init( + &self.config, + node_clone, + &self.playout_stat.chain, + last_index, + ); } } } @@ -269,6 +275,7 @@ impl Iterator for CurrentProgram { self.check_update(self.playout_stat.list_init.load(Ordering::SeqCst)); if self.playout_stat.list_init.load(Ordering::SeqCst) { + trace!("Init playlist, from next iterator"); if self.json_path.is_some() { self.init_clip(); } @@ -281,6 +288,7 @@ impl Iterator for CurrentProgram { self.current_node = self.nodes.lock().unwrap()[last_index].clone(); let new_node = self.nodes.lock().unwrap()[last_index].clone(); let new_length = new_node.begin.unwrap() + new_node.duration; + trace!("Init playlist after playlist end"); self.check_for_next_playlist(); @@ -304,13 +312,17 @@ impl Iterator for CurrentProgram { current_time += self.config.playlist.length_sec.unwrap() + 1.0; } - let mut media = Media::new(0, "", false); + let mut nodes = self.nodes.lock().unwrap(); + let index = nodes.len(); + + let mut media = Media::new(index, "", false); media.begin = Some(current_time); media.duration = duration; media.out = duration; - self.current_node = gen_source(&self.config, media, &self.playout_stat.chain); - let mut nodes = self.nodes.lock().unwrap(); + self.current_node = + gen_source(&self.config, media, &self.playout_stat.chain, last_index); + nodes.push(self.current_node.clone()); self.index.store(nodes.len(), Ordering::SeqCst); } @@ -326,8 +338,9 @@ impl Iterator for CurrentProgram { let mut is_last = false; let index = self.index.load(Ordering::SeqCst); let nodes = self.nodes.lock().unwrap(); + let last_index = nodes.len() - 1; - if index == nodes.len() - 1 { + if index == last_index { is_last = true } @@ -336,6 +349,7 @@ impl Iterator for CurrentProgram { &self.config, is_last, &self.playout_stat, + last_index, ); drop(nodes); @@ -370,6 +384,7 @@ impl Iterator for CurrentProgram { &self.config, self.current_node.clone(), &self.playout_stat.chain, + 0, ); self.nodes.lock().unwrap().push(self.current_node.clone()); self.last_next_ad(); @@ -385,10 +400,12 @@ impl Iterator for CurrentProgram { // Get first clip from next playlist. self.index.store(0, Ordering::SeqCst); + let last_index = self.nodes.lock().unwrap().len() - 1; self.current_node = gen_source( &self.config, self.nodes.lock().unwrap()[0].clone(), &self.playout_stat.chain, + last_index, ); self.last_next_ad(); self.current_node.last_ad = last_ad; @@ -409,12 +426,15 @@ fn timed_source( config: &PlayoutConfig, last: bool, playout_stat: &PlayoutStatus, + last_index: usize, ) -> Media { let (delta, total_delta) = get_delta(config, &node.begin.unwrap()); let mut shifted_delta = delta; let mut new_node = node.clone(); new_node.process = Some(false); + trace!("timed source ist last: {last}"); + if config.playlist.length.contains(':') { let time_shift = playout_stat.time_shift.lock().unwrap(); @@ -443,11 +463,11 @@ fn timed_source( { // when we are in the 24 hour range, get the clip new_node.process = Some(true); - new_node = gen_source(config, node, &playout_stat.chain); + new_node = gen_source(config, node, &playout_stat.chain, last_index); } else if total_delta <= 0.0 { info!("Begin is over play time, skip: {}", node.source); } else if total_delta < node.duration - node.seek || last { - new_node = handle_list_end(config, node, total_delta, &playout_stat.chain); + new_node = handle_list_end(config, node, total_delta, &playout_stat.chain, last_index); } new_node @@ -458,6 +478,7 @@ pub fn gen_source( config: &PlayoutConfig, mut node: Media, filter_chain: &Option>>>, + last_index: usize, ) -> Media { let duration = node.out - node.seek; @@ -476,7 +497,16 @@ pub fn gen_source( node.cmd = Some(seek_and_length(&node)); } } else { - error!("Source not found: \"{}\"", node.source); + trace!( + "clip index: {:?} | last index: {:?}", + node.index.unwrap_or_default(), + last_index + ); + + if node.index.unwrap_or_default() < last_index { + error!("Source not found: \"{}\"", node.source); + } + warn!("Generate filler with {duration:.2} seconds length!"); let probe = MediaProbe::new(&config.storage.filler_clip); @@ -532,6 +562,7 @@ fn handle_list_init( config: &PlayoutConfig, mut node: Media, filter_chain: &Option>>>, + last_index: usize, ) -> Media { debug!("Playlist init"); let (_, total_delta) = get_delta(config, &node.begin.unwrap()); @@ -542,7 +573,8 @@ fn handle_list_init( } node.out = out; - gen_source(config, node, filter_chain) + + gen_source(config, node, filter_chain, last_index) } /// when we come to last clip in playlist, @@ -553,6 +585,7 @@ fn handle_list_end( mut node: Media, total_delta: f64, filter_chain: &Option>>>, + last_index: usize, ) -> Media { debug!("Playlist end"); @@ -580,5 +613,5 @@ fn handle_list_end( node.process = Some(true); - gen_source(config, node, filter_chain) + gen_source(config, node, filter_chain, last_index) } diff --git a/ffplayout-engine/src/main.rs b/ffplayout-engine/src/main.rs index 02662726..4548f15f 100644 --- a/ffplayout-engine/src/main.rs +++ b/ffplayout-engine/src/main.rs @@ -2,7 +2,7 @@ use std::{ fs::{self, File}, path::{Path, PathBuf}, process::exit, - sync::{Arc, Mutex}, + sync::{atomic::AtomicBool, Arc, Mutex}, thread, }; @@ -19,8 +19,9 @@ use ffplayout::{ }; use ffplayout_lib::utils::{ - generate_playlist, import::import_file, init_logging, send_mail, validate_ffmpeg, - OutputMode::*, PlayerControl, PlayoutStatus, ProcessControl, + generate_playlist, get_date, import::import_file, init_logging, is_remote, send_mail, + validate_ffmpeg, validate_playlist, JsonPlaylist, OutputMode::*, PlayerControl, PlayoutStatus, + ProcessControl, }; #[cfg(debug_assertions)] @@ -159,6 +160,39 @@ fn main() { } } + if args.validate { + let mut playlist_path = Path::new(&config.playlist.path).to_owned(); + let start_sec = config.playlist.start_sec.unwrap(); + let date = get_date(false, start_sec, 0.0); + + if playlist_path.is_dir() || is_remote(&config.playlist.path) { + let d: Vec<&str> = date.split('-').collect(); + playlist_path = playlist_path + .join(d[0]) + .join(d[1]) + .join(date.clone()) + .with_extension("json"); + } + + let f = File::options() + .read(true) + .write(false) + .open(&playlist_path) + .expect("Could not open json playlist file."); + + let playlist: JsonPlaylist = match serde_json::from_reader(f) { + Ok(p) => p, + Err(e) => { + error!("{e:?}"); + exit(1) + } + }; + + validate_playlist(playlist, Arc::new(AtomicBool::new(false)), config); + + exit(0); + } + if config.rpc_server.enable { // If RPC server is enable we also fire up a JSON RPC server. thread::spawn(move || run_server(config_clone, play_ctl, play_stat, proc_ctl2)); diff --git a/ffplayout-engine/src/output/desktop.rs b/ffplayout-engine/src/output/desktop.rs index da37d075..0762614c 100644 --- a/ffplayout-engine/src/output/desktop.rs +++ b/ffplayout-engine/src/output/desktop.rs @@ -41,7 +41,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { }) { enc_cmd.append(&mut cmd); } else { - warn!("Given output parameter a skipped, they are not supported by ffplay!"); + warn!("Given output parameters are skipped, they are not supported by ffplay!"); } } diff --git a/ffplayout-engine/src/utils/arg_parse.rs b/ffplayout-engine/src/utils/arg_parse.rs index cdcffea2..c4ea1e28 100644 --- a/ffplayout-engine/src/utils/arg_parse.rs +++ b/ffplayout-engine/src/utils/arg_parse.rs @@ -78,6 +78,9 @@ pub struct Args { #[cfg(debug_assertions)] #[clap(long, help = "fake date time, for debugging")] pub fake_time: Option, + + #[clap(long, help = "validate given playlist")] + pub validate: bool, } /// Get arguments from command line, and return them. diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index 646af39c..faee87d8 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -39,6 +39,10 @@ pub fn get_config(args: Args) -> PlayoutConfig { config.general.generate = Some(gen); } + if args.validate { + config.general.validate = true; + } + if let Some(paths) = args.paths { config.storage.paths = paths; } diff --git a/lib/src/utils/config.rs b/lib/src/utils/config.rs index 2b84630b..3b55668e 100644 --- a/lib/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -157,6 +157,9 @@ pub struct General { #[serde(skip_serializing, skip_deserializing)] pub ffmpeg_libs: Vec, + + #[serde(default, skip_serializing, skip_deserializing)] + pub validate: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 8326a1a0..84a1ad53 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -147,10 +147,16 @@ pub fn validate_playlist( if valid_source(&item.source) { if let Err(e) = check_media(item.clone(), pos, begin, &config) { error!("{e}"); + } else if config.general.validate { + debug!( + "Source at {}, seems fine: {}", + sec_to_time(begin), + item.source + ) }; } else { error!( - "Source on position {pos} {} not exists: \"{}\"", + "Source on position {pos} {} not exists: {}", sec_to_time(begin), item.source ); diff --git a/lib/src/utils/logging.rs b/lib/src/utils/logging.rs index 9aeaea1b..97e41ca8 100644 --- a/lib/src/utils/logging.rs +++ b/lib/src/utils/logging.rs @@ -238,6 +238,7 @@ pub fn init_logging( } else { let term_config = log_config .clone() + .set_level_color(Level::Trace, Some(Color::Ansi256(11))) .set_level_color(Level::Debug, Some(Color::Ansi256(12))) .set_level_color(Level::Info, Some(Color::Ansi256(10))) .set_level_color(Level::Warn, Some(Color::Ansi256(208))) diff --git a/tests/src/engine_cmd.rs b/tests/src/engine_cmd.rs index d6c9c36e..67faa13f 100644 --- a/tests/src/engine_cmd.rs +++ b/tests/src/engine_cmd.rs @@ -15,7 +15,7 @@ fn video_audio_input() { config.processing.logo = logo_path.to_string_lossy().to_string(); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let test_filter_cmd = vec_strings![ @@ -41,7 +41,7 @@ fn video_audio_custom_filter1_input() { config.processing.custom_filter = "[0:v]gblur=2[c_v_out];[0:a]volume=0.2[c_a_out]".to_string(); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let test_filter_cmd = vec_strings![ "-filter_complex", @@ -68,7 +68,7 @@ fn video_audio_custom_filter2_input() { .to_string(); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let test_filter_cmd = vec_strings![ "-filter_complex", @@ -94,7 +94,7 @@ fn video_audio_custom_filter3_input() { "[v_in];movie=logo.png[l];[v_in][l]overlay[c_v_out];[0:a]volume=0.2[c_a_out]".to_string(); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let test_filter_cmd = vec_strings![ "-filter_complex", @@ -119,7 +119,7 @@ fn dual_audio_aevalsrc_input() { config.processing.add_logo = false; let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let test_filter_cmd = vec_strings![ @@ -145,7 +145,7 @@ fn dual_audio_input() { config.processing.add_logo = false; let media_obj = Media::new(0, "./assets/dual_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let test_filter_cmd = vec_strings![ "-filter_complex", @@ -171,7 +171,7 @@ fn video_separate_audio_input() { let mut media_obj = Media::new(0, "./assets/no_audio.mp4", true); media_obj.audio = "./assets/audio.mp3".to_string(); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let test_filter_cmd = vec_strings![ "-filter_complex", @@ -1333,7 +1333,7 @@ fn video_audio_hls() { ]); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let enc_prefix = vec_strings![ "-hide_banner", @@ -1422,7 +1422,7 @@ fn video_audio_sub_meta_hls() { ]); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let enc_prefix = vec_strings![ "-hide_banner", @@ -1512,7 +1512,7 @@ fn video_multi_audio_hls() { ]); let media_obj = Media::new(0, "./assets/dual_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let enc_prefix = vec_strings![ "-hide_banner", @@ -1617,7 +1617,7 @@ fn multi_video_audio_hls() { ]); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let enc_prefix = vec_strings![ "-hide_banner", @@ -1733,7 +1733,7 @@ fn multi_video_multi_audio_hls() { ]); let media_obj = Media::new(0, "./assets/dual_audio.mp4", true); - let media = gen_source(&config, media_obj, &None); + let media = gen_source(&config, media_obj, &None, 1); let enc_prefix = vec_strings![ "-hide_banner",