diff --git a/ffplayout-engine/src/input/ingest.rs b/ffplayout-engine/src/input/ingest.rs index 28e01f4e..168d7ed1 100644 --- a/ffplayout-engine/src/input/ingest.rs +++ b/ffplayout-engine/src/input/ingest.rs @@ -90,7 +90,8 @@ pub fn ingest_server( server_cmd.append(&mut stream_input.clone()); if let Some(mut filter) = dummy_media.filter { - server_cmd.append(&mut filter.cmd); + server_cmd.append(&mut filter.cmd()); + server_cmd.append(&mut filter.map()); } server_cmd.append(&mut config.processing.settings.unwrap()); diff --git a/ffplayout-engine/src/output/hls.rs b/ffplayout-engine/src/output/hls.rs index fead9b6e..ff4a8570 100644 --- a/ffplayout-engine/src/output/hls.rs +++ b/ffplayout-engine/src/output/hls.rs @@ -65,7 +65,7 @@ fn ingest_to_hls_server( loop { dummy_media.add_filter(&config, &playout_stat.chain); - let server_cmd = prepare_output_cmd(server_prefix.clone(), &dummy_media.filter, &config); + let server_cmd = prepare_output_cmd(&config, server_prefix.clone(), &dummy_media.filter); debug!( "Server CMD: \"ffmpeg {}\"", @@ -180,7 +180,7 @@ pub fn write_hls( let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format]; enc_prefix.append(&mut cmd); - let enc_cmd = prepare_output_cmd(enc_prefix, &node.filter, config); + let enc_cmd = prepare_output_cmd(config, enc_prefix, &node.filter); debug!( "HLS writer CMD: \"ffmpeg {}\"", diff --git a/ffplayout-engine/src/output/mod.rs b/ffplayout-engine/src/output/mod.rs index 08783a38..206d4091 100644 --- a/ffplayout-engine/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -103,7 +103,8 @@ pub fn player( dec_cmd.append(&mut cmd); if let Some(mut filter) = node.filter { - dec_cmd.append(&mut filter.cmd); + dec_cmd.append(&mut filter.cmd()); + dec_cmd.append(&mut filter.map()); } dec_cmd.append(&mut config.processing.clone().settings.unwrap()); diff --git a/ffplayout-engine/src/output/null.rs b/ffplayout-engine/src/output/null.rs index 9e4f6a0e..9ca04396 100644 --- a/ffplayout-engine/src/output/null.rs +++ b/ffplayout-engine/src/output/null.rs @@ -26,7 +26,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { "pipe:0" ]; - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, config); + let enc_cmd = prepare_output_cmd(config, enc_prefix, &media.filter); debug!( "Encoder CMD: \"ffmpeg {}\"", diff --git a/ffplayout-engine/src/output/stream.rs b/ffplayout-engine/src/output/stream.rs index 29769c67..b53016a6 100644 --- a/ffplayout-engine/src/output/stream.rs +++ b/ffplayout-engine/src/output/stream.rs @@ -26,7 +26,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { "pipe:0" ]; - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, config); + let enc_cmd = prepare_output_cmd(config, enc_prefix, &media.filter); debug!( "Encoder CMD: \"ffmpeg {}\"", diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index f0b72bf5..3aabf72f 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -9,8 +9,8 @@ pub mod arg_parse; pub use arg_parse::Args; use ffplayout_lib::{ - filter::Filters, - utils::{time_to_sec, OutputMode::*, PlayoutConfig, ProcessMode::*}, + filter::{FilterType::*, Filters}, + utils::{time_to_sec, PlayoutConfig, ProcessMode::*}, }; /// Read command line arguments, and override the config with them. @@ -87,19 +87,69 @@ 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(); + let re_a = Regex::new(r"(\[0:a(:[0-9]+)?\]|\[a[\w_-]+\])").unwrap(); + let nr = Regex::new(r"\[[\w:-_]+([0-9])\]").unwrap(); + + 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(); + let re_a = Regex::new(r"\[0:a(:(?P[0-9]+))?\]").unwrap(); + 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 +/// Seek for multiple outputs and add mapping for it. pub fn prepare_output_cmd( + config: &PlayoutConfig, mut cmd: Vec, filters: &Option, - config: &PlayoutConfig, ) -> Vec { let mut output_params = config.out.clone().output_cmd.unwrap(); - let params_len = output_params.len(); let mut new_params = vec![]; let mut output_filter = String::new(); - let re_map = Regex::new(r"(\[?[0-9]:[av](:[0-9]+)?\]?|-map|\[[a-z_0-9]+\])").unwrap(); + let re_map = Regex::new(r"(\[?[0-9]:[av](:[0-9]+)?\]?|-map$|\[[a-z_0-9]+\])").unwrap(); + let re_multi = Regex::new(r"\[[\w:_-]+\]\[[\w:_-]+\]").unwrap(); if let Some(mut filter) = filters.clone() { // Loop over output parameters @@ -110,49 +160,40 @@ pub fn prepare_output_cmd( if param != "-filter_complex" { if i > 0 && output_params[i - 1] == "-filter_complex" { output_filter = param.clone(); - } else if !output_filter.contains("split") { - if !re_map.is_match(param) { - // Skip mapping parameters, when no split filter is set + } 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()); } } else { new_params.push(param.clone()); } - } - // Check if parameter is a output - if i > 0 - && !param.starts_with('-') - && !output_params[i - 1].starts_with('-') - && i < params_len - 1 - { - // add mapping to following outputs - new_params.append(&mut filter.output_map.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 + new_params.append(&mut filter.map().clone()); + } } } - if filter.cmd.contains(&"-filter_complex".to_string()) { - output_params = new_params; - // Process A/V mapping - // - // when filter in output_param are found and we are in HLS mode, fix the mapping - if !output_filter.is_empty() && config.out.mode == HLS { - let re = Regex::new(r"0:a:(?P[0-9]+)").unwrap(); - output_filter = re - .replace_all(&output_filter, "aout${num}") - .to_string() - .replace("[0:a]", &filter.audio_map[0]) - .replace("[0:v]", &filter.video_map[0]) - .replace("[0:v:0]", &filter.video_map[0]); + output_params = new_params; - filter.cmd[1].push_str(&format!(";{output_filter}")); - filter.cmd.drain(2..); - cmd.append(&mut filter.cmd); - } else { - cmd.append(&mut filter.cmd); - } + 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 { - cmd.append(&mut filter.cmd); + process_filters(&mut filter, &output_filter); + cmd.append(&mut filter.cmd()); + cmd.append(&mut filter.map()); } } diff --git a/lib/src/filter/mod.rs b/lib/src/filter/mod.rs index c29b8839..4c39e07c 100644 --- a/lib/src/filter/mod.rs +++ b/lib/src/filter/mod.rs @@ -36,9 +36,8 @@ use FilterType::*; #[derive(Debug, Clone)] pub struct Filters { - audio_chain: String, - video_chain: String, - pub final_chain: String, + pub audio_chain: String, + pub video_chain: String, pub audio_map: Vec, pub video_map: Vec, pub output_map: Vec, @@ -47,7 +46,6 @@ pub struct Filters { video_position: i32, audio_last: i32, video_last: i32, - pub cmd: Vec, } impl Filters { @@ -55,7 +53,6 @@ impl Filters { Self { audio_chain: String::new(), video_chain: String::new(), - final_chain: String::new(), audio_map: vec![], video_map: vec![], output_map: vec![], @@ -64,7 +61,6 @@ impl Filters { video_position: 0, audio_last: -1, video_last: -1, - cmd: vec![], } } @@ -115,43 +111,56 @@ impl Filters { } } - fn close_chains(&mut self) { - // add final output selector + pub fn cmd(&mut self) -> Vec { + let mut v_chain = self.video_chain.clone(); + let mut a_chain = self.audio_chain.clone(); + if self.video_last >= 0 { - self.video_chain - .push_str(&format!("[vout{}]", self.video_last)); - } else { - self.output_map - .append(&mut vec!["-map".to_string(), "0:v".to_string()]); + v_chain.push_str(&format!("[vout{}]", self.video_last)); } if self.audio_last >= 0 { - self.audio_chain - .push_str(&format!("[aout{}]", self.audio_last)); - } else { - for i in 0..self.audio_track_count { - self.output_map.append(&mut vec![ - "-map".to_string(), - format!("{}:a:{i}", self.audio_position), - ]); - } + a_chain.push_str(&format!("[aout{}]", self.audio_last)); } + + let mut f_chain = v_chain; + let mut cmd = vec![]; + + if !a_chain.is_empty() { + f_chain.push(';'); + f_chain.push_str(&a_chain); + } + + if !f_chain.is_empty() { + cmd.push("-filter_complex".to_string()); + cmd.push(f_chain); + } + + cmd } - fn build_final_chain(&mut self) { - self.final_chain.push_str(&self.video_chain); + pub fn map(&mut self) -> Vec { + let mut o_map = self.output_map.clone(); - if !self.audio_chain.is_empty() { - self.final_chain.push(';'); - self.final_chain.push_str(&self.audio_chain); + if self.video_last == -1 { + let v_map = "0:v".to_string(); + + if !o_map.contains(&v_map) { + o_map.append(&mut vec!["-map".to_string(), v_map]); + }; } - if !self.final_chain.is_empty() { - self.cmd.push("-filter_complex".to_string()); - self.cmd.push(self.final_chain.clone()); + if self.audio_last == -1 { + 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!["-map".to_string(), a_map]); + }; + } } - self.cmd.append(&mut self.output_map.clone()); + o_map } } @@ -421,9 +430,6 @@ pub fn filter_chains( if node.unit == Encoder { add_text(node, &mut filters, config, filter_chain); - filters.close_chains(); - filters.build_final_chain(); - return filters; } @@ -493,8 +499,5 @@ pub fn filter_chains( custom(&list_af, &mut filters, i, Audio); } - filters.close_chains(); - filters.build_final_chain(); - filters } diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index ab975cf4..99af83ed 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -57,12 +57,13 @@ fn check_media( let mut filter = node.filter.unwrap_or_default(); - if filter.cmd.len() > 1 { - filter.cmd[1] = filter.cmd[1].replace("realtime=speed=1", "null") + if filter.cmd().len() > 1 { + filter.cmd()[1] = filter.cmd()[1].replace("realtime=speed=1", "null") } enc_cmd.append(&mut node.cmd.unwrap_or_default()); - enc_cmd.append(&mut filter.cmd); + enc_cmd.append(&mut filter.cmd()); + 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 8cde7d66..1fda7d7b 100644 --- a/tests/src/engine_cmd.rs +++ b/tests/src/engine_cmd.rs @@ -20,18 +20,17 @@ fn video_audio_input() { let test_filter_cmd = vec_strings![ "-filter_complex", - format!("[0:v:0]scale=1024:576,null[v];movie={}:loop=0,setpts=N/(FRAME_RATE*TB),format=rgba,colorchannelmixer=aa=0.7[l];[v][l]overlay=W-w-12:12:shortest=1[vout0];[0:a:0]anull[aout0]", config.processing.logo), - "-map", - "[vout0]", - "-map", - "[aout0]" + format!("[0:v:0]scale=1024:576,null[v];movie={}:loop=0,setpts=N/(FRAME_RATE*TB),format=rgba,colorchannelmixer=aa=0.7[l];[v][l]overlay=W-w-12:12:shortest=1[vout0];[0:a:0]anull[aout0]", config.processing.logo) ]; + 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.unwrap().cmd, test_filter_cmd); + assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -47,20 +46,17 @@ fn dual_audio_aevalsrc_input() { let test_filter_cmd = vec_strings![ "-filter_complex", - "[0:v:0]scale=1024:576[vout0];[0:a:0]anull[aout0];aevalsrc=0:channel_layout=stereo:duration=30:sample_rate=48000,anull[aout1]", - "-map", - "[vout0]", - "-map", - "[aout0]", - "-map", - "[aout1]" + "[0:v:0]scale=1024:576[vout0];[0:a:0]anull[aout0];aevalsrc=0:channel_layout=stereo:duration=30:sample_rate=48000,anull[aout1]" ]; + let test_filter_map = vec_strings!["-map", "[vout0]", "-map", "[aout0]", "-map", "[aout1]"]; + assert_eq!( media.cmd, Some(vec_strings!["-i", "./assets/with_audio.mp4"]) ); - assert_eq!(media.filter.unwrap().cmd, test_filter_cmd); + assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -75,20 +71,17 @@ fn dual_audio_input() { let test_filter_cmd = vec_strings![ "-filter_complex", - "[0:v:0]scale=1024:576[vout0];[0:a:0]anull[aout0];[0:a:1]anull[aout1]", - "-map", - "[vout0]", - "-map", - "[aout0]", - "-map", - "[aout1]" + "[0:v:0]scale=1024:576[vout0];[0:a:0]anull[aout0];[0:a:1]anull[aout1]" ]; + let test_filter_map = vec_strings!["-map", "[vout0]", "-map", "[aout0]", "-map", "[aout1]"]; + assert_eq!( media.cmd, Some(vec_strings!["-i", "./assets/dual_audio.mp4"]) ); - assert_eq!(media.filter.unwrap().cmd, test_filter_cmd); + assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -104,13 +97,11 @@ fn video_separate_audio_input() { let test_filter_cmd = vec_strings![ "-filter_complex", - "[0:v:0]scale=1024:576[vout0];[1:a:0]anull[aout0]", - "-map", - "[vout0]", - "-map", - "[aout0]" + "[0:v:0]scale=1024:576[vout0];[1:a:0]anull[aout0]" ]; + let test_filter_map = vec_strings!["-map", "[vout0]", "-map", "[aout0]"]; + assert_eq!( media.cmd, Some(vec_strings![ @@ -122,7 +113,8 @@ fn video_separate_audio_input() { "30" ]) ); - assert_eq!(media.filter.unwrap().cmd, test_filter_cmd); + assert_eq!(media.filter.clone().unwrap().cmd(), test_filter_cmd); + assert_eq!(media.filter.unwrap().map(), test_filter_map); } #[test] @@ -146,8 +138,75 @@ fn video_audio_stream() { "rtmp://localhost/live/stream" ]); - let mut enc_cmd = vec![]; - let mut output_cmd = config.out.output_cmd.as_ref().unwrap().clone(); + let enc_prefix = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-re", + "-i", + "pipe:0" + ]; + + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &None); + + let test_cmd = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-re", + "-i", + "pipe:0", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]; + + assert_eq!(enc_cmd, test_cmd); +} + +#[test] +fn video_audio_filter_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 = false; + config.out.output_cmd = Some(vec_strings![ + "-filter_complex", + "[0:v]gblur=2[vout0];[0:a]volume=0.2[aout0]", + "-map", + "[vout0]", + "-map", + "[aout0]", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]); + + let mut media = Media::new(0, "", false); + media.unit = Encoder; + media.add_filter(&config, &None); let enc_prefix = vec_strings![ "-hide_banner", @@ -159,9 +218,7 @@ fn video_audio_stream() { "pipe:0" ]; - enc_cmd.append(&mut output_cmd); - - let enc_cmd = prepare_output_cmd(enc_prefix, &None, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner", @@ -171,6 +228,260 @@ fn video_audio_stream() { "-re", "-i", "pipe:0", + "-filter_complex", + "[0:v:0]gblur=2[vout0];[0:a:0]volume=0.2[aout0]", + "-map", + "[vout0]", + "-map", + "[aout0]", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]; + + assert_eq!(enc_cmd, test_cmd); +} + +#[test] +fn video_audio_filter2_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_cmd = Some(vec_strings![ + "-filter_complex", + "[0:v]gblur=2[vout0];[0:a]volume=0.2[aout0]", + "-map", + "[vout0]", + "-map", + "[aout0]", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]); + + let mut media = Media::new(0, "", false); + media.unit = Encoder; + media.add_filter(&config, &None); + + let socket = config + .text + .zmq_stream_socket + .clone() + .unwrap() + .replace(':', "\\:"); + + let enc_prefix = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-re", + "-i", + "pipe:0" + ]; + + 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='',gblur=2[vout0];[0:a:0]volume=0.2[aout0]"), + "-map", + "[vout0]", + "-map", + "[aout0]", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]; + + assert_eq!(enc_cmd, test_cmd); +} + +#[test] +fn video_audio_filter3_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_cmd = Some(vec_strings![ + "-filter_complex", + "[0:v]null[o];movie=/path/to/lower_third.png[l];[o][l]overlay=shortest=1[v_out0]", + "-map", + "[v_out0]", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]); + + let mut media = Media::new(0, "", false); + media.unit = Encoder; + media.add_filter(&config, &None); + + let socket = config + .text + .zmq_stream_socket + .clone() + .unwrap() + .replace(':', "\\:"); + + let enc_prefix = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-re", + "-i", + "pipe:0" + ]; + + 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=''[vout0];[vout0]null[o];movie=/path/to/lower_third.png[l];[o][l]overlay=shortest=1[v_out0]"), + "-map", + "[v_out0]", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]; + + assert_eq!(enc_cmd, test_cmd); +} + +#[test] +fn video_audio_filter4_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_cmd = Some(vec_strings![ + "-filter_complex", + "[0:v]null[o];movie=/path/to/lower_third.png[l];[o][l]overlay=shortest=1[v_out0];[0:a:0]volume=0.2[a_out0]", + "-map", + "[v_out0]", + "-map", + "[a_out0]", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+global_header", + "-f", + "flv", + "rtmp://localhost/live/stream" + ]); + + let mut media = Media::new(0, "", false); + media.unit = Encoder; + media.add_filter(&config, &None); + + let socket = config + .text + .zmq_stream_socket + .clone() + .unwrap() + .replace(':', "\\:"); + + let enc_prefix = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-re", + "-i", + "pipe:0" + ]; + + 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=''[vout0];[vout0]null[o];movie=/path/to/lower_third.png[l];[o][l]overlay=shortest=1[v_out0];[0:a:0]volume=0.2[a_out0]"), + "-map", + "[v_out0]", + "-map", + "[a_out0]", "-c:v", "libx264", "-c:a", @@ -216,10 +527,6 @@ fn video_dual_audio_stream() { media.unit = Encoder; media.add_filter(&config, &None); - let mut enc_cmd = vec![]; - - let mut output_cmd = config.out.output_cmd.as_ref().unwrap().clone(); - let enc_prefix = vec_strings![ "-hide_banner", "-nostats", @@ -230,9 +537,7 @@ fn video_dual_audio_stream() { "pipe:0" ]; - enc_cmd.append(&mut output_cmd); - - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner", @@ -289,7 +594,6 @@ fn video_dual_audio_filter_stream() { "srt://127.0.0.1:40051" ]); - let mut enc_cmd = vec![]; let enc_prefix = vec_strings![ "-hide_banner", "-nostats", @@ -311,11 +615,7 @@ fn video_dual_audio_filter_stream() { media.unit = Encoder; media.add_filter(&config, &None); - let mut output_cmd = config.out.output_cmd.as_ref().unwrap().clone(); - - enc_cmd.append(&mut output_cmd); - - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner", @@ -387,9 +687,6 @@ fn video_audio_multi_stream() { "rtmp://localhost:1936/live/stream" ]); - let mut enc_cmd = vec![]; - let mut output_cmd = config.out.output_cmd.as_ref().unwrap().clone(); - let enc_prefix = vec_strings![ "-hide_banner", "-nostats", @@ -400,9 +697,7 @@ fn video_audio_multi_stream() { "pipe:0" ]; - enc_cmd.append(&mut output_cmd); - - let enc_cmd = prepare_output_cmd(enc_prefix, &None, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &None); let test_cmd = vec_strings![ "-hide_banner", @@ -494,9 +789,6 @@ fn video_dual_audio_multi_stream() { "srt://127.0.0.1:40052" ]); - let mut enc_cmd = vec![]; - let mut output_cmd = config.out.output_cmd.as_ref().unwrap().clone(); - let enc_prefix = vec_strings![ "-hide_banner", "-nostats", @@ -507,9 +799,7 @@ fn video_dual_audio_multi_stream() { "pipe:0" ]; - enc_cmd.append(&mut output_cmd); - - let enc_cmd = prepare_output_cmd(enc_prefix, &None, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &None); let test_cmd = vec_strings![ "-hide_banner", @@ -614,7 +904,6 @@ fn video_dual_audio_multi_filter_stream() { "srt://127.0.0.1:40052" ]); - let mut enc_cmd = vec![]; let enc_prefix = vec_strings![ "-hide_banner", "-nostats", @@ -636,11 +925,7 @@ fn video_dual_audio_multi_filter_stream() { media.unit = Encoder; media.add_filter(&config, &None); - let mut output_cmd = config.out.output_cmd.as_ref().unwrap().clone(); - - enc_cmd.append(&mut output_cmd); - - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner", @@ -694,8 +979,6 @@ fn video_dual_audio_multi_filter_stream() { "srt://127.0.0.1:40052" ]; - // println!("{enc_cmd:?}"); - assert_eq!(enc_cmd, test_cmd); } @@ -742,7 +1025,7 @@ fn video_audio_hls() { "./assets/with_audio.mp4" ]; - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner", @@ -784,6 +1067,99 @@ fn video_audio_hls() { assert_eq!(enc_cmd, test_cmd); } +#[test] +fn video_audio_sub_meta_hls() { + let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + config.out.mode = HLS; + config.processing.add_logo = false; + config.text.add_text = false; + config.out.output_cmd = Some(vec_strings![ + "-map", + "0:s:0", + "-map_metadata", + "0", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+cgop", + "-f", + "hls", + "-hls_time", + "6", + "-hls_list_size", + "600", + "-hls_flags", + "append_list+delete_segments+omit_endlist", + "-hls_segment_filename", + "/usr/share/ffplayout/public/live/stream-%d.ts", + "/usr/share/ffplayout/public/live/stream.m3u8" + ]); + + let media_obj = Media::new(0, "./assets/with_audio.mp4", true); + let media = gen_source(&config, media_obj, &None); + + let enc_prefix = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-re", + "-i", + "./assets/with_audio.mp4" + ]; + + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); + + let test_cmd = vec_strings![ + "-hide_banner", + "-nostats", + "-v", + "level+error", + "-re", + "-i", + "./assets/with_audio.mp4", + "-filter_complex", + "[0:v:0]scale=1024:576,realtime=speed=1[vout0];[0:a:0]anull[aout0]", + "-map", + "[vout0]", + "-map", + "[aout0]", + "-map", + "0:s:0", + "-map_metadata", + "0", + "-c:v", + "libx264", + "-c:a", + "aac", + "-ar", + "44100", + "-b:a", + "128k", + "-flags", + "+cgop", + "-f", + "hls", + "-hls_time", + "6", + "-hls_list_size", + "600", + "-hls_flags", + "append_list+delete_segments+omit_endlist", + "-hls_segment_filename", + "/usr/share/ffplayout/public/live/stream-%d.ts", + "/usr/share/ffplayout/public/live/stream.m3u8" + ]; + + assert_eq!(enc_cmd, test_cmd); +} + #[test] fn video_multi_audio_hls() { let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); @@ -828,7 +1204,7 @@ fn video_multi_audio_hls() { "./assets/dual_audio.mp4" ]; - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner", @@ -931,7 +1307,7 @@ fn multi_video_audio_hls() { "./assets/with_audio.mp4" ]; - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner", @@ -1047,7 +1423,7 @@ fn multi_video_multi_audio_hls() { "./assets/dual_audio.mp4" ]; - let enc_cmd = prepare_output_cmd(enc_prefix, &media.filter, &config); + let enc_cmd = prepare_output_cmd(&config, enc_prefix, &media.filter); let test_cmd = vec_strings![ "-hide_banner",