From 9c5122696dc9065ff670c54abd0f87945b8865e1 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 7 Sep 2022 11:33:13 +0200 Subject: [PATCH] validate file compression settings and filtering - fix length from filler clip in playlist generator - serialize values only when string is not empty - compare also audio and custom filter on playlist existing check --- Cargo.lock | 2 +- ffplayout-api/Cargo.toml | 2 +- lib/src/utils/generator.rs | 2 +- lib/src/utils/json_validate.rs | 102 +++++++++++++++++++++------------ lib/src/utils/mod.rs | 36 +++++++++--- 5 files changed, 96 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7659f4e8..ecaa9b5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -960,7 +960,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.6.0" +version = "0.6.1" dependencies = [ "actix-files", "actix-multipart", diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index b89eb28e..f6887174 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -4,7 +4,7 @@ description = "Rest API for ffplayout" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.6.0" +version = "0.6.1" edition = "2021" [dependencies] diff --git a/lib/src/utils/generator.rs b/lib/src/utils/generator.rs index a36a766a..192eaf33 100644 --- a/lib/src/utils/generator.rs +++ b/lib/src/utils/generator.rs @@ -141,7 +141,7 @@ pub fn generate_playlist( length += duration; } else if filler_length > 0.0 && filler_length > total_length - length { - filler.out = filler_length - (total_length - length); + filler.out = total_length - length; playlist.program.push(filler); break; diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index aa676e84..b599d6bb 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -10,44 +10,57 @@ use std::{ use simplelog::*; use crate::utils::{ - format_log_line, sec_to_time, valid_source, vec_strings, Media, JsonPlaylist, PlayoutConfig, + format_log_line, loop_image, sec_to_time, seek_and_length, valid_source, vec_strings, + JsonPlaylist, Media, PlayoutConfig, IMAGE_FORMAT, }; /// check if ffmpeg can read the file and apply filter to it. -fn check_media(item: Media, begin: f64, config: &PlayoutConfig) -> Result<(), Error> { - let mut clip = item; - clip.add_probe(); - clip.add_filter(config, &Arc::new(Mutex::new(vec![]))); +fn check_media( + mut node: Media, + pos: usize, + begin: f64, + config: &PlayoutConfig, +) -> Result<(), Error> { + let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", "level+error"]; + let mut error_list = vec![]; - let enc_cmd = vec_strings![ - "-hide_banner", - "-nostats", - "-v", - "level+error", - "-ignore_chapters", - "1", - "-i", - clip.source, - "-t", - "0.25", - "-f", - "null", - "-" - ]; + node.add_probe(); - if clip.probe.and_then(|p| p.format).is_none() { + if node.probe.clone().and_then(|p| p.format).is_none() { return Err(Error::new( ErrorKind::Other, format!( - "No Metadata at {}, from file \"{}\"", + "No Metadata at position {pos} {}, from file \"{}\"", sec_to_time(begin), - clip.source + node.source ), )); } + // take care, that no seek and length command is added. + node.seek = 0.0; + node.out = node.duration; + + if node + .source + .rsplit_once('.') + .map(|(_, e)| e.to_lowercase()) + .filter(|c| IMAGE_FORMAT.contains(&c.as_str())) + .is_some() + { + node.cmd = Some(loop_image(&node)); + } else { + node.cmd = Some(seek_and_length(&node)); + } + + node.add_filter(config, &Arc::new(Mutex::new(vec![]))); + + enc_cmd.append(&mut node.cmd.unwrap_or_default()); + enc_cmd.append(&mut node.filter.unwrap_or_default()); + enc_cmd.append(&mut vec_strings!["-t", "0.15", "-f", "null", "-"]); + let mut enc_proc = match Command::new("ffmpeg") - .args(enc_cmd) + .args(enc_cmd.clone()) .stderr(Stdio::piped()) .spawn() { @@ -61,18 +74,31 @@ fn check_media(item: Media, begin: f64, config: &PlayoutConfig) -> Result<(), Er let line = line?; if line.contains("[error]") { - error!( - "[Validator] {}", - format_log_line(line, "error") - ); + let log_line = format_log_line(line, "error"); + + if !error_list.contains(&log_line) { + error_list.push(log_line); + } } else if line.contains("[fatal]") { - error!( - "[Validator] {}", - format_log_line(line, "fatal") - ) + let log_line = format_log_line(line, "fatal"); + + if !error_list.contains(&log_line) { + error_list.push(log_line); + } } } + if !error_list.is_empty() { + error!( + "[Validator] Compressing error on position {pos} {}: {}:\n{}", + sec_to_time(begin), + node.source, + error_list.join("\n") + ) + } + + error_list.clear(); + Ok(()) } @@ -94,20 +120,22 @@ pub fn validate_playlist( length += begin; - debug!("validate playlist from: {date}"); + debug!("Validate playlist from: {date}"); - for item in playlist.program.iter() { + for (index, item) in playlist.program.iter().enumerate() { if is_terminated.load(Ordering::SeqCst) { return; } + let pos = index + 1; + if valid_source(&item.source) { - if let Err(e) = check_media(item.clone(), begin, &config) { + if let Err(e) = check_media(item.clone(), pos, begin, &config) { error!("{e}"); }; } else { error!( - "Source on position {} not exists: \"{}\"", + "Source on position {pos} {} not exists: \"{}\"", sec_to_time(begin), item.source ); @@ -122,4 +150,6 @@ pub fn validate_playlist( sec_to_time(length - begin), ); } + + debug!("Validation done..."); } diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index bb65102e..f07d241a 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -55,12 +55,20 @@ pub struct Media { pub out: f64, pub duration: f64, - #[serde(default, deserialize_with = "null_string")] + #[serde( + default, + deserialize_with = "null_string", + skip_serializing_if = "is_empty_string" + )] pub category: String, #[serde(deserialize_with = "null_string")] pub source: String, - #[serde(default, deserialize_with = "null_string")] + #[serde( + default, + deserialize_with = "null_string", + skip_serializing_if = "is_empty_string" + )] pub audio: String, #[serde(skip_serializing, skip_deserializing)] @@ -69,7 +77,7 @@ pub struct Media { #[serde(skip_serializing, skip_deserializing)] pub filter: Option>, - #[serde(default)] + #[serde(default, skip_serializing_if = "is_empty_string")] pub custom_filter: String, #[serde(skip_serializing, skip_deserializing)] @@ -158,6 +166,8 @@ impl PartialEq for Media { && self.duration == other.duration && self.source == other.source && self.category == other.category + && self.audio == other.audio + && self.custom_filter == other.custom_filter } } @@ -170,6 +180,11 @@ where Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or_default()) } +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_empty_string(st: &String) -> bool { + *st == String::new() +} + /// We use the ffprobe crate, but we map the metadata to our needs. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct MediaProbe { @@ -463,12 +478,15 @@ pub fn seek_and_length(node: &Media) -> Vec { source_cmd.append(&mut vec_strings!["-ss", node.seek]) } - source_cmd.append(&mut vec_strings![ - "-ignore_chapters", - "1", - "-i", - node.source.clone() - ]); + if file_extension(Path::new(&node.source)) + .unwrap_or_default() + .to_lowercase() + == "mp4" + { + source_cmd.append(&mut vec_strings!["-ignore_chapters", "1"]); + } + + source_cmd.append(&mut vec_strings!["-i", node.source.clone()]); if Path::new(&node.audio).is_file() { let audio_probe = MediaProbe::new(&node.audio);