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 {
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);
}
}
}

View File

@ -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::<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 {
// 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::<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!(

View File

@ -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<std::io::Error> for ProcError {
}
}
impl From<FfProbeError> for ProcError {
fn from(err: FfProbeError) -> Self {
Self::Ffprobe(err)
}
}
impl From<regex::Error> for ProcError {
fn from(err: regex::Error) -> Self {
Self::Regex(err.to_string())

View File

@ -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 <yellow>{pos}</> {}, from file <b><magenta>\"{}\"</></b>",
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<AtomicBool>,
mut config: PlayoutConfig,
) {
@ -173,14 +163,16 @@ pub fn validate_playlist(
debug!("Validate playlist from: <yellow>{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 <yellow>{pos:0>3}</> {} not exists: <b><magenta>{}</></b>",
"Error on position <yellow>{pos:0>3}</> <b><magenta>{}</></b>, file: {}",
sec_to_time(begin),
item.source
);

View File

@ -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::<f64>().ok())
.unwrap_or_default()
if !probe.audio_streams.is_empty() {
self.duration_audio = probe.audio_streams[0]
.duration
.clone()
.and_then(|d| d.parse::<f64>().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<Format>,
pub format: ffprobe::Format,
pub audio_streams: Vec<FFStream>,
pub video_streams: Vec<FFStream>,
}
impl MediaProbe {
pub fn new(input: &str) -> Self {
pub fn new(input: &str) -> Result<Self, ProcError> {
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 <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)
}
/// 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<f64>) {
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.