add generic drawtext, add loudnorm, read config from same path, fix src in Media::new()
This commit is contained in:
parent
8f3300f925
commit
dc4337581e
@ -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.
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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
40
src/filter/v_drawtext.rs
Normal 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
|
||||
}
|
@ -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());
|
||||
|
@ -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>>
|
||||
}
|
||||
_ => {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user