validate next clip in a thread, to have less delta

This commit is contained in:
jb-alvarado 2023-11-30 15:42:20 +01:00
parent 94e02ac367
commit c6329c470a
5 changed files with 142 additions and 123 deletions

View File

@ -138,18 +138,22 @@ pub async fn browser(
} else if path.is_file() && !path_obj.folders_only { } else if path.is_file() && !path_obj.folders_only {
if let Some(ext) = file_extension(&path) { if let Some(ext) = file_extension(&path) {
if extensions.contains(&ext.to_string().to_lowercase()) { if extensions.contains(&ext.to_string().to_lowercase()) {
let media = MediaProbe::new(&path.display().to_string()); match MediaProbe::new(&path.display().to_string()) {
let mut duration = 0.0; Ok(probe) => {
let mut duration = 0.0;
if let Some(dur) = media.format.and_then(|f| f.duration) { if let Some(dur) = probe.format.duration {
duration = dur.parse().unwrap_or(0.0) duration = dur.parse().unwrap_or_default()
} }
let video = VideoFile { let video = VideoFile {
name: path.file_name().unwrap().to_string_lossy().to_string(), name: path.file_name().unwrap().to_string_lossy().to_string(),
duration, duration,
};
files.push(video);
}
Err(e) => error!("{e:?}"),
}; };
files.push(video);
} }
} }
} }

View File

@ -5,6 +5,7 @@ use std::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Mutex, Arc, Mutex,
}, },
thread,
}; };
use serde_json::json; use serde_json::json;
@ -13,7 +14,7 @@ use simplelog::*;
use ffplayout_lib::utils::{ use ffplayout_lib::utils::{
controller::PlayerControl, gen_dummy, get_delta, get_sec, is_close, is_remote, 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, 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. /// Struct for current playlist.
@ -579,9 +580,17 @@ pub fn gen_source(
trace!("Clip out: {duration}, duration: {}", node.duration); trace!("Clip out: {duration}, duration: {}", node.duration);
if valid_source(&node.source) { let node_begin = node.begin;
node.add_probe(true); 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 if node
.source .source
.rsplit_once('.') .rsplit_once('.')
@ -655,45 +664,50 @@ pub fn gen_source(
item.index = Some(i); 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::<f64>().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 { } else {
// Create colored placeholder. match MediaProbe::new(&config.storage.filler.to_string_lossy()) {
let (source, cmd) = gen_dummy(config, duration); Ok(probe) => {
node.source = source; if config
node.cmd = Some(cmd); .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::<f64>().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!( warn!(

View File

@ -1,15 +1,18 @@
use std::io; use std::io;
use derive_more::Display; use derive_more::Display;
use ffprobe::FfProbeError;
#[derive(Debug, Display)] #[derive(Debug, Display)]
pub enum ProcError { pub enum ProcError {
#[display(fmt = "Failed to spawn ffmpeg/ffprobe. {}", _0)] #[display(fmt = "Failed to spawn ffmpeg/ffprobe. {}", _0)]
CommandSpawn(io::Error), CommandSpawn(io::Error),
#[display(fmt = "Failed to read data from ffmpeg/ffprobe. {}", _0)] #[display(fmt = "IO Error {}", _0)]
IO(io::Error), IO(io::Error),
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Custom(String), Custom(String),
#[display(fmt = "{}", _0)]
Ffprobe(FfProbeError),
#[display(fmt = "Regex compile error {}", _0)] #[display(fmt = "Regex compile error {}", _0)]
Regex(String), Regex(String),
#[display(fmt = "Thread error {}", _0)] #[display(fmt = "Thread error {}", _0)]
@ -22,6 +25,12 @@ impl From<std::io::Error> for ProcError {
} }
} }
impl From<FfProbeError> for ProcError {
fn from(err: FfProbeError) -> Self {
Self::Ffprobe(err)
}
}
impl From<regex::Error> for ProcError { impl From<regex::Error> for ProcError {
fn from(err: regex::Error) -> Self { fn from(err: regex::Error) -> Self {
Self::Regex(err.to_string()) Self::Regex(err.to_string())

View File

@ -13,8 +13,8 @@ use simplelog::*;
use crate::filter::FilterType::Audio; use crate::filter::FilterType::Audio;
use crate::utils::{ use crate::utils::{
errors::ProcError, loop_image, sec_to_time, seek_and_length, valid_source, vec_strings, errors::ProcError, loop_image, sec_to_time, seek_and_length, vec_strings, JsonPlaylist, Media,
JsonPlaylist, Media, OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT, OutputMode::Null, PlayoutConfig, FFMPEG_IGNORE_ERRORS, IMAGE_FORMAT,
}; };
/// Validate a single media file. /// Validate a single media file.
@ -44,16 +44,6 @@ fn check_media(
enc_cmd.append(&mut vec_strings!["-ss", seek]); 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 <yellow>{pos}</> {}, from file <b><magenta>\"{}\"</></b>",
sec_to_time(begin),
node.source
)));
}
// Take care, that no seek and length command is added. // Take care, that no seek and length command is added.
node.seek = 0.0; node.seek = 0.0;
node.out = node.duration; 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. /// This function we run in a thread, to don't block the main function.
pub fn validate_playlist( pub fn validate_playlist(
playlist: JsonPlaylist, mut playlist: JsonPlaylist,
is_terminated: Arc<AtomicBool>, is_terminated: Arc<AtomicBool>,
mut config: PlayoutConfig, mut config: PlayoutConfig,
) { ) {
@ -173,14 +163,16 @@ pub fn validate_playlist(
debug!("Validate playlist from: <yellow>{date}</>"); debug!("Validate playlist from: <yellow>{date}</>");
let timer = Instant::now(); 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) { if is_terminated.load(Ordering::SeqCst) {
return; return;
} }
let pos = index + 1; 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) { if let Err(e) = check_media(item.clone(), pos, begin, &config) {
error!("{e}"); error!("{e}");
} else if config.general.validate { } else if config.general.validate {
@ -189,10 +181,10 @@ pub fn validate_playlist(
sec_to_time(begin), sec_to_time(begin),
item.source item.source
) )
}; }
} else { } else {
error!( error!(
"Source on position <yellow>{pos:0>3}</> {} not exists: <b><magenta>{}</></b>", "Error on position <yellow>{pos:0>3}</> <b><magenta>{}</></b>, file: {}",
sec_to_time(begin), sec_to_time(begin),
item.source item.source
); );

View File

@ -13,7 +13,7 @@ use std::{
use std::env; use std::env;
use chrono::{prelude::*, Duration}; use chrono::{prelude::*, Duration};
use ffprobe::{ffprobe, Format, Stream as FFStream}; use ffprobe::{ffprobe, Stream as FFStream};
use rand::prelude::*; use rand::prelude::*;
use regex::Regex; use regex::Regex;
use reqwest::header; use reqwest::header;
@ -45,6 +45,7 @@ pub use controller::{
PlayerControl, PlayoutStatus, ProcessControl, PlayerControl, PlayoutStatus, ProcessControl,
ProcessUnit::{self, *}, ProcessUnit::{self, *},
}; };
use errors::ProcError;
pub use generator::generate_playlist; pub use generator::generate_playlist;
pub use json_serializer::{read_json, JsonPlaylist}; pub use json_serializer::{read_json, JsonPlaylist};
pub use json_validate::validate_playlist; pub use json_validate::validate_playlist;
@ -121,14 +122,18 @@ impl Media {
let mut probe = None; let mut probe = None;
if do_probe && (is_remote(src) || Path::new(src).is_file()) { 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 duration = p
.as_ref() .format
.and_then(|p| p.format.as_ref()) .duration
.and_then(|f| f.duration.as_ref()) .unwrap_or_default()
{ .parse()
duration = dur.parse().unwrap() .unwrap_or_default();
}
Err(e) => error!("{e:?}"),
} }
} }
@ -156,32 +161,40 @@ impl Media {
pub fn add_probe(&mut self, check_audio: bool) { pub fn add_probe(&mut self, check_audio: bool) {
if self.probe.is_none() { if self.probe.is_none() {
let probe = MediaProbe::new(&self.source); match MediaProbe::new(&self.source) {
self.probe = Some(probe.clone()); Ok(probe) => {
self.probe = Some(probe.clone());
if let Some(dur) = probe if let Some(dur) = probe
.format .format
.and_then(|f| f.duration) .duration
.map(|d| d.parse().unwrap()) .map(|d| d.parse().unwrap_or_default())
.filter(|d| !is_close(*d, self.duration, 0.5)) .filter(|d| !is_close(*d, self.duration, 0.5))
{ {
self.duration = dur; self.duration = dur;
if self.out == 0.0 { if self.out == 0.0 {
self.out = dur; self.out = dur;
}
}
} }
} Err(e) => error!("{e:?}"),
};
if check_audio && Path::new(&self.audio).is_file() { if check_audio && Path::new(&self.audio).is_file() {
let probe_audio = MediaProbe::new(&self.audio); match MediaProbe::new(&self.audio) {
self.probe_audio = Some(probe_audio.clone()); Ok(probe) => {
self.probe_audio = Some(probe.clone());
if !probe_audio.audio_streams.is_empty() { if !probe.audio_streams.is_empty() {
self.duration_audio = probe_audio.audio_streams[0] self.duration_audio = probe.audio_streams[0]
.duration .duration
.clone() .clone()
.and_then(|d| d.parse::<f64>().ok()) .and_then(|d| d.parse::<f64>().ok())
.unwrap_or_default() .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. /// We use the ffprobe crate, but we map the metadata to our needs.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct MediaProbe { pub struct MediaProbe {
pub format: Option<Format>, pub format: ffprobe::Format,
pub audio_streams: Vec<FFStream>, pub audio_streams: Vec<FFStream>,
pub video_streams: Vec<FFStream>, pub video_streams: Vec<FFStream>,
} }
impl MediaProbe { impl MediaProbe {
pub fn new(input: &str) -> Self { pub fn new(input: &str) -> Result<Self, ProcError> {
let probe = ffprobe(input); let probe = ffprobe(input);
let mut a_stream = vec![]; let mut a_stream = vec![];
let mut v_stream = vec![]; let mut v_stream = vec![];
@ -253,27 +266,13 @@ impl MediaProbe {
} }
} }
MediaProbe { Ok(MediaProbe {
format: Some(obj.format), format: obj.format,
audio_streams: a_stream, audio_streams: a_stream,
video_streams: v_stream, video_streams: v_stream,
} })
}
Err(e) => {
if Path::new(input).is_file() {
error!(
"Can't read source <b><magenta>{input}</></b> with ffprobe! Error: {e:?}"
);
} else if !input.is_empty() {
error!("File not exists: <b><magenta>{input}</></b>");
}
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) 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. /// Check if next input is valid, and probe it.
pub fn valid_source(source: &str) -> bool { pub fn validate_next(player_control: PlayerControl, node_begin: Option<f64>) {
if is_remote(source) && !MediaProbe::new(source).video_streams.is_empty() { let mut list = player_control.current_list.lock().unwrap();
return true; 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. /// Check if file can include or has to exclude.