validate next clip in a thread, to have less delta
This commit is contained in:
parent
94e02ac367
commit
c6329c470a
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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!(
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user