support filler folder

This commit is contained in:
jb-alvarado 2023-07-23 21:25:35 +02:00
parent 52d44e7a2a
commit 98d1d5d606
16 changed files with 289 additions and 169 deletions

View File

@ -95,11 +95,11 @@ playlist:
infinit: false
storage:
help_text: Play ordered or randomly files from path. 'filler_clip' is for fill
the end to reach 24 hours, it will loop when is necessary. 'extensions' search
help_text: 'filler' is for playing instead of a missing file or fill the end to reach 24
hours, can be a file or folder, it will loop when is necessary. 'extensions' search
only files with this extension. Set 'shuffle' to 'true' to pick files randomly.
path: "/var/lib/ffplayout/tv-media"
filler_clip: "/var/lib/ffplayout/tv-media/filler/filler.mp4"
filler: "/var/lib/ffplayout/tv-media/filler/filler.mp4"
extensions:
- "mp4"
- "mkv"

View File

@ -17,7 +17,7 @@ use notify::{
use notify_debouncer_full::new_debouncer;
use simplelog::*;
use ffplayout_lib::utils::{include_file, Media, PlayoutConfig};
use ffplayout_lib::utils::{include_file_extension, Media, PlayoutConfig};
/// Create a watcher, which monitor file changes.
/// When a change is register, update the current file list.
@ -52,7 +52,7 @@ pub fn watchman(
Create(CreateKind::File) | Modify(ModifyKind::Name(RenameMode::To)) => {
let new_path = &event.paths[0];
if new_path.is_file() && include_file(config.clone(), new_path) {
if new_path.is_file() && include_file_extension(&config, new_path) {
let index = sources.lock().unwrap().len();
let media = Media::new(index, &new_path.to_string_lossy(), false);
@ -63,7 +63,7 @@ pub fn watchman(
Remove(RemoveKind::File) | Modify(ModifyKind::Name(RenameMode::From)) => {
let old_path = &event.paths[0];
if !old_path.is_file() && include_file(config.clone(), old_path) {
if !old_path.is_file() && include_file_extension(&config, old_path) {
sources
.lock()
.unwrap()
@ -83,7 +83,7 @@ pub fn watchman(
let media = Media::new(index, &new_path.to_string_lossy(), false);
media_list[index] = media;
info!("Move file: <b><magenta>{old_path:?}</></b> to <b><magenta>{new_path:?}</></b>");
} else if include_file(config.clone(), new_path) {
} else if include_file_extension(&config, new_path) {
let index = media_list.len();
let media = Media::new(index, &new_path.to_string_lossy(), false);

View File

@ -1,8 +1,5 @@
use std::{
sync::{
atomic::{AtomicBool, AtomicUsize},
Arc, Mutex,
},
sync::{atomic::AtomicBool, Arc},
thread,
};
@ -18,13 +15,12 @@ pub use folder::watchman;
pub use ingest::ingest_server;
pub use playlist::CurrentProgram;
use ffplayout_lib::utils::folder::FolderSource;
use ffplayout_lib::utils::{controller::PlayerControl, folder::FolderSource};
/// Create a source iterator from playlist, or from folder.
pub fn source_generator(
config: PlayoutConfig,
current_list: Arc<Mutex<Vec<Media>>>,
index: Arc<AtomicUsize>,
player_control: &PlayerControl,
playout_stat: PlayoutStatus,
is_terminated: Arc<AtomicBool>,
) -> Box<dyn Iterator<Item = Media>> {
@ -37,8 +33,8 @@ pub fn source_generator(
);
let config_clone = config.clone();
let folder_source = FolderSource::new(&config, playout_stat.chain, current_list, index);
let node_clone = folder_source.nodes.clone();
let folder_source = FolderSource::new(&config, playout_stat.chain, player_control);
let node_clone = folder_source.player_control.current_list.clone();
// Spawn a thread to monitor folder for file changes.
thread::spawn(move || watchman(config_clone, is_terminated.clone(), node_clone));
@ -47,8 +43,7 @@ pub fn source_generator(
}
Playlist => {
info!("Playout in playlist mode");
let program =
CurrentProgram::new(&config, playout_stat, is_terminated, current_list, index);
let program = CurrentProgram::new(&config, playout_stat, is_terminated, player_control);
Box::new(program) as Box<dyn Iterator<Item = Media>>
}

View File

@ -2,7 +2,7 @@ use std::{
fs,
path::Path,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
@ -11,9 +11,9 @@ use serde_json::json;
use simplelog::*;
use ffplayout_lib::utils::{
check_sync, 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,
check_sync, 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,
};
/// Struct for current playlist.
@ -26,9 +26,8 @@ pub struct CurrentProgram {
json_mod: Option<String>,
json_path: Option<String>,
json_date: String,
pub nodes: Arc<Mutex<Vec<Media>>>,
player_control: PlayerControl,
current_node: Media,
index: Arc<AtomicUsize>,
is_terminated: Arc<AtomicBool>,
playout_stat: PlayoutStatus,
}
@ -38,8 +37,7 @@ impl CurrentProgram {
config: &PlayoutConfig,
playout_stat: PlayoutStatus,
is_terminated: Arc<AtomicBool>,
current_list: Arc<Mutex<Vec<Media>>>,
global_index: Arc<AtomicUsize>,
player_control: &PlayerControl,
) -> Self {
let json = read_json(config, None, is_terminated.clone(), true, 0.0);
@ -47,7 +45,7 @@ impl CurrentProgram {
info!("Read Playlist: <b><magenta>{}</></b>", file);
}
*current_list.lock().unwrap() = json.program;
*player_control.current_list.lock().unwrap() = json.program;
*playout_stat.current_date.lock().unwrap() = json.date.clone();
if *playout_stat.date.lock().unwrap() != json.date {
@ -68,9 +66,8 @@ impl CurrentProgram {
json_mod: json.modified,
json_path: json.current_file,
json_date: json.date,
nodes: current_list,
player_control: player_control.clone(),
current_node: Media::new(0, "", false),
index: global_index,
is_terminated,
playout_stat,
}
@ -87,7 +84,7 @@ impl CurrentProgram {
self.json_path = json.current_file;
self.json_mod = json.modified;
*self.nodes.lock().unwrap() = json.program;
*self.player_control.current_list.lock().unwrap() = json.program;
} else if Path::new(&self.json_path.clone().unwrap()).is_file()
|| is_remote(&self.json_path.clone().unwrap())
{
@ -109,7 +106,7 @@ impl CurrentProgram {
);
self.json_mod = json.modified;
*self.nodes.lock().unwrap() = json.program;
*self.player_control.current_list.lock().unwrap() = json.program;
self.playout_stat.list_init.store(true, Ordering::SeqCst);
}
@ -120,14 +117,15 @@ impl CurrentProgram {
);
let mut media = Media::new(0, "", false);
media.begin = Some(get_sec());
// TODO: Works not well with filler folder
media.duration = DUMMY_LEN;
media.out = DUMMY_LEN;
self.json_path = None;
*self.nodes.lock().unwrap() = vec![media.clone()];
*self.player_control.current_list.lock().unwrap() = vec![media.clone()];
self.current_node = media;
self.playout_stat.list_init.store(true, Ordering::SeqCst);
self.index.store(0, Ordering::SeqCst);
self.player_control.current_index.store(0, Ordering::SeqCst);
}
}
@ -145,7 +143,9 @@ impl CurrentProgram {
let mut next_start = self.current_node.begin.unwrap() - start_sec + duration + delta;
if self.index.load(Ordering::SeqCst) == self.nodes.lock().unwrap().len() - 1 {
if self.player_control.current_index.load(Ordering::SeqCst)
== self.player_control.current_list.lock().unwrap().len() - 1
{
next_start += self.config.general.stop_threshold;
}
@ -182,8 +182,8 @@ impl CurrentProgram {
self.json_path = json.current_file.clone();
self.json_mod = json.modified;
self.json_date = json.date;
*self.nodes.lock().unwrap() = json.program;
self.index.store(0, Ordering::SeqCst);
*self.player_control.current_list.lock().unwrap() = json.program;
self.player_control.current_index.store(0, Ordering::SeqCst);
if json.current_file.is_none() {
self.playout_stat.list_init.store(true, Ordering::SeqCst);
@ -193,8 +193,8 @@ impl CurrentProgram {
// Check if last and/or next clip is a advertisement.
fn last_next_ad(&mut self) {
let index = self.index.load(Ordering::SeqCst);
let current_list = self.nodes.lock().unwrap();
let index = self.player_control.current_index.load(Ordering::SeqCst);
let current_list = self.player_control.current_list.lock().unwrap();
if index + 1 < current_list.len() && &current_list[index + 1].category == "advertisement" {
self.current_node.next_ad = Some(true);
@ -233,10 +233,17 @@ impl CurrentProgram {
time_sec += *shift;
}
for (i, item) in self.nodes.lock().unwrap().iter_mut().enumerate() {
for (i, item) in self
.player_control
.current_list
.lock()
.unwrap()
.iter_mut()
.enumerate()
{
if item.begin.unwrap() + item.out - item.seek > time_sec {
self.playout_stat.list_init.store(false, Ordering::SeqCst);
self.index.store(i, Ordering::SeqCst);
self.player_control.current_index.store(i, Ordering::SeqCst);
break;
}
@ -249,8 +256,11 @@ impl CurrentProgram {
if !self.playout_stat.list_init.load(Ordering::SeqCst) {
let time_sec = self.get_current_time();
let index = self.index.fetch_add(1, Ordering::SeqCst);
let nodes = self.nodes.lock().unwrap();
let index = self
.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
let nodes = self.player_control.current_list.lock().unwrap();
let last_index = nodes.len() - 1;
// de-instance node to preserve original values in list
@ -261,6 +271,7 @@ impl CurrentProgram {
&self.config,
node_clone,
&self.playout_stat.chain,
&self.player_control,
last_index,
);
}
@ -284,9 +295,10 @@ impl Iterator for CurrentProgram {
// On init load, playlist could be not long enough,
// so we check if we can take the next playlist already,
// or we fill the gap with a dummy.
let last_index = self.nodes.lock().unwrap().len() - 1;
self.current_node = self.nodes.lock().unwrap()[last_index].clone();
let new_node = self.nodes.lock().unwrap()[last_index].clone();
let last_index = self.player_control.current_list.lock().unwrap().len() - 1;
self.current_node =
self.player_control.current_list.lock().unwrap()[last_index].clone();
let new_node = self.player_control.current_list.lock().unwrap()[last_index].clone();
let new_length = new_node.begin.unwrap() + new_node.duration;
trace!("Init playlist after playlist end");
@ -312,7 +324,7 @@ impl Iterator for CurrentProgram {
current_time += self.config.playlist.length_sec.unwrap() + 1.0;
}
let mut nodes = self.nodes.lock().unwrap();
let mut nodes = self.player_control.current_list.lock().unwrap();
let index = nodes.len();
let mut media = Media::new(index, "", false);
@ -320,11 +332,18 @@ impl Iterator for CurrentProgram {
media.duration = duration;
media.out = duration;
self.current_node =
gen_source(&self.config, media, &self.playout_stat.chain, last_index);
self.current_node = gen_source(
&self.config,
media,
&self.playout_stat.chain,
&self.player_control,
last_index,
);
nodes.push(self.current_node.clone());
self.index.store(nodes.len(), Ordering::SeqCst);
self.player_control
.current_index
.store(nodes.len(), Ordering::SeqCst);
}
}
@ -333,11 +352,13 @@ impl Iterator for CurrentProgram {
return Some(self.current_node.clone());
}
if self.index.load(Ordering::SeqCst) < self.nodes.lock().unwrap().len() {
if self.player_control.current_index.load(Ordering::SeqCst)
< self.player_control.current_list.lock().unwrap().len()
{
self.check_for_next_playlist();
let mut is_last = false;
let index = self.index.load(Ordering::SeqCst);
let nodes = self.nodes.lock().unwrap();
let index = self.player_control.current_index.load(Ordering::SeqCst);
let nodes = self.player_control.current_list.lock().unwrap();
let last_index = nodes.len() - 1;
if index == last_index {
@ -349,12 +370,15 @@ impl Iterator for CurrentProgram {
&self.config,
is_last,
&self.playout_stat,
&self.player_control,
last_index,
);
drop(nodes);
self.last_next_ad();
self.index.fetch_add(1, Ordering::SeqCst);
self.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
Some(self.current_node.clone())
} else {
@ -370,7 +394,7 @@ impl Iterator for CurrentProgram {
{
// Test if playlist is to early finish,
// and if we have to fill it with a placeholder.
let index = self.index.load(Ordering::SeqCst);
let index = self.player_control.current_index.load(Ordering::SeqCst);
self.current_node = Media::new(index, "", false);
self.current_node.begin = Some(get_sec());
let mut duration = total_delta.abs();
@ -384,33 +408,40 @@ impl Iterator for CurrentProgram {
&self.config,
self.current_node.clone(),
&self.playout_stat.chain,
&self.player_control,
0,
);
self.nodes.lock().unwrap().push(self.current_node.clone());
self.player_control
.current_list
.lock()
.unwrap()
.push(self.current_node.clone());
self.last_next_ad();
self.current_node.last_ad = last_ad;
self.current_node
.add_filter(&self.config, &self.playout_stat.chain);
self.index.fetch_add(1, Ordering::SeqCst);
self.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
return Some(self.current_node.clone());
}
// Get first clip from next playlist.
self.index.store(0, Ordering::SeqCst);
let last_index = self.nodes.lock().unwrap().len() - 1;
self.player_control.current_index.store(0, Ordering::SeqCst);
self.current_node = gen_source(
&self.config,
self.nodes.lock().unwrap()[0].clone(),
self.player_control.current_list.lock().unwrap()[0].clone(),
&self.playout_stat.chain,
last_index,
&self.player_control,
0,
);
self.last_next_ad();
self.current_node.last_ad = last_ad;
self.index.store(1, Ordering::SeqCst);
self.player_control.current_index.store(1, Ordering::SeqCst);
Some(self.current_node.clone())
}
@ -426,6 +457,7 @@ fn timed_source(
config: &PlayoutConfig,
last: bool,
playout_stat: &PlayoutStatus,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
let (delta, total_delta) = get_delta(config, &node.begin.unwrap());
@ -463,11 +495,24 @@ fn timed_source(
{
// when we are in the 24 hour range, get the clip
new_node.process = Some(true);
new_node = gen_source(config, node, &playout_stat.chain, last_index);
new_node = gen_source(
config,
node,
&playout_stat.chain,
player_control,
last_index,
);
} else if total_delta <= 0.0 {
info!("Begin is over play time, skip: {}", node.source);
} else if total_delta < node.duration - node.seek || last {
new_node = handle_list_end(config, node, total_delta, &playout_stat.chain, last_index);
new_node = handle_list_end(
config,
node,
total_delta,
&playout_stat.chain,
player_control,
last_index,
);
}
new_node
@ -478,6 +523,7 @@ pub fn gen_source(
config: &PlayoutConfig,
mut node: Media,
filter_chain: &Option<Arc<Mutex<Vec<String>>>>,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
let duration = node.out - node.seek;
@ -507,38 +553,63 @@ pub fn gen_source(
error!("Source not found: <b><magenta>\"{}\"</></b>", node.source);
}
warn!("Generate filler with <yellow>{duration:.2}</> seconds length!");
let probe = MediaProbe::new(&config.storage.filler_clip);
if config
.storage
.filler_clip
.rsplit_once('.')
.map(|(_, e)| e.to_lowercase())
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
.is_some()
if Path::new(&config.storage.filler).is_dir()
&& !player_control.filler_list.lock().unwrap().is_empty()
{
node.source = config.storage.filler_clip.clone();
node.cmd = Some(loop_image(&node));
node.probe = Some(probe);
} else if let Some(length) = 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_clip.clone();
node.duration = length;
node.out = duration;
let filler_index = player_control.filler_index.fetch_add(1, Ordering::SeqCst);
let mut filler_media = player_control.filler_list.lock().unwrap()[filler_index].clone();
if filler_index == player_control.filler_list.lock().unwrap().len() - 1 {
player_control.filler_index.store(0, Ordering::SeqCst)
}
filler_media.add_probe();
if filler_media.duration > duration {
filler_media.out = duration;
}
warn!(
"Generate filler with <yellow>{:.2}</> seconds length!",
filler_media.out
);
node = filler_media;
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);
warn!("Generate filler with <yellow>{duration:.2}</> seconds length!");
let probe = MediaProbe::new(&config.storage.filler);
if config
.storage
.filler
.rsplit_once('.')
.map(|(_, e)| e.to_lowercase())
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
.is_some()
{
node.source = config.storage.filler.clone();
node.cmd = Some(loop_image(&node));
node.probe = Some(probe);
} else if let Some(length) = 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();
node.duration = length;
node.out = 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);
}
}
}
@ -562,6 +633,7 @@ fn handle_list_init(
config: &PlayoutConfig,
mut node: Media,
filter_chain: &Option<Arc<Mutex<Vec<String>>>>,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
debug!("Playlist init");
@ -574,7 +646,7 @@ fn handle_list_init(
node.out = out;
gen_source(config, node, filter_chain, last_index)
gen_source(config, node, filter_chain, player_control, last_index)
}
/// when we come to last clip in playlist,
@ -585,6 +657,7 @@ fn handle_list_end(
mut node: Media,
total_delta: f64,
filter_chain: &Option<Arc<Mutex<Vec<String>>>>,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
debug!("Playlist end");
@ -613,5 +686,5 @@ fn handle_list_end(
node.process = Some(true);
gen_source(config, node, filter_chain, last_index)
gen_source(config, node, filter_chain, player_control, last_index)
}

View File

@ -19,9 +19,9 @@ use ffplayout::{
};
use ffplayout_lib::utils::{
generate_playlist, get_date, import::import_file, init_logging, is_remote, send_mail,
test_tcp_port, validate_ffmpeg, validate_playlist, JsonPlaylist, OutputMode::*, PlayerControl,
PlayoutStatus, ProcessControl,
folder::fill_filler_list, generate_playlist, get_date, import::import_file, init_logging,
is_remote, send_mail, test_tcp_port, validate_ffmpeg, validate_playlist, JsonPlaylist,
OutputMode::*, PlayerControl, PlayoutStatus, ProcessControl,
};
#[cfg(debug_assertions)]
@ -99,7 +99,8 @@ fn main() {
let play_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new();
let proc_control = ProcessControl::new();
let play_ctl = play_control.clone();
let play_ctl1 = play_control.clone();
let play_ctl2 = play_control.clone();
let play_stat = playout_stat.clone();
let proc_ctl1 = proc_control.clone();
let proc_ctl2 = proc_control.clone();
@ -122,7 +123,8 @@ fn main() {
exit(1);
};
let config_clone = config.clone();
let config_clone1 = config.clone();
let config_clone2 = config.clone();
if ![2, 4, 6, 8].contains(&config.processing.audio_channels) {
error!(
@ -200,7 +202,7 @@ fn main() {
exit(1)
}
thread::spawn(move || run_server(config_clone, play_ctl, play_stat, proc_ctl2));
thread::spawn(move || run_server(config_clone1, play_ctl1, play_stat, proc_ctl2));
}
status_file(&config.general.stat_file, &playout_stat);
@ -210,11 +212,20 @@ fn main() {
config.general.config_path
);
if Path::new(&config.storage.filler).is_dir() {
debug!(
"Fill filler list from: <b><magenta>{}</></b>",
config.storage.filler
);
thread::spawn(move || fill_filler_list(config_clone2, play_ctl2));
}
match config.out.mode {
// write files/playlist to HLS m3u8 playlist
HLS => write_hls(&config, play_control, playout_stat, proc_control),
// play on desktop or stream to a remote target
_ => player(&config, play_control, playout_stat, proc_control),
_ => player(&config, &play_control, playout_stat, proc_control),
}
info!("Playout done...");

View File

@ -2,9 +2,7 @@ use std::process::{self, Command, Stdio};
use simplelog::*;
use ffplayout_lib::filter::v_drawtext;
use ffplayout_lib::utils::PlayoutConfig;
use ffplayout_lib::vec_strings;
use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings};
/// Desktop Output
///

View File

@ -137,7 +137,7 @@ fn ingest_to_hls_server(
/// Write with single ffmpeg instance directly to a HLS playlist.
pub fn write_hls(
config: &PlayoutConfig,
play_control: PlayerControl,
player_control: PlayerControl,
playout_stat: PlayoutStatus,
proc_control: ProcessControl,
) {
@ -148,8 +148,7 @@ pub fn write_hls(
let get_source = source_generator(
config.clone(),
play_control.current_list.clone(),
play_control.index.clone(),
&player_control,
playout_stat,
proc_control.is_terminated.clone(),
);
@ -160,7 +159,7 @@ pub fn write_hls(
}
for node in get_source {
*play_control.current_media.lock().unwrap() = Some(node.clone());
*player_control.current_media.lock().unwrap() = Some(node.clone());
let mut cmd = match node.cmd {
Some(cmd) => cmd,

View File

@ -37,7 +37,7 @@ use ffplayout_lib::vec_strings;
/// When ingest stops, it switch back to playlist/folder mode.
pub fn player(
config: &PlayoutConfig,
play_control: PlayerControl,
play_control: &PlayerControl,
playout_stat: PlayoutStatus,
proc_control: ProcessControl,
) {
@ -50,8 +50,7 @@ pub fn player(
// get source iterator
let get_source = source_generator(
config.clone(),
play_control.current_list.clone(),
play_control.index.clone(),
play_control,
playout_stat,
proc_control.is_terminated.clone(),
);
@ -105,7 +104,10 @@ pub fn player(
if Path::new(&config.task.path).is_file() {
thread::spawn(move || task_runner::run(task_config, task_node, server_running));
} else {
error!("<bright-blue>{}</> executable not exists!", config.task.path);
error!(
"<bright-blue>{}</> executable not exists!",
config.task.path
);
}
}

View File

@ -15,8 +15,8 @@ use tiny_http::{Header, Method, Request, Response, Server};
use crate::rpc::zmq_send;
use crate::utils::{get_data_map, get_media_map};
use ffplayout_lib::utils::{
get_delta, write_status, Ingest, OutputMode::*, PlayerControl, PlayoutConfig,
PlayoutStatus, ProcessControl,
get_delta, write_status, Ingest, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
ProcessControl,
};
#[derive(Default, Deserialize, Clone, Debug)]
@ -175,7 +175,7 @@ fn control_back(
let current_date = playout_stat.current_date.lock().unwrap().clone();
let current_list = play_control.current_list.lock().unwrap();
let mut date = playout_stat.date.lock().unwrap();
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let mut time_shift = playout_stat.time_shift.lock().unwrap();
if index > 1 && current_list.len() > 1 {
@ -191,7 +191,7 @@ fn control_back(
info!("Move to last clip");
let mut data_map = Map::new();
let mut media = current_list[index - 2].clone();
play_control.index.fetch_sub(2, Ordering::SeqCst);
play_control.current_index.fetch_sub(2, Ordering::SeqCst);
media.add_probe();
let (delta, _) = get_delta(config, &media.begin.unwrap_or(0.0));
@ -221,7 +221,7 @@ fn control_next(
let current_date = playout_stat.current_date.lock().unwrap().clone();
let current_list = play_control.current_list.lock().unwrap();
let mut date = playout_stat.date.lock().unwrap();
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let mut time_shift = playout_stat.time_shift.lock().unwrap();
if index < current_list.len() {
@ -370,7 +370,7 @@ fn media_current(
/// media info: get infos about next clip
fn media_next(config: &PlayoutConfig, play_control: &PlayerControl) -> Response<Cursor<Vec<u8>>> {
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let current_list = play_control.current_list.lock().unwrap();
if index < current_list.len() {
@ -386,7 +386,7 @@ fn media_next(config: &PlayoutConfig, play_control: &PlayerControl) -> Response<
/// media info: get infos about last clip
fn media_last(config: &PlayoutConfig, play_control: &PlayerControl) -> Response<Cursor<Vec<u8>>> {
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let current_list = play_control.current_list.lock().unwrap();
if index > 1 && index - 2 < current_list.len() {

View File

@ -269,7 +269,8 @@ pub struct Storage {
pub path: String,
#[serde(skip_serializing, skip_deserializing)]
pub paths: Vec<String>,
pub filler_clip: String,
#[serde(alias = "filler_clip")]
pub filler: String,
pub extensions: Vec<String>,
pub shuffle: bool,
}

View File

@ -163,11 +163,13 @@ impl ProcessControl {
// }
/// Global player control, to get infos about current clip etc.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct PlayerControl {
pub current_media: Arc<Mutex<Option<Media>>>,
pub current_list: Arc<Mutex<Vec<Media>>>,
pub index: Arc<AtomicUsize>,
pub filler_list: Arc<Mutex<Vec<Media>>>,
pub current_index: Arc<AtomicUsize>,
pub filler_index: Arc<AtomicUsize>,
}
impl PlayerControl {
@ -175,7 +177,9 @@ impl PlayerControl {
Self {
current_media: Arc::new(Mutex::new(None)),
current_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
index: Arc::new(AtomicUsize::new(0)),
filler_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
current_index: Arc::new(AtomicUsize::new(0)),
filler_index: Arc::new(AtomicUsize::new(0)),
}
}
}

View File

@ -1,7 +1,7 @@
use std::{
path::Path,
sync::{
atomic::{AtomicUsize, Ordering},
atomic::Ordering,
{Arc, Mutex},
},
};
@ -10,7 +10,9 @@ use rand::{seq::SliceRandom, thread_rng};
use simplelog::*;
use walkdir::WalkDir;
use crate::utils::{get_sec, include_file, Media, PlayoutConfig};
use crate::utils::{
controller::PlayerControl, get_sec, include_file_extension, Media, PlayoutConfig,
};
/// Folder Sources
///
@ -19,17 +21,15 @@ use crate::utils::{get_sec, include_file, Media, PlayoutConfig};
pub struct FolderSource {
config: PlayoutConfig,
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
pub nodes: Arc<Mutex<Vec<Media>>>,
pub player_control: PlayerControl,
current_node: Media,
index: Arc<AtomicUsize>,
}
impl FolderSource {
pub fn new(
config: &PlayoutConfig,
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
current_list: Arc<Mutex<Vec<Media>>>,
global_index: Arc<AtomicUsize>,
player_control: &PlayerControl,
) -> Self {
let mut path_list = vec![];
let mut media_list = vec![];
@ -52,11 +52,10 @@ impl FolderSource {
.into_iter()
.flat_map(|e| e.ok())
.filter(|f| f.path().is_file())
.filter(|f| include_file_extension(config, f.path()))
{
if include_file(config.clone(), entry.path()) {
let media = Media::new(0, &entry.path().to_string_lossy(), false);
media_list.push(media);
}
let media = Media::new(0, &entry.path().to_string_lossy(), false);
media_list.push(media);
}
}
@ -81,20 +80,19 @@ impl FolderSource {
index += 1;
}
*current_list.lock().unwrap() = media_list;
*player_control.current_list.lock().unwrap() = media_list;
Self {
config: config.clone(),
filter_chain,
nodes: current_list,
player_control: player_control.clone(),
current_node: Media::new(0, "", false),
index: global_index,
}
}
fn shuffle(&mut self) {
let mut rng = thread_rng();
let mut nodes = self.nodes.lock().unwrap();
let mut nodes = self.player_control.current_list.lock().unwrap();
nodes.shuffle(&mut rng);
@ -104,7 +102,7 @@ impl FolderSource {
}
fn sort(&mut self) {
let mut nodes = self.nodes.lock().unwrap();
let mut nodes = self.player_control.current_list.lock().unwrap();
nodes.sort_by(|d1, d2| d1.source.cmp(&d2.source));
@ -119,15 +117,19 @@ impl Iterator for FolderSource {
type Item = Media;
fn next(&mut self) -> Option<Self::Item> {
if self.index.load(Ordering::SeqCst) < self.nodes.lock().unwrap().len() {
let i = self.index.load(Ordering::SeqCst);
self.current_node = self.nodes.lock().unwrap()[i].clone();
if self.player_control.current_index.load(Ordering::SeqCst)
< self.player_control.current_list.lock().unwrap().len()
{
let i = self.player_control.current_index.load(Ordering::SeqCst);
self.current_node = self.player_control.current_list.lock().unwrap()[i].clone();
self.current_node.add_probe();
self.current_node
.add_filter(&self.config, &self.filter_chain);
self.current_node.begin = Some(get_sec());
self.index.fetch_add(1, Ordering::SeqCst);
self.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
Some(self.current_node.clone())
} else {
@ -145,15 +147,38 @@ impl Iterator for FolderSource {
self.sort();
}
self.current_node = self.nodes.lock().unwrap()[0].clone();
self.current_node = self.player_control.current_list.lock().unwrap()[0].clone();
self.current_node.add_probe();
self.current_node
.add_filter(&self.config, &self.filter_chain);
self.current_node.begin = Some(get_sec());
self.index.store(1, Ordering::SeqCst);
self.player_control.current_index.store(1, Ordering::SeqCst);
Some(self.current_node.clone())
}
}
}
pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) {
let mut filler_list = vec![];
for (index, entry) in WalkDir::new(&config.storage.filler)
.into_iter()
.flat_map(|e| e.ok())
.filter(|f| f.path().is_file())
.filter(|f| include_file_extension(&config, f.path()))
.enumerate()
{
let media = Media::new(index, &entry.path().to_string_lossy(), false);
filler_list.push(media);
}
if config.storage.shuffle {
let mut rng = thread_rng();
filler_list.shuffle(&mut rng);
}
*player_control.filler_list.lock().unwrap() = filler_list;
}

View File

@ -11,12 +11,11 @@ use std::{
io::Error,
path::Path,
process::exit,
sync::{atomic::AtomicUsize, Arc, Mutex},
};
use simplelog::*;
use super::folder::FolderSource;
use super::{folder::FolderSource, PlayerControl};
use crate::utils::{
get_date_range, json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig,
};
@ -36,8 +35,7 @@ pub fn generate_playlist(
}
}
};
let current_list = Arc::new(Mutex::new(vec![Media::new(0, "", false)]));
let index = Arc::new(AtomicUsize::new(0));
let player_control = PlayerControl::new();
let playlist_root = Path::new(&config.playlist.path);
let mut playlists = vec![];
let mut date_range = vec![];
@ -64,8 +62,8 @@ pub fn generate_playlist(
date_range = get_date_range(&date_range)
}
let media_list = FolderSource::new(config, None, current_list, index);
let list_length = media_list.nodes.lock().unwrap().len();
let media_list = FolderSource::new(config, None, &player_control);
let list_length = media_list.player_control.current_list.lock().unwrap().len();
for date in date_range {
let d: Vec<&str> = date.split('-').collect();
@ -90,7 +88,8 @@ pub fn generate_playlist(
playlist_file.display()
);
let mut filler = Media::new(0, &config.storage.filler_clip, true);
// TODO: handle filler folder
let mut filler = Media::new(0, &config.storage.filler, true);
let filler_length = filler.duration;
let mut length = 0.0;
let mut round = 0;

View File

@ -586,7 +586,7 @@ pub fn valid_source(source: &str) -> bool {
/// Check if file can include or has to exclude.
/// For example when a file is on given HLS output path, it should exclude.
/// Or when the file extension is set under storage config it can be include.
pub fn include_file(config: PlayoutConfig, file_path: &Path) -> bool {
pub fn include_file_extension(config: &PlayoutConfig, file_path: &Path) -> bool {
let mut include = false;
if let Some(ext) = file_extension(file_path) {
@ -614,6 +614,7 @@ pub fn include_file(config: PlayoutConfig, file_path: &Path) -> bool {
if let Some(m3u8_path) = config
.out
.output_cmd
.clone()
.unwrap_or_else(|| vec![String::new()])
.iter()
.find(|s| s.contains(".m3u8") && !s.contains("master.m3u8"))

View File

@ -2,20 +2,21 @@ use std::fs;
use ffplayout::{input::playlist::gen_source, utils::prepare_output_cmd};
use ffplayout_lib::{
utils::{Media, OutputMode::*, PlayoutConfig, ProcessUnit::*},
utils::{Media, OutputMode::*, PlayerControl, PlayoutConfig, ProcessUnit::*},
vec_strings,
};
#[test]
fn video_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = true;
let logo_path = fs::canonicalize("./assets/logo.png").unwrap();
config.processing.logo = logo_path.to_string_lossy().to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd =
vec_strings![
@ -36,12 +37,13 @@ fn video_audio_input() {
#[test]
fn video_audio_custom_filter1_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter = "[0:v]gblur=2[c_v_out];[0:a]volume=0.2[c_a_out]".to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -61,6 +63,7 @@ fn video_audio_custom_filter1_input() {
#[test]
fn video_audio_custom_filter2_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter =
@ -68,7 +71,7 @@ fn video_audio_custom_filter2_input() {
.to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -88,13 +91,14 @@ fn video_audio_custom_filter2_input() {
#[test]
fn video_audio_custom_filter3_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter =
"[v_in];movie=logo.png[l];[v_in][l]overlay[c_v_out];[0:a]volume=0.2[c_a_out]".to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -114,12 +118,13 @@ fn video_audio_custom_filter3_input() {
#[test]
fn dual_audio_aevalsrc_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 2;
config.processing.add_logo = false;
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd =
vec_strings![
@ -140,12 +145,13 @@ fn dual_audio_aevalsrc_input() {
#[test]
fn dual_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 2;
config.processing.add_logo = false;
let media_obj = Media::new(0, "./assets/dual_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -165,13 +171,14 @@ fn dual_audio_input() {
#[test]
fn video_separate_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 1;
config.processing.add_logo = false;
let mut media_obj = Media::new(0, "./assets/no_audio.mp4", true);
media_obj.audio = "./assets/audio.mp3".to_string();
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -1305,6 +1312,7 @@ fn video_audio_text_filter_stream() {
#[test]
fn video_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.text.add_text = false;
@ -1333,7 +1341,7 @@ fn video_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1390,6 +1398,7 @@ fn video_audio_hls() {
#[test]
fn video_audio_sub_meta_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.text.add_text = false;
@ -1422,7 +1431,7 @@ fn video_audio_sub_meta_hls() {
]);
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1483,6 +1492,7 @@ fn video_audio_sub_meta_hls() {
#[test]
fn video_multi_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -1512,7 +1522,7 @@ fn video_multi_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/dual_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1571,6 +1581,7 @@ fn video_multi_audio_hls() {
#[test]
fn multi_video_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.text.add_text = false;
@ -1617,7 +1628,7 @@ fn multi_video_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1684,6 +1695,7 @@ fn multi_video_audio_hls() {
#[test]
fn multi_video_multi_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -1733,7 +1745,7 @@ fn multi_video_multi_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/dual_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",

View File

@ -31,7 +31,7 @@ fn playlist_change_at_midnight() {
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.playlist.path = "assets/playlists".into();
config.storage.filler_clip = "assets/with_audio.mp4".into();
config.storage.filler = "assets/with_audio.mp4".into();
config.logging.log_to_file = false;
config.logging.timestamp = false;
config.out.mode = Null;
@ -51,7 +51,7 @@ fn playlist_change_at_midnight() {
thread::spawn(move || timed_stop(28, proc_ctl));
player(&config, play_control, playout_stat.clone(), proc_control);
player(&config, &play_control, playout_stat.clone(), proc_control);
let playlist_date = &*playout_stat.current_date.lock().unwrap();
@ -72,7 +72,7 @@ fn playlist_change_at_six() {
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.playlist.path = "assets/playlists".into();
config.storage.filler_clip = "assets/with_audio.mp4".into();
config.storage.filler = "assets/with_audio.mp4".into();
config.logging.log_to_file = false;
config.logging.timestamp = false;
config.out.mode = Null;
@ -92,7 +92,7 @@ fn playlist_change_at_six() {
thread::spawn(move || timed_stop(28, proc_ctl));
player(&config, play_control, playout_stat.clone(), proc_control);
player(&config, &play_control, playout_stat.clone(), proc_control);
let playlist_date = &*playout_stat.current_date.lock().unwrap();