diff --git a/ffplayout-engine/src/input/ingest.rs b/ffplayout-engine/src/input/ingest.rs index 0646e305..168d7ed1 100644 --- a/ffplayout-engine/src/input/ingest.rs +++ b/ffplayout-engine/src/input/ingest.rs @@ -91,7 +91,7 @@ pub fn ingest_server( if let Some(mut filter) = dummy_media.filter { server_cmd.append(&mut filter.cmd()); - server_cmd.append(&mut filter.map(None)); + server_cmd.append(&mut filter.map()); } server_cmd.append(&mut config.processing.settings.unwrap()); diff --git a/ffplayout-engine/src/output/mod.rs b/ffplayout-engine/src/output/mod.rs index 1cb2adbf..206d4091 100644 --- a/ffplayout-engine/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -104,7 +104,7 @@ pub fn player( if let Some(mut filter) = node.filter { dec_cmd.append(&mut filter.cmd()); - dec_cmd.append(&mut filter.map(None)); + dec_cmd.append(&mut filter.map()); } dec_cmd.append(&mut config.processing.clone().settings.unwrap()); diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index 5627ac9e..e742a591 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -11,6 +11,7 @@ pub use arg_parse::Args; use ffplayout_lib::{ filter::Filters, utils::{time_to_sec, PlayoutConfig, ProcessMode::*}, + vec_strings, }; /// Read command line arguments, and override the config with them. @@ -97,18 +98,14 @@ pub fn prepare_output_cmd( ) -> Vec { let mut output_params = config.out.clone().output_cmd.unwrap(); let mut new_params = vec![]; - let mut count = 1; - 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 mut count = 0; + let re_v = Regex::new(r"\[?0:v(:0)?\]?").unwrap(); if let Some(mut filter) = filters.clone() { - println!("filter: {filter:#?}\n"); for (i, param) in output_params.iter().enumerate() { - 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. + if filter.video_out_link.len() > count && re_v.is_match(param) { + new_params.push(filter.video_out_link[count].clone()); + } else { new_params.push(param.clone()); } @@ -118,10 +115,20 @@ pub fn prepare_output_cmd( && !output_params[i - 1].starts_with('-') && i < output_params.len() - 1 { - // add mapping to following outputs - new_params.append(&mut filter.map(Some(count))); + count += 1; - count += 1 + if filter.video_out_link.len() > count + && !output_params.contains(&"-map".to_string()) + { + new_params.append(&mut vec_strings![ + "-map", + filter.video_out_link[count].clone() + ]); + + for i in 0..config.processing.audio_tracks { + new_params.append(&mut vec_strings!["-map", format!("0:a:{i}")]); + } + } } } @@ -129,10 +136,17 @@ pub fn prepare_output_cmd( cmd.append(&mut filter.cmd()); - if config.out.output_count > 1 { - cmd.append(&mut filter.map(Some(0))); - } else { - cmd.append(&mut filter.map(None)); + if !filter.map().iter().all(|item| output_params.contains(item)) + && filter.output_chain.is_empty() + && filter.video_out_link.is_empty() + { + cmd.append(&mut filter.map()) + } else if &output_params[0] != "-map" && !filter.video_out_link.is_empty() { + cmd.append(&mut vec_strings!["-map", filter.video_out_link[0].clone()]); + + for i in 0..config.processing.audio_tracks { + cmd.append(&mut vec_strings!["-map", format!("0:a:{i}")]); + } } } diff --git a/lib/src/filter/custom_filter.rs b/lib/src/filter/custom.rs similarity index 54% rename from lib/src/filter/custom_filter.rs rename to lib/src/filter/custom.rs index c37094b6..fe45fd3e 100644 --- a/lib/src/filter/custom_filter.rs +++ b/lib/src/filter/custom.rs @@ -1,43 +1,36 @@ +use regex::Regex; use simplelog::*; -fn strip_str(mut input: &str) -> String { - input = input.strip_prefix(';').unwrap_or(input); - input = input.strip_prefix("[0:v]").unwrap_or(input); - input = input.strip_prefix("[0:a]").unwrap_or(input); - input = input.strip_suffix(';').unwrap_or(input); - input = input.strip_suffix("[c_v_out]").unwrap_or(input); - input = input.strip_suffix("[c_a_out]").unwrap_or(input); - - input.to_string() -} - /// Apply custom filters -pub fn custom_filter(filter: &str) -> (String, String) { +pub fn filter_node(filter: &str) -> (String, String) { + let re = Regex::new(r"^;?(\[[^\[]+\])?|\[[^\[]+\]$").unwrap(); // match start/end link; let mut video_filter = String::new(); let mut audio_filter = String::new(); + // match chain with audio and video filter if filter.contains("[c_v_out]") && filter.contains("[c_a_out]") { let v_pos = filter.find("[c_v_out]").unwrap(); let a_pos = filter.find("[c_a_out]").unwrap(); let mut delimiter = "[c_v_out]"; + // split delimiter should be first filter output link if v_pos > a_pos { delimiter = "[c_a_out]"; } if let Some((f_1, f_2)) = filter.split_once(delimiter) { if f_2.contains("[c_a_out]") { - video_filter = strip_str(f_1); - audio_filter = strip_str(f_2); + video_filter = re.replace_all(f_1, "").to_string(); + audio_filter = re.replace_all(f_2, "").to_string(); } else { - video_filter = strip_str(f_2); - audio_filter = strip_str(f_1); + video_filter = re.replace_all(f_2, "").to_string(); + audio_filter = re.replace_all(f_1, "").to_string(); } } } else if filter.contains("[c_v_out]") { - video_filter = strip_str(filter); + video_filter = re.replace_all(filter, "").to_string(); } else if filter.contains("[c_a_out]") { - audio_filter = strip_str(filter); + audio_filter = re.replace_all(filter, "").to_string(); } else if !filter.is_empty() && filter != "~" { error!("Custom filter is not well formatted, use correct out link names (\"[c_v_out]\" and/or \"[c_a_out]\"). Filter skipped!") } diff --git a/lib/src/filter/mod.rs b/lib/src/filter/mod.rs index e93ce9a2..2578dcbe 100644 --- a/lib/src/filter/mod.rs +++ b/lib/src/filter/mod.rs @@ -8,12 +8,11 @@ use regex::Regex; use simplelog::*; mod a_loudnorm; +mod custom; pub mod v_drawtext; use crate::utils::{ - controller::ProcessUnit::{self, *}, - fps_calc, get_delta, is_close, Media, MediaProbe, - OutputMode::*, + controller::ProcessUnit::*, fps_calc, get_delta, is_close, Media, MediaProbe, OutputMode::*, PlayoutConfig, }; @@ -40,6 +39,7 @@ use FilterType::*; pub struct Filters { pub audio_chain: String, pub video_chain: String, + pub output_chain: Vec, pub audio_map: Vec, pub video_map: Vec, pub audio_out_link: Vec, @@ -57,6 +57,7 @@ impl Filters { Self { audio_chain: String::new(), video_chain: String::new(), + output_chain: vec![], audio_map: vec![], video_map: vec![], audio_out_link: vec![], @@ -118,6 +119,10 @@ impl Filters { } pub fn cmd(&mut self) -> Vec { + if !self.output_chain.is_empty() { + return self.output_chain.clone(); + } + let mut v_chain = self.video_chain.clone(); let mut a_chain = self.audio_chain.clone(); @@ -145,33 +150,9 @@ impl Filters { cmd } - pub fn map(&mut self, output_number: Option) -> Vec { + pub fn map(&mut self) -> Vec { let mut o_map = self.output_map.clone(); - if let Some(n) = output_number { - o_map.clear(); - - if self.video_out_link.len() > n { - o_map.append(&mut vec_strings!["-map", self.video_out_link[n]]) - } else { - o_map.append(&mut vec_strings!["-map", "0:v"]) - } - - if self.audio_out_link.len() > n { - o_map.append(&mut vec_strings!["-map", self.audio_out_link[n]]) - } else { - for i in 0..self.audio_track_count { - let a_map = format!("{}:a:{i}", self.audio_position); - - if !o_map.contains(&a_map) { - o_map.append(&mut vec_strings!["-map", a_map]); - }; - } - } - - return o_map; - } - if self.video_last == -1 { let v_map = "0:v".to_string(); @@ -463,114 +444,48 @@ pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: Fil } } -/// 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 +/// Process output filter chain and add new filters to existing ones. +fn process_output_filters(config: &PlayoutConfig, chain: &mut Filters, custom_filter: &str) { + let filter = + if (config.text.add_text && !config.text.text_from_filename) || config.out.mode == HLS { + let re_v = Regex::new(r"\[[0:]+[v^\[]+([:0]+)?\]").unwrap(); // match video filter input link + let _re_a = Regex::new(r"\[[0:]+[a^\[]+([:0]+)?\]").unwrap(); // match video filter input link + let mut cf = custom_filter.to_string(); - 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); + if !chain.video_chain.is_empty() { + cf = re_v + .replace(&cf, &format!("{},", chain.video_chain)) + .to_string() } - } - 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); - } + if !chain.audio_chain.is_empty() { + let audio_split = chain + .audio_chain + .split(';') + .enumerate() + .map(|(i, p)| p.replace(&format!("[aout{i}]"), "")) + .collect::>(); + + for i in 0..config.processing.audio_tracks { + cf = cf.replace( + &format!("[0:a:{i}]"), + &format!("{},", &audio_split[i as usize]), + ) + } + } + + cf + } else { + custom_filter.to_string() + }; + + chain.output_chain = vec_strings!["-filter_complex", filter] +} + +fn custom(filter: &str, chain: &mut Filters, nr: i32, filter_type: FilterType) { + if !filter.is_empty() { + chain.add_filter(filter, nr, filter_type); } - - // 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( @@ -584,9 +499,9 @@ pub fn filter_chains( 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) + process_output_filters(config, &mut filters, &f) } else if config.out.output_count > 1 { - split_filter(&mut filters, config.out.output_count, 0, Video) + split_filter(&mut filters, config.out.output_count, 0, Video); } return filters; @@ -624,6 +539,12 @@ pub fn filter_chains( overlay(node, &mut filters, config); realtime(node, &mut filters, config); + let (proc_vf, proc_af) = custom::filter_node(&config.processing.custom_filter); + let (list_vf, list_af) = custom::filter_node(&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 @@ -648,15 +569,16 @@ 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); + if config.out.mode == HLS { + if let Some(f) = config.out.output_filter.clone() { + process_output_filters(config, &mut filters, &f) + } + } filters } diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index daa3217a..99af83ed 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -63,7 +63,7 @@ fn check_media( enc_cmd.append(&mut node.cmd.unwrap_or_default()); enc_cmd.append(&mut filter.cmd()); - enc_cmd.append(&mut filter.map(None)); + enc_cmd.append(&mut filter.map()); enc_cmd.append(&mut vec_strings!["-t", "0.1", "-f", "null", "-"]); let mut enc_proc = match Command::new("ffmpeg") diff --git a/tests/src/engine_cmd.rs b/tests/src/engine_cmd.rs index 6ac03ff3..bfa5ed03 100644 --- a/tests/src/engine_cmd.rs +++ b/tests/src/engine_cmd.rs @@ -30,7 +30,7 @@ fn video_audio_input() { Some(vec_strings!["-i", "./assets/with_audio.mp4"]) ); assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); - assert_eq!(media.filter.unwrap().map(None), test_filter_map); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -38,7 +38,7 @@ 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(); + config.processing.custom_filter = "[0:v]gblur=2[c_v_out];[0:a]volume=0.2[c_a_out]".to_string(); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); let media = gen_source(&config, media_obj, &None); @@ -55,7 +55,7 @@ fn video_audio_custom_filter1_input() { Some(vec_strings!["-i", "./assets/with_audio.mp4"]) ); assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); - assert_eq!(media.filter.unwrap().map(None), test_filter_map); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -64,7 +64,8 @@ fn video_audio_custom_filter2_input() { 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(); + "[0:v]null[v];movie=logo.png[l];[v][l]overlay[c_v_out];[0:a]volume=0.2[c_a_out]" + .to_string(); let media_obj = Media::new(0, "./assets/with_audio.mp4", true); let media = gen_source(&config, media_obj, &None); @@ -81,7 +82,7 @@ fn video_audio_custom_filter2_input() { Some(vec_strings!["-i", "./assets/with_audio.mp4"]) ); assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); - assert_eq!(media.filter.unwrap().map(None), test_filter_map); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -107,7 +108,7 @@ fn dual_audio_aevalsrc_input() { Some(vec_strings!["-i", "./assets/with_audio.mp4"]) ); assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); - assert_eq!(media.filter.unwrap().map(None), test_filter_map); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -132,7 +133,7 @@ fn dual_audio_input() { Some(vec_strings!["-i", "./assets/dual_audio.mp4"]) ); assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); - assert_eq!(media.filter.unwrap().map(None), test_filter_map); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -165,7 +166,7 @@ fn video_separate_audio_input() { ]) ); assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); - assert_eq!(media.filter.unwrap().map(None), test_filter_map); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -228,7 +229,7 @@ fn video_audio_stream() { } #[test] -fn video_audio_filter_stream() { +fn video_audio_filter1_stream() { let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); config.out.mode = Stream; config.processing.add_logo = false; @@ -279,7 +280,7 @@ fn video_audio_filter_stream() { "-i", "pipe:0", "-filter_complex", - "[0:v:0]gblur=2[vout0];[0:a:0]volume=0.2[aout0]", + "[0:v]gblur=2[vout0];[0:a]volume=0.2[aout0]", "-map", "[vout0]", "-map", @@ -362,7 +363,7 @@ fn video_audio_filter2_stream() { "-i", "pipe:0", "-filter_complex", - format!("[0:v:0]zmq=b=tcp\\\\://'{socket}',drawtext@dyntext=text='',gblur=2[vout0];[0:a:0]volume=0.2[aout0]"), + format!("[0:v:0]zmq=b=tcp\\\\://'{socket}',drawtext@dyntext=text='',gblur=2[vout0];[0:a]volume=0.2[aout0]"), "-map", "[vout0]", "-map", @@ -398,7 +399,9 @@ fn video_audio_filter3_stream() { ); config.out.output_cmd = Some(vec_strings![ "-map", - "[v_out0]", + "[vout0]", + "-map", + "0:a", "-c:v", "libx264", "-c:a", @@ -450,7 +453,7 @@ fn video_audio_filter3_stream() { "-map", "[vout0]", "-map", - "0:a:0", + "0:a", "-c:v", "libx264", "-c:a", @@ -909,6 +912,119 @@ fn video_dual_audio_multi_stream() { assert_eq!(enc_cmd, test_cmd); } +#[test] +fn video_audio_text_multi_stream() { + let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + config.out.mode = Stream; + config.processing.add_logo = false; + config.text.add_text = true; + config.text.fontfile = String::new(); + config.out.output_count = 2; + config.out.output_cmd = Some(vec_strings![ + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "mpegts", + "srt://127.0.0.1:40051", + "-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_dual_audio_multi_filter_stream() { let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); @@ -1044,6 +1160,7 @@ fn video_audio_text_filter_stream() { config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 1; + config.text.add_text = true; config.text.fontfile = String::new(); config.out.output_count = 2; config.out.output_cmd = Some(vec_strings![ @@ -1497,7 +1614,7 @@ fn multi_video_audio_hls() { "-i", "./assets/with_audio.mp4", "-filter_complex", - "[0:v:0]scale=1024:576,realtime=speed=1[vout0];[0:a:0]anull[aout0];[vout0]split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[aout0]asplit=2[a1][a2]", + "[0:v:0]scale=1024:576,realtime=speed=1,split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[0:a]asplit=2[a1][a2]", "-map", "[v1_out]", "-map", @@ -1613,7 +1730,7 @@ fn multi_video_multi_audio_hls() { "-i", "./assets/dual_audio.mp4", "-filter_complex", - "[0:v:0]scale=1024:576,realtime=speed=1[vout0];[0:a:0]anull[aout0];[0:a:1]anull[aout1];[vout0]split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[aout0]asplit=2[a_0_1][a_0_2];[aout1]asplit=2[a_1_1][a_1_2]", + "[0:v:0]scale=1024:576,realtime=speed=1,split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[0:a:0]anull,asplit=2[a_0_1][a_0_2];[0:a:1]anull,asplit=2[a_1_1][a_1_2]", "-map", "[v1_out]", "-map",