From dc4337581e606329c10808cb564aebb55fc1dc36 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 16 Mar 2022 13:18:53 +0100 Subject: [PATCH] add generic drawtext, add loudnorm, read config from same path, fix src in Media::new() --- assets/ffplayout.yml | 24 +++++++--------- examples/logging.rs | 16 +++++++---- src/filter/mod.rs | 53 ++++++++++++++++++++++-------------- src/filter/v_drawtext.rs | 40 +++++++++++++++++++++++++++ src/output/desktop.rs | 14 ++++++---- src/output/mod.rs | 2 +- src/output/stream.rs | 27 ++++++++++++++---- src/utils/config.rs | 59 ++++++++++++++++++++++++++-------------- src/utils/mod.rs | 4 +-- 9 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 src/filter/v_drawtext.rs diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index 72f13ea9..f2307f56 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -33,18 +33,15 @@ logging: ffmpeg_level: "error" processing: - helptext: Set playing mode, like playlist; folder, or you own custom one. - Default processing, for all clips that they get prepared in that way, - so the output is unique. 'aspect' must be a float number. 'logo' is only used - if the path exist. 'logo_scale' scale the logo to target size, leave it blank - when no scaling is needed, format is 'number:number', for example '100:-1' - for proportional scaling. With 'logo_opacity' logo can become transparent. - With 'logo_filter' 'overlay=W-w-12:12' you can modify the logo position. - With 'use_loudnorm' you can activate single pass EBU R128 loudness normalization. - 'loud_*' can adjust the loudnorm filter. 'output_count' sets the outputs for - the filtering, > 1 gives the option to use the same filters for multiple outputs. - This outputs can be taken in 'stream_param', names will be vout2, vout3; - aout2, aout2 etc. + helptext: Default processing, for all clips that they get prepared in that way, + so the output is unique. Set playing mode, like playlist, or folder. + 'aspect' must be a float number. 'logo' is only used if the path exist. + 'logo_scale' scale the logo to target size, leave it blank when no scaling + is needed, format is 'number:number', for example '100:-1' for proportional + scaling. With 'logo_opacity' logo can become transparent. With 'logo_filter' + 'overlay=W-w-12:12' you can modify the logo position. With 'use_loudnorm' + you can activate single pass EBU R128 loudness normalization. + 'loud_*' can adjust the loudnorm filter. mode: playlist width: 1024 height: 576 @@ -59,7 +56,6 @@ processing: loud_i: -18 loud_tp: -1.5 loud_lra: 11 - output_count: 1 volume: 1 ingest: @@ -109,7 +105,7 @@ text: fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" text_from_filename: false style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4" - regex: "^(.*)_" + regex: ^.+[/\\](.*)(.mp4|.mkv)$ out: helptext: The final playout compression. Set the settings to your needs. diff --git a/examples/logging.rs b/examples/logging.rs index a03d43d3..a7d817f4 100644 --- a/examples/logging.rs +++ b/examples/logging.rs @@ -1,11 +1,11 @@ extern crate log; extern crate simplelog; +use std::{thread::sleep, time::Duration}; + use simplelog::*; use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate}; - -// use crate::{Config, SharedLogger}; use log::{Level, LevelFilter, Log, Metadata, Record}; pub struct LogMailer { @@ -31,12 +31,15 @@ impl Log for LogMailer { if self.enabled(record.metadata()) { match record.level() { Level::Error => { - println!("Send Error Mail: {:?}\n{:?}", record, self.config) - }, + println!("Send Error Mail: {:?}", record.args()) + } Level::Warn => { println!("Send Warn Mail: {:?}", record.args()) } - _ => () + Level::Info => { + println!("Send Info Mail: {:?}", record.args()) + } + _ => (), } } } @@ -102,7 +105,7 @@ fn main() { ColorChoice::Auto, ), WriteLogger::new(LevelFilter::Debug, file_config, log()), - LogMailer::new(LevelFilter::Warn, mail_config), + LogMailer::new(LevelFilter::Info, mail_config), ]) .unwrap(); @@ -113,5 +116,6 @@ fn main() { for idx in 1..10 { info!("{idx}"); + sleep(Duration::from_secs(2)); } } diff --git a/src/filter/mod.rs b/src/filter/mod.rs index fe9b48e9..91781550 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -1,8 +1,9 @@ use std::path::Path; -use regex::Regex; use simplelog::*; +pub mod v_drawtext; + use crate::utils::{is_close, GlobalConfig, Media}; #[derive(Debug, Clone)] @@ -196,28 +197,22 @@ fn add_text(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) { // add drawtext filter for lower thirds messages if config.text.add_text && config.text.over_pre { - let mut font: String = "".to_string(); - let filter: String; + let filter = v_drawtext::filter_node(node); - if Path::new(&config.text.fontfile).is_file() { - font = format!(":fontfile='{}'", config.text.fontfile) + chain.add_filter(filter, "video".into()); + + match &chain.video_chain { + Some(filters) => { + for (i, f) in filters.split(",").enumerate() { + if f.contains("drawtext") && !config.text.text_from_filename { + debug!("drawtext node is on index: {i}"); + break; + } + } + } + + None => (), } - - if config.text.text_from_filename { - let source = node.source.clone(); - let regex: Regex = Regex::new(config.text.regex.as_str()).unwrap(); - let text: String = regex.captures(source.as_str()).unwrap()[0].to_string(); - - let escape = text.replace("'", "'\\\\\\''").replace("%", "\\\\\\%"); - filter = format!("drawtext=text='{escape}':{}{font}", config.text.style) - } else { - filter = format!( - "null,zmq=b=tcp\\\\://'{}',drawtext=text=''{}", - config.text.bind_address, font - ) - } - - chain.add_filter(filter, "video".into()) } } @@ -251,6 +246,21 @@ fn extend_audio(node: &mut Media, chain: &mut Filters) { } } +fn add_loudnorm(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) { + // add single pass loudnorm filter to audio line + + let audio_streams = node.probe.clone().unwrap().audio_streams.unwrap(); + + if audio_streams.len() != 0 && config.processing.add_loudnorm { + let loud_filter = format!( + "loudnorm=I={}:TP={}:LRA={}", + config.processing.loud_i, config.processing.loud_tp, config.processing.loud_lra + ); + + chain.add_filter(loud_filter, "audio".into()); + } +} + fn audio_volume(chain: &mut Filters, config: &GlobalConfig) { if config.processing.volume != 1.0 { chain.add_filter( @@ -311,6 +321,7 @@ pub fn filter_chains(node: &mut Media) -> Vec { add_text(node, &mut filters, &config); add_audio(node, &mut filters); extend_audio(node, &mut filters); + add_loudnorm(node, &mut filters, &config); } fade(node, &mut filters, "video".into()); diff --git a/src/filter/v_drawtext.rs b/src/filter/v_drawtext.rs new file mode 100644 index 00000000..6682effd --- /dev/null +++ b/src/filter/v_drawtext.rs @@ -0,0 +1,40 @@ +use std::{ + path::Path +}; + +use regex::Regex; + +use crate::utils::{GlobalConfig, Media}; + +pub fn filter_node(node: &mut Media) -> String { + let config = GlobalConfig::global(); + let mut filter = String::new(); + let mut font = String::new(); + + if config.text.add_text { + if Path::new(&config.text.fontfile).is_file() { + font = format!(":fontfile='{}'", config.text.fontfile) + } + + if config.text.over_pre && config.text.text_from_filename { + let source = node.source.clone(); + let regex: Regex = Regex::new(&config.text.regex).unwrap(); + + let text: String = match regex.captures(&source) { + Some(t) => t[1].to_string(), + None => source, + }; + + let escape = text.replace("'", "'\\\\\\''").replace("%", "\\\\\\%"); + filter = format!("drawtext=text='{escape}':{}{font}", config.text.style) + } else { + filter = format!( + "zmq=b=tcp\\\\://'{}',drawtext=text=''{}", + config.text.bind_address.replace(":", "\\:"), + font + ) + } + } + + filter +} diff --git a/src/output/desktop.rs b/src/output/desktop.rs index 7c2ccb40..6ca46561 100644 --- a/src/output/desktop.rs +++ b/src/output/desktop.rs @@ -5,7 +5,8 @@ use std::{ use simplelog::*; -use crate::utils::GlobalConfig; +use crate::utils::{GlobalConfig, Media}; +use crate::filter::v_drawtext; pub fn output(log_format: String) -> process::Child { let config = GlobalConfig::global(); @@ -22,13 +23,14 @@ pub fn output(log_format: String) -> process::Child { ]; if config.text.add_text && !config.text.over_pre { - let text_filter: String = format!( - "null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'", - config.text.bind_address.replace(":", "\\:"), - config.text.fontfile + info!( + "Using drawtext filter, listening on address: {}", + config.text.bind_address ); - enc_filter = vec!["-vf".to_string(), text_filter]; + let mut filter: String = "null,".to_string(); + filter.push_str(v_drawtext::filter_node(&mut Media::new(0, "".to_string())).as_str()); + enc_filter = vec!["-vf".to_string(), filter]; } enc_cmd.append(&mut enc_filter.iter().map(String::as_str).collect()); diff --git a/src/output/mod.rs b/src/output/mod.rs index 5a502d80..5ad828ff 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -60,7 +60,7 @@ pub fn play() { Box::new(folder_source) as Box> } "playlist" => { - info!("Playout in playlist mode."); + info!("Playout in playlist mode"); Box::new(CurrentProgram::new()) as Box> } _ => { diff --git a/src/output/stream.rs b/src/output/stream.rs index 3f806d03..f34a550d 100644 --- a/src/output/stream.rs +++ b/src/output/stream.rs @@ -5,11 +5,13 @@ use std::{ use simplelog::*; -use crate::utils::GlobalConfig; +use crate::utils::{GlobalConfig, Media}; +use crate::filter::v_drawtext; pub fn output(log_format: String) -> process::Child { let config = GlobalConfig::global(); let mut enc_filter: Vec = vec![]; + let mut preview: Vec<&str> = vec![]; let mut enc_cmd = vec![ "-hide_banner", @@ -22,16 +24,29 @@ pub fn output(log_format: String) -> process::Child { ]; if config.text.add_text && !config.text.over_pre { - let text_filter: String = format!( - "null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'", - config.text.bind_address.replace(":", "\\:"), - config.text.fontfile + info!( + "Using drawtext filter, listening on address: {}", + config.text.bind_address ); - enc_filter = vec!["-vf".to_string(), text_filter]; + let mut filter: String = "[0:v]null,".to_string(); + filter.push_str(v_drawtext::filter_node(&mut Media::new(0, "".to_string())).as_str()); + + if config.out.preview { + filter.push_str(",split=2[v_out1][v_out2]"); + + preview = vec!["-map", "[v_out1]", "-map", "0:a"]; + preview.append(&mut config.out.preview_param.iter().map(String::as_str).collect()); + preview.append(&mut vec!["-map", "[v_out2]", "-map", "0:a"]); + } + + enc_filter = vec!["-filter_complex".to_string(), filter]; + } else if config.out.preview { + preview = config.out.preview_param.iter().map(String::as_str).collect() } enc_cmd.append(&mut enc_filter.iter().map(String::as_str).collect()); + enc_cmd.append(&mut preview); enc_cmd.append(&mut config.out.stream_param.iter().map(String::as_str).collect()); debug!("Encoder CMD: {:?}", enc_cmd); diff --git a/src/utils/config.rs b/src/utils/config.rs index 6f6f55b7..401581ce 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1,7 +1,12 @@ use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; use serde_yaml::{self}; -use std::{fs::File, path::Path, process}; +use std::{ + env, + fs::File, + path::{Path, PathBuf}, + process, +}; use crate::utils::{get_args, time_to_sec}; @@ -60,7 +65,6 @@ pub struct Processing { pub loud_i: f32, pub loud_tp: f32, pub loud_lra: f32, - pub output_count: u32, pub volume: f64, pub settings: Option>, } @@ -113,26 +117,30 @@ static INSTANCE: OnceCell = OnceCell::new(); impl GlobalConfig { fn new() -> Self { let args = get_args(); - let mut config_path: String = "ffplayout.yml".to_string(); + let mut config_path = match env::current_exe() { + Ok(path) => path.parent().unwrap().join("ffplayout.yml"), + Err(_) => PathBuf::from("./ffplayout.yml"), + }; if args.config.is_some() { - config_path = args.config.unwrap(); + config_path = PathBuf::from(args.config.unwrap()); } else if Path::new("/etc/ffplayout/ffplayout.yml").is_file() { - config_path = "/etc/ffplayout/ffplayout.yml".to_string(); + config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); } let f = match File::open(&config_path) { Ok(file) => file, Err(err) => { println!( - "'{config_path}' doesn't exists!\n{}\n\nSystem error: {err}", - "Put 'ffplayout.yml' in '/etc/playout/' or beside the executable!" + "'{:?}' doesn't exists!\n{}\n\nSystem error: {err}", + config_path, "Put 'ffplayout.yml' in '/etc/playout/' or beside the executable!" ); process::exit(0x0100); } }; - let mut config: GlobalConfig = serde_yaml::from_reader(f).expect("Could not read config file."); + let mut config: GlobalConfig = + serde_yaml::from_reader(f).expect("Could not read config file."); let fps = config.processing.fps.to_string(); let bitrate = config.processing.width * config.processing.height / 10; config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start)); @@ -143,7 +151,7 @@ impl GlobalConfig { config.playlist.length_sec = Some(86400.0); } - let settings = vec![ + let mut settings: Vec = vec![ "-pix_fmt", "yuv420p", "-r", @@ -160,22 +168,19 @@ impl GlobalConfig { format!("{}k", bitrate).as_str(), "-bufsize", format!("{}k", bitrate / 2).as_str(), - "-c:a", - "s302m", - "-strict", - "-2", - "-ar", - "48000", - "-ac", - "2", - "-f", - "mpegts", - "-", ] .iter() .map(|&s| s.into()) .collect(); + settings.append(&mut pre_audio_codec(config.processing.add_loudnorm)); + settings.append( + &mut vec!["-ar", "48000", "-ac", "2", "-f", "mpegts", "-"] + .iter() + .map(|&s| s.into()) + .collect(), + ); + config.processing.settings = Some(settings); if args.log.is_some() { @@ -229,6 +234,20 @@ impl GlobalConfig { } } +fn pre_audio_codec(add_loudnorm: bool) -> Vec { + // when add_loudnorm is False we use a different audio encoder, + // s302m has higher quality, but is experimental + // and works not well together with the loudnorm filter + + let mut codec = vec!["-c:a", "s302m", "-strict", "-2"]; + + if add_loudnorm { + codec = vec!["-c:a", "mp2", "-b:a", "384k"]; + } + + codec.iter().map(|&s| s.into()).collect() +} + pub fn init_config() { let config = GlobalConfig::new(); INSTANCE.set(config).unwrap(); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 699e0b54..254b59bb 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -48,11 +48,11 @@ pub struct Media { } impl Media { - fn new(index: usize, src: String) -> Self { + pub fn new(index: usize, src: String) -> Self { let mut duration: f64 = 0.0; let mut probe = None; - if Path::new("src").is_file() { + if Path::new(&src).is_file() { probe = Some(MediaProbe::new(src.clone())); duration = match probe.clone().unwrap().format.unwrap().duration {