add silence detection for validation
This commit is contained in:
parent
81e3274789
commit
ea83160ba6
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -477,15 +477,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-executor"
|
name = "async-executor"
|
||||||
version = "1.6.0"
|
version = "1.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0"
|
checksum = "fc5ea910c42e5ab19012bab31f53cb4d63d54c3a27730f9a833a88efcf4bb52d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock 2.8.0",
|
"async-lock 3.1.1",
|
||||||
"async-task",
|
"async-task",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"fastrand 2.0.1",
|
"fastrand 2.0.1",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 2.0.1",
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -535,9 +535,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-lock"
|
name = "async-lock"
|
||||||
version = "3.1.0"
|
version = "3.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "deb2ab2aa8a746e221ab826c73f48bc6ba41be6763f0855cb249eb6d154cf1d7"
|
checksum = "655b9c7fe787d3b25cc0f804a1a8401790f0c5bc395beb5a64dc77d8de079105"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener 3.1.0",
|
"event-listener 3.1.0",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
@ -689,7 +689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel 2.1.0",
|
"async-channel 2.1.0",
|
||||||
"async-lock 3.1.0",
|
"async-lock 3.1.1",
|
||||||
"async-task",
|
"async-task",
|
||||||
"fastrand 2.0.1",
|
"fastrand 2.0.1",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
@ -1273,6 +1273,7 @@ version = "0.20.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
"derive_more",
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"file-rotate",
|
"file-rotate",
|
||||||
"lettre",
|
"lettre",
|
||||||
@ -1464,7 +1465,11 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb"
|
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"fastrand 2.0.1",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"memchr",
|
||||||
|
"parking",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2711,9 +2716,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.24"
|
version = "0.38.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
|
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.1",
|
"bitflags 2.4.1",
|
||||||
"errno",
|
"errno",
|
||||||
@ -3346,7 +3351,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand 2.0.1",
|
"fastrand 2.0.1",
|
||||||
"redox_syscall 0.4.1",
|
"redox_syscall 0.4.1",
|
||||||
"rustix 0.38.24",
|
"rustix 0.38.25",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3650,9 +3655,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.5.0"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
@ -35,6 +35,8 @@ logging:
|
|||||||
false will set log timestamps to UTC. Path to /var/log/ only if you run this
|
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.
|
program as daemon. 'level' can be DEBUG, INFO, WARNING, ERROR.
|
||||||
'ffmpeg_level' can be 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
|
log_to_file: true
|
||||||
backup_count: 7
|
backup_count: 7
|
||||||
local_time: true
|
local_time: true
|
||||||
@ -43,6 +45,7 @@ logging:
|
|||||||
level: DEBUG
|
level: DEBUG
|
||||||
ffmpeg_level: error
|
ffmpeg_level: error
|
||||||
ingest_level: warning
|
ingest_level: warning
|
||||||
|
detect_silence: false
|
||||||
|
|
||||||
processing:
|
processing:
|
||||||
help_text: Default processing for all clips, to have them unique. Mode can be playlist
|
help_text: Default processing for all clips, to have them unique. Mode can be playlist
|
||||||
|
@ -11,6 +11,7 @@ edition.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] }
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
|
derive_more = "0.99"
|
||||||
ffprobe = "0.3"
|
ffprobe = "0.3"
|
||||||
file-rotate = "0.7"
|
file-rotate = "0.7"
|
||||||
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false }
|
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false }
|
||||||
|
@ -222,6 +222,8 @@ pub struct Logging {
|
|||||||
pub level: LevelFilter,
|
pub level: LevelFilter,
|
||||||
pub ffmpeg_level: String,
|
pub ffmpeg_level: String,
|
||||||
pub ingest_level: Option<String>,
|
pub ingest_level: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub detect_silence: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
27
lib/src/utils/errors.rs
Normal file
27
lib/src/utils/errors.rs
Normal file
@ -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<std::io::Error> for ProcError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
Self::CommandSpawn(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<regex::Error> for ProcError {
|
||||||
|
fn from(err: regex::Error) -> Self {
|
||||||
|
Self::Regex(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, BufReader, Error, ErrorKind},
|
io::{BufRead, BufReader},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
@ -7,11 +7,13 @@ use std::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
|
use crate::filter::FilterType::Audio;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
loop_image, sec_to_time, seek_and_length, valid_source, vec_strings, JsonPlaylist, Media,
|
errors::ProcError, loop_image, sec_to_time, seek_and_length, valid_source, vec_strings,
|
||||||
OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT,
|
JsonPlaylist, Media, OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, 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.
|
||||||
@ -20,23 +22,26 @@ fn check_media(
|
|||||||
pos: usize,
|
pos: usize,
|
||||||
begin: f64,
|
begin: f64,
|
||||||
config: &PlayoutConfig,
|
config: &PlayoutConfig,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), ProcError> {
|
||||||
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", "level+error"];
|
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"];
|
||||||
let mut error_list = vec![];
|
let mut error_list = vec![];
|
||||||
let mut config = config.clone();
|
let mut config = config.clone();
|
||||||
config.out.mode = Null;
|
config.out.mode = Null;
|
||||||
|
|
||||||
|
let mut process_length = 0.1;
|
||||||
|
|
||||||
|
if config.logging.detect_silence {
|
||||||
|
process_length = 15.0;
|
||||||
|
}
|
||||||
|
|
||||||
node.add_probe();
|
node.add_probe();
|
||||||
|
|
||||||
if node.probe.clone().and_then(|p| p.format).is_none() {
|
if node.probe.clone().and_then(|p| p.format).is_none() {
|
||||||
return Err(Error::new(
|
return Err(ProcError::Custom(format!(
|
||||||
ErrorKind::Other,
|
"No Metadata at position <yellow>{pos}</> {}, from file <b><magenta>\"{}\"</></b>",
|
||||||
format!(
|
sec_to_time(begin),
|
||||||
"No Metadata at position <yellow>{pos}</> {}, from file <b><magenta>\"{}\"</></b>",
|
node.source
|
||||||
sec_to_time(begin),
|
)));
|
||||||
node.source
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// take care, that no seek and length command is added.
|
// 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();
|
let mut filter = node.filter.unwrap_or_default();
|
||||||
|
|
||||||
if filter.cmd().len() > 1 {
|
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 node.cmd.unwrap_or_default());
|
||||||
enc_cmd.append(&mut filter.cmd());
|
enc_cmd.append(&mut filter.cmd());
|
||||||
enc_cmd.append(&mut filter.map());
|
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")
|
let mut enc_proc = Command::new("ffmpeg")
|
||||||
.args(enc_cmd.clone())
|
.args(enc_cmd)
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.spawn()
|
.spawn()?;
|
||||||
{
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
Ok(proc) => proc,
|
|
||||||
};
|
|
||||||
|
|
||||||
let enc_err = BufReader::new(enc_proc.stderr.take().unwrap());
|
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() {
|
for line in enc_err.lines() {
|
||||||
let line = line?;
|
let line = line?;
|
||||||
@ -91,11 +102,25 @@ fn check_media(
|
|||||||
error_list.push(log_line);
|
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::<f32>().unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(end) = re_end.captures(&line).and_then(|c| c.get(2)) {
|
||||||
|
silence_end = end.as_str().parse::<f32>().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() {
|
if !error_list.is_empty() {
|
||||||
error!(
|
error!(
|
||||||
"<bright black>[Validator]</> ffmpeg error on position <yellow>{pos}</> - {}: <b><magenta>{}</></b>:\n{}",
|
"<bright black>[Validator]</> ffmpeg error on position <yellow>{pos}</> - {}: <b><magenta>{}</></b>: {}",
|
||||||
sec_to_time(begin),
|
sec_to_time(begin),
|
||||||
node.source,
|
node.source,
|
||||||
error_list.join("\n")
|
error_list.join("\n")
|
||||||
|
@ -23,6 +23,7 @@ use simplelog::*;
|
|||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod controller;
|
pub mod controller;
|
||||||
|
pub mod errors;
|
||||||
pub mod folder;
|
pub mod folder;
|
||||||
pub mod generator;
|
pub mod generator;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
|
Loading…
Reference in New Issue
Block a user