diff --git a/ffplayout-api/src/utils/files.rs b/ffplayout-api/src/utils/files.rs index 11e2fadf..3717b76f 100644 --- a/ffplayout-api/src/utils/files.rs +++ b/ffplayout-api/src/utils/files.rs @@ -138,18 +138,22 @@ pub async fn browser( } else if path.is_file() && !path_obj.folders_only { if let Some(ext) = file_extension(&path) { if extensions.contains(&ext.to_string().to_lowercase()) { - let media = MediaProbe::new(&path.display().to_string()); - let mut duration = 0.0; + match MediaProbe::new(&path.display().to_string()) { + Ok(probe) => { + let mut duration = 0.0; - if let Some(dur) = media.format.and_then(|f| f.duration) { - duration = dur.parse().unwrap_or(0.0) - } + if let Some(dur) = probe.format.duration { + duration = dur.parse().unwrap_or_default() + } - let video = VideoFile { - name: path.file_name().unwrap().to_string_lossy().to_string(), - duration, + let video = VideoFile { + name: path.file_name().unwrap().to_string_lossy().to_string(), + duration, + }; + files.push(video); + } + Err(e) => error!("{e:?}"), }; - files.push(video); } } } diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index 49aed131..be26277e 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -5,6 +5,7 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, + thread, }; use serde_json::json; @@ -13,7 +14,7 @@ use simplelog::*; use ffplayout_lib::utils::{ controller::PlayerControl, gen_dummy, get_delta, get_sec, is_close, is_remote, json_serializer::read_json, loop_filler, loop_image, modified_time, seek_and_length, - valid_source, Media, MediaProbe, PlayoutConfig, PlayoutStatus, DUMMY_LEN, IMAGE_FORMAT, + validate_next, Media, MediaProbe, PlayoutConfig, PlayoutStatus, DUMMY_LEN, IMAGE_FORMAT, }; /// Struct for current playlist. @@ -579,9 +580,17 @@ pub fn gen_source( trace!("Clip out: {duration}, duration: {}", node.duration); - if valid_source(&node.source) { - node.add_probe(true); + let node_begin = node.begin; + let player_ctl = player_control.clone(); + thread::spawn(move || validate_next(player_ctl, node_begin)); + + if node.probe.is_none() { + node.add_probe(true); + } + + // separate if condition, because of node.add_probe() in last condition + if node.probe.is_some() { if node .source .rsplit_once('.') @@ -655,45 +664,50 @@ pub fn gen_source( item.index = Some(i); } } - } else if filler_source.is_file() { - let probe = MediaProbe::new(&config.storage.filler.to_string_lossy()); - - if config - .storage - .filler - .to_string_lossy() - .to_string() - .rsplit_once('.') - .map(|(_, e)| e.to_lowercase()) - .filter(|c| IMAGE_FORMAT.contains(&c.as_str())) - .is_some() - { - node.source = config.storage.filler.clone().to_string_lossy().to_string(); - node.cmd = Some(loop_image(&node)); - node.probe = Some(probe); - } else if let Some(filler_duration) = probe - .clone() - .format - .and_then(|f| f.duration) - .and_then(|d| d.parse::().ok()) - { - // Create placeholder from config filler. - node.source = config.storage.filler.clone().to_string_lossy().to_string(); - node.out = duration; - node.duration = filler_duration; - node.cmd = Some(loop_filler(&node)); - node.probe = Some(probe); - } else { - // Create colored placeholder. - let (source, cmd) = gen_dummy(config, duration); - node.source = source; - node.cmd = Some(cmd); - } } else { - // Create colored placeholder. - let (source, cmd) = gen_dummy(config, duration); - node.source = source; - node.cmd = Some(cmd); + match MediaProbe::new(&config.storage.filler.to_string_lossy()) { + Ok(probe) => { + if config + .storage + .filler + .to_string_lossy() + .to_string() + .rsplit_once('.') + .map(|(_, e)| e.to_lowercase()) + .filter(|c| IMAGE_FORMAT.contains(&c.as_str())) + .is_some() + { + node.source = config.storage.filler.clone().to_string_lossy().to_string(); + node.cmd = Some(loop_image(&node)); + node.probe = Some(probe); + } else if let Some(filler_duration) = probe + .clone() + .format + .duration + .and_then(|d| d.parse::().ok()) + { + // Create placeholder from config filler. + node.source = config.storage.filler.clone().to_string_lossy().to_string(); + node.out = duration; + node.duration = filler_duration; + node.cmd = Some(loop_filler(&node)); + node.probe = Some(probe); + } else { + // Create colored placeholder. + let (source, cmd) = gen_dummy(config, duration); + node.source = source; + node.cmd = Some(cmd); + } + } + Err(e) => { + error!("{e:?}"); + + // Create colored placeholder. + let (source, cmd) = gen_dummy(config, duration); + node.source = source; + node.cmd = Some(cmd); + } + } } warn!( diff --git a/lib/src/utils/errors.rs b/lib/src/utils/errors.rs index 21723574..f0a4dccd 100644 --- a/lib/src/utils/errors.rs +++ b/lib/src/utils/errors.rs @@ -1,15 +1,18 @@ use std::io; use derive_more::Display; +use ffprobe::FfProbeError; #[derive(Debug, Display)] pub enum ProcError { #[display(fmt = "Failed to spawn ffmpeg/ffprobe. {}", _0)] CommandSpawn(io::Error), - #[display(fmt = "Failed to read data from ffmpeg/ffprobe. {}", _0)] + #[display(fmt = "IO Error {}", _0)] IO(io::Error), #[display(fmt = "{}", _0)] Custom(String), + #[display(fmt = "{}", _0)] + Ffprobe(FfProbeError), #[display(fmt = "Regex compile error {}", _0)] Regex(String), #[display(fmt = "Thread error {}", _0)] @@ -22,6 +25,12 @@ impl From for ProcError { } } +impl From for ProcError { + fn from(err: FfProbeError) -> Self { + Self::Ffprobe(err) + } +} + impl From for ProcError { fn from(err: regex::Error) -> Self { Self::Regex(err.to_string()) diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 000cce71..8b6ae667 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -13,8 +13,8 @@ use simplelog::*; use crate::filter::FilterType::Audio; use crate::utils::{ - errors::ProcError, loop_image, sec_to_time, seek_and_length, valid_source, vec_strings, - JsonPlaylist, Media, OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT, + errors::ProcError, loop_image, sec_to_time, seek_and_length, vec_strings, JsonPlaylist, Media, + OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT, }; /// Validate a single media file. @@ -44,16 +44,6 @@ fn check_media( enc_cmd.append(&mut vec_strings!["-ss", seek]); } - node.add_probe(false); - - if node.probe.clone().and_then(|p| p.format).is_none() { - return Err(ProcError::Custom(format!( - "No Metadata at position {pos} {}, from file \"{}\"", - sec_to_time(begin), - node.source - ))); - } - // Take care, that no seek and length command is added. node.seek = 0.0; node.out = node.duration; @@ -154,7 +144,7 @@ fn check_media( /// /// This function we run in a thread, to don't block the main function. pub fn validate_playlist( - playlist: JsonPlaylist, + mut playlist: JsonPlaylist, is_terminated: Arc, mut config: PlayoutConfig, ) { @@ -173,14 +163,16 @@ pub fn validate_playlist( debug!("Validate playlist from: {date}"); let timer = Instant::now(); - for (index, item) in playlist.program.iter().enumerate() { + for (index, item) in playlist.program.iter_mut().enumerate() { if is_terminated.load(Ordering::SeqCst) { return; } let pos = index + 1; - if valid_source(&item.source) { + item.add_probe(false); + + if item.probe.is_some() { if let Err(e) = check_media(item.clone(), pos, begin, &config) { error!("{e}"); } else if config.general.validate { @@ -189,10 +181,10 @@ pub fn validate_playlist( sec_to_time(begin), item.source ) - }; + } } else { error!( - "Source on position {pos:0>3} {} not exists: {}", + "Error on position {pos:0>3} {}, file: {}", sec_to_time(begin), item.source ); diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index da5997b6..fbfaa88a 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -13,7 +13,7 @@ use std::{ use std::env; use chrono::{prelude::*, Duration}; -use ffprobe::{ffprobe, Format, Stream as FFStream}; +use ffprobe::{ffprobe, Stream as FFStream}; use rand::prelude::*; use regex::Regex; use reqwest::header; @@ -45,6 +45,7 @@ pub use controller::{ PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::{self, *}, }; +use errors::ProcError; pub use generator::generate_playlist; pub use json_serializer::{read_json, JsonPlaylist}; pub use json_validate::validate_playlist; @@ -121,14 +122,18 @@ impl Media { let mut probe = None; if do_probe && (is_remote(src) || Path::new(src).is_file()) { - probe = Some(MediaProbe::new(src)); + match MediaProbe::new(src) { + Ok(p) => { + probe = Some(p.clone()); - if let Some(dur) = probe - .as_ref() - .and_then(|p| p.format.as_ref()) - .and_then(|f| f.duration.as_ref()) - { - duration = dur.parse().unwrap() + duration = p + .format + .duration + .unwrap_or_default() + .parse() + .unwrap_or_default(); + } + Err(e) => error!("{e:?}"), } } @@ -156,32 +161,40 @@ impl Media { pub fn add_probe(&mut self, check_audio: bool) { if self.probe.is_none() { - let probe = MediaProbe::new(&self.source); - self.probe = Some(probe.clone()); + match MediaProbe::new(&self.source) { + Ok(probe) => { + self.probe = Some(probe.clone()); - if let Some(dur) = probe - .format - .and_then(|f| f.duration) - .map(|d| d.parse().unwrap()) - .filter(|d| !is_close(*d, self.duration, 0.5)) - { - self.duration = dur; + if let Some(dur) = probe + .format + .duration + .map(|d| d.parse().unwrap_or_default()) + .filter(|d| !is_close(*d, self.duration, 0.5)) + { + self.duration = dur; - if self.out == 0.0 { - self.out = dur; + if self.out == 0.0 { + self.out = dur; + } + } } - } + Err(e) => error!("{e:?}"), + }; if check_audio && Path::new(&self.audio).is_file() { - let probe_audio = MediaProbe::new(&self.audio); - self.probe_audio = Some(probe_audio.clone()); + match MediaProbe::new(&self.audio) { + Ok(probe) => { + self.probe_audio = Some(probe.clone()); - if !probe_audio.audio_streams.is_empty() { - self.duration_audio = probe_audio.audio_streams[0] - .duration - .clone() - .and_then(|d| d.parse::().ok()) - .unwrap_or_default() + if !probe.audio_streams.is_empty() { + self.duration_audio = probe.audio_streams[0] + .duration + .clone() + .and_then(|d| d.parse::().ok()) + .unwrap_or_default() + } + } + Err(e) => error!("{e:?}"), } } } @@ -226,13 +239,13 @@ fn is_empty_string(st: &String) -> bool { /// We use the ffprobe crate, but we map the metadata to our needs. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct MediaProbe { - pub format: Option, + pub format: ffprobe::Format, pub audio_streams: Vec, pub video_streams: Vec, } impl MediaProbe { - pub fn new(input: &str) -> Self { + pub fn new(input: &str) -> Result { let probe = ffprobe(input); let mut a_stream = vec![]; let mut v_stream = vec![]; @@ -253,27 +266,13 @@ impl MediaProbe { } } - MediaProbe { - format: Some(obj.format), + Ok(MediaProbe { + format: obj.format, audio_streams: a_stream, video_streams: v_stream, - } - } - Err(e) => { - if Path::new(input).is_file() { - error!( - "Can't read source {input} with ffprobe! Error: {e:?}" - ); - } else if !input.is_empty() { - error!("File not exists: {input}"); - } - - MediaProbe { - format: None, - audio_streams: vec![], - video_streams: vec![], - } + }) } + Err(e) => Err(ProcError::Ffprobe(e)), } } } @@ -599,15 +598,16 @@ pub fn is_remote(path: &str) -> bool { Regex::new(r"^https?://.*").unwrap().is_match(path) } -/// Validate input +/// Validate next input /// -/// Check if input is a remote source, or from storage and see if it exists. -pub fn valid_source(source: &str) -> bool { - if is_remote(source) && !MediaProbe::new(source).video_streams.is_empty() { - return true; +/// Check if next input is valid, and probe it. +pub fn validate_next(player_control: PlayerControl, node_begin: Option) { + let mut list = player_control.current_list.lock().unwrap(); + if let Some(index) = list.iter().position(|r| r.begin == node_begin) { + if let Some(next_item) = list.get_mut(index + 1) { + next_item.add_probe(true) + } } - - Path::new(&source).is_file() } /// Check if file can include or has to exclude.