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 {
|
} 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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!(
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user