generate playlists based on template, serialize paths to PathBuf

This commit is contained in:
jb-alvarado 2023-09-05 14:46:32 +02:00
parent d2c72d56fe
commit 0c51f8303c
34 changed files with 981 additions and 653 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ ffpapi.1.gz
/public/
tmp/
.vscode/
assets/playlist_template.json

684
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
[workspace]
members = ["ffplayout-api", "ffplayout-engine", "lib", "tests"]
default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
resolver = "2"
[workspace.package]
version = "0.20.0"

View File

@ -54,6 +54,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for
- import playlist from text or m3u file, with CLI or frontend
- audio only, for radio mode (experimental *)
- [Piggyback Mode](/ffplayout-api/README.md#piggyback-mode), mostly for non Linux systems (experimental *)
- generate playlist based on [template](/docs/playlist_gen.md) (experimental *)
For preview stream, read: [/docs/preview_stream.md](/docs/preview_stream.md)

View File

@ -21,6 +21,10 @@ Using live ingest to inject a live stream.
The different output modes.
### **[Playlist Generation](/docs/playlist_gen.md)**
Generate playlists based on template.
### **[Multi Audio Tracks](/docs/multi_audio.md)**
Output multiple audio tracks.

71
docs/playlist_gen.md Normal file
View File

@ -0,0 +1,71 @@
## Playlist generation template
It is possible to generate playlists based on templates. A template could look like:
```JSON
{
"sources": [
{
"start": "00:00:00",
"duration": "02:00:00",
"shuffle": true,
"paths": [
"/path/to/folder/1"
]
},
{
"start": "02:00:00",
"duration": "04:00:00",
"shuffle": false,
"paths": [
"/path/to/folder/2",
"/path/to/folder/3",
"/path/to/folder/4"
]
},
{
"start": "06:00:00",
"duration": "10:00:00",
"shuffle": true,
"paths": [
"/path/to/folder/5"
]
},
{
"start": "16:00:00",
"duration": "06:00:00",
"shuffle": false,
"paths": [
"/path/to/folder/6",
"/path/to/folder/7"
]
},
{
"start": "22:00:00",
"duration": "02:00:00",
"shuffle": true,
"paths": [
"/path/to/folder/8"
]
}
]
}
```
This can be used as file and run through CLI:
```BASH
ffplayout -g 2023-09-04 - 2023-09-10 --template 'path/to/playlist_template.json'
```
Or through API:
```BASH
curl -X POST http://127.0.0.1:8787/api/playlist/1/generate/2023-00-05
-H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
--data '{ "paths": "template": {"sources": [\
{"start": "00:00:00", "duration": "10:00:00", "shuffle": true, "paths": ["path/1", "path/2"]}, \
{"start": "10:00:00", "duration": "14:00:00", "shuffle": false, "paths": ["path/3", "path/4"]}]}}'
```

View File

@ -8,7 +8,7 @@
///
/// For all endpoints an (Bearer) authentication is required.\
/// `{id}` represent the channel id, and at default is 1.
use std::{collections::HashMap, env, fs, path::Path};
use std::{collections::HashMap, env, fs, path::PathBuf};
use actix_multipart::Multipart;
use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder};
@ -46,6 +46,7 @@ use crate::{
use ffplayout_lib::{
utils::{
get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist, PlayoutConfig,
Template,
},
vec_strings,
};
@ -72,19 +73,20 @@ pub struct DateObj {
#[derive(Debug, Deserialize, Serialize)]
struct FileObj {
#[serde(default)]
path: String,
path: PathBuf,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct PathsObj {
#[serde(default)]
paths: Vec<String>,
template: Option<Template>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ImportObj {
#[serde(default)]
file: String,
file: PathBuf,
#[serde(default)]
date: String,
}
@ -724,7 +726,7 @@ pub async fn get_playlist(
/// ```BASH
/// curl -X POST http://127.0.0.1:8787/api/playlist/1/
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
/// -- data "{<JSON playlist data>}"
/// --data "{<JSON playlist data>}"
/// ```
#[post("/playlist/{id}/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
@ -746,7 +748,7 @@ pub async fn save_playlist(
/// ```BASH
/// curl -X POST http://127.0.0.1:8787/api/playlist/1/generate/2022-06-20
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
/// /// -- data '{ "paths": [<list of paths>] }' # <- data is optional
/// /// --data '{ "paths": [<list of paths>] }' # <- data is optional
/// ```
#[post("/playlist/{id}/generate/{date}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
@ -764,10 +766,11 @@ pub async fn gen_playlist(
for path in &obj.paths {
let (p, _, _) = norm_abs_path(&config.storage.path, path);
path_list.push(p.to_string_lossy().to_string());
path_list.push(p);
}
config.storage.paths = path_list;
config.general.template = obj.template.clone();
}
match generate_playlist(config, channel.name).await {
@ -921,8 +924,8 @@ async fn import_playlist(
payload: Multipart,
obj: web::Query<ImportObj>,
) -> Result<HttpResponse, ServiceError> {
let file = Path::new(&obj.file).file_name().unwrap_or_default();
let path = env::temp_dir().join(file).to_string_lossy().to_string();
let file = obj.file.file_name().unwrap_or_default();
let path = env::temp_dir().join(file);
let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
let channel = handles::select_channel(&pool.clone().into_inner(), &id).await?;

View File

@ -82,8 +82,8 @@ async fn create_schema(conn: &Pool<Sqlite>) -> Result<SqliteQueryResult, sqlx::E
pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std::error::Error>> {
let db_path = db_path()?;
if !Sqlite::database_exists(&db_path).await.unwrap_or(false) {
Sqlite::create_database(&db_path).await.unwrap();
if !Sqlite::database_exists(db_path).await.unwrap_or(false) {
Sqlite::create_database(db_path).await.unwrap();
let pool = db_pool().await?;

View File

@ -7,7 +7,7 @@ use crate::utils::db_path;
pub async fn db_pool() -> Result<Pool<Sqlite>, sqlx::Error> {
let db_path = db_path().unwrap();
let conn = SqlitePool::connect(&db_path).await?;
let conn = SqlitePool::connect(db_path).await?;
Ok(conn)
}

View File

@ -83,11 +83,9 @@ async fn main() -> std::io::Result<()> {
};
if let Some(conn) = args.listen {
if let Ok(p) = db_path() {
if !Path::new(&p).is_file() {
error!("Database is not initialized! Init DB first and add admin user.");
exit(1);
}
if db_path().is_err() {
error!("Database is not initialized! Init DB first and add admin user.");
exit(1);
}
init_config(&pool).await;
let ip_port = conn.split(':').collect::<Vec<&str>>();

View File

@ -1,4 +1,4 @@
use std::{fs, path::Path};
use std::{fs, path::PathBuf};
use rand::prelude::*;
use simplelog::*;
@ -31,22 +31,14 @@ pub async fn create_channel(
Err(_) => rand::thread_rng().gen_range(71..99),
};
let mut config =
PlayoutConfig::new(Some("/usr/share/ffplayout/ffplayout.yml.orig".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from(
"/usr/share/ffplayout/ffplayout.yml.orig",
)));
config.general.stat_file = format!(".ffp_{channel_name}",);
config.logging.path = Path::new(&config.logging.path)
.join(&channel_name)
.to_string_lossy()
.to_string();
config.logging.path = config.logging.path.join(&channel_name);
config.rpc_server.address = format!("127.0.0.1:70{:7>2}", channel_num);
config.playlist.path = Path::new(&config.playlist.path)
.join(channel_name)
.to_string_lossy()
.to_string();
config.playlist.path = config.playlist.path.join(channel_name);
config.out.output_param = config
.out

View File

@ -1,4 +1,8 @@
use std::{fs, io::Write, path::PathBuf};
use std::{
fs,
io::Write,
path::{Path, PathBuf},
};
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse};
@ -51,10 +55,9 @@ pub struct VideoFile {
/// Normalize absolut path
///
/// This function takes care, that it is not possible to break out from root_path.
/// It also gives alway a relative path back.
pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, String) {
let mut path = PathBuf::from(root_path);
let path_relative = RelativePath::new(root_path)
/// It also gives always a relative path back.
pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, String) {
let path_relative = RelativePath::new(&root_path.to_string_lossy())
.normalize()
.to_string()
.replace("../", "");
@ -62,13 +65,15 @@ pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, Str
.normalize()
.to_string()
.replace("../", "");
let path_suffix = path
let path_suffix = root_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if input_path.starts_with(root_path) || source_relative.starts_with(&path_relative) {
if input_path.starts_with(&*root_path.to_string_lossy())
|| source_relative.starts_with(&path_relative)
{
source_relative = source_relative
.strip_prefix(&path_relative)
.and_then(|s| s.strip_prefix('/'))
@ -82,9 +87,9 @@ pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, Str
.to_string();
}
path = path.join(&source_relative);
let path = &root_path.join(&source_relative);
(path, path_suffix, source_relative)
(path.to_path_buf(), path_suffix, source_relative)
}
/// File Browser
@ -300,7 +305,7 @@ pub async fn upload(
conn: &Pool<Sqlite>,
id: i32,
mut payload: Multipart,
path: &str,
path: &Path,
abs_path: bool,
) -> Result<HttpResponse, ServiceError> {
while let Some(mut field) = payload.try_next().await? {
@ -318,9 +323,9 @@ pub async fn upload(
let filepath;
if abs_path {
filepath = PathBuf::from(path);
filepath = path.to_path_buf();
} else {
let target_path = valid_path(conn, id, path).await?;
let target_path = valid_path(conn, id, &path.to_string_lossy()).await?;
filepath = target_path.join(filename);
}

View File

@ -74,21 +74,25 @@ pub async fn init_config(conn: &Pool<Sqlite>) {
INSTANCE.set(config).unwrap();
}
pub fn db_path() -> Result<String, Box<dyn std::error::Error>> {
pub fn db_path() -> Result<&'static str, Box<dyn std::error::Error>> {
let sys_path = Path::new("/usr/share/ffplayout/db");
let mut db_path = "./ffplayout.db".to_string();
let mut db_path = "./ffplayout.db";
if sys_path.is_dir() && !sys_path.writable() {
error!("Path {} is not writable!", sys_path.display());
}
if sys_path.is_dir() && sys_path.writable() {
db_path = "/usr/share/ffplayout/db/ffplayout.db".to_string();
db_path = "/usr/share/ffplayout/db/ffplayout.db";
} else if Path::new("./assets").is_dir() {
db_path = "./assets/ffplayout.db".to_string();
db_path = "./assets/ffplayout.db";
}
Ok(db_path)
if Path::new(db_path).is_file() {
return Ok(db_path);
}
Err(format!("DB path {db_path} not exists!").into())
}
pub async fn run_args(mut args: Args) -> Result<(), i32> {

View File

@ -3,7 +3,7 @@ use std::{fs, path::PathBuf};
use simplelog::*;
use sqlx::{Pool, Sqlite};
use crate::utils::{errors::ServiceError, playout_config};
use crate::utils::{errors::ServiceError, files::norm_abs_path, playout_config};
use ffplayout_lib::utils::{
generate_playlist as playlist_generator, json_reader, json_writer, JsonPlaylist, PlayoutConfig,
};
@ -78,16 +78,32 @@ pub async fn write_playlist(
}
pub async fn generate_playlist(
config: PlayoutConfig,
mut config: PlayoutConfig,
channel: String,
) -> Result<JsonPlaylist, ServiceError> {
if let Some(mut template) = config.general.template.take() {
for source in template.sources.iter_mut() {
let mut paths = vec![];
for path in &source.paths {
let (safe_path, _, _) =
norm_abs_path(&config.storage.path, &path.to_string_lossy());
paths.push(safe_path);
}
source.paths = paths;
}
config.general.template = Some(template);
}
match playlist_generator(&config, Some(channel)) {
Ok(playlists) => {
if !playlists.is_empty() {
Ok(playlists[0].clone())
} else {
Err(ServiceError::Conflict(
"Playlist could not be written, possible already exists!".into(),
"The playlist could not be written, maybe it already exists!".into(),
))
}
}

View File

@ -16,8 +16,10 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "std"
clap = { version = "4.3", features = ["derive"] }
crossbeam-channel = "0.5"
futures = "0.3"
itertools = "0.11"
notify = "6.0"
notify-debouncer-full = { version = "*", default-features = false }
rand = "0.8"
regex = "1"
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }

View File

@ -28,8 +28,8 @@ pub fn source_generator(
Folder => {
info!("Playout in folder mode");
debug!(
"Monitor folder: <b><magenta>{}</></b>",
&config.storage.path
"Monitor folder: <b><magenta>{:?}</></b>",
config.storage.path
);
let config_clone = config.clone();

View File

@ -40,7 +40,7 @@ impl CurrentProgram {
is_terminated: Arc<AtomicBool>,
player_control: &PlayerControl,
) -> Self {
let json = read_json(config, None, is_terminated.clone(), true, 0.0);
let json = read_json(config, None, is_terminated.clone(), true, false);
if let Some(file) = &json.current_file {
info!("Read Playlist: <b><magenta>{}</></b>", file);
@ -78,7 +78,7 @@ impl CurrentProgram {
fn check_update(&mut self, seek: bool) {
if self.json_path.is_none() {
// If the playlist was missing, we check here to see if it came back.
let json = read_json(&self.config, None, self.is_terminated.clone(), seek, 0.0);
let json = read_json(&self.config, None, self.is_terminated.clone(), seek, false);
if let Some(file) = &json.current_file {
info!("Read Playlist: <b><magenta>{file}</></b>");
@ -105,7 +105,7 @@ impl CurrentProgram {
self.json_path.clone(),
self.is_terminated.clone(),
false,
0.0,
false,
);
self.json_mod = json.modified;
@ -133,17 +133,20 @@ impl CurrentProgram {
}
// Check if day is past and it is time for a new playlist.
fn check_for_next_playlist(&mut self) {
fn check_for_next_playlist(&mut self) -> bool {
let current_time = get_sec();
let start_sec = self.config.playlist.start_sec.unwrap();
let target_length = self.config.playlist.length_sec.unwrap();
let (delta, total_delta) = get_delta(&self.config, &current_time);
let mut duration = self.current_node.out;
let mut next = false;
if self.current_node.duration > self.current_node.out {
duration = self.current_node.duration
}
trace!("delta: {delta}, total_delta: {total_delta}");
let mut next_start =
self.current_node.begin.unwrap_or_default() - start_sec + duration + delta;
@ -153,21 +156,20 @@ impl CurrentProgram {
next_start += self.config.general.stop_threshold;
}
trace!("next_start: {next_start}, target_length: {target_length}");
// Check if we over the target length or we are close to it, if so we load the next playlist.
if next_start >= target_length
|| is_close(total_delta, 0.0, 2.0)
|| is_close(total_delta, target_length, 2.0)
{
let json = read_json(
&self.config,
None,
self.is_terminated.clone(),
false,
next_start,
);
trace!("get next day");
next = true;
let json = read_json(&self.config, None, self.is_terminated.clone(), false, true);
if let Some(file) = &json.current_file {
info!("Read Playlist: <b><magenta>{}</></b>", file);
info!("Read next Playlist: <b><magenta>{}</></b>", file);
}
let data = json!({
@ -192,8 +194,12 @@ impl CurrentProgram {
if json.current_file.is_none() {
self.playout_stat.list_init.store(true, Ordering::SeqCst);
} else {
self.playout_stat.list_init.store(false, Ordering::SeqCst);
}
}
next
}
// Check if last and/or next clip is a advertisement.
@ -257,6 +263,7 @@ impl CurrentProgram {
// Prepare init clip.
fn init_clip(&mut self) {
trace!("init_clip");
self.get_current_clip();
if !self.playout_stat.list_init.load(Ordering::SeqCst) {
@ -307,47 +314,48 @@ impl Iterator for CurrentProgram {
let new_length = new_node.begin.unwrap_or_default() + new_node.duration;
trace!("Init playlist after playlist end");
self.check_for_next_playlist();
let next_playlist = self.check_for_next_playlist();
if new_length
>= self.config.playlist.length_sec.unwrap()
+ self.config.playlist.start_sec.unwrap()
{
self.init_clip();
} else if next_playlist
&& self.player_control.current_list.lock().unwrap().len() > 1
{
let index = self
.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
self.current_node = gen_source(
&self.config,
self.player_control.current_list.lock().unwrap()[index].clone(),
&self.playout_stat.chain,
&self.player_control,
0,
);
return Some(self.current_node.clone());
} else {
// fill missing length from playlist
let mut current_time = get_sec();
let (_, total_delta) = get_delta(&self.config, &current_time);
let mut out = total_delta.abs();
let mut duration = out + 0.001;
trace!("Total delta on list init: {total_delta}");
let filler_index = self.player_control.filler_index.load(Ordering::SeqCst);
let mut filler =
self.player_control.filler_list.lock().unwrap()[filler_index].clone();
filler.add_probe();
// If there is no filler, the duration of the dummy clip should not be too long.
// This would take away the ability to restart the playlist when the playout registers a change.
if filler.duration > 0.0 {
if duration > filler.duration {
out = filler.duration;
}
duration = filler.duration;
} else if DUMMY_LEN > total_delta {
duration = total_delta;
out = total_delta;
let out = if DUMMY_LEN > total_delta {
total_delta
} else {
duration = DUMMY_LEN;
out = DUMMY_LEN;
}
DUMMY_LEN
};
let duration = out + 0.001;
if self.json_path.is_some() {
// When playlist is missing, we always need to init the playlist the next iteration.
self.playout_stat.list_init.store(false, Ordering::SeqCst);
self.playout_stat.list_init.store(true, Ordering::SeqCst);
}
if self.config.playlist.start_sec.unwrap() > current_time {
@ -429,28 +437,14 @@ impl Iterator for CurrentProgram {
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 out = total_delta.abs();
let mut duration = out + 0.001;
let filler_index = self.player_control.filler_index.load(Ordering::SeqCst);
let mut filler =
self.player_control.filler_list.lock().unwrap()[filler_index].clone();
filler.add_probe();
if filler.duration > 0.0 {
if duration > filler.duration {
out = filler.duration;
}
duration = filler.duration;
} else if DUMMY_LEN > total_delta {
duration = total_delta;
out = total_delta;
let out = if DUMMY_LEN > total_delta {
total_delta
} else {
duration = DUMMY_LEN;
out = DUMMY_LEN;
}
DUMMY_LEN
};
let duration = out + 0.001;
self.current_node.duration = duration;
self.current_node.out = out;
@ -605,7 +599,7 @@ pub fn gen_source(
error!("Source not found: <b><magenta>\"{}\"</></b>", node.source);
}
let filler_source = Path::new(&config.storage.filler);
let filler_source = &config.storage.filler;
if filler_source.is_dir() && !player_control.filler_list.lock().unwrap().is_empty() {
let filler_index = player_control.filler_index.fetch_add(1, Ordering::SeqCst);
@ -629,17 +623,19 @@ pub fn gen_source(
node.cmd = Some(loop_filler(&node));
node.probe = filler_media.probe;
} else if filler_source.is_file() {
let probe = MediaProbe::new(&config.storage.filler);
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();
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
@ -649,7 +645,7 @@ pub fn gen_source(
.and_then(|d| d.parse::<f64>().ok())
{
// Create placeholder from config filler.
node.source = config.storage.filler.clone();
node.source = config.storage.filler.clone().to_string_lossy().to_string();
node.out = if node.duration > duration && filler_duration > duration {
duration

View File

@ -1,6 +1,6 @@
use std::{
fs::{self, File},
path::{Path, PathBuf},
path::PathBuf,
process::exit,
sync::{atomic::AtomicBool, Arc, Mutex},
thread,
@ -107,7 +107,7 @@ fn main() {
let messages = Arc::new(Mutex::new(Vec::new()));
// try to create logging folder, if not exist
if config.logging.log_to_file && !Path::new(&config.logging.path).is_dir() {
if config.logging.log_to_file && config.logging.path.is_dir() {
if let Err(e) = fs::create_dir_all(&config.logging.path) {
println!("Logging path not exists! {e}");
@ -163,11 +163,11 @@ fn main() {
}
if args.validate {
let mut playlist_path = Path::new(&config.playlist.path).to_owned();
let mut playlist_path = config.playlist.path.clone();
let start_sec = config.playlist.start_sec.unwrap();
let date = get_date(false, start_sec, 0.0);
let date = get_date(false, start_sec, false);
if playlist_path.is_dir() || is_remote(&config.playlist.path) {
if playlist_path.is_dir() || is_remote(&playlist_path.to_string_lossy()) {
let d: Vec<&str> = date.split('-').collect();
playlist_path = playlist_path
.join(d[0])
@ -213,7 +213,9 @@ fn main() {
);
// Fill filler list, can also be a single file.
thread::spawn(move || fill_filler_list(config_clone2, play_ctl2));
thread::spawn(move || {
fill_filler_list(&config_clone2, Some(play_ctl2));
});
match config.out.mode {
// write files/playlist to HLS m3u8 playlist

View File

@ -1,6 +1,5 @@
use std::{
io::{prelude::*, BufReader, BufWriter, Read},
path::Path,
process::{Command, Stdio},
sync::atomic::Ordering,
thread::{self, sleep},
@ -101,11 +100,11 @@ pub fn player(
let task_node = node.clone();
let server_running = proc_control.server_is_running.load(Ordering::SeqCst);
if Path::new(&config.task.path).is_file() {
if config.task.path.is_file() {
thread::spawn(move || task_runner::run(task_config, task_node, server_running));
} else {
error!(
"<bright-blue>{}</> executable not exists!",
"<bright-blue>{:?}</> executable not exists!",
config.task.path
);
}

View File

@ -1,3 +1,5 @@
use std::path::PathBuf;
use clap::Parser;
use ffplayout_lib::utils::{OutputMode, ProcessMode};
@ -13,10 +15,24 @@ pub struct Args {
pub channel: Option<String>,
#[clap(short, long, help = "File path to ffplayout.yml")]
pub config: Option<String>,
pub config: Option<PathBuf>,
#[clap(short, long, help = "File path for logging")]
pub log: Option<String>,
pub log: Option<PathBuf>,
#[clap(
short,
long,
help = "Target date (YYYY-MM-DD) for text/m3u to playlist import"
)]
pub date: Option<String>,
#[cfg(debug_assertions)]
#[clap(long, help = "fake date time, for debugging")]
pub fake_time: Option<String>,
#[clap(short, long, help = "Play folder content")]
pub folder: Option<PathBuf>,
#[clap(
short,
@ -27,37 +43,14 @@ pub struct Args {
)]
pub generate: Option<Vec<String>>,
#[clap(long, help = "Optional path list for playlist generations", num_args = 1..)]
pub paths: Option<Vec<String>>,
#[clap(short = 'm', long, help = "Playing mode: folder, playlist")]
pub play_mode: Option<ProcessMode>,
#[clap(short, long, help = "Play folder content")]
pub folder: Option<String>,
#[clap(
short,
long,
help = "Target date (YYYY-MM-DD) for text/m3u to playlist import"
)]
pub date: Option<String>,
#[clap(
long,
help = "Import a given text/m3u file and create a playlist from it"
)]
pub import: Option<String>,
pub import: Option<PathBuf>,
#[clap(short, long, help = "Path to playlist, or playlist root folder.")]
pub playlist: Option<String>,
#[clap(
short,
long,
help = "Start time in 'hh:mm:ss', 'now' for start with first"
)]
pub start: Option<String>,
#[clap(short, long, help = "Loop playlist infinitely")]
pub infinit: bool,
#[clap(
short = 't',
@ -69,8 +62,24 @@ pub struct Args {
#[clap(long, help = "Override logging level")]
pub level: Option<String>,
#[clap(short, long, help = "Loop playlist infinitely")]
pub infinit: bool,
#[clap(long, help = "Optional path list for playlist generations", num_args = 1..)]
pub paths: Option<Vec<PathBuf>>,
#[clap(short = 'm', long, help = "Playing mode: folder, playlist")]
pub play_mode: Option<ProcessMode>,
#[clap(short, long, help = "Path to playlist, or playlist root folder.")]
pub playlist: Option<PathBuf>,
#[clap(
short,
long,
help = "Start time in 'hh:mm:ss', 'now' for start with first"
)]
pub start: Option<String>,
#[clap(short = 'T', long, help = "JSON Template file for generating playlist")]
pub template: Option<PathBuf>,
#[clap(short, long, help = "Set output mode: desktop, hls, null, stream")]
pub output: Option<OutputMode>,
@ -78,10 +87,6 @@ pub struct Args {
#[clap(short, long, help = "Set audio volume")]
pub volume: Option<f64>,
#[cfg(debug_assertions)]
#[clap(long, help = "fake date time, for debugging")]
pub fake_time: Option<String>,
#[clap(long, help = "validate given playlist")]
pub validate: bool,
}

View File

@ -1,7 +1,4 @@
use std::{
path::{Path, PathBuf},
process::exit,
};
use std::{fs::File, path::PathBuf, process::exit};
use regex::Regex;
use serde_json::{json, Map, Value};
@ -14,8 +11,8 @@ pub use arg_parse::Args;
use ffplayout_lib::{
filter::Filters,
utils::{
get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media, OutputMode::*,
PlayoutConfig, ProcessMode::*,
config::Template, get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media,
OutputMode::*, PlayoutConfig, ProcessMode::*,
},
vec_strings,
};
@ -33,7 +30,7 @@ pub fn get_config(args: Args) -> PlayoutConfig {
exit(1)
}
Some(path.display().to_string())
Some(path)
}
None => args.config,
};
@ -48,12 +45,33 @@ pub fn get_config(args: Args) -> PlayoutConfig {
config.general.validate = true;
}
if let Some(template_file) = args.template {
let f = File::options()
.read(true)
.write(false)
.open(template_file)
.expect("JSON template file");
let mut template: Template = match serde_json::from_reader(f) {
Ok(p) => p,
Err(e) => {
error!("Template file not readable! {e}");
exit(1)
}
};
template.sources.sort_by(|d1, d2| d1.start.cmp(&d2.start));
config.general.template = Some(template);
}
if let Some(paths) = args.paths {
config.storage.paths = paths;
}
if let Some(log_path) = args.log {
if Path::new(&log_path).is_dir() {
if log_path.is_dir() {
config.logging.log_to_file = true;
}
config.logging.path = log_path;

View File

@ -9,7 +9,7 @@ repository.workspace = true
edition.workspace = true
[dependencies]
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] }
crossbeam-channel = "0.5"
ffprobe = "0.3"
file-rotate = "0.7"

View File

@ -6,6 +6,7 @@ use std::{
str::FromStr,
};
use chrono::NaiveTime;
use log::LevelFilter;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use shlex::split;
@ -124,6 +125,19 @@ where
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Template {
pub sources: Vec<Source>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Source {
pub start: NaiveTime,
pub duration: NaiveTime,
pub shuffle: bool,
pub paths: Vec<PathBuf>,
}
/// Global Config
///
/// This we init ones, when ffplayout is starting and use them globally in the hole program.
@ -163,6 +177,9 @@ pub struct General {
#[serde(skip_serializing, skip_deserializing)]
pub ffmpeg_libs: Vec<String>,
#[serde(skip_serializing, skip_deserializing)]
pub template: Option<Template>,
#[serde(default, skip_serializing, skip_deserializing)]
pub validate: bool,
}
@ -196,7 +213,7 @@ pub struct Logging {
pub local_time: bool,
pub timestamp: bool,
#[serde(alias = "log_path")]
pub path: String,
pub path: PathBuf,
#[serde(
alias = "log_level",
serialize_with = "log_level_to_string",
@ -255,7 +272,7 @@ pub struct Ingest {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Playlist {
pub help_text: String,
pub path: String,
pub path: PathBuf,
pub day_start: String,
#[serde(skip_serializing, skip_deserializing)]
@ -272,11 +289,11 @@ pub struct Playlist {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Storage {
pub help_text: String,
pub path: String,
pub path: PathBuf,
#[serde(skip_serializing, skip_deserializing)]
pub paths: Vec<String>,
pub paths: Vec<PathBuf>,
#[serde(alias = "filler_clip")]
pub filler: String,
pub filler: PathBuf,
pub extensions: Vec<String>,
pub shuffle: bool,
}
@ -304,7 +321,7 @@ pub struct Text {
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Task {
pub enable: bool,
pub path: String,
pub path: PathBuf,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -335,11 +352,11 @@ fn default_channels() -> u8 {
impl PlayoutConfig {
/// Read config from YAML file, and set some extra config values.
pub fn new(cfg_path: Option<String>) -> Self {
pub fn new(cfg_path: Option<PathBuf>) -> Self {
let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml");
if let Some(cfg) = cfg_path {
config_path = PathBuf::from(cfg);
config_path = cfg;
}
if !config_path.is_file() {

View File

@ -184,7 +184,7 @@ impl PlayerControl {
Self {
current_media: Arc::new(Mutex::new(None)),
current_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
filler_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
filler_list: Arc::new(Mutex::new(vec![])),
current_index: Arc::new(AtomicUsize::new(0)),
filler_index: Arc::new(AtomicUsize::new(0)),
}

View File

@ -1,9 +1,6 @@
use std::{
path::Path,
sync::{
atomic::Ordering,
{Arc, Mutex},
},
use std::sync::{
atomic::Ordering,
{Arc, Mutex},
};
use rand::{seq::SliceRandom, thread_rng};
@ -37,18 +34,18 @@ impl FolderSource {
if config.general.generate.is_some() && !config.storage.paths.is_empty() {
for path in &config.storage.paths {
path_list.push(path.clone())
path_list.push(path)
}
} else {
path_list.push(config.storage.path.clone())
path_list.push(&config.storage.path)
}
for path in &path_list {
if !Path::new(path).is_dir() {
error!("Path not exists: <b><magenta>{path}</></b>");
if !path.is_dir() {
error!("Path not exists: <b><magenta>{path:?}</></b>");
}
for entry in WalkDir::new(path.clone())
for entry in WalkDir::new(path)
.into_iter()
.flat_map(|e| e.ok())
.filter(|f| f.path().is_file())
@ -90,6 +87,22 @@ impl FolderSource {
}
}
pub fn from_list(
config: &PlayoutConfig,
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
player_control: &PlayerControl,
list: Vec<Media>,
) -> Self {
*player_control.current_list.lock().unwrap() = list;
Self {
config: config.clone(),
filter_chain,
player_control: player_control.clone(),
current_node: Media::new(0, "", false),
}
}
fn shuffle(&mut self) {
let mut rng = thread_rng();
let mut nodes = self.player_control.current_list.lock().unwrap();
@ -160,23 +173,30 @@ impl Iterator for FolderSource {
}
}
pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) {
pub fn fill_filler_list(
config: &PlayoutConfig,
player_control: Option<PlayerControl>,
) -> Vec<Media> {
let mut filler_list = vec![];
let filler_path = &config.storage.filler;
if Path::new(&config.storage.filler).is_dir() {
debug!(
"Fill filler list from: <b><magenta>{}</></b>",
config.storage.filler
);
if filler_path.is_dir() {
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()))
.filter(|f| include_file_extension(config, f.path()))
.enumerate()
{
filler_list.push(Media::new(index, &entry.path().to_string_lossy(), false));
let mut media = Media::new(index, &entry.path().to_string_lossy(), false);
if let Some(control) = player_control.as_ref() {
control.filler_list.lock().unwrap().push(media);
} else {
media.add_probe();
filler_list.push(media);
}
}
if config.storage.shuffle {
@ -190,9 +210,17 @@ pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) {
for (index, item) in filler_list.iter_mut().enumerate() {
item.index = Some(index);
}
} else {
filler_list.push(Media::new(0, &config.storage.filler, false));
} else if filler_path.is_file() {
let mut media = Media::new(0, &config.storage.filler.to_string_lossy(), false);
if let Some(control) = player_control.as_ref() {
control.filler_list.lock().unwrap().push(media);
} else {
media.add_probe();
filler_list.push(media);
}
}
*player_control.filler_list.lock().unwrap() = filler_list;
filler_list
}

View File

@ -4,22 +4,196 @@
///
/// The generator takes the files from storage, which are set in config.
/// It also respect the shuffle/sort mode.
///
/// Beside that it is really very basic, without any logic.
use std::{
fs::{create_dir_all, write},
io::Error,
path::Path,
process::exit,
};
use chrono::Timelike;
use rand::{seq::SliceRandom, thread_rng, Rng};
use simplelog::*;
use walkdir::WalkDir;
use super::{folder::FolderSource, PlayerControl};
use crate::utils::{
get_date_range, json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig,
folder::fill_filler_list, gen_dummy, get_date_range, include_file_extension,
json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig, Template,
};
fn sum_durations(clip_list: &Vec<Media>) -> f64 {
let mut list_duration = 0.0;
for item in clip_list {
list_duration += item.out
}
list_duration
}
fn random_list(clip_list: Vec<Media>, total_length: f64) -> Vec<Media> {
let mut max_attempts = 10000;
let mut randomized_clip_list: Vec<Media> = vec![];
let mut target_duration = 0.0;
let clip_list_length = clip_list.len();
let usage_limit = (total_length / sum_durations(&clip_list)).floor() + 1.0;
let mut last_clip = Media::new(0, "", false);
while target_duration < total_length && max_attempts > 0 {
let index = rand::thread_rng().gen_range(0..clip_list_length);
let selected_clip = clip_list[index].clone();
let selected_clip_count = randomized_clip_list
.iter()
.filter(|&n| *n == selected_clip)
.count() as f64;
if selected_clip_count == usage_limit
|| last_clip == selected_clip
|| target_duration + selected_clip.duration > total_length
{
max_attempts -= 1;
continue;
}
target_duration += selected_clip.duration;
randomized_clip_list.push(selected_clip.clone());
max_attempts -= 1;
last_clip = selected_clip;
}
randomized_clip_list
}
fn ordered_list(clip_list: Vec<Media>, total_length: f64) -> Vec<Media> {
let mut index = 0;
let mut skip_count = 0;
let mut ordered_clip_list: Vec<Media> = vec![];
let mut target_duration = 0.0;
let clip_list_length = clip_list.len();
while target_duration < total_length && skip_count < clip_list_length {
if index == clip_list_length {
index = 0;
}
let selected_clip = clip_list[index].clone();
if sum_durations(&ordered_clip_list) + selected_clip.duration > total_length
|| (!ordered_clip_list.is_empty()
&& selected_clip == ordered_clip_list[ordered_clip_list.len() - 1])
{
skip_count += 1;
index += 1;
continue;
}
target_duration += selected_clip.duration;
ordered_clip_list.push(selected_clip);
index += 1;
}
ordered_clip_list
}
fn filler_list(config: &PlayoutConfig, total_length: f64) -> Vec<Media> {
let filler_list = fill_filler_list(config, None);
let mut index = 0;
let mut filler_clip_list: Vec<Media> = vec![];
let mut target_duration = 0.0;
let clip_list_length = filler_list.len();
if clip_list_length > 0 {
while target_duration < total_length {
if index == clip_list_length {
index = 0;
}
let selected_clip = filler_list[index].clone();
target_duration += selected_clip.duration;
filler_clip_list.push(selected_clip);
index += 1;
}
let over_length = target_duration - total_length;
let last_index = filler_clip_list.len() - 1;
filler_clip_list[last_index].out = filler_clip_list[last_index].duration - over_length;
} else {
let mut dummy = Media::new(0, "", false);
let (source, cmd) = gen_dummy(config, total_length);
dummy.source = source;
dummy.cmd = Some(cmd);
dummy.duration = total_length;
dummy.out = total_length;
filler_clip_list.push(dummy);
}
filler_clip_list
}
pub fn generate_from_template(
config: &PlayoutConfig,
player_control: &PlayerControl,
template: Template,
) -> FolderSource {
let mut media_list = vec![];
let mut rng = thread_rng();
let mut index: usize = 0;
for source in template.sources {
let mut full_list = vec![];
let duration = (source.duration.hour() as f64 * 3600.0)
+ (source.duration.minute() as f64 * 60.0)
+ source.duration.second() as f64;
debug!("Generating playlist block with <yellow>{duration:.2}</> seconds length");
for path in source.paths {
debug!("Search files in <b><magenta>{path:?}</></b>");
for entry in WalkDir::new(path)
.into_iter()
.flat_map(|e| e.ok())
.filter(|f| f.path().is_file())
.filter(|f| include_file_extension(config, f.path()))
{
let media = Media::new(0, &entry.path().to_string_lossy(), true);
full_list.push(media);
}
}
let mut timed_list = if source.shuffle {
full_list.shuffle(&mut rng);
random_list(full_list, duration)
} else {
full_list.sort_by(|d1, d2| d1.source.cmp(&d2.source));
ordered_list(full_list, duration)
};
let total_length = sum_durations(&timed_list);
if duration > total_length {
let mut filler = filler_list(config, duration - total_length);
timed_list.append(&mut filler);
}
media_list.append(&mut timed_list);
}
for item in media_list.iter_mut() {
item.index = Some(index);
index += 1;
}
FolderSource::from_list(config, None, player_control, media_list)
}
/// Generate playlists
pub fn generate_playlist(
config: &PlayoutConfig,
@ -36,9 +210,10 @@ pub fn generate_playlist(
}
};
let player_control = PlayerControl::new();
let playlist_root = Path::new(&config.playlist.path);
let playlist_root = &config.playlist.path;
let mut playlists = vec![];
let mut date_range = vec![];
let mut from_template = false;
let channel = match channel_name {
Some(name) => name,
@ -47,8 +222,8 @@ pub fn generate_playlist(
if !playlist_root.is_dir() {
error!(
"Playlist folder <b><magenta>{}</></b> not exists!",
&config.playlist.path
"Playlist folder <b><magenta>{:?}</></b> not exists!",
config.playlist.path
);
exit(1);
@ -62,8 +237,16 @@ pub fn generate_playlist(
date_range = get_date_range(&date_range)
}
let media_list = FolderSource::new(config, None, &player_control);
let list_length = media_list.player_control.current_list.lock().unwrap().len();
// gives an iterator with infinit length
let folder_iter = if let Some(template) = &config.general.template {
from_template = true;
generate_from_template(config, &player_control, template.clone())
} else {
FolderSource::new(config, None, &player_control)
};
let list_length = player_control.current_list.lock().unwrap().len();
for date in date_range {
let d: Vec<&str> = date.split('-').collect();
@ -71,6 +254,8 @@ pub fn generate_playlist(
let month = d[1];
let playlist_path = playlist_root.join(year).join(month);
let playlist_file = &playlist_path.join(format!("{date}.json"));
let mut length = 0.0;
let mut round = 0;
create_dir_all(playlist_path)?;
@ -88,12 +273,6 @@ pub fn generate_playlist(
playlist_file.display()
);
// 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;
let mut playlist = JsonPlaylist {
channel: channel.clone(),
date,
@ -103,30 +282,38 @@ pub fn generate_playlist(
program: vec![],
};
for item in media_list.clone() {
let duration = item.duration;
if from_template {
let media_list = player_control.current_list.lock().unwrap();
playlist.program = media_list.to_vec();
} else {
for item in folder_iter.clone() {
let duration = item.duration;
if total_length > length + duration {
playlist.program.push(item);
if total_length >= length + duration {
playlist.program.push(item);
length += duration;
} else if filler_length > 0.0 && filler_length > total_length - length {
filler.out = total_length - length;
playlist.program.push(filler);
length += duration;
} else if round == list_length - 1 {
break;
} else {
round += 1;
}
}
break;
} else if round == list_length - 1 {
break;
} else {
round += 1;
let list_duration = sum_durations(&playlist.program);
if config.playlist.length_sec.unwrap() > list_duration {
let time_left = config.playlist.length_sec.unwrap() - list_duration;
let mut fillers = filler_list(config, time_left);
playlist.program.append(&mut fillers);
}
}
playlists.push(playlist.clone());
let json: String = serde_json::to_string_pretty(&playlist)?;
write(playlist_file, json)?;
playlists.push(playlist);
}
Ok(playlists)

View File

@ -12,7 +12,7 @@ pub fn import_file(
config: &PlayoutConfig,
date: &str,
channel_name: Option<String>,
path: &str,
path: &Path,
) -> Result<String, Error> {
let file = File::open(path)?;
let reader = BufReader::new(file);
@ -25,13 +25,13 @@ pub fn import_file(
program: vec![],
};
let playlist_root = Path::new(&config.playlist.path);
let playlist_root = &config.playlist.path;
if !playlist_root.is_dir() {
return Err(Error::new(
ErrorKind::Other,
format!(
"Playlist folder <b><magenta>{}</></b> not exists!",
&config.playlist.path,
"Playlist folder <b><magenta>{:?}</></b> not exists!",
config.playlist.path,
),
));
}

View File

@ -142,14 +142,14 @@ pub fn read_json(
path: Option<String>,
is_terminated: Arc<AtomicBool>,
seek: bool,
next_start: f64,
get_next: bool,
) -> JsonPlaylist {
let config_clone = config.clone();
let mut playlist_path = Path::new(&config.playlist.path).to_owned();
let mut playlist_path = config.playlist.path.clone();
let start_sec = config.playlist.start_sec.unwrap();
let date = get_date(seek, start_sec, next_start);
let date = get_date(seek, start_sec, get_next);
if playlist_path.is_dir() || is_remote(&config.playlist.path) {
if playlist_path.is_dir() || is_remote(&config.playlist.path.to_string_lossy()) {
let d: Vec<&str> = date.split('-').collect();
playlist_path = playlist_path
.join(d[0])

View File

@ -165,7 +165,7 @@ pub fn validate_playlist(
begin += item.out - item.seek;
}
if !config.playlist.infinit && length > begin + 1.0 {
if !config.playlist.infinit && length > begin + 1.2 {
error!(
"Playlist from <yellow>{date}</> not long enough, <yellow>{}</> needed!",
sec_to_time(length - begin),

View File

@ -2,7 +2,7 @@ extern crate log;
extern crate simplelog;
use std::{
path::Path,
path::PathBuf,
sync::{atomic::Ordering, Arc, Mutex},
thread::{self, sleep},
time::Duration,
@ -199,21 +199,18 @@ pub fn init_logging(
};
};
if app_config.log_to_file && &app_config.path != "none" {
if app_config.log_to_file && !app_config.path.exists() {
let file_config = log_config
.clone()
.set_time_format_custom(format_description!(
"[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:5]]"
))
.build();
let mut log_path = "logs/ffplayout.log".to_string();
let mut log_path = PathBuf::from("logs/ffplayout.log");
if Path::new(&app_config.path).is_dir() {
log_path = Path::new(&app_config.path)
.join("ffplayout.log")
.display()
.to_string();
} else if Path::new(&app_config.path).is_file() {
if app_config.path.is_dir() {
log_path = app_config.path.join("ffplayout.log");
} else if app_config.path.is_file() {
log_path = app_config.path
} else {
println!("Logging path not exists!")

View File

@ -13,7 +13,7 @@ use std::{
use std::env;
use chrono::{prelude::*, Duration};
use ffprobe::{ffprobe, Format, Stream};
use ffprobe::{ffprobe, Format, Stream as FFStream};
use rand::prelude::*;
use regex::Regex;
use reqwest::header;
@ -38,7 +38,7 @@ pub use config::{
OutputMode::{self, *},
PlayoutConfig,
ProcessMode::{self, *},
DUMMY_LEN, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, IMAGE_FORMAT,
Template, DUMMY_LEN, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, IMAGE_FORMAT,
};
pub use controller::{
PlayerControl, PlayoutStatus, ProcessControl,
@ -205,8 +205,8 @@ fn is_empty_string(st: &String) -> bool {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct MediaProbe {
pub format: Option<Format>,
pub audio_streams: Vec<Stream>,
pub video_streams: Vec<Stream>,
pub audio_streams: Vec<FFStream>,
pub video_streams: Vec<FFStream>,
}
impl MediaProbe {
@ -321,14 +321,14 @@ pub fn get_sec() -> f64 {
///
/// - When time is before playlist start, get date from yesterday.
/// - When given next_start is over target length (normally a full day), get date from tomorrow.
pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
pub fn get_date(seek: bool, start: f64, get_next: bool) -> String {
let local: DateTime<Local> = time_now();
if seek && start > get_sec() {
return (local - Duration::days(1)).format("%Y-%m-%d").to_string();
}
if start == 0.0 && next_start >= 86400.0 {
if start == 0.0 && get_next && get_sec() > 86398.0 {
return (local + Duration::days(1)).format("%Y-%m-%d").to_string();
}

View File

@ -1,4 +1,4 @@
use std::fs;
use std::{fs, path::PathBuf};
use ffplayout::{input::playlist::gen_source, utils::prepare_output_cmd};
use ffplayout_lib::{
@ -8,7 +8,7 @@ use ffplayout_lib::{
#[test]
fn video_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = true;
@ -36,7 +36,7 @@ fn video_audio_input() {
#[test]
fn video_audio_custom_filter1_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
@ -62,7 +62,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 mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
@ -90,7 +90,7 @@ 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 mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
@ -117,7 +117,7 @@ fn video_audio_custom_filter3_input() {
#[test]
fn dual_audio_aevalsrc_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 2;
@ -144,7 +144,7 @@ fn dual_audio_aevalsrc_input() {
#[test]
fn dual_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 2;
@ -170,7 +170,7 @@ fn dual_audio_input() {
#[test]
fn video_separate_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 1;
@ -204,7 +204,7 @@ fn video_separate_audio_input() {
#[test]
fn video_audio_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.out.output_cmd = Some(vec_strings![
@ -263,7 +263,7 @@ fn video_audio_stream() {
#[test]
fn video_audio_filter1_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.text.add_text = false;
@ -338,7 +338,7 @@ fn video_audio_filter1_stream() {
#[test]
fn video_audio_filter2_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.text.add_text = true;
@ -421,7 +421,7 @@ fn video_audio_filter2_stream() {
#[test]
fn video_audio_filter3_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.text.add_text = true;
@ -507,7 +507,7 @@ fn video_audio_filter3_stream() {
#[test]
fn video_audio_filter4_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.text.add_text = true;
@ -593,7 +593,7 @@ fn video_audio_filter4_stream() {
#[test]
fn video_dual_audio_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -664,7 +664,7 @@ fn video_dual_audio_stream() {
#[test]
fn video_dual_audio_filter_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -744,7 +744,7 @@ fn video_dual_audio_filter_stream() {
#[test]
fn video_audio_multi_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.out.output_cmd = Some(vec_strings![
@ -833,7 +833,7 @@ fn video_audio_multi_stream() {
#[test]
fn video_dual_audio_multi_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -947,7 +947,7 @@ fn video_dual_audio_multi_stream() {
#[test]
fn video_audio_text_multi_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.text.add_text = true;
@ -1060,7 +1060,7 @@ fn video_audio_text_multi_stream() {
#[test]
fn video_dual_audio_multi_filter_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -1189,7 +1189,7 @@ fn video_dual_audio_multi_filter_stream() {
#[test]
fn video_audio_text_filter_stream() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.audio_tracks = 1;
@ -1311,7 +1311,7 @@ fn video_audio_text_filter_stream() {
#[test]
fn video_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
@ -1397,7 +1397,7 @@ fn video_audio_hls() {
#[test]
fn video_audio_sub_meta_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
@ -1491,7 +1491,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 mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
@ -1580,7 +1580,7 @@ fn video_multi_audio_hls() {
#[test]
fn multi_video_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
@ -1694,7 +1694,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 mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;

View File

@ -17,6 +17,48 @@ fn timed_stop(sec: u64, proc_ctl: ProcessControl) {
proc_ctl.stop_all();
}
#[test]
#[serial]
#[ignore]
fn playlist_missing() {
let mut config = PlayoutConfig::new(None);
config.mail.recipient = "".into();
config.processing.mode = Playlist;
config.ingest.enable = false;
config.text.add_text = false;
config.playlist.day_start = "00:00:00".into();
config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.playlist.path = "assets/playlists".into();
config.storage.filler = "assets/with_audio.mp4".into();
config.logging.log_to_file = false;
config.logging.timestamp = false;
config.logging.level = LevelFilter::Trace;
config.out.mode = Null;
config.out.output_count = 1;
config.out.output_filter = None;
config.out.output_cmd = Some(vec_strings!["-f", "null", "-"]);
let play_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new();
let proc_control = ProcessControl::new();
let proc_ctl = proc_control.clone();
let logging = init_logging(&config, None, None);
CombinedLogger::init(logging).unwrap_or_default();
mock_time::set_mock_time("2023-02-07T23:59:45");
thread::spawn(move || timed_stop(28, proc_ctl));
player(&config, &play_control, playout_stat.clone(), proc_control);
let playlist_date = &*playout_stat.current_date.lock().unwrap();
assert_eq!(playlist_date, "2023-02-08");
}
#[test]
#[serial]
#[ignore]
@ -34,6 +76,7 @@ fn playlist_change_at_midnight() {
config.storage.filler = "assets/with_audio.mp4".into();
config.logging.log_to_file = false;
config.logging.timestamp = false;
config.logging.level = LevelFilter::Trace;
config.out.mode = Null;
config.out.output_count = 1;
config.out.output_filter = None;

View File

@ -1,3 +1,5 @@
use std::path::PathBuf;
#[cfg(test)]
use chrono::prelude::*;
@ -22,7 +24,7 @@ fn mock_date_time() {
fn get_date_yesterday() {
mock_time::set_mock_time("2022-05-20T05:59:24");
let date = get_date(true, 21600.0, 86400.0);
let date = get_date(true, 21600.0, false);
assert_eq!("2022-05-19".to_string(), date);
}
@ -31,14 +33,14 @@ fn get_date_yesterday() {
fn get_date_tomorrow() {
mock_time::set_mock_time("2022-05-20T23:59:30");
let date = get_date(false, 0.0, 86400.01);
let date = get_date(false, 0.0, true);
assert_eq!("2022-05-21".to_string(), date);
}
#[test]
fn test_delta() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
config.mail.recipient = "".into();
config.processing.mode = Playlist;
config.playlist.day_start = "00:00:00".into();