diff --git a/Cargo.lock b/Cargo.lock index e02d39d0..23e3b06f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml index 3533c9de..48b50cef 100644 --- a/ffplayout-engine/Cargo.toml +++ b/ffplayout-engine/Cargo.toml @@ -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" diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index 1c73e396..2387c02e 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -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::().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 { - 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[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 { 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); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 2a99e6d9..c540d0cb 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -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] diff --git a/lib/src/filter/mod.rs b/lib/src/filter/mod.rs index 4c39e07c..3a7ef74d 100644 --- a/lib/src/filter/mod.rs +++ b/lib/src/filter/mod.rs @@ -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, pub video_map: Vec, + pub audio_out_link: Vec, + pub video_out_link: Vec, pub output_map: Vec, 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::() + .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::() + .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::().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::().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 } diff --git a/lib/src/utils/config.rs b/lib/src/utils/config.rs index ca7d0674..bfd89364 100644 --- a/lib/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -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, #[serde(skip_serializing, skip_deserializing)] pub output_cmd: Option>, } @@ -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, diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 194f991c..b504645c 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.1.0" +version = "0.1.1" edition = "2021" publish = false diff --git a/tests/src/engine_cmd.rs b/tests/src/engine_cmd.rs index 1fda7d7b..6229efe8 100644 --- a/tests/src/engine_cmd.rs +++ b/tests/src/engine_cmd.rs @@ -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()));