From ea83160ba63bb8723de1f004f6449b37a1ea2593 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 20 Nov 2023 12:30:49 +0100 Subject: [PATCH] add silence detection for validation --- Cargo.lock | 29 ++++++++------ assets/ffplayout.yml | 3 ++ lib/Cargo.toml | 1 + lib/src/utils/config.rs | 2 + lib/src/utils/errors.rs | 27 +++++++++++++ lib/src/utils/json_validate.rs | 71 +++++++++++++++++++++++----------- lib/src/utils/mod.rs | 1 + 7 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 lib/src/utils/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 6b905d75..f0d00193 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,15 +477,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" +checksum = "fc5ea910c42e5ab19012bab31f53cb4d63d54c3a27730f9a833a88efcf4bb52d" dependencies = [ - "async-lock 2.8.0", + "async-lock 3.1.1", "async-task", "concurrent-queue", "fastrand 2.0.1", - "futures-lite 1.13.0", + "futures-lite 2.0.1", "slab", ] @@ -535,9 +535,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb2ab2aa8a746e221ab826c73f48bc6ba41be6763f0855cb249eb6d154cf1d7" +checksum = "655b9c7fe787d3b25cc0f804a1a8401790f0c5bc395beb5a64dc77d8de079105" dependencies = [ "event-listener 3.1.0", "event-listener-strategy", @@ -689,7 +689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel 2.1.0", - "async-lock 3.1.0", + "async-lock 3.1.1", "async-task", "fastrand 2.0.1", "futures-io", @@ -1273,6 +1273,7 @@ version = "0.20.1" dependencies = [ "chrono", "crossbeam-channel", + "derive_more", "ffprobe", "file-rotate", "lettre", @@ -1464,7 +1465,11 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" dependencies = [ + "fastrand 2.0.1", "futures-core", + "futures-io", + "memchr", + "parking", "pin-project-lite", ] @@ -2711,9 +2716,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.24" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags 2.4.1", "errno", @@ -3346,7 +3351,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall 0.4.1", - "rustix 0.38.24", + "rustix 0.38.25", "windows-sys", ] @@ -3650,9 +3655,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", ] diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index 7e12c263..b6bcc087 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -35,6 +35,8 @@ logging: false will set log timestamps to UTC. Path to /var/log/ only if you run this program as daemon. 'level' can be DEBUG, INFO, WARNING, ERROR. 'ffmpeg_level' can be info, warning, error. + 'detect_silence' logs an error message if the audio line is silent for 15 + seconds during the validation process. log_to_file: true backup_count: 7 local_time: true @@ -43,6 +45,7 @@ logging: level: DEBUG ffmpeg_level: error ingest_level: warning + detect_silence: false processing: help_text: Default processing for all clips, to have them unique. Mode can be playlist diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 2b76ca5a..79f55660 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -11,6 +11,7 @@ edition.workspace = true [dependencies] chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] } crossbeam-channel = "0.5" +derive_more = "0.99" ffprobe = "0.3" file-rotate = "0.7" lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false } diff --git a/lib/src/utils/config.rs b/lib/src/utils/config.rs index 0b2bbbc5..2beb101c 100644 --- a/lib/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -222,6 +222,8 @@ pub struct Logging { pub level: LevelFilter, pub ffmpeg_level: String, pub ingest_level: Option, + #[serde(default)] + pub detect_silence: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/lib/src/utils/errors.rs b/lib/src/utils/errors.rs new file mode 100644 index 00000000..1f797c51 --- /dev/null +++ b/lib/src/utils/errors.rs @@ -0,0 +1,27 @@ +use std::io; + +use derive_more::Display; + +#[derive(Debug, Display)] +pub enum ProcError { + #[display(fmt = "Failed to spawn ffmpeg/ffprobe. {}", _0)] + CommandSpawn(io::Error), + #[display(fmt = "Failed to read data from ffmpeg/ffprobe. {}", _0)] + IO(io::Error), + #[display(fmt = "{}", _0)] + Custom(String), + #[display(fmt = "Regex compile error {}", _0)] + Regex(String), +} + +impl From for ProcError { + fn from(err: std::io::Error) -> Self { + Self::CommandSpawn(err) + } +} + +impl From for ProcError { + fn from(err: regex::Error) -> Self { + Self::Regex(err.to_string()) + } +} diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 803a24bd..d48ae943 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -1,5 +1,5 @@ use std::{ - io::{BufRead, BufReader, Error, ErrorKind}, + io::{BufRead, BufReader}, process::{Command, Stdio}, sync::{ atomic::{AtomicBool, Ordering}, @@ -7,11 +7,13 @@ use std::{ }, }; +use regex::Regex; use simplelog::*; +use crate::filter::FilterType::Audio; use crate::utils::{ - loop_image, sec_to_time, seek_and_length, valid_source, vec_strings, JsonPlaylist, Media, - OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT, + errors::ProcError, loop_image, sec_to_time, seek_and_length, valid_source, vec_strings, + JsonPlaylist, Media, OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT, }; /// check if ffmpeg can read the file and apply filter to it. @@ -20,23 +22,26 @@ fn check_media( pos: usize, begin: f64, config: &PlayoutConfig, -) -> Result<(), Error> { - let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", "level+error"]; +) -> Result<(), ProcError> { + let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"]; let mut error_list = vec![]; let mut config = config.clone(); config.out.mode = Null; + let mut process_length = 0.1; + + if config.logging.detect_silence { + process_length = 15.0; + } + node.add_probe(); if node.probe.clone().and_then(|p| p.format).is_none() { - return Err(Error::new( - ErrorKind::Other, - format!( - "No Metadata at position {pos} {}, from file \"{}\"", - sec_to_time(begin), - node.source - ), - )); + return Err(ProcError::Custom(format!( + "No Metadata at position {pos} {}, from file \"{}\"", + sec_to_time(begin), + node.source + ))); } // take care, that no seek and length command is added. @@ -60,24 +65,30 @@ fn check_media( let mut filter = node.filter.unwrap_or_default(); if filter.cmd().len() > 1 { - filter.cmd()[1] = filter.cmd()[1].replace("realtime=speed=1", "null") + let re_clean = Regex::new(r"volume=[0-9.]+")?; + + filter.audio_chain = re_clean + .replace_all(&filter.audio_chain, "anull") + .to_string(); } + filter.add_filter("silencedetect=n=-30dB", 0, Audio); + enc_cmd.append(&mut node.cmd.unwrap_or_default()); enc_cmd.append(&mut filter.cmd()); enc_cmd.append(&mut filter.map()); - enc_cmd.append(&mut vec_strings!["-t", "0.1", "-f", "null", "-"]); + enc_cmd.append(&mut vec_strings!["-t", process_length, "-f", "null", "-"]); - let mut enc_proc = match Command::new("ffmpeg") - .args(enc_cmd.clone()) + let mut enc_proc = Command::new("ffmpeg") + .args(enc_cmd) .stderr(Stdio::piped()) - .spawn() - { - Err(e) => return Err(e), - Ok(proc) => proc, - }; + .spawn()?; let enc_err = BufReader::new(enc_proc.stderr.take().unwrap()); + let mut silence_start = 0.0; + let mut silence_end = 0.0; + let re_start = Regex::new(r"silence_start: ([0-9]+:)?([0-9.]+)")?; + let re_end = Regex::new(r"silence_end: ([0-9]+:)?([0-9.]+)")?; for line in enc_err.lines() { let line = line?; @@ -91,11 +102,25 @@ fn check_media( error_list.push(log_line); } } + + if config.logging.detect_silence { + if let Some(start) = re_start.captures(&line).and_then(|c| c.get(2)) { + silence_start = start.as_str().parse::().unwrap_or_default(); + } + + if let Some(end) = re_end.captures(&line).and_then(|c| c.get(2)) { + silence_end = end.as_str().parse::().unwrap_or_default() + 0.5; + } + } + } + + if silence_end - silence_start > process_length { + error_list.push("Audio is totally silent!".to_string()); } if !error_list.is_empty() { error!( - "[Validator] ffmpeg error on position {pos} - {}: {}:\n{}", + "[Validator] ffmpeg error on position {pos} - {}: {}: {}", sec_to_time(begin), node.source, error_list.join("\n") diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index 17f7c8c0..9c933ad2 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -23,6 +23,7 @@ use simplelog::*; pub mod config; pub mod controller; +pub mod errors; pub mod folder; pub mod generator; pub mod import;