From fd8e4738cad8bbeae5cfb599a2c2ce4876c6edc9 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 6 Sep 2022 17:11:43 +0200 Subject: [PATCH 1/5] work on better validation --- Cargo.lock | 4 +- ffplayout-engine/Cargo.toml | 2 +- lib/Cargo.toml | 2 +- lib/src/filter/mod.rs | 4 +- lib/src/utils/json_validate.rs | 89 +++++++++++++++++++++++++++++----- 5 files changed, 82 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f672e953..7659f4e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -941,7 +941,7 @@ dependencies = [ [[package]] name = "ffplayout" -version = "0.15.0" +version = "0.15.1" dependencies = [ "chrono", "clap", @@ -990,7 +990,7 @@ dependencies = [ [[package]] name = "ffplayout-lib" -version = "0.15.0" +version = "0.15.1" dependencies = [ "chrono", "crossbeam-channel", diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml index b7cddfea..5c9826c8 100644 --- a/ffplayout-engine/Cargo.toml +++ b/ffplayout-engine/Cargo.toml @@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.15.0" +version = "0.15.1" edition = "2021" [dependencies] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f406a48b..2f9e38a7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -4,7 +4,7 @@ description = "Library for ffplayout" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.15.0" +version = "0.15.1" edition = "2021" [dependencies] diff --git a/lib/src/filter/mod.rs b/lib/src/filter/mod.rs index 03231508..c5d22692 100644 --- a/lib/src/filter/mod.rs +++ b/lib/src/filter/mod.rs @@ -173,11 +173,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { { let mut logo_chain = v_overlay::filter_node(config, false); - if node.last_ad.unwrap() { + if node.last_ad.unwrap_or(false) { logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1") } - if node.next_ad.unwrap() { + if node.next_ad.unwrap_or(false) { logo_chain.push_str( format!(",fade=out:st={}:d=1.0:alpha=1", node.out - node.seek - 1.0).as_str(), ) diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 2eb9930e..aa676e84 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -1,11 +1,80 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, +use std::{ + io::{BufRead, BufReader, Error, ErrorKind}, + process::{Command, Stdio}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, }; use simplelog::*; -use crate::utils::{sec_to_time, valid_source, JsonPlaylist, MediaProbe, PlayoutConfig}; +use crate::utils::{ + format_log_line, sec_to_time, valid_source, vec_strings, Media, JsonPlaylist, PlayoutConfig, +}; + +/// 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![]))); + + let enc_cmd = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-ignore_chapters", + "1", + "-i", + clip.source, + "-t", + "0.25", + "-f", + "null", + "-" + ]; + + if clip.probe.and_then(|p| p.format).is_none() { + return Err(Error::new( + ErrorKind::Other, + format!( + "No Metadata at {}, from file \"{}\"", + sec_to_time(begin), + clip.source + ), + )); + } + + let mut enc_proc = match Command::new("ffmpeg") + .args(enc_cmd) + .stderr(Stdio::piped()) + .spawn() + { + Err(e) => return Err(e), + Ok(proc) => proc, + }; + + let enc_err = BufReader::new(enc_proc.stderr.take().unwrap()); + + for line in enc_err.lines() { + let line = line?; + + if line.contains("[error]") { + error!( + "[Validator] {}", + format_log_line(line, "error") + ); + } else if line.contains("[fatal]") { + error!( + "[Validator] {}", + format_log_line(line, "fatal") + ) + } + } + + Ok(()) +} /// Validate a given playlist, to check if: /// @@ -33,15 +102,9 @@ pub fn validate_playlist( } if valid_source(&item.source) { - let probe = MediaProbe::new(&item.source); - - if probe.format.is_none() { - error!( - "No Metadata at {}, from file \"{}\"", - sec_to_time(begin), - item.source - ); - } + if let Err(e) = check_media(item.clone(), begin, &config) { + error!("{e}"); + }; } else { error!( "Source on position {} not exists: \"{}\"", From 9c5122696dc9065ff670c54abd0f87945b8865e1 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 7 Sep 2022 11:33:13 +0200 Subject: [PATCH 2/5] 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); From cbc452f705ff821cae81db52df40e0ece2eb0faa Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 7 Sep 2022 11:39:53 +0200 Subject: [PATCH 3/5] unify filter quotation --- docs/custom_filters.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/custom_filters.md b/docs/custom_filters.md index ef3a8717..24d9e622 100644 --- a/docs/custom_filters.md +++ b/docs/custom_filters.md @@ -9,13 +9,13 @@ It is possible to apply only video or audio filters, or both. For a better under #### Apply Gaussian blur and volume filter: ```YAML -custom_filter: 'gblur=5[c_v_out];volume=0.5[c_a_out]' +custom_filter: "gblur=5[c_v_out];volume=0.5[c_a_out]" ``` #### Add lower third: ```YAML -custom_filter: '[v_in];movie=/path/to/lower_third.png:loop=0,scale=1024:576,setpts=N/(25*TB)[lower];[v_in][lower]overlay=0:0:shortest=1[c_v_out]' +custom_filter: "[v_in];movie=/path/to/lower_third.png:loop=0,scale=1024:576,setpts=N/(25*TB)[lower];[v_in][lower]overlay=0:0:shortest=1[c_v_out]" ``` Pay attention to the filter prefix `[v_in];`, this is necessary to get the output from the regular filters. @@ -23,7 +23,7 @@ Pay attention to the filter prefix `[v_in];`, this is necessary to get the outpu #### Paint effect ```YAML -custom_filter: edgedetect=mode=colormix:high=0[c_v_out] +custom_filter: "edgedetect=mode=colormix:high=0[c_v_out]" ``` ### Where the filters applied in stream mode @@ -50,7 +50,7 @@ The **custom filter** from **config -> processing** and from **playlist** got ap This example takes a image and a animated mov clip with alpha and overlays them two times on different positions in time: ```YAML -custom_filter: '[v_in];movie=image_input.png:s=v,loop=loop=250.0:size=1:start=0,scale=1024:576,split=2[lower_1_out_1][lower_1_out_2];[lower_1_out_1]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+5.0/TB[fade_1];[v_in][fade_1]overlay=enable=between(t\,5.0\,15.0)[base_1];[lower_1_out_2]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+30.0/TB[fade_2];[base_1][fade_2]overlay=enable=between(t\,30.0\,40.0)[base_2];movie=animated_input.mov:s=v,scale=1024:576,split=2[lower_2_out_1][lower_2_out_2];[lower_2_out_1]fifo,setpts=PTS+20.0/TB[layer_1];[base_2][layer_1]overlay=repeatlast=0[base_3];[lower_2_out_2]fifo,setpts=PTS+50.0/TB[layer_2];[base_3][layer_2]overlay=repeatlast=0[c_v_out]' +custom_filter: "[v_in];movie=image_input.png:s=v,loop=loop=250.0:size=1:start=0,scale=1024:576,split=2[lower_1_out_1][lower_1_out_2];[lower_1_out_1]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+5.0/TB[fade_1];[v_in][fade_1]overlay=enable=between(t\,5.0\,15.0)[base_1];[lower_1_out_2]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+30.0/TB[fade_2];[base_1][fade_2]overlay=enable=between(t\,30.0\,40.0)[base_2];movie=animated_input.mov:s=v,scale=1024:576,split=2[lower_2_out_1][lower_2_out_2];[lower_2_out_1]fifo,setpts=PTS+20.0/TB[layer_1];[base_2][layer_1]overlay=repeatlast=0[base_3];[lower_2_out_2]fifo,setpts=PTS+50.0/TB[layer_2];[base_3][layer_2]overlay=repeatlast=0[c_v_out]" ``` And here are the explanation for each filter: From 7ec4e0d5e570968a9727caa20e8a06708b605bde Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 7 Sep 2022 11:48:18 +0200 Subject: [PATCH 4/5] remove error log in favor of debug log --- ffplayout-api/src/utils/control.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ffplayout-api/src/utils/control.rs b/ffplayout-api/src/utils/control.rs index 8f5b0539..692d43aa 100644 --- a/ffplayout-api/src/utils/control.rs +++ b/ffplayout-api/src/utils/control.rs @@ -5,7 +5,6 @@ use reqwest::{ Client, Response, }; use serde::{Deserialize, Serialize}; -use simplelog::*; use crate::utils::{errors::ServiceError, handles::db_get_channel, playout_config}; use ffplayout_lib::vec_strings; @@ -146,10 +145,7 @@ where .await { Ok(result) => Ok(result), - Err(e) => { - error!("{e:?}"); - Err(ServiceError::ServiceUnavailable(e.to_string())) - } + Err(e) => Err(ServiceError::ServiceUnavailable(e.to_string())), } } From 46140b42839485a37127a7add8818b7f6abf8417 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 7 Sep 2022 13:44:21 +0200 Subject: [PATCH 5/5] better responsive control --- ffplayout-frontend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffplayout-frontend b/ffplayout-frontend index 37ccb38b..a1b203eb 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 37ccb38b91b34095d06673952a913be3cf79eb0a +Subproject commit a1b203ebcfa0b3d9109ac049d332c2a45ad19e70