Merge pull request #570 from jb-alvarado/master

advanced config, new behavior for infinit mode
This commit is contained in:
jb-alvarado 2024-03-27 10:44:43 +00:00 committed by GitHub
commit 53b101d70e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1302 additions and 653 deletions

808
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
resolver = "2"
[workspace.package]
version = "0.20.5"
version = "0.21.0-beta1"
license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout"
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]

30
assets/advanced.yml Normal file
View File

@ -0,0 +1,30 @@
help: Changing these settings is for advanced users only! There will be no support or guarantee that it will be stable after changing them.
decoder:
input_param:
# output_param get also applied to ingest instance.
output_param:
filters:
deinterlace: # yadif=0:-1:0
pad_scale_w: # scale={}:-1,
pad_scale_h: # scale=-1:{},
pad_video: # '{}pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2'
fps: # fps={}
scale: # scale={}:{}
set_dar: # setdar=dar={}
fade_in: # '{}fade=in:st=0:d=0.5'
fade_out: # '{}fade=out:st={}:d=1.0'
overlay_logo_scale: # ',scale={}'
overlay_logo: # null[v];movie={}:loop=0,setpts=N/(FRAME_RATE*TB),format=rgba,colorchannelmixer=aa={}{}[l];[v][l]{}:shortest=1
overlay_logo_fade_in: # ',fade=in:st=0:d=1.0:alpha=1'
overlay_logo_fade_out: # ',fade=out:st={}:d=1.0:alpha=1'
tpad: # tpad=stop_mode=add:stop_duration={}
drawtext_from_file: # drawtext=text='{}':{}{}
drawtext_from_zmq: # zmq=b=tcp\\\\://'{}',drawtext@dyntext={}
aevalsrc: # aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000
apad: # apad=whole_dur={}
volume: # volume={}
split: # split={}{}
encoder:
input_param:
ingest:
input_param:

View File

@ -34,7 +34,7 @@ logging:
'backup_count' says how long log files will be saved in days. 'local_time' to
false will set log timestamps to UTC. Path to /var/log/ only if you run this
program as daemon. 'level' can be DEBUG, INFO, WARNING, ERROR.
'ffmpeg_level' can be info, warning, error.
'ffmpeg_level/ingest_level' can be info, warning, error.
'detect_silence' logs an error message if the audio line is silent for 15
seconds during the validation process.
log_to_file: true

View File

@ -35,7 +35,7 @@ path-clean = "1.0"
rand = "0.8"
regex = "1"
relative-path = "1.8"
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
rpassword = "7.2"
sanitize-filename = "0.5"
serde = { version = "1.0", features = ["derive"] }

View File

@ -1,6 +1,6 @@
use actix_web::error::ErrorUnauthorized;
use actix_web::Error;
use chrono::{Duration, Utc};
use chrono::{TimeDelta, Utc};
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
@ -23,7 +23,7 @@ impl Claims {
id,
username,
role,
exp: (Utc::now() + Duration::days(JWT_EXPIRATION_DAYS)).timestamp(),
exp: (Utc::now() + TimeDelta::try_days(JWT_EXPIRATION_DAYS).unwrap()).timestamp(),
}
}
}

View File

@ -26,7 +26,7 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, SaltString},
Argon2, PasswordHasher, PasswordVerifier,
};
use chrono::{DateTime, Datelike, Duration, Local, NaiveDateTime, TimeZone, Utc};
use chrono::{DateTime, Datelike, Local, NaiveDateTime, TimeDelta, TimeZone, Utc};
use path_clean::PathClean;
use regex::Regex;
use serde::{Deserialize, Serialize};
@ -817,7 +817,7 @@ pub async fn save_playlist(
data: web::Json<JsonPlaylist>,
) -> Result<impl Responder, ServiceError> {
match write_playlist(&pool.into_inner(), *id, data.into_inner()).await {
Ok(res) => Ok(res),
Ok(res) => Ok(web::Json(res)),
Err(e) => Err(e),
}
}
@ -885,7 +885,7 @@ pub async fn del_playlist(
params: web::Path<(i32, String)>,
) -> Result<impl Responder, ServiceError> {
match delete_playlist(&pool.into_inner(), params.0, &params.1).await {
Ok(_) => Ok(format!("Delete playlist from {} success!", params.1)),
Ok(m) => Ok(web::Json(m)),
Err(e) => Err(e),
}
}
@ -1149,7 +1149,7 @@ async fn get_program(
}
let date_range = get_date_range(&vec_strings![
(after - Duration::days(days)).format("%Y-%m-%d"),
(after - TimeDelta::try_days(days).unwrap_or_default()).format("%Y-%m-%d"),
"-",
before.format("%Y-%m-%d")
]);
@ -1194,7 +1194,8 @@ async fn get_program(
program.push(p_item);
}
naive += Duration::milliseconds(((item.out - item.seek) * 1000.0) as i64);
naive += TimeDelta::try_milliseconds(((item.out - item.seek) * 1000.0) as i64)
.unwrap_or_default();
}
}

View File

@ -114,7 +114,11 @@ pub async fn generate_playlist(
}
}
pub async fn delete_playlist(conn: &Pool<Sqlite>, id: i32, date: &str) -> Result<(), ServiceError> {
pub async fn delete_playlist(
conn: &Pool<Sqlite>,
id: i32,
date: &str,
) -> Result<String, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let mut playlist_path = PathBuf::from(&config.playlist.path);
let d: Vec<&str> = date.split('-').collect();
@ -125,11 +129,14 @@ pub async fn delete_playlist(conn: &Pool<Sqlite>, id: i32, date: &str) -> Result
.with_extension("json");
if playlist_path.is_file() {
if let Err(e) = fs::remove_file(playlist_path) {
match fs::remove_file(playlist_path) {
Ok(_) => Ok(format!("Delete playlist from {date} success!")),
Err(e) => {
error!("{e}");
return Err(ServiceError::InternalServerError);
};
Err(ServiceError::InternalServerError)
}
}
} else {
Ok(format!("No playlist to delete on: {date}"))
}
Ok(())
}

View File

@ -21,7 +21,7 @@ notify = "6.0"
notify-debouncer-full = { version = "*", default-features = false }
rand = "0.8"
regex = "1"
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
simplelog = { version = "0.12", features = ["paris"] }
@ -77,6 +77,11 @@ assets = [
"/etc/sudoers.d/",
"644",
],
[
"../assets/advanced.yml",
"/etc/ffplayout/",
"644",
],
[
"../assets/ffplayout.yml",
"/etc/ffplayout/",
@ -153,6 +158,11 @@ assets = [
"/etc/ffplayout/",
"644",
],
[
"../assets/advanced.yml",
"/etc/ffplayout/",
"644",
],
[
"../assets/logo.png",
"/usr/share/ffplayout/",
@ -192,6 +202,7 @@ license = "GPL-3.0"
assets = [
{ source = "../target/x86_64-unknown-linux-musl/release/ffpapi", dest = "/usr/bin/ffpapi", mode = "755" },
{ source = "../target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" },
{ source = "../assets/advanced.yml", dest = "/etc/ffplayout/advanced.yml", mode = "644", config = true },
{ source = "../assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true },
{ source = "../assets/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" },
{ source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" },

View File

@ -14,7 +14,7 @@ use ffplayout_lib::{
controller::ProcessUnit::*, test_tcp_port, Media, PlayoutConfig, ProcessControl,
FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS,
},
vec_strings,
vec_strings, ADVANCED_CONFIG,
};
fn server_monitor(
@ -61,6 +61,10 @@ pub fn ingest_server(
dummy_media.unit = Ingest;
dummy_media.add_filter(&config, &None);
if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.ingest.input_cmd {
server_cmd.append(&mut ingest_input_cmd.clone());
}
server_cmd.append(&mut stream_input.clone());
if let Some(mut filter) = dummy_media.filter {

View File

@ -11,9 +11,11 @@ use serde_json::json;
use simplelog::*;
use ffplayout_lib::utils::{
controller::PlayerControl, gen_dummy, get_delta, is_close, is_remote,
json_serializer::read_json, loop_filler, loop_image, modified_time, seek_and_length,
time_in_seconds, Media, MediaProbe, PlayoutConfig, PlayoutStatus, IMAGE_FORMAT,
controller::PlayerControl,
gen_dummy, get_delta, is_close, is_remote,
json_serializer::{read_json, set_defaults},
loop_filler, loop_image, modified_time, seek_and_length, time_in_seconds, JsonPlaylist, Media,
MediaProbe, PlayoutConfig, PlayoutStatus, IMAGE_FORMAT,
};
/// Struct for current playlist.
@ -24,9 +26,7 @@ pub struct CurrentProgram {
config: PlayoutConfig,
start_sec: f64,
end_sec: f64,
json_mod: Option<String>,
json_path: Option<String>,
json_date: String,
json_playlist: JsonPlaylist,
player_control: PlayerControl,
current_node: Media,
is_terminated: Arc<AtomicBool>,
@ -47,9 +47,10 @@ impl CurrentProgram {
config: config.clone(),
start_sec: config.playlist.start_sec.unwrap(),
end_sec: config.playlist.length_sec.unwrap(),
json_mod: None,
json_path: None,
json_date: String::new(),
json_playlist: JsonPlaylist::new(
"1970-01-01".to_string(),
config.playlist.start_sec.unwrap(),
),
player_control: player_control.clone(),
current_node: Media::new(0, "", false),
is_terminated,
@ -65,9 +66,9 @@ impl CurrentProgram {
let mut get_current = false;
let mut reload = false;
if let Some(path) = self.json_path.clone() {
if let Some(path) = self.json_playlist.path.clone() {
if (Path::new(&path).is_file() || is_remote(&path))
&& self.json_mod != modified_time(&path)
&& self.json_playlist.modified != modified_time(&path)
{
info!("Reload playlist <b><magenta>{path}</></b>");
self.playout_stat.list_init.store(true, Ordering::SeqCst);
@ -79,26 +80,24 @@ impl CurrentProgram {
}
if get_current {
let json = read_json(
&self.config,
self.json_playlist = read_json(
&mut self.config,
&self.player_control,
self.json_path.clone(),
self.json_playlist.path.clone(),
self.is_terminated.clone(),
seek,
false,
);
if !reload {
if let Some(file) = &json.current_file {
if let Some(file) = &self.json_playlist.path {
info!("Read playlist: <b><magenta>{file}</></b>");
}
}
self.json_path = json.current_file;
self.json_mod = json.modified;
*self.player_control.current_list.lock().unwrap() = json.program;
*self.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone();
if self.json_path.is_none() {
if self.json_playlist.path.is_none() {
trace!("missing playlist");
self.current_node = Media::new(0, "", false);
@ -137,15 +136,16 @@ impl CurrentProgram {
trace!("next_start: {next_start}, end_sec: {}", self.end_sec);
// Check if we over the target length or we are close to it, if so we load the next playlist.
if next_start >= self.end_sec
if !self.config.playlist.infinit
&& (next_start >= self.end_sec
|| is_close(total_delta, 0.0, 2.0)
|| is_close(total_delta, self.end_sec, 2.0)
|| is_close(total_delta, self.end_sec, 2.0))
{
trace!("get next day");
next = true;
let json = read_json(
&self.config,
self.json_playlist = read_json(
&mut self.config,
&self.player_control,
None,
self.is_terminated.clone(),
@ -153,18 +153,14 @@ impl CurrentProgram {
true,
);
if let Some(file) = &json.current_file {
if let Some(file) = &self.json_playlist.path {
info!("Read next playlist: <b><magenta>{file}</></b>");
}
self.playout_stat.list_init.store(false, Ordering::SeqCst);
self.set_status(json.date.clone());
self.set_status(self.json_playlist.date.clone());
self.json_path = json.current_file.clone();
self.json_mod = json.modified;
self.json_date = json.date;
*self.player_control.current_list.lock().unwrap() = json.program;
*self.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone();
self.player_control.current_index.store(0, Ordering::SeqCst);
} else {
self.load_or_update_playlist(seek)
@ -212,7 +208,7 @@ impl CurrentProgram {
let mut time_sec = time_in_seconds();
if time_sec < self.start_sec {
time_sec += self.config.playlist.length_sec.unwrap()
time_sec += 86400.0 // self.config.playlist.length_sec.unwrap();
}
time_sec
@ -231,6 +227,15 @@ impl CurrentProgram {
time_sec += *shift;
}
drop(shift);
if self.config.playlist.infinit
&& self.json_playlist.length.unwrap() < 86400.0
&& time_sec > self.json_playlist.length.unwrap() + self.start_sec
{
self.recalculate_begin(true)
}
for (i, item) in self
.player_control
.current_list
@ -266,14 +271,14 @@ impl CurrentProgram {
// de-instance node to preserve original values in list
let mut node_clone = nodes[index].clone();
// Important! When no manual drop is happen here, lock is still active in handle_list_init
drop(nodes);
trace!("Clip from init: {}", node_clone.source);
node_clone.seek += time_sec
- (node_clone.begin.unwrap() - *self.playout_stat.time_shift.lock().unwrap());
// Important! When no manual drop is happen here, lock is still active in handle_list_init
drop(nodes);
self.current_node = handle_list_init(
&self.config,
node_clone,
@ -297,7 +302,6 @@ impl CurrentProgram {
fn fill_end(&mut self, total_delta: f64) {
// Fill end from playlist
let index = self.player_control.current_index.load(Ordering::SeqCst);
let mut media = Media::new(index, "", false);
media.begin = Some(time_in_seconds());
@ -327,6 +331,20 @@ impl CurrentProgram {
.current_index
.fetch_add(1, Ordering::SeqCst);
}
fn recalculate_begin(&mut self, extend: bool) {
debug!("Infinit playlist reaches end, recalculate clip begins.");
let mut time_sec = time_in_seconds();
if extend {
time_sec = self.start_sec + self.json_playlist.length.unwrap();
}
self.json_playlist.start_sec = Some(time_sec);
set_defaults(&mut self.json_playlist);
*self.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone();
}
}
/// Build the playlist iterator
@ -334,7 +352,7 @@ impl Iterator for CurrentProgram {
type Item = Media;
fn next(&mut self) -> Option<Self::Item> {
self.last_json_path = self.json_path.clone();
self.last_json_path = self.json_playlist.path.clone();
self.last_node_ad = self.current_node.last_ad;
self.check_for_playlist(self.playout_stat.list_init.load(Ordering::SeqCst));
@ -342,7 +360,7 @@ impl Iterator for CurrentProgram {
trace!("Init playlist, from next iterator");
let mut init_clip_is_filler = false;
if self.json_path.is_some() {
if self.json_playlist.path.is_some() {
init_clip_is_filler = self.init_clip();
}
@ -420,7 +438,7 @@ impl Iterator for CurrentProgram {
let (_, total_delta) = get_delta(&self.config, &self.start_sec);
if !self.config.playlist.infinit
&& self.last_json_path == self.json_path
&& self.last_json_path == self.json_playlist.path
&& total_delta.abs() > 1.0
{
// Playlist is to early finish,
@ -438,6 +456,10 @@ impl Iterator for CurrentProgram {
drop(c_list);
if self.config.playlist.infinit {
self.recalculate_begin(false)
}
self.player_control.current_index.store(0, Ordering::SeqCst);
self.current_node = gen_source(
&self.config,
@ -502,6 +524,7 @@ fn timed_source(
if (total_delta > node.out - node.seek && !last)
|| node.index.unwrap() < 2
|| !config.playlist.length.contains(':')
|| config.playlist.infinit
{
// when we are in the 24 hour range, get the clip
new_node.process = Some(true);
@ -742,7 +765,7 @@ fn handle_list_init(
debug!("Playlist init");
let (_, total_delta) = get_delta(config, &node.begin.unwrap());
if node.out - node.seek > total_delta {
if !config.playlist.infinit && node.out - node.seek > total_delta {
node.out = total_delta + node.seek;
}

View File

@ -103,9 +103,12 @@ fn main() -> Result<(), ProcError> {
let messages = Arc::new(Mutex::new(Vec::new()));
// try to create logging folder, if not exist
if config.logging.log_to_file && config.logging.path.is_dir() {
if config.logging.log_to_file
&& !config.logging.path.is_dir()
&& !config.logging.path.ends_with(".log")
{
if let Err(e) = fs::create_dir_all(&config.logging.path) {
println!("Logging path not exists! {e}");
eprintln!("Logging path not exists! {e}");
exit(1);
}

View File

@ -2,7 +2,7 @@ use std::process::{self, Command, Stdio};
use simplelog::*;
use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings};
use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings, ADVANCED_CONFIG};
/// Desktop Output
///
@ -10,16 +10,18 @@ use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings};
pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
let mut enc_filter: Vec<String> = vec![];
let mut enc_cmd = vec_strings![
"-hide_banner",
"-nostats",
"-v",
log_format,
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format];
if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd {
enc_cmd.append(&mut encoder_input_cmd.clone());
}
enc_cmd.append(&mut vec_strings![
"-i",
"pipe:0",
"-window_title",
"ffplayout"
];
]);
if let Some(mut cmd) = config.out.output_cmd.clone() {
if !cmd.iter().any(|i| {

View File

@ -34,7 +34,7 @@ use ffplayout_lib::{
controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media,
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
},
vec_strings,
vec_strings, ADVANCED_CONFIG,
};
/// Ingest Server for HLS
@ -47,10 +47,15 @@ fn ingest_to_hls_server(
let mut server_prefix = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"];
let stream_input = config.ingest.input_cmd.clone().unwrap();
server_prefix.append(&mut stream_input.clone());
let mut dummy_media = Media::new(0, "Live Stream", false);
dummy_media.unit = Ingest;
if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.ingest.input_cmd {
server_prefix.append(&mut ingest_input_cmd.clone());
}
server_prefix.append(&mut stream_input.clone());
let mut is_running;
if let Some(url) = stream_input.iter().find(|s| s.contains("://")) {
@ -197,6 +202,10 @@ pub fn write_hls(
let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd {
enc_prefix.append(&mut encoder_input_cmd.clone());
}
let mut read_rate = 1.0;
if let Some(begin) = &node.begin {

View File

@ -19,11 +19,14 @@ pub use hls::write_hls;
use crate::input::{ingest_server, source_generator};
use crate::utils::task_runner;
use ffplayout_lib::utils::{
use ffplayout_lib::vec_strings;
use ffplayout_lib::{
utils::{
sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
ProcessControl, ProcessUnit::*,
},
ADVANCED_CONFIG,
};
use ffplayout_lib::vec_strings;
/// Player
///
@ -104,8 +107,18 @@ pub fn player(
continue;
}
let c_index = if cfg!(debug_assertions) {
format!(
" ({}/{})",
node.index.unwrap() + 1,
play_control.current_list.lock().unwrap().len()
)
} else {
String::new()
};
info!(
"Play for <yellow>{}</>: <b><magenta>{} {}</></b>",
"Play for <yellow>{}</>{c_index}: <b><magenta>{} {}</></b>",
sec_to_time(node.out - node.seek),
node.source,
node.audio
@ -130,6 +143,11 @@ pub fn player(
}
let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
if let Some(decoder_input_cmd) = &ADVANCED_CONFIG.decoder.input_cmd {
dec_cmd.append(&mut decoder_input_cmd.clone());
}
dec_cmd.append(&mut cmd);
if let Some(mut filter) = node.filter {

View File

@ -65,7 +65,7 @@ pub fn get_config(args: Args) -> Result<PlayoutConfig, ProcError> {
}
if let Some(log_path) = args.log {
if log_path != Path::new("none") && log_path.is_dir() {
if log_path != Path::new("none") {
config.logging.log_to_file = true;
config.logging.path = log_path;
} else {

@ -1 +1 @@
Subproject commit 737d86f901a9275b13e164378ad1c231c6370c1f
Subproject commit 3802e75c461eeea0febf20c1ea2c97cb89dd8d41

View File

@ -14,13 +14,14 @@ crossbeam-channel = "0.5"
derive_more = "0.99"
ffprobe = "0.3"
file-rotate = "0.7"
lazy_static = "1.4"
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false }
lexical-sort = "0.3"
log = "0.4"
num-traits = "0.2"
rand = "0.8"
regex = "1"
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"

View File

@ -11,8 +11,10 @@ mod custom;
pub mod v_drawtext;
use crate::utils::{
controller::ProcessUnit::*, fps_calc, is_close, Media, OutputMode::*, PlayoutConfig,
controller::ProcessUnit::*, custom_format, fps_calc, is_close, Media, OutputMode::*,
PlayoutConfig,
};
use crate::ADVANCED_CONFIG;
use super::vec_strings;
@ -184,7 +186,12 @@ impl Default for Filters {
fn deinterlace(field_order: &Option<String>, chain: &mut Filters) {
if let Some(order) = field_order {
if order != "progressive" {
chain.add_filter("yadif=0:-1:0", 0, Video)
let deinterlace = match &ADVANCED_CONFIG.decoder.filters.deinterlace {
Some(deinterlace) => deinterlace.clone(),
None => "yadif=0:-1:0".to_string(),
};
chain.add_filter(&deinterlace, 0, Video);
}
}
}
@ -195,25 +202,45 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl
if let (Some(w), Some(h)) = (v_stream.width, v_stream.height) {
if w > config.processing.width && aspect > config.processing.aspect {
scale = format!("scale={}:-1,", config.processing.width);
scale = match &ADVANCED_CONFIG.decoder.filters.pad_scale_w {
Some(pad_scale_w) => custom_format(pad_scale_w, &[&config.processing.width]),
None => format!("scale={}:-1,", config.processing.width),
};
} else if h > config.processing.height && aspect < config.processing.aspect {
scale = format!("scale=-1:{},", config.processing.height);
scale = match &ADVANCED_CONFIG.decoder.filters.pad_scale_h {
Some(pad_scale_h) => custom_format(pad_scale_h, &[&config.processing.width]),
None => format!("scale=-1:{},", config.processing.height),
};
}
}
chain.add_filter(
&format!(
"{scale}pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2",
config.processing.width, config.processing.height
let pad = match &ADVANCED_CONFIG.decoder.filters.pad_video {
Some(pad_video) => custom_format(
pad_video,
&[
&scale,
&config.processing.width.to_string(),
&config.processing.height.to_string(),
],
),
0,
Video,
)
None => format!(
"{}pad=max(iw\\,ih*({1}/{2})):ow/({1}/{2}):(ow-iw)/2:(oh-ih)/2",
scale, config.processing.width, config.processing.height
),
};
chain.add_filter(&pad, 0, Video)
}
}
fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) {
if fps != config.processing.fps {
chain.add_filter(&format!("fps={}", config.processing.fps), 0, Video)
let fps_filter = match &ADVANCED_CONFIG.decoder.filters.fps {
Some(fps) => custom_format(fps, &[&config.processing.fps]),
None => format!("fps={}", config.processing.fps),
};
chain.add_filter(&fps_filter, 0, Video)
}
}
@ -227,39 +254,49 @@ fn scale(
// width: i64, height: i64
if let (Some(w), Some(h)) = (width, height) {
if w != config.processing.width || h != config.processing.height {
chain.add_filter(
&format!(
let scale = match &ADVANCED_CONFIG.decoder.filters.scale {
Some(scale) => custom_format(
scale,
&[&config.processing.width, &config.processing.height],
),
None => format!(
"scale={}:{}",
config.processing.width, config.processing.height
),
0,
Video,
);
};
chain.add_filter(&scale, 0, Video);
} else {
chain.add_filter("null", 0, Video);
}
if !is_close(aspect, config.processing.aspect, 0.03) {
chain.add_filter(
&format!("setdar=dar={}", config.processing.aspect),
0,
Video,
)
let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar {
Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]),
None => format!("setdar=dar={}", config.processing.aspect),
};
chain.add_filter(&dar, 0, Video);
}
} else {
chain.add_filter(
&format!(
let scale = match &ADVANCED_CONFIG.decoder.filters.scale {
Some(scale) => custom_format(
scale,
&[&config.processing.width, &config.processing.height],
),
None => format!(
"scale={}:{}",
config.processing.width, config.processing.height
),
0,
Video,
);
chain.add_filter(
&format!("setdar=dar={}", config.processing.aspect),
0,
Video,
)
};
chain.add_filter(&scale, 0, Video);
let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar {
Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]),
None => format!("setdar=dar={}", config.processing.aspect),
};
chain.add_filter(&dar, 0, Video);
}
}
@ -276,15 +313,21 @@ fn fade(node: &mut Media, chain: &mut Filters, nr: i32, filter_type: FilterType)
}
if node.seek > 0.0 || node.unit == Ingest {
chain.add_filter(&format!("{t}fade=in:st=0:d=0.5"), nr, filter_type)
let fade_in = match &ADVANCED_CONFIG.decoder.filters.fade_in {
Some(fade) => custom_format(fade, &[t]),
None => format!("{t}fade=in:st=0:d=0.5"),
};
chain.add_filter(&fade_in, nr, filter_type);
}
if (node.out != node.duration && node.out - node.seek > 1.0) || fade_audio {
chain.add_filter(
&format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0)),
nr,
filter_type,
)
let fade_out = match &ADVANCED_CONFIG.decoder.filters.fade_out {
Some(fade) => custom_format(fade, &[t, &(node.out - node.seek - 1.0).to_string()]),
None => format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0)),
};
chain.add_filter(&fade_out, nr, filter_type);
}
}
@ -296,23 +339,39 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
let mut scale = String::new();
if !config.processing.logo_scale.is_empty() {
scale = format!(",scale={}", config.processing.logo_scale);
scale = match &ADVANCED_CONFIG.decoder.filters.overlay_logo_scale {
Some(logo_scale) => custom_format(logo_scale, &[&config.processing.logo_scale]),
None => format!(",scale={}", config.processing.logo_scale),
}
}
let mut logo_chain = format!(
let mut logo_chain = match &ADVANCED_CONFIG.decoder.filters.overlay_logo {
Some(overlay) => custom_format(overlay, &[
&config.processing.logo.replace('\\', "/").replace(':', "\\\\:"),
&config.processing.logo_opacity.to_string(),
&scale.to_string(),
&config.processing.logo_filter,
]),
None => format!(
"null[v];movie={}:loop=0,setpts=N/(FRAME_RATE*TB),format=rgba,colorchannelmixer=aa={}{scale}[l];[v][l]{}:shortest=1",
config.processing.logo.replace('\\', "/").replace(':', "\\\\:"), config.processing.logo_opacity, config.processing.logo_filter
);
)
};
if node.last_ad {
logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1")
match &ADVANCED_CONFIG.decoder.filters.overlay_logo_fade_in {
Some(fade_in) => logo_chain.push_str(fade_in),
None => logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1"),
}
}
if node.next_ad {
logo_chain.push_str(&format!(
",fade=out:st={}:d=1.0:alpha=1",
node.out - node.seek - 1.0
))
let length = node.out - node.seek - 1.0;
match &ADVANCED_CONFIG.decoder.filters.overlay_logo_fade_out {
Some(fade_out) => logo_chain.push_str(&custom_format(fade_out, &[length])),
None => logo_chain.push_str(&format!(",fade=out:st={length}:d=1.0:alpha=1")),
}
}
chain.add_filter(&logo_chain, 0, Video);
@ -328,14 +387,14 @@ fn extend_video(node: &mut Media, chain: &mut Filters) {
.and_then(|v| v.parse::<f64>().ok())
{
if node.out - node.seek > video_duration - node.seek + 0.1 && node.duration >= node.out {
chain.add_filter(
&format!(
"tpad=stop_mode=add:stop_duration={}",
(node.out - node.seek) - (video_duration - node.seek)
),
0,
Video,
)
let duration = (node.out - node.seek) - (video_duration - node.seek);
let tpad = match &ADVANCED_CONFIG.decoder.filters.tpad {
Some(pad) => custom_format(pad, &[duration]),
None => format!("tpad=stop_mode=add:stop_duration={duration}"),
};
chain.add_filter(&tpad, 0, Video)
}
}
}
@ -357,10 +416,14 @@ fn add_text(
}
fn add_audio(node: &Media, chain: &mut Filters, nr: i32) {
let audio = format!(
let audio = match &ADVANCED_CONFIG.decoder.filters.aevalsrc {
Some(aevalsrc) => custom_format(aevalsrc, &[node.out - node.seek]),
None => format!(
"aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000",
node.out - node.seek
);
),
};
chain.add_filter(&audio, nr, Audio);
}
@ -375,11 +438,12 @@ fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) {
{
if node.out - node.seek > audio_duration - node.seek + 0.1 && node.duration >= node.out
{
chain.add_filter(
&format!("apad=whole_dur={}", node.out - node.seek),
nr,
Audio,
)
let apad = match &ADVANCED_CONFIG.decoder.filters.apad {
Some(apad) => custom_format(apad, &[node.out - node.seek]),
None => format!("apad=whole_dur={}", node.out - node.seek),
};
chain.add_filter(&apad, nr, Audio)
}
}
}
@ -387,7 +451,12 @@ fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) {
fn audio_volume(chain: &mut Filters, config: &PlayoutConfig, nr: i32) {
if config.processing.volume != 1.0 {
chain.add_filter(&format!("volume={}", config.processing.volume), nr, Audio)
let volume = match &ADVANCED_CONFIG.decoder.filters.volume {
Some(volume) => custom_format(volume, &[config.processing.volume]),
None => format!("volume={}", config.processing.volume),
};
chain.add_filter(&volume, nr, Audio)
}
}
@ -418,8 +487,12 @@ pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: Fil
}
}
let split_filter = format!("split={count}{}", out_link.join(""));
chain.add_filter(&split_filter, nr, filter_type);
let split = match &ADVANCED_CONFIG.decoder.filters.split {
Some(split) => custom_format(split, &[count.to_string(), out_link.join("")]),
None => format!("split={count}{}", out_link.join("")),
};
chain.add_filter(&split, nr, filter_type);
}
}

View File

@ -6,7 +6,8 @@ use std::{
use regex::Regex;
use crate::utils::{controller::ProcessUnit::*, Media, PlayoutConfig};
use crate::utils::{controller::ProcessUnit::*, custom_format, Media, PlayoutConfig};
use crate::ADVANCED_CONFIG;
pub fn filter_node(
config: &PlayoutConfig,
@ -43,7 +44,11 @@ pub fn filter_node(
.replace('\'', "'\\\\\\''")
.replace('%', "\\\\\\%")
.replace(':', "\\:");
filter = format!("drawtext=text='{escaped_text}':{}{font}", config.text.style)
filter = match &ADVANCED_CONFIG.decoder.filters.drawtext_from_file {
Some(drawtext) => custom_format(drawtext, &[&escaped_text, &config.text.style, &font]),
None => format!("drawtext=text='{escaped_text}':{}{font}", config.text.style),
};
} else if let Some(socket) = zmq_socket {
let mut filter_cmd = format!("text=''{font}");
@ -53,10 +58,13 @@ pub fn filter_node(
}
}
filter = format!(
filter = match &ADVANCED_CONFIG.decoder.filters.drawtext_from_zmq {
Some(drawtext) => custom_format(drawtext, &[&socket.replace(':', "\\:"), &filter_cmd]),
None => format!(
"zmq=b=tcp\\\\://'{}',drawtext@dyntext={filter_cmd}",
socket.replace(':', "\\:")
)
),
};
}
filter

View File

@ -1,6 +1,16 @@
use std::sync::Arc;
extern crate log;
extern crate simplelog;
use lazy_static::lazy_static;
pub mod filter;
pub mod macros;
pub mod utils;
use utils::advanced_config::AdvancedConfig;
lazy_static! {
pub static ref ADVANCED_CONFIG: Arc<AdvancedConfig> = Arc::new(AdvancedConfig::new());
}

View File

@ -0,0 +1,102 @@
use std::{
env,
fs::File,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use shlex::split;
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct AdvancedConfig {
pub help: Option<String>,
pub decoder: DecoderConfig,
pub encoder: EncoderConfig,
pub ingest: IngestConfig,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct DecoderConfig {
pub input_param: Option<String>,
pub output_param: Option<String>,
pub filters: Filters,
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
#[serde(skip_serializing, skip_deserializing)]
pub output_cmd: Option<Vec<String>>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct EncoderConfig {
pub input_param: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct IngestConfig {
pub input_param: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Filters {
pub deinterlace: Option<String>,
pub pad_scale_w: Option<String>,
pub pad_scale_h: Option<String>,
pub pad_video: Option<String>,
pub fps: Option<String>,
pub scale: Option<String>,
pub set_dar: Option<String>,
pub fade_in: Option<String>,
pub fade_out: Option<String>,
pub overlay_logo_scale: Option<String>,
pub overlay_logo: Option<String>,
pub overlay_logo_fade_in: Option<String>,
pub overlay_logo_fade_out: Option<String>,
pub tpad: Option<String>,
pub drawtext_from_file: Option<String>,
pub drawtext_from_zmq: Option<String>,
pub aevalsrc: Option<String>,
pub apad: Option<String>,
pub volume: Option<String>,
pub split: Option<String>,
}
impl AdvancedConfig {
pub fn new() -> Self {
let mut config: AdvancedConfig = Default::default();
let mut config_path = PathBuf::from("/etc/ffplayout/advanced.yml");
if !config_path.is_file() {
if Path::new("./assets/advanced.yml").is_file() {
config_path = PathBuf::from("./assets/advanced.yml")
} else if let Some(p) = env::current_exe().ok().as_ref().and_then(|op| op.parent()) {
config_path = p.join("advanced.yml")
};
}
if let Ok(f) = File::open(&config_path) {
config = serde_yaml::from_reader(f).expect("Could not read advanced config file");
if let Some(input_parm) = &config.decoder.input_param {
config.decoder.input_cmd = split(input_parm);
}
if let Some(output_param) = &config.decoder.output_param {
config.decoder.output_cmd = split(output_param);
}
if let Some(input_param) = &config.encoder.input_param {
config.encoder.input_cmd = split(input_param);
}
if let Some(input_param) = &config.ingest.input_param {
config.ingest.input_cmd = split(input_param);
}
};
config
}
}

View File

@ -11,6 +11,8 @@ use log::LevelFilter;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use shlex::split;
use crate::ADVANCED_CONFIG;
use super::vec_strings;
use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*};
@ -375,7 +377,7 @@ impl PlayoutConfig {
let f = match File::open(&config_path) {
Ok(file) => file,
Err(_) => {
println!(
eprintln!(
"{config_path:?} doesn't exists!\nPut \"ffplayout.yml\" in \"/etc/playout/\" or beside the executable!"
);
process::exit(1);
@ -424,6 +426,19 @@ impl PlayoutConfig {
config.processing.audio_tracks = 1
}
let mut process_cmd = vec_strings![];
if config.processing.audio_only {
process_cmd.append(&mut vec_strings!["-vn"]);
} else if config.processing.copy_video {
process_cmd.append(&mut vec_strings!["-c:v", "copy"]);
} else if let Some(decoder_cmd) = &ADVANCED_CONFIG.decoder.output_cmd {
if !decoder_cmd.contains(&"-r".to_string()) {
process_cmd.append(&mut vec_strings!["-r", &config.processing.fps]);
}
process_cmd.append(&mut decoder_cmd.clone());
} else {
let bitrate = format!(
"{}k",
config.processing.width * config.processing.height / 16
@ -434,13 +449,6 @@ impl PlayoutConfig {
(config.processing.width * config.processing.height / 16) / 2
);
let mut process_cmd = vec_strings![];
if config.processing.audio_only {
process_cmd.append(&mut vec_strings!["-vn"]);
} else if config.processing.copy_video {
process_cmd.append(&mut vec_strings!["-c:v", "copy"]);
} else {
process_cmd.append(&mut vec_strings![
"-pix_fmt",
"yuv420p",
@ -463,7 +471,7 @@ impl PlayoutConfig {
if config.processing.copy_audio {
process_cmd.append(&mut vec_strings!["-c:a", "copy"]);
} else {
} else if ADVANCED_CONFIG.decoder.output_cmd.is_none() {
process_cmd.append(&mut pre_audio_codec(
&config.processing.custom_filter,
&config.ingest.custom_filter,

View File

@ -272,8 +272,9 @@ pub fn generate_playlist(
let mut playlist = JsonPlaylist {
channel: channel.clone(),
date,
current_file: None,
path: None,
start_sec: None,
length: None,
modified: None,
program: vec![],
};

View File

@ -19,8 +19,9 @@ pub fn import_file(
let mut playlist = JsonPlaylist {
channel: channel_name.unwrap_or_else(|| "Channel 1".to_string()),
date: date.to_string(),
current_file: None,
path: None,
start_sec: None,
length: None,
modified: None,
program: vec![],
};

View File

@ -9,8 +9,8 @@ use std::{
use simplelog::*;
use crate::utils::{
controller::ProcessUnit::*, get_date, is_remote, modified_time, time_from_header,
validate_playlist, Media, PlayerControl, PlayoutConfig, DUMMY_LEN,
get_date, is_remote, modified_time, time_from_header, validate_playlist, Media, PlayerControl,
PlayoutConfig, DUMMY_LEN,
};
/// This is our main playlist object, it holds all necessary information for the current day.
@ -24,7 +24,10 @@ pub struct JsonPlaylist {
pub start_sec: Option<f64>,
#[serde(skip_serializing, skip_deserializing)]
pub current_file: Option<String>,
pub length: Option<f64>,
#[serde(skip_serializing, skip_deserializing)]
pub path: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub modified: Option<String>,
@ -33,7 +36,7 @@ pub struct JsonPlaylist {
}
impl JsonPlaylist {
fn new(date: String, start: f64) -> Self {
pub fn new(date: String, start: f64) -> Self {
let mut media = Media::new(0, "", false);
media.begin = Some(start);
media.duration = DUMMY_LEN;
@ -42,7 +45,8 @@ impl JsonPlaylist {
channel: "Channel 1".into(),
date,
start_sec: Some(start),
current_file: None,
length: Some(86400.0),
path: None,
modified: None,
program: vec![media],
}
@ -61,13 +65,9 @@ fn default_channel() -> String {
"Channel 1".to_string()
}
fn set_defaults(
mut playlist: JsonPlaylist,
current_file: String,
mut start_sec: f64,
) -> JsonPlaylist {
playlist.current_file = Some(current_file);
playlist.start_sec = Some(start_sec);
pub fn set_defaults(playlist: &mut JsonPlaylist) {
let mut start_sec = playlist.start_sec.unwrap();
let mut length = 0.0;
// Add extra values to every media clip
for (i, item) in playlist.program.iter_mut().enumerate() {
@ -78,69 +78,18 @@ fn set_defaults(
item.process = Some(true);
item.filter = None;
start_sec += item.out - item.seek;
let dur = item.out - item.seek;
start_sec += dur;
length += dur;
}
playlist
}
fn loop_playlist(
config: &PlayoutConfig,
current_file: String,
mut playlist: JsonPlaylist,
) -> JsonPlaylist {
let start_sec = config.playlist.start_sec.unwrap();
let mut begin = start_sec;
let length = config.playlist.length_sec.unwrap();
let mut program_list = vec![];
let mut index = 0;
playlist.current_file = Some(current_file);
playlist.start_sec = Some(start_sec);
'program_looper: loop {
for item in playlist.program.iter() {
let media = Media {
index: Some(index),
begin: Some(begin),
seek: item.seek,
out: item.out,
duration: item.duration,
duration_audio: item.duration_audio,
category: item.category.clone(),
source: item.source.clone(),
audio: item.audio.clone(),
cmd: item.cmd.clone(),
probe: item.probe.clone(),
probe_audio: item.probe_audio.clone(),
process: Some(true),
unit: Decoder,
last_ad: false,
next_ad: false,
filter: None,
custom_filter: String::new(),
};
if begin < start_sec + length {
program_list.push(media);
} else {
break 'program_looper;
}
begin += item.out - item.seek;
index += 1;
}
}
playlist.program = program_list;
playlist
playlist.length = Some(length)
}
/// Read json playlist file, fills JsonPlaylist struct and set some extra values,
/// which we need to process.
pub fn read_json(
config: &PlayoutConfig,
config: &mut PlayoutConfig,
player_control: &PlayerControl,
path: Option<String>,
is_terminated: Arc<AtomicBool>,
@ -179,6 +128,8 @@ pub fn read_json(
if let Ok(body) = resp.text() {
let mut playlist: JsonPlaylist =
serde_json::from_str(&body).expect("Could't read remote json playlist.");
playlist.path = Some(current_file);
playlist.start_sec = Some(start_sec);
if let Some(time) = time_from_header(&headers) {
playlist.modified = Some(time.to_string());
@ -197,10 +148,9 @@ pub fn read_json(
});
}
match config.playlist.infinit {
true => return loop_playlist(config, current_file, playlist),
false => return set_defaults(playlist, current_file, start_sec),
}
set_defaults(&mut playlist);
return playlist;
}
}
}
@ -225,6 +175,8 @@ pub fn read_json(
playlist = JsonPlaylist::new(date, start_sec)
}
playlist.path = Some(current_file);
playlist.start_sec = Some(start_sec);
playlist.modified = modified;
let list_clone = playlist.clone();
@ -235,10 +187,9 @@ pub fn read_json(
});
}
match config.playlist.infinit {
true => return loop_playlist(config, current_file, playlist),
false => return set_defaults(playlist, current_file, start_sec),
}
set_defaults(&mut playlist);
return playlist;
}
error!("Playlist <b><magenta>{current_file}</></b> not exist!");

View File

@ -236,5 +236,16 @@ pub fn validate_playlist(
);
}
debug!("Validation done, in {:.3?} ...", timer.elapsed(),);
if config.general.validate {
info!(
"[Validation] Playlist length: <yellow>{}</>",
sec_to_time(begin - config.playlist.start_sec.unwrap())
);
}
debug!(
"Validation done, in <yellow>{:.3?}</>, playlist length: <yellow>{}</> ...",
timer.elapsed(),
sec_to_time(begin - config.playlist.start_sec.unwrap())
);
}

View File

@ -218,7 +218,7 @@ pub fn init_logging(
} else if app_config.path.is_file() {
log_path = app_config.path
} else {
println!("Logging path not exists!")
eprintln!("Logging path not exists!")
}
let log_file = FileRotate::new(

View File

@ -1,18 +1,18 @@
use std::{
ffi::OsStr,
fmt,
fs::{self, metadata, File},
io::{BufRead, BufReader, Error},
net::TcpListener,
path::{Path, PathBuf},
process::{exit, ChildStderr, Command, Stdio},
sync::{Arc, Mutex},
time::{self, UNIX_EPOCH},
};
#[cfg(not(windows))]
use std::env;
use chrono::{prelude::*, Duration};
use chrono::{prelude::*, TimeDelta};
use ffprobe::{ffprobe, Stream as FFStream};
use rand::prelude::*;
use regex::Regex;
@ -21,6 +21,7 @@ use serde::{de::Deserializer, Deserialize, Serialize};
use serde_json::json;
use simplelog::*;
pub mod advanced_config;
pub mod config;
pub mod controller;
pub mod errors;
@ -359,11 +360,15 @@ pub fn get_date(seek: bool, start: f64, get_next: bool) -> String {
let local: DateTime<Local> = time_now();
if seek && start > time_in_seconds() {
return (local - Duration::days(1)).format("%Y-%m-%d").to_string();
return (local - TimeDelta::try_days(1).unwrap())
.format("%Y-%m-%d")
.to_string();
}
if start == 0.0 && get_next && time_in_seconds() > 86397.9 {
return (local + Duration::days(1)).format("%Y-%m-%d").to_string();
return (local + TimeDelta::try_days(1).unwrap())
.format("%Y-%m-%d")
.to_string();
}
local.format("%Y-%m-%d").to_string()
@ -421,11 +426,12 @@ pub fn time_to_sec(time_str: &str) -> f64 {
/// Convert floating number (seconds) to a formatted time string.
pub fn sec_to_time(sec: f64) -> String {
let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64);
// Create DateTime from SystemTime
let date_time = DateTime::<Utc>::from(d);
date_time.format("%H:%M:%S%.3f").to_string()
format!(
"{:0>2}:{:0>2}:{:06.3}",
(sec / 60.0 / 60.0) as i32,
(sec / 60.0 % 60.0) as i32,
(sec % 60.0),
)
}
/// get file extension
@ -457,22 +463,27 @@ pub fn sum_durations(clip_list: &Vec<Media>) -> f64 {
pub fn get_delta(config: &PlayoutConfig, begin: &f64) -> (f64, f64) {
let mut current_time = time_in_seconds();
let start = config.playlist.start_sec.unwrap();
let length = time_to_sec(&config.playlist.length);
let length = config.playlist.length_sec.unwrap_or(86400.0);
let mut target_length = 86400.0;
if length > 0.0 && length != target_length {
target_length = length
}
if begin == &start && start == 0.0 && 86400.0 - current_time < 4.0 {
current_time -= target_length
current_time -= 86400.0
} else if start >= current_time && begin != &start {
current_time += target_length
current_time += 86400.0
}
let mut current_delta = begin - current_time;
if is_close(current_delta, 86400.0, config.general.stop_threshold) {
current_delta -= 86400.0
if is_close(
current_delta.abs(),
86400.0,
config.general.stop_threshold + 2.0,
) {
current_delta = current_delta.abs() - 86400.0
}
let total_delta = if current_time < start {
@ -910,7 +921,11 @@ pub fn get_date_range(date_range: &[String]) -> Vec<String> {
let days = duration.num_days() + 1;
for day in 0..days {
range.push((start + Duration::days(day)).format("%Y-%m-%d").to_string());
range.push(
(start + TimeDelta::try_days(day).unwrap())
.format("%Y-%m-%d")
.to_string(),
);
}
range
@ -928,6 +943,46 @@ pub fn parse_log_level_filter(s: &str) -> Result<LevelFilter, &'static str> {
}
}
pub fn custom_format<T: fmt::Display>(template: &str, args: &[T]) -> String {
let mut filled_template = String::new();
let mut arg_iter = args.iter().map(|x| format!("{}", x));
let mut template_iter = template.chars();
while let Some(c) = template_iter.next() {
if c == '{' {
if let Some(nc) = template_iter.next() {
if nc == '{' {
filled_template.push('{');
} else if nc == '}' {
if let Some(arg) = arg_iter.next() {
filled_template.push_str(&arg);
} else {
filled_template.push(c);
filled_template.push(nc);
}
} else if let Some(n) = nc.to_digit(10) {
filled_template.push_str(&args[n as usize].to_string());
} else {
filled_template.push(nc);
}
}
} else if c == '}' {
if let Some(nc) = template_iter.next() {
if nc == '}' {
filled_template.push('}');
continue;
} else {
filled_template.push(nc);
}
}
} else {
filled_template.push(c);
}
}
filled_template
}
pub fn home_dir() -> Option<PathBuf> {
home_dir_inner()
}
@ -954,7 +1009,7 @@ pub mod mock_time {
use std::cell::RefCell;
thread_local! {
static DATE_TIME_DIFF: RefCell<Option<Duration>> = RefCell::new(None);
static DATE_TIME_DIFF: RefCell<Option<TimeDelta>> = const { RefCell::new(None) };
}
pub fn time_now() -> DateTime<Local> {

View File

@ -36,7 +36,7 @@ for target in "${targets[@]}"; do
cp ./target/${target}/release/ffpapi.exe .
cp ./target/${target}/release/ffplayout.exe .
zip -r "ffplayout-v${version}_${target}.zip" assets docker docs LICENSE README.md CHANGELOG.md ffplayout.exe ffpapi.exe -x *.db
zip -r "ffplayout-v${version}_${target}.zip" assets docker docs LICENSE README.md CHANGELOG.md ffplayout.exe ffpapi.exe -x *.db -x *.db-shm -x *.db-wal -x '11-ffplayout' -x *.service
rm -f ffplayout.exe ffpapi.exe
elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then
if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then
@ -54,7 +54,7 @@ for target in "${targets[@]}"; do
cp ./target/${target}/release/ffpapi .
cp ./target/${target}/release/ffplayout .
tar -czvf "ffplayout-v${version}_${target}.tar.gz" --exclude='*.db' --exclude='*.db-shm' --exclude='*.db-wal' assets docker docs LICENSE README.md CHANGELOG.md ffplayout ffpapi
tar -czvf "ffplayout-v${version}_${target}.tar.gz" --exclude='*.db' --exclude='*.db-shm' --exclude='*.db-wal' --exclude='11-ffplayout' --exclude='*.service' assets docker docs LICENSE README.md CHANGELOG.md ffplayout ffpapi
rm -f ffplayout ffpapi
else
if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then
@ -72,11 +72,11 @@ for target in "${targets[@]}"; do
echo ""
done
if [[ "${#targets[@]}" == "3" ]] || [[ $targets == "x86_64-unknown-linux-musl" ]]; then
if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "x86_64-unknown-linux-musl" ]]; then
cargo deb --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_amd64.deb
cargo generate-rpm --payload-compress none --target=x86_64-unknown-linux-musl -p ffplayout-engine -o ffplayout-${version}-1.x86_64.rpm
fi
if [[ "${#targets[@]}" == "3" ]] || [[ $targets == "aarch64-unknown-linux-gnu" ]]; then
if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "aarch64-unknown-linux-gnu" ]]; then
cargo deb --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_arm64.deb
fi

View File

@ -20,7 +20,7 @@ lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transpor
log = "0.4"
rand = "0.8"
regex = "1"
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"

View File

@ -0,0 +1,288 @@
{
"channel": "Test 1",
"date": "2024-02-01",
"program": [
{
"in": 0,
"out": 10.0,
"duration": 10.0,
"source": "tests/assets/media_sorted/DarkGray_00-00-10.mp4"
},
{
"in": 0,
"out": 30.0,
"duration": 30.0,
"source": "tests/assets/media_sorted/Olive_00-00-30.mp4"
},
{
"in": 0,
"out": 15.0,
"duration": 15.0,
"source": "tests/assets/media_sorted/Indigo_00-00-15.mp4"
},
{
"in": 0,
"out": 25.0,
"duration": 25.0,
"source": "tests/assets/media_sorted/DarkOrchid_00-00-25.mp4"
},
{
"in": 0,
"out": 45.0,
"duration": 45.0,
"source": "tests/assets/media_sorted/Orange_00-00-45.mp4"
},
{
"in": 0,
"out": 20.0,
"duration": 20.0,
"source": "tests/assets/media_sorted/LightGoldenRodYellow_00-00-20.mp4"
},
{
"in": 0,
"out": 30.0,
"duration": 30.0,
"source": "tests/assets/media_sorted/Cyan_00-00-30.mp4"
},
{
"in": 0,
"out": 50.0,
"duration": 50.0,
"source": "tests/assets/media_sorted/Cornsilk_00-00-50.mp4"
},
{
"in": 0,
"out": 15.0,
"duration": 15.0,
"source": "tests/assets/media_sorted/LightSeaGreen_00-00-15.mp4"
},
{
"in": 0,
"out": 30.0,
"duration": 30.0,
"source": "tests/assets/media_sorted/Yellow_00-00-30.mp4"
},
{
"in": 0,
"out": 30.0,
"duration": 30.0,
"source": "tests/assets/media_sorted/Aqua_00-00-30.mp4"
},
{
"in": 0,
"out": 1500.0,
"duration": 1500.0,
"source": "tests/assets/media_sorted/MediumSeaGreen_00-25-00.mp4"
},
{
"in": 0,
"out": 1800.0,
"duration": 1800.0,
"source": "tests/assets/media_sorted/MediumOrchid_00-30-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/Plum_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/Plum_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/Plum_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/Plum_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/Plum_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/Plum_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/Plum_01-00-00.mp4"
},
{
"in": 0,
"out": 3600.0,
"duration": 3600.0,
"source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4"
},
{
"in": 0,
"out": 1800.0,
"duration": 1800.0,
"source": "tests/assets/media_sorted/MediumOrchid_00-30-00.mp4"
},
{
"in": 0,
"out": 1500.0,
"duration": 1500.0,
"source": "tests/assets/media_sorted/MediumSeaGreen_00-25-00.mp4"
},
{
"in": 0,
"out": 10.0,
"duration": 10.0,
"source": "tests/assets/media_sorted/DarkGray_00-00-10.mp4"
},
{
"in": 0,
"out": 30.0,
"duration": 30.0,
"source": "tests/assets/media_sorted/Olive_00-00-30.mp4"
},
{
"in": 0,
"out": 15.0,
"duration": 15.0,
"source": "tests/assets/media_sorted/Indigo_00-00-15.mp4"
},
{
"in": 0,
"out": 25.0,
"duration": 25.0,
"source": "tests/assets/media_sorted/DarkOrchid_00-00-25.mp4"
},
{
"in": 0,
"out": 45.0,
"duration": 45.0,
"source": "tests/assets/media_sorted/Orange_00-00-45.mp4"
},
{
"in": 0,
"out": 20.0,
"duration": 20.0,
"source": "tests/assets/media_sorted/LightGoldenRodYellow_00-00-20.mp4"
},
{
"in": 0,
"out": 30.0,
"duration": 30.0,
"source": "tests/assets/media_sorted/Cyan_00-00-30.mp4"
},
{
"in": 0,
"out": 50.0,
"duration": 50.0,
"source": "tests/assets/media_sorted/Cornsilk_00-00-50.mp4"
},
{
"in": 0,
"out": 15.0,
"duration": 15.0,
"source": "tests/assets/media_sorted/LightSeaGreen_00-00-15.mp4"
},
{
"in": 0,
"out": 30.0,
"duration": 30.0,
"source": "tests/assets/media_sorted/Yellow_00-00-30.mp4"
}
]
}