add generic drawtext, add loudnorm, read config from same path, fix src in Media::new()

This commit is contained in:
jb-alvarado 2022-03-16 13:18:53 +01:00
parent 8f3300f925
commit dc4337581e
9 changed files with 163 additions and 76 deletions

View File

@ -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.

View File

@ -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));
}
}

View File

@ -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: <yellow>{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<String> {
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());

40
src/filter/v_drawtext.rs Normal file
View File

@ -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
}

View File

@ -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: <yellow>{}</>",
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());

View File

@ -60,7 +60,7 @@ pub fn play() {
Box::new(folder_source) as Box<dyn Iterator<Item = Media>>
}
"playlist" => {
info!("Playout in playlist mode.");
info!("Playout in playlist mode");
Box::new(CurrentProgram::new()) as Box<dyn Iterator<Item = Media>>
}
_ => {

View File

@ -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<String> = 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: <yellow>{}</>",
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: <bright-blue>{:?}</>", enc_cmd);

View File

@ -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<Vec<String>>,
}
@ -113,26 +117,30 @@ static INSTANCE: OnceCell<GlobalConfig> = 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<String> = 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<String> {
// 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();

View File

@ -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 {