work on new output filter function

This commit is contained in:
jb-alvarado 2022-10-21 15:31:24 +02:00
parent 2dbcc70bec
commit 9a67aa1776
8 changed files with 393 additions and 113 deletions
Cargo.lock
ffplayout-engine
lib
tests

6
Cargo.lock generated

@ -984,7 +984,7 @@ dependencies = [
[[package]]
name = "ffplayout"
version = "0.16.0"
version = "0.16.1"
dependencies = [
"chrono",
"clap",
@ -1034,7 +1034,7 @@ dependencies = [
[[package]]
name = "ffplayout-lib"
version = "0.16.0"
version = "0.16.1"
dependencies = [
"chrono",
"crossbeam-channel",
@ -2826,7 +2826,7 @@ dependencies = [
[[package]]
name = "tests"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"chrono",
"crossbeam-channel",

@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg"
license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md"
version = "0.16.0"
version = "0.16.1"
edition = "2021"
default-run = "ffplayout"

@ -9,7 +9,7 @@ pub mod arg_parse;
pub use arg_parse::Args;
use ffplayout_lib::{
filter::{FilterType::*, Filters},
filter::Filters,
utils::{time_to_sec, PlayoutConfig, ProcessMode::*},
};
@ -87,56 +87,6 @@ pub fn get_config(args: Args) -> PlayoutConfig {
config
}
/// Process filter from output_param
///
/// Split filter string and add them to the existing filtergraph.
fn process_filters(filters: &mut Filters, output_filter: &str) {
let re_v = Regex::new(r"(\[0:v(:[0-9]+)?\]|\[v[\w_-]+\])").unwrap(); // match video filter links
let re_a = Regex::new(r"(\[0:a(:[0-9]+)?\]|\[a[\w_-]+\])").unwrap(); // match audio filter links
let nr = Regex::new(r"\[[\w:-_]+([0-9])\]").unwrap(); // match filter link and get track number
for f in output_filter.split(';') {
if re_v.is_match(f) {
let filter_str = re_v.replace_all(f, "").to_string();
filters.add_filter(&filter_str, 0, Video);
} else if re_a.is_match(f) {
let filter_str = re_a.replace_all(f, "").to_string();
let track_nr = nr.replace(f, "$1").parse::<i32>().unwrap_or_default();
filters.add_filter(&filter_str, track_nr, Audio);
}
}
}
/// Process filter with multiple in- or output
///
/// Concat filter to the existing filters and adjust filter connections.
/// Output mapping is up to the user.
fn process_multi_in_out(filters: &mut Filters, output_filter: &str) -> Vec<String> {
let v_map = filters.video_map[filters.video_map.len() - 1].clone();
let mut new_filter = format!("{}{v_map}", filters.video_chain);
let re_v = Regex::new(r"\[0:v(:[0-9]+)?\]").unwrap(); // match video input
let re_a = Regex::new(r"\[0:a(:(?P<num>[0-9]+))?\]").unwrap(); // match audio input, with getting track number
let mut o_filter = re_v.replace(output_filter, v_map).to_string();
if !filters.audio_map.is_empty() {
let a_map = filters.audio_map[filters.audio_map.len() - 1].clone();
let a_filter = format!("{}{a_map}", filters.audio_chain);
new_filter.push(';');
new_filter.push_str(&a_filter);
o_filter = re_a
.replace_all(&o_filter, "[aout$num]")
.to_string()
.replace("[aout]", "[aout0]"); // when no number matched, set default 0
}
new_filter.push(';');
new_filter.push_str(&o_filter);
vec!["-filter_complex".to_string(), new_filter]
}
/// Prepare output parameters
///
/// Seek for multiple outputs and add mapping for it.
@ -147,51 +97,54 @@ pub fn prepare_output_cmd(
) -> Vec<String> {
let mut output_params = config.out.clone().output_cmd.unwrap();
let mut new_params = vec![];
let mut output_filter = String::new();
let mut count = 0;
let re_map = Regex::new(r"(\[?[0-9]:[av](:[0-9]+)?\]?|-map$|\[[a-z_0-9]+\])").unwrap(); // match a/v filter links and mapping
let re_multi = Regex::new(r"\[[\w:_-]+\]\[[\w:_-]+\]").unwrap(); // match multiple filter in/outputs links
if let Some(mut filter) = filters.clone() {
// Check if it contains a filtergraph and set correct output mapping.
println!("filter: {filter:#?}\n");
for (i, param) in output_params.iter().enumerate() {
if param != "-filter_complex" {
if i > 0 && output_params[i - 1] == "-filter_complex" {
output_filter = param.clone();
} else if !re_multi.is_match(&output_filter) {
if !re_map.is_match(param)
|| (i < output_params.len() - 2
&& (output_params[i + 1].contains("0:s") || param.contains("0:s")))
{
// Skip mapping parameters, when no multi in/out filter is set.
// Only add subtitle mapping.
new_params.push(param.clone());
}
if !re_map.is_match(param)
|| (i < output_params.len() - 2
&& (output_params[i + 1].contains("0:s") || param.contains("0:s")))
{
// Skip mapping parameters, when no multi in/out filter is set.
// Only add subtitle mapping.
new_params.push(param.clone());
}
// Check if parameter is a output
if i > 0
&& !param.starts_with('-')
&& !output_params[i - 1].starts_with('-')
&& i < output_params.len() - 1
{
// add mapping to following outputs
if filter.video_out_link.len() > count {
new_params.append(&mut vec![
"-map".to_string(),
filter.video_out_link[count].clone(),
]);
} else {
new_params.push(param.clone());
new_params.append(&mut vec!["-map".to_string(), "0:v".to_string()]);
}
// Check if parameter is a output
if i > 0
&& !param.starts_with('-')
&& !output_params[i - 1].starts_with('-')
&& i < output_params.len() - 1
{
// add mapping to following outputs
new_params.append(&mut filter.map().clone());
if filter.audio_out_link.len() > count {
new_params.append(&mut vec![
"-map".to_string(),
filter.audio_out_link[count].clone(),
]);
} else if filter.audio_out_link.is_empty() {
new_params.append(&mut vec!["-map".to_string(), "0:a:0".to_string()]);
}
count += 1
}
}
output_params = new_params;
if re_multi.is_match(&output_filter) {
let mut split_filter = process_multi_in_out(&mut filter, &output_filter);
cmd.append(&mut split_filter);
} else {
process_filters(&mut filter, &output_filter);
cmd.append(&mut filter.cmd());
cmd.append(&mut filter.map());
}
cmd.append(&mut filter.cmd());
cmd.append(&mut filter.map());
}
cmd.append(&mut output_params);

@ -4,7 +4,7 @@ description = "Library for ffplayout"
license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md"
version = "0.16.0"
version = "0.16.1"
edition = "2021"
[dependencies]

@ -4,16 +4,16 @@ use std::{
sync::{Arc, Mutex},
};
use regex::Regex;
use simplelog::*;
mod a_loudnorm;
mod custom_filter;
pub mod v_drawtext;
// get_delta
use self::custom_filter::custom_filter;
use crate::utils::{
controller::ProcessUnit::*, fps_calc, get_delta, is_close, Media, MediaProbe, OutputMode::*,
controller::ProcessUnit::{self, *},
fps_calc, get_delta, is_close, Media, MediaProbe,
OutputMode::*,
PlayoutConfig,
};
@ -40,6 +40,8 @@ pub struct Filters {
pub video_chain: String,
pub audio_map: Vec<String>,
pub video_map: Vec<String>,
pub audio_out_link: Vec<String>,
pub video_out_link: Vec<String>,
pub output_map: Vec<String>,
audio_track_count: i32,
audio_position: i32,
@ -55,6 +57,8 @@ impl Filters {
video_chain: String::new(),
audio_map: vec![],
video_map: vec![],
audio_out_link: vec![],
video_out_link: vec![],
output_map: vec![],
audio_track_count,
audio_position,
@ -115,11 +119,11 @@ impl Filters {
let mut v_chain = self.video_chain.clone();
let mut a_chain = self.audio_chain.clone();
if self.video_last >= 0 {
if self.video_last >= 0 && !v_chain.ends_with(']') {
v_chain.push_str(&format!("[vout{}]", self.video_last));
}
if self.audio_last >= 0 {
if self.audio_last >= 0 && !a_chain.ends_with(']') {
a_chain.push_str(&format!("[aout{}]", self.audio_last));
}
@ -414,12 +418,135 @@ fn realtime(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
}
}
fn custom(filter: &str, chain: &mut Filters, nr: i32, filter_type: FilterType) {
if !filter.is_empty() {
chain.add_filter(filter, nr, filter_type);
pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: FilterType) {
if count > 1 {
let out_link = match filter_type {
Audio => &mut chain.audio_out_link,
Video => &mut chain.video_out_link,
};
for i in 0..count {
let link = format!("[{filter_type}out_{nr}_{i}]");
if !out_link.contains(&link) {
out_link.push(link)
}
}
let split_filter = format!("split={count}{}", out_link.join(""));
chain.add_filter(&split_filter, nr, filter_type);
}
}
/// Process custom/output filter
///
/// Split filter string and add them to the existing filtergraph.
fn process_custom_filters(
config: &PlayoutConfig,
chain: &mut Filters,
custom_filter: &str,
unit: ProcessUnit,
) {
let re_v =
Regex::new(r"(^\[([0-9:]+)?[v^\[]([\w:_-]+)?\]|\[([0-9:]+)?[v^\[]([\w:_-]+)?\]$)").unwrap(); // match first/last video filter links
let re_a =
Regex::new(r"(^\[([0-9:]+)?[a^\[]([\w:_-]+)?\]|\[([0-9:]+)?[a^\[]([\w:_-]+)?\]$)").unwrap(); // match first/last audio filter links
let re_v_delim = Regex::new(r";\[[0-9:]+[v^\[]([0-9:]+)?\]").unwrap(); // match video filter link as delimiter
let re_a_delim = Regex::new(r";\[[0-9:]+[a^\[]([0-9:]+)?\]").unwrap(); // match video filter link as delimiter
let mut video_filter = String::new();
let mut audio_filter = String::new();
if custom_filter.contains("split") && unit == Decoder {
error!("No split filter in {unit} allow! Skip filter...");
return;
}
if let Some(v_d) = re_v_delim.find(custom_filter) {
if let Some((a, v)) = custom_filter.split_once(v_d.as_str()) {
video_filter = re_v.replace_all(v, "").to_string();
audio_filter = re_a.replace_all(a, "").to_string();
}
} else if let Some(a_d) = re_a_delim.find(custom_filter) {
if let Some((v, a)) = custom_filter.split_once(a_d.as_str()) {
video_filter = re_v.replace_all(v, "").to_string();
audio_filter = re_a.replace_all(a, "").to_string();
}
} else if re_v.is_match(custom_filter) {
video_filter = re_v.replace_all(custom_filter, "").to_string();
} else if re_a.is_match(custom_filter) {
audio_filter = re_a.replace_all(custom_filter, "").to_string();
}
if video_filter.contains("split") {
let nr = Regex::new(".*split=([0-9]+).*").unwrap();
let re_s = Regex::new(r"(;?\[[^\]]*\])?,?split=[0-9]+(\[[\w]+\])+;?").unwrap();
let split_nr = nr
.replace(&video_filter, "$1")
.parse::<usize>()
.unwrap_or(1);
let new_filter = re_s.replace_all(&video_filter, "").to_string();
if !new_filter.is_empty() {
chain.add_filter(&new_filter, 0, Video);
}
split_filter(chain, split_nr, 0, Video);
} else if !video_filter.is_empty() {
chain.add_filter(&video_filter, 0, Video);
}
if audio_filter.contains("asplit") {
let nr = Regex::new(".*split=([0-9]+).*").unwrap();
let re_s = Regex::new(r"(;?\[[^\]]*\])?,?asplit=[0-9]+(\[[\w]+\])+;?").unwrap();
let split_nr = nr
.replace(&audio_filter, "$1")
.parse::<usize>()
.unwrap_or(1);
let new_filter = re_s.replace_all(&audio_filter, "").to_string();
if !new_filter.is_empty() {
for i in 0..config.processing.audio_tracks {
chain.add_filter(&new_filter, i, Audio);
}
}
for i in 0..config.processing.audio_tracks {
split_filter(chain, split_nr, i, Audio);
}
} else if !audio_filter.is_empty() {
for i in 0..config.processing.audio_tracks {
chain.add_filter(&audio_filter, i, Audio);
}
}
// for f in custom_filter.split(delim) {
// if re_v.is_match(f) {
// if f.contains("split") {
// let re = Regex::new(r"split=([0-9]+)").unwrap();
// let split_nr = re.replace(f, "$1").parse::<usize>().unwrap_or(1);
// split_filter(chain, split_nr, 0, Video);
// } else {
// let filter_str = re_v.replace_all(f, "").to_string();
// chain.add_filter(&filter_str, 0, Video);
// }
// } else if re_a.is_match(f) {
// for i in 0..config.processing.audio_tracks {
// if f.contains("asplit") {
// let re = Regex::new(r"asplit=([0-9]+)").unwrap();
// let split_nr = re.replace(f, "$1").parse::<usize>().unwrap_or(1);
// split_filter(chain, split_nr, i, Audio);
// } else {
// let filter_str = re_a.replace_all(f, "").to_string();
// chain.add_filter(&filter_str, i, Audio);
// }
// }
// }
// }
}
pub fn filter_chains(
config: &PlayoutConfig,
node: &mut Media,
@ -430,6 +557,12 @@ pub fn filter_chains(
if node.unit == Encoder {
add_text(node, &mut filters, config, filter_chain);
if let Some(f) = config.out.output_filter.clone() {
process_custom_filters(config, &mut filters, &f, Encoder)
} else if config.out.output_count > 1 {
split_filter(&mut filters, config.out.output_count, 0, Video)
}
return filters;
}
@ -465,12 +598,6 @@ pub fn filter_chains(
overlay(node, &mut filters, config);
realtime(node, &mut filters, config);
let (proc_vf, proc_af) = custom_filter(&config.processing.custom_filter);
let (list_vf, list_af) = custom_filter(&node.custom_filter);
custom(&proc_vf, &mut filters, 0, Video);
custom(&list_vf, &mut filters, 0, Video);
for i in 0..config.processing.audio_tracks {
if node
.probe
@ -487,6 +614,7 @@ pub fn filter_chains(
);
add_audio(node, &mut filters, i);
}
// add at least anull filter, for correct filter construction,
// is important for split filter in HLS mode
filters.add_filter("anull", i, Audio);
@ -494,10 +622,15 @@ pub fn filter_chains(
add_loudnorm(node, &mut filters, config, i);
fade(node, &mut filters, i, Audio);
audio_volume(&mut filters, config, i);
custom(&proc_af, &mut filters, i, Audio);
custom(&list_af, &mut filters, i, Audio);
}
process_custom_filters(
config,
&mut filters,
&config.processing.custom_filter,
node.unit,
);
process_custom_filters(config, &mut filters, &node.custom_filter, node.unit);
filters
}

@ -229,6 +229,10 @@ pub struct Out {
pub mode: OutputMode,
pub output_param: String,
#[serde(skip_serializing, skip_deserializing)]
pub output_count: usize,
#[serde(skip_serializing, skip_deserializing)]
pub output_filter: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub output_cmd: Option<Vec<String>>,
}
@ -308,10 +312,30 @@ impl PlayoutConfig {
config.ingest.input_cmd = split(config.ingest.input_param.as_str());
config.out.output_count = 1;
config.out.output_filter = None;
if config.out.mode == Null {
config.out.output_cmd = Some(vec_strings!["-f", "null", "-"]);
} else {
config.out.output_cmd = split(config.out.output_param.as_str());
} else if let Some(mut cmd) = split(config.out.output_param.as_str()) {
// get output count according to the var_stream_map value, or by counting output parameters
if let Some(i) = cmd.clone().iter().position(|m| m == "-var_stream_map") {
config.out.output_count = cmd[i + 1].split_whitespace().count();
} else {
config.out.output_count = cmd
.iter()
.enumerate()
.filter(|(i, p)| i > &0 && !p.starts_with('-') && !cmd[i - 1].starts_with('-'))
.count();
}
if let Some(i) = cmd.clone().iter().position(|r| r == "-filter_complex") {
config.out.output_filter = Some(cmd[i + 1].clone());
cmd.remove(i);
cmd.remove(i + 1);
}
config.out.output_cmd = Some(cmd);
}
// when text overlay without text_from_filename is on, turn also the RPC server on,

@ -1,6 +1,6 @@
[package]
name = "tests"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
publish = false

@ -33,6 +33,57 @@ fn video_audio_input() {
assert_eq!(media.filter.unwrap().map(), test_filter_map);
}
#[test]
fn video_audio_custom_filter1_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter = "[0:v]gblur=2;[0:a]volume=0.2".to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None);
let test_filter_cmd = vec_strings![
"-filter_complex",
"[0:v:0]scale=1024:576,gblur=2[vout0];[0:a:0]anull,volume=0.2[aout0]"
];
let test_filter_map = vec_strings!["-map", "[vout0]", "-map", "[aout0]"];
assert_eq!(
media.cmd,
Some(vec_strings!["-i", "./assets/with_audio.mp4"])
);
assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd);
assert_eq!(media.filter.unwrap().map(), test_filter_map);
}
#[test]
fn video_audio_custom_filter2_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter =
"[0:v]null[v];movie=logo.png[l];[v][l]overlay[vout0];[0:a]volume=0.2[aout0]".to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None);
let test_filter_cmd = vec_strings![
"-filter_complex",
"[0:v:0]scale=1024:576,null[v];movie=logo.png[l];[v][l]overlay[vout0];[0:a:0]anull,volume=0.2[aout0]"
];
let test_filter_map = vec_strings!["-map", "[vout0]", "-map", "[aout0]"];
assert_eq!(
media.cmd,
Some(vec_strings!["-i", "./assets/with_audio.mp4"])
);
assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd);
assert_eq!(media.filter.unwrap().map(), test_filter_map);
}
#[test]
fn dual_audio_aevalsrc_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
@ -182,9 +233,8 @@ fn video_audio_filter_stream() {
config.out.mode = Stream;
config.processing.add_logo = false;
config.text.add_text = false;
config.out.output_filter = Some("[0:v]gblur=2[vout0];[0:a]volume=0.2[aout0]".to_string());
config.out.output_cmd = Some(vec_strings![
"-filter_complex",
"[0:v]gblur=2[vout0];[0:a]volume=0.2[aout0]",
"-map",
"[vout0]",
"-map",
@ -259,9 +309,8 @@ fn video_audio_filter2_stream() {
config.processing.add_logo = false;
config.text.add_text = true;
config.text.fontfile = String::new();
config.out.output_filter = Some("[0:v]gblur=2[vout0];[0:a]volume=0.2[aout0]".to_string());
config.out.output_cmd = Some(vec_strings![
"-filter_complex",
"[0:v]gblur=2[vout0];[0:a]volume=0.2[aout0]",
"-map",
"[vout0]",
"-map",
@ -982,6 +1031,127 @@ fn video_dual_audio_multi_filter_stream() {
assert_eq!(enc_cmd, test_cmd);
}
#[test]
fn video_audio_text_filter_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.audio_tracks = 1;
config.text.fontfile = String::new();
config.out.output_count = 2;
config.out.output_cmd = Some(vec_strings![
"-map",
"0:v",
"-map",
"0:a:0",
"-c:v",
"libx264",
"-c:a",
"aac",
"-ar",
"44100",
"-b:a",
"128k",
"-flags",
"+global_header",
"-f",
"mpegts",
"srt://127.0.0.1:40051",
"-map",
"0:v",
"-map",
"0:a:0",
"-s",
"512x288",
"-c:v",
"libx264",
"-c:a",
"aac",
"-ar",
"44100",
"-b:a",
"128k",
"-flags",
"+global_header",
"-f",
"mpegts",
"srt://127.0.0.1:40052"
]);
let enc_prefix = vec_strings![
"-hide_banner",
"-nostats",
"-v",
"level+error",
"-re",
"-i",
"pipe:0"
];
let socket = config
.text
.zmq_stream_socket
.clone()
.unwrap()
.replace(':', "\\:");
let mut media = Media::new(0, "", false);
media.unit = Encoder;
media.add_filter(&config, &None);
let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter);
let test_cmd = vec_strings![
"-hide_banner",
"-nostats",
"-v",
"level+error",
"-re",
"-i",
"pipe:0",
"-filter_complex",
format!("[0:v:0]zmq=b=tcp\\\\://'{socket}',drawtext@dyntext=text='',split=2[vout_0_0][vout_0_1]"),
"-map",
"[vout_0_0]",
"-map",
"0:a:0",
"-c:v",
"libx264",
"-c:a",
"aac",
"-ar",
"44100",
"-b:a",
"128k",
"-flags",
"+global_header",
"-f",
"mpegts",
"srt://127.0.0.1:40051",
"-map",
"[vout_0_1]",
"-map",
"0:a:0",
"-s",
"512x288",
"-c:v",
"libx264",
"-c:a",
"aac",
"-ar",
"44100",
"-b:a",
"128k",
"-flags",
"+global_header",
"-f",
"mpegts",
"srt://127.0.0.1:40052"
];
assert_eq!(enc_cmd, test_cmd);
}
#[test]
fn video_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));