diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 844ddd8c..66f83c07 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -1,10 +1,237 @@ -use crate::utils::MediaProbe; +use serde::{Deserialize, Serialize}; +use std::path::Path; -fn deinterlace(probe: MediaProbe) -> String { - if probe.video_streams.unwrap()[0].field_order.is_some() - && probe.video_streams.unwrap()[0].field_order.unwrap() != "progressive".to_string() { - "yadif=0:-1:0" +use crate::utils::{is_close, Config, Program}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Filters { + pub audio_chain: Option, + pub video_chain: Option, + pub audio_map: Option, + pub video_map: Option, +} + +impl Filters { + pub fn new() -> Self { + Filters { + audio_chain: None, + video_chain: None, + audio_map: Some("0:a".to_string()), + video_map: Some("0:v".to_string()), + } } - "" + fn add_filter(&mut self, filter: String, codec_type: String) { + match codec_type.as_str() { + "audio" => match &self.audio_chain { + Some(ac) => { + if filter.starts_with(";") || filter.starts_with("[") { + self.audio_chain = Some(format!("{}{}", ac, filter)) + } else { + self.audio_chain = Some(format!("{},{}", ac, filter)) + } + } + None => { + self.audio_chain = Some(filter); + self.audio_map = Some("[aout1]".to_string()); + } + }, + "video" => match &self.video_chain { + Some(vc) => { + if filter.starts_with(";") || filter.starts_with("[") { + self.video_chain = Some(format!("{}{}", vc, filter)) + } else { + self.video_chain = Some(format!("{},{}", vc, filter)) + } + } + None => { + self.video_chain = Some(filter); + self.video_map = Some("[vout1]".to_string()); + } + }, + _ => (), + } + } +} + +fn deinterlace(field_order: Option, chain: &mut Filters) { + if field_order.is_some() && field_order.unwrap() != "progressive".to_string() { + chain.add_filter("yadif=0:-1:0".into(), "video".into()) + } +} + +fn pad(aspect: f64, chain: &mut Filters, config: &Config) { + if is_close(aspect, config.processing.aspect, 0.03) { + if aspect < config.processing.aspect { + chain.add_filter( + format!( + "pad=ih*{}/{}/sar:ih:(ow-iw)/2:(oh-ih)/2", + config.processing.width, config.processing.height + ) + .into(), + "video".into(), + ) + } else if aspect > config.processing.aspect { + chain.add_filter( + format!( + "pad=iw:iw*{}/{}/sar:(ow-iw)/2:(oh-ih)/2", + config.processing.width, config.processing.height + ) + .into(), + "video".into(), + ) + } + } +} + +fn fps(fps: f64, chain: &mut Filters, config: &Config) { + if fps != config.processing.fps { + chain.add_filter( + format!("fps={}", config.processing.fps).into(), + "video".into(), + ) + } +} + +fn scale(width: i64, height: i64, aspect: f64, chain: &mut Filters, config: &Config) { + if width != config.processing.width || height != config.processing.height { + chain.add_filter( + format!( + "scale={}:{}", + config.processing.width, config.processing.height + ) + .into(), + "video".into(), + ) + } + + if is_close(aspect, config.processing.aspect, 0.03) { + chain.add_filter( + format!("setdar=dar={}", config.processing.aspect).into(), + "video".into(), + ) + } +} + +fn fade(node: &mut Program, chain: &mut Filters, codec_type: String) { + let mut t = "".to_string(); + + if codec_type == "audio".to_string() { + t = "a".to_string() + } + + if node.seek > 0.0 { + chain.add_filter(format!("{t}fade=in:st=0:d=0.5"), codec_type.clone()) + } + + if node.out != node.duration && node.out - node.seek - 1.0 > 0.0 { + chain.add_filter( + format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0)).into(), + codec_type, + ) + } +} + +fn overlay(node: &mut Program, chain: &mut Filters, config: &Config, last_ad: bool, next_ad: bool) { + if config.processing.add_logo + && Path::new(&config.processing.logo).is_file() + && node.category != "advertisement".to_string() + { + let opacity = format!( + "format=rgba,colorchannelmixer=aa={}", + config.processing.logo_opacity + ); + let logo_loop = "loop=loop=-1:size=1:start=0"; + let mut logo_chain = format!("[v];movie={},{logo_loop},{opacity}", config.processing.logo); + + if last_ad { + logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1") + } + + if next_ad { + logo_chain.push_str( + format!(",fade=out:st={}:d=1.0:alpha=1", node.out - node.seek - 1.0).as_str(), + ) + } + + logo_chain + .push_str(format!("[l];[v][l]{}:shortest=1", config.processing.logo_filter).as_str()); + + chain.add_filter(logo_chain, "video".into()); + } +} + +pub fn filter_chains(node: &mut Program, config: &Config, last: bool, next: bool) -> Vec { + let mut filters = Filters::new(); + let probe = node.probe.clone(); + + match probe { + Some(p) => { + // let a_stream = &p.audio_streams.unwrap()[0]; + let v_stream = &p.video_streams.unwrap()[0]; + let aspect_string = v_stream.display_aspect_ratio.clone().unwrap(); + let aspect_vec: Vec<&str> = aspect_string.split(':').collect(); + let w: f64 = aspect_vec[0].parse().unwrap(); + let h: f64 = aspect_vec[1].parse().unwrap(); + let source_aspect: f64 = w as f64 / h as f64; + let frame_rate_vec: Vec<&str> = v_stream.r_frame_rate.split('/').collect(); + let rate: f64 = frame_rate_vec[0].parse().unwrap(); + let factor: f64 = frame_rate_vec[1].parse().unwrap(); + let frames_per_second: f64 = rate / factor; + + deinterlace(v_stream.field_order.clone(), &mut filters); + pad(source_aspect, &mut filters, &config); + fps(frames_per_second, &mut filters, &config); + scale( + v_stream.width.unwrap(), + v_stream.height.unwrap(), + source_aspect, + &mut filters, + &config, + ); + fade(node, &mut filters, "audio".into()); + fade(node, &mut filters, "video".into()); + overlay(node, &mut filters, &config, last, next); + } + None => { + println!("Clip has no media probe object. No filter applied!") + } + } + + let mut filter_cmd = vec!["-filter_complex".to_string()]; + let mut filter_str: String = "".to_string(); + let mut filter_map: Vec = vec![]; + + if filters.audio_chain.is_some() { + filter_str.push_str( + format!( + "[0:a]{}{};", + filters.audio_chain.unwrap(), + filters.audio_map.clone().unwrap() + ) + .as_str(), + ); + filter_map.append(&mut vec!["-map".to_string(), filters.audio_map.unwrap()]); + } else { + filter_map.append(&mut vec!["-map".to_string(), "0:a".to_string()]); + } + + if filters.video_chain.is_some() { + filter_str.push_str( + format!( + "[0:v]{}{}", + filters.video_chain.unwrap(), + filters.video_map.clone().unwrap() + ) + .as_str(), + ); + filter_map.append(&mut vec!["-map".to_string(), filters.video_map.unwrap()]); + } else { + filter_map.append(&mut vec!["-map".to_string(), "0:v".to_string()]); + } + + filter_cmd.push(filter_str); + filter_cmd.append(&mut filter_map); + + filter_cmd } diff --git a/src/output/desktop.rs b/src/output/desktop.rs index 48534d38..32089ea1 100644 --- a/src/output/desktop.rs +++ b/src/output/desktop.rs @@ -4,11 +4,11 @@ use std::{ process::{Command, Stdio}, }; -use crate::utils::{program, sec_to_time}; +use crate::utils::{program, sec_to_time, Config}; -pub fn play(settings: Option>) -> io::Result<()> { - let get_source = program(); - let dec_settings = settings.unwrap(); +pub fn play(config: Config) -> io::Result<()> { + let get_source = program(config.clone()); + let dec_settings = config.processing.settings.unwrap(); let mut enc_proc = Command::new("ffplay") .args([ @@ -29,9 +29,10 @@ pub fn play(settings: Option>) -> io::Result<()> { if let Some(mut enc_input) = enc_proc.stdin.take() { for node in get_source { - println!("Play: {:#?}", node); + // println!("Play: {:#?}", node); println!("Node begin: {:?}", sec_to_time(node.begin.unwrap())); let cmd = node.cmd.unwrap(); + let filter = node.filter.unwrap(); let mut dec_cmd = vec![ "-v", @@ -41,6 +42,7 @@ pub fn play(settings: Option>) -> io::Result<()> { ]; dec_cmd.append(&mut cmd.iter().map(String::as_str).collect()); + dec_cmd.append(&mut filter.iter().map(String::as_str).collect()); dec_cmd.append(&mut dec_settings.iter().map(String::as_str).collect()); let mut dec_proc = Command::new("ffmpeg") diff --git a/src/utils/playlist.rs b/src/utils/playlist.rs index c4256407..358b629f 100644 --- a/src/utils/playlist.rs +++ b/src/utils/playlist.rs @@ -1,10 +1,12 @@ use crate::utils::{ - get_config, json_reader::{read_json, Program}, - modified_time, MediaProbe, + modified_time, Config, MediaProbe, }; +use crate::filter::filter_chains; + pub struct CurrentProgram { + config: Config, json_mod: String, json_path: String, nodes: Vec, @@ -12,11 +14,11 @@ pub struct CurrentProgram { } impl CurrentProgram { - fn new() -> Self { - let config = get_config(); + fn new(config: Config) -> Self { let json = read_json(&config, true); Self { + config: config, json_mod: json.modified.unwrap(), json_path: json.current_file.unwrap(), nodes: json.program.into(), @@ -25,12 +27,11 @@ impl CurrentProgram { } fn check_update(&mut self) { - let config = get_config(); let mod_time = modified_time(self.json_path.clone()); if !mod_time.unwrap().to_string().eq(&self.json_mod) { // when playlist has changed, reload it - let json = read_json(&config, true); + let json = read_json(&self.config, true); self.json_mod = json.modified.unwrap(); self.nodes = json.program.into(); @@ -40,6 +41,10 @@ impl CurrentProgram { fn append_probe(&mut self, node: &mut Program) { node.probe = Some(MediaProbe::new(node.source.clone())) } + + fn add_filter(&mut self, node: &mut Program, last: bool, next: bool) { + node.filter = Some(filter_chains(node, &self.config, last, next)); + } } impl Iterator for CurrentProgram { @@ -49,14 +54,32 @@ impl Iterator for CurrentProgram { if self.idx < self.nodes.len() { self.check_update(); let mut current = self.nodes[self.idx].clone(); + let mut last = false; + let mut next = false; + + if self.idx > 0 && self.nodes[self.idx - 1].category == "advertisement" { + last = true + } + self.idx += 1; + if self.idx <= self.nodes.len() - 1 && self.nodes[self.idx].category == "advertisement" { + next = true + } + self.append_probe(&mut current); + self.add_filter(&mut current, last, next); Some(current) } else { - let config = get_config(); - let json = read_json(&config, false); + let mut last = false; + let mut next = false; + + if self.nodes[self.idx - 1].category == "advertisement" { + last = true + } + + let json = read_json(&self.config, false); self.json_mod = json.modified.unwrap(); self.json_path = json.current_file.unwrap(); self.nodes = json.program.into(); @@ -64,13 +87,18 @@ impl Iterator for CurrentProgram { let mut current = self.nodes[0].clone(); + if self.nodes[self.idx].category == "advertisement" { + next = true + } + self.append_probe(&mut current); + self.add_filter(&mut current, last, next); Some(current) } } } -pub fn program() -> CurrentProgram { - CurrentProgram::new() +pub fn program(config: Config) -> CurrentProgram { + CurrentProgram::new(config) }