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
This commit is contained in:
jb-alvarado 2022-09-07 11:33:13 +02:00
parent fd8e4738ca
commit 9c5122696d
5 changed files with 96 additions and 48 deletions

2
Cargo.lock generated
View File

@ -960,7 +960,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-api" name = "ffplayout-api"
version = "0.6.0" version = "0.6.1"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-multipart", "actix-multipart",

View File

@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.6.0" version = "0.6.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -141,7 +141,7 @@ pub fn generate_playlist(
length += duration; length += duration;
} else if filler_length > 0.0 && filler_length > total_length - length { } 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); playlist.program.push(filler);
break; break;

View File

@ -10,44 +10,57 @@ use std::{
use simplelog::*; use simplelog::*;
use crate::utils::{ 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. /// check if ffmpeg can read the file and apply filter to it.
fn check_media(item: Media, begin: f64, config: &PlayoutConfig) -> Result<(), Error> { fn check_media(
let mut clip = item; mut node: Media,
clip.add_probe(); pos: usize,
clip.add_filter(config, &Arc::new(Mutex::new(vec![]))); 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![ node.add_probe();
"-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() { if node.probe.clone().and_then(|p| p.format).is_none() {
return Err(Error::new( return Err(Error::new(
ErrorKind::Other, ErrorKind::Other,
format!( format!(
"No Metadata at <yellow>{}</>, from file <b><magenta>\"{}\"</></b>", "No Metadata at position <yellow>{pos}</> {}, from file <b><magenta>\"{}\"</></b>",
sec_to_time(begin), 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") let mut enc_proc = match Command::new("ffmpeg")
.args(enc_cmd) .args(enc_cmd.clone())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
{ {
@ -61,17 +74,30 @@ fn check_media(item: Media, begin: f64, config: &PlayoutConfig) -> Result<(), Er
let line = line?; let line = line?;
if line.contains("[error]") { if line.contains("[error]") {
error!( let log_line = format_log_line(line, "error");
"<bright black>[Validator]</> {}",
format_log_line(line, "error") if !error_list.contains(&log_line) {
); error_list.push(log_line);
}
} else if line.contains("[fatal]") { } else if line.contains("[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!( error!(
"<bright black>[Validator]</> {}", "<bright black>[Validator]</> Compressing error on position <yellow>{pos}</> {}: <b><magenta>{}</></b>:\n{}",
format_log_line(line, "fatal") sec_to_time(begin),
node.source,
error_list.join("\n")
) )
} }
}
error_list.clear();
Ok(()) Ok(())
} }
@ -94,20 +120,22 @@ pub fn validate_playlist(
length += begin; length += begin;
debug!("validate playlist from: <yellow>{date}</>"); debug!("Validate playlist from: <yellow>{date}</>");
for item in playlist.program.iter() { for (index, item) in playlist.program.iter().enumerate() {
if is_terminated.load(Ordering::SeqCst) { if is_terminated.load(Ordering::SeqCst) {
return; return;
} }
let pos = index + 1;
if valid_source(&item.source) { 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}"); error!("{e}");
}; };
} else { } else {
error!( error!(
"Source on position <yellow>{}</> not exists: <b><magenta>\"{}\"</></b>", "Source on position <yellow>{pos}</> {} not exists: <b><magenta>\"{}\"</></b>",
sec_to_time(begin), sec_to_time(begin),
item.source item.source
); );
@ -122,4 +150,6 @@ pub fn validate_playlist(
sec_to_time(length - begin), sec_to_time(length - begin),
); );
} }
debug!("Validation done...");
} }

View File

@ -55,12 +55,20 @@ pub struct Media {
pub out: f64, pub out: f64,
pub duration: 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, pub category: String,
#[serde(deserialize_with = "null_string")] #[serde(deserialize_with = "null_string")]
pub source: 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, pub audio: String,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
@ -69,7 +77,7 @@ pub struct Media {
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub filter: Option<Vec<String>>, pub filter: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "is_empty_string")]
pub custom_filter: String, pub custom_filter: String,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
@ -158,6 +166,8 @@ impl PartialEq for Media {
&& self.duration == other.duration && self.duration == other.duration
&& self.source == other.source && self.source == other.source
&& self.category == other.category && 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()) 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. /// We use the ffprobe crate, but we map the metadata to our needs.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct MediaProbe { pub struct MediaProbe {
@ -463,12 +478,15 @@ pub fn seek_and_length(node: &Media) -> Vec<String> {
source_cmd.append(&mut vec_strings!["-ss", node.seek]) source_cmd.append(&mut vec_strings!["-ss", node.seek])
} }
source_cmd.append(&mut vec_strings![ if file_extension(Path::new(&node.source))
"-ignore_chapters", .unwrap_or_default()
"1", .to_lowercase()
"-i", == "mp4"
node.source.clone() {
]); 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() { if Path::new(&node.audio).is_file() {
let audio_probe = MediaProbe::new(&node.audio); let audio_probe = MediaProbe::new(&node.audio);