Merge pull request #570 from jb-alvarado/master
advanced config, new behavior for infinit mode
This commit is contained in:
commit
53b101d70e
808
Cargo.lock
generated
808
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.20.5"
|
version = "0.21.0-beta1"
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
repository = "https://github.com/ffplayout/ffplayout"
|
repository = "https://github.com/ffplayout/ffplayout"
|
||||||
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]
|
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]
|
||||||
|
30
assets/advanced.yml
Normal file
30
assets/advanced.yml
Normal 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:
|
@ -34,7 +34,7 @@ logging:
|
|||||||
'backup_count' says how long log files will be saved in days. 'local_time' to
|
'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
|
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.
|
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
|
'detect_silence' logs an error message if the audio line is silent for 15
|
||||||
seconds during the validation process.
|
seconds during the validation process.
|
||||||
log_to_file: true
|
log_to_file: true
|
||||||
|
@ -35,7 +35,7 @@ path-clean = "1.0"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
relative-path = "1.8"
|
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"
|
rpassword = "7.2"
|
||||||
sanitize-filename = "0.5"
|
sanitize-filename = "0.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use actix_web::error::ErrorUnauthorized;
|
use actix_web::error::ErrorUnauthorized;
|
||||||
use actix_web::Error;
|
use actix_web::Error;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ impl Claims {
|
|||||||
id,
|
id,
|
||||||
username,
|
username,
|
||||||
role,
|
role,
|
||||||
exp: (Utc::now() + Duration::days(JWT_EXPIRATION_DAYS)).timestamp(),
|
exp: (Utc::now() + TimeDelta::try_days(JWT_EXPIRATION_DAYS).unwrap()).timestamp(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ use argon2::{
|
|||||||
password_hash::{rand_core::OsRng, PasswordHash, SaltString},
|
password_hash::{rand_core::OsRng, PasswordHash, SaltString},
|
||||||
Argon2, PasswordHasher, PasswordVerifier,
|
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 path_clean::PathClean;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -817,7 +817,7 @@ pub async fn save_playlist(
|
|||||||
data: web::Json<JsonPlaylist>,
|
data: web::Json<JsonPlaylist>,
|
||||||
) -> Result<impl Responder, ServiceError> {
|
) -> Result<impl Responder, ServiceError> {
|
||||||
match write_playlist(&pool.into_inner(), *id, data.into_inner()).await {
|
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),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -885,7 +885,7 @@ pub async fn del_playlist(
|
|||||||
params: web::Path<(i32, String)>,
|
params: web::Path<(i32, String)>,
|
||||||
) -> Result<impl Responder, ServiceError> {
|
) -> Result<impl Responder, ServiceError> {
|
||||||
match delete_playlist(&pool.into_inner(), params.0, ¶ms.1).await {
|
match delete_playlist(&pool.into_inner(), params.0, ¶ms.1).await {
|
||||||
Ok(_) => Ok(format!("Delete playlist from {} success!", params.1)),
|
Ok(m) => Ok(web::Json(m)),
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1149,7 +1149,7 @@ async fn get_program(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let date_range = get_date_range(&vec_strings![
|
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")
|
before.format("%Y-%m-%d")
|
||||||
]);
|
]);
|
||||||
@ -1194,7 +1194,8 @@ async fn get_program(
|
|||||||
program.push(p_item);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 (config, _) = playout_config(conn, &id).await?;
|
||||||
let mut playlist_path = PathBuf::from(&config.playlist.path);
|
let mut playlist_path = PathBuf::from(&config.playlist.path);
|
||||||
let d: Vec<&str> = date.split('-').collect();
|
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");
|
.with_extension("json");
|
||||||
|
|
||||||
if playlist_path.is_file() {
|
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}");
|
error!("{e}");
|
||||||
return Err(ServiceError::InternalServerError);
|
Err(ServiceError::InternalServerError)
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(format!("No playlist to delete on: {date}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ notify = "6.0"
|
|||||||
notify-debouncer-full = { version = "*", default-features = false }
|
notify-debouncer-full = { version = "*", default-features = false }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1"
|
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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
simplelog = { version = "0.12", features = ["paris"] }
|
simplelog = { version = "0.12", features = ["paris"] }
|
||||||
@ -77,6 +77,11 @@ assets = [
|
|||||||
"/etc/sudoers.d/",
|
"/etc/sudoers.d/",
|
||||||
"644",
|
"644",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"../assets/advanced.yml",
|
||||||
|
"/etc/ffplayout/",
|
||||||
|
"644",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"../assets/ffplayout.yml",
|
"../assets/ffplayout.yml",
|
||||||
"/etc/ffplayout/",
|
"/etc/ffplayout/",
|
||||||
@ -153,6 +158,11 @@ assets = [
|
|||||||
"/etc/ffplayout/",
|
"/etc/ffplayout/",
|
||||||
"644",
|
"644",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"../assets/advanced.yml",
|
||||||
|
"/etc/ffplayout/",
|
||||||
|
"644",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"../assets/logo.png",
|
"../assets/logo.png",
|
||||||
"/usr/share/ffplayout/",
|
"/usr/share/ffplayout/",
|
||||||
@ -192,6 +202,7 @@ license = "GPL-3.0"
|
|||||||
assets = [
|
assets = [
|
||||||
{ source = "../target/x86_64-unknown-linux-musl/release/ffpapi", dest = "/usr/bin/ffpapi", mode = "755" },
|
{ 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 = "../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/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/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" },
|
||||||
{ source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" },
|
{ source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" },
|
||||||
|
@ -14,7 +14,7 @@ use ffplayout_lib::{
|
|||||||
controller::ProcessUnit::*, test_tcp_port, Media, PlayoutConfig, ProcessControl,
|
controller::ProcessUnit::*, test_tcp_port, Media, PlayoutConfig, ProcessControl,
|
||||||
FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS,
|
FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS,
|
||||||
},
|
},
|
||||||
vec_strings,
|
vec_strings, ADVANCED_CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn server_monitor(
|
fn server_monitor(
|
||||||
@ -61,6 +61,10 @@ pub fn ingest_server(
|
|||||||
dummy_media.unit = Ingest;
|
dummy_media.unit = Ingest;
|
||||||
dummy_media.add_filter(&config, &None);
|
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());
|
server_cmd.append(&mut stream_input.clone());
|
||||||
|
|
||||||
if let Some(mut filter) = dummy_media.filter {
|
if let Some(mut filter) = dummy_media.filter {
|
||||||
|
@ -11,9 +11,11 @@ use serde_json::json;
|
|||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
use ffplayout_lib::utils::{
|
use ffplayout_lib::utils::{
|
||||||
controller::PlayerControl, gen_dummy, get_delta, is_close, is_remote,
|
controller::PlayerControl,
|
||||||
json_serializer::read_json, loop_filler, loop_image, modified_time, seek_and_length,
|
gen_dummy, get_delta, is_close, is_remote,
|
||||||
time_in_seconds, Media, MediaProbe, PlayoutConfig, PlayoutStatus, IMAGE_FORMAT,
|
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.
|
/// Struct for current playlist.
|
||||||
@ -24,9 +26,7 @@ pub struct CurrentProgram {
|
|||||||
config: PlayoutConfig,
|
config: PlayoutConfig,
|
||||||
start_sec: f64,
|
start_sec: f64,
|
||||||
end_sec: f64,
|
end_sec: f64,
|
||||||
json_mod: Option<String>,
|
json_playlist: JsonPlaylist,
|
||||||
json_path: Option<String>,
|
|
||||||
json_date: String,
|
|
||||||
player_control: PlayerControl,
|
player_control: PlayerControl,
|
||||||
current_node: Media,
|
current_node: Media,
|
||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
@ -47,9 +47,10 @@ impl CurrentProgram {
|
|||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
start_sec: config.playlist.start_sec.unwrap(),
|
start_sec: config.playlist.start_sec.unwrap(),
|
||||||
end_sec: config.playlist.length_sec.unwrap(),
|
end_sec: config.playlist.length_sec.unwrap(),
|
||||||
json_mod: None,
|
json_playlist: JsonPlaylist::new(
|
||||||
json_path: None,
|
"1970-01-01".to_string(),
|
||||||
json_date: String::new(),
|
config.playlist.start_sec.unwrap(),
|
||||||
|
),
|
||||||
player_control: player_control.clone(),
|
player_control: player_control.clone(),
|
||||||
current_node: Media::new(0, "", false),
|
current_node: Media::new(0, "", false),
|
||||||
is_terminated,
|
is_terminated,
|
||||||
@ -65,9 +66,9 @@ impl CurrentProgram {
|
|||||||
let mut get_current = false;
|
let mut get_current = false;
|
||||||
let mut reload = 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))
|
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>");
|
info!("Reload playlist <b><magenta>{path}</></b>");
|
||||||
self.playout_stat.list_init.store(true, Ordering::SeqCst);
|
self.playout_stat.list_init.store(true, Ordering::SeqCst);
|
||||||
@ -79,26 +80,24 @@ impl CurrentProgram {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if get_current {
|
if get_current {
|
||||||
let json = read_json(
|
self.json_playlist = read_json(
|
||||||
&self.config,
|
&mut self.config,
|
||||||
&self.player_control,
|
&self.player_control,
|
||||||
self.json_path.clone(),
|
self.json_playlist.path.clone(),
|
||||||
self.is_terminated.clone(),
|
self.is_terminated.clone(),
|
||||||
seek,
|
seek,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if !reload {
|
if !reload {
|
||||||
if let Some(file) = &json.current_file {
|
if let Some(file) = &self.json_playlist.path {
|
||||||
info!("Read playlist: <b><magenta>{file}</></b>");
|
info!("Read playlist: <b><magenta>{file}</></b>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.json_path = json.current_file;
|
*self.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone();
|
||||||
self.json_mod = json.modified;
|
|
||||||
*self.player_control.current_list.lock().unwrap() = json.program;
|
|
||||||
|
|
||||||
if self.json_path.is_none() {
|
if self.json_playlist.path.is_none() {
|
||||||
trace!("missing playlist");
|
trace!("missing playlist");
|
||||||
|
|
||||||
self.current_node = Media::new(0, "", false);
|
self.current_node = Media::new(0, "", false);
|
||||||
@ -137,15 +136,16 @@ impl CurrentProgram {
|
|||||||
trace!("next_start: {next_start}, end_sec: {}", self.end_sec);
|
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.
|
// 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, 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");
|
trace!("get next day");
|
||||||
next = true;
|
next = true;
|
||||||
|
|
||||||
let json = read_json(
|
self.json_playlist = read_json(
|
||||||
&self.config,
|
&mut self.config,
|
||||||
&self.player_control,
|
&self.player_control,
|
||||||
None,
|
None,
|
||||||
self.is_terminated.clone(),
|
self.is_terminated.clone(),
|
||||||
@ -153,18 +153,14 @@ impl CurrentProgram {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(file) = &json.current_file {
|
if let Some(file) = &self.json_playlist.path {
|
||||||
info!("Read next playlist: <b><magenta>{file}</></b>");
|
info!("Read next playlist: <b><magenta>{file}</></b>");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.playout_stat.list_init.store(false, Ordering::SeqCst);
|
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.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone();
|
||||||
self.json_mod = json.modified;
|
|
||||||
self.json_date = json.date;
|
|
||||||
|
|
||||||
*self.player_control.current_list.lock().unwrap() = json.program;
|
|
||||||
self.player_control.current_index.store(0, Ordering::SeqCst);
|
self.player_control.current_index.store(0, Ordering::SeqCst);
|
||||||
} else {
|
} else {
|
||||||
self.load_or_update_playlist(seek)
|
self.load_or_update_playlist(seek)
|
||||||
@ -212,7 +208,7 @@ impl CurrentProgram {
|
|||||||
let mut time_sec = time_in_seconds();
|
let mut time_sec = time_in_seconds();
|
||||||
|
|
||||||
if time_sec < self.start_sec {
|
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
|
time_sec
|
||||||
@ -231,6 +227,15 @@ impl CurrentProgram {
|
|||||||
time_sec += *shift;
|
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
|
for (i, item) in self
|
||||||
.player_control
|
.player_control
|
||||||
.current_list
|
.current_list
|
||||||
@ -266,14 +271,14 @@ impl CurrentProgram {
|
|||||||
// de-instance node to preserve original values in list
|
// de-instance node to preserve original values in list
|
||||||
let mut node_clone = nodes[index].clone();
|
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);
|
trace!("Clip from init: {}", node_clone.source);
|
||||||
|
|
||||||
node_clone.seek += time_sec
|
node_clone.seek += time_sec
|
||||||
- (node_clone.begin.unwrap() - *self.playout_stat.time_shift.lock().unwrap());
|
- (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.current_node = handle_list_init(
|
||||||
&self.config,
|
&self.config,
|
||||||
node_clone,
|
node_clone,
|
||||||
@ -297,7 +302,6 @@ impl CurrentProgram {
|
|||||||
|
|
||||||
fn fill_end(&mut self, total_delta: f64) {
|
fn fill_end(&mut self, total_delta: f64) {
|
||||||
// Fill end from playlist
|
// Fill end from playlist
|
||||||
|
|
||||||
let index = self.player_control.current_index.load(Ordering::SeqCst);
|
let index = self.player_control.current_index.load(Ordering::SeqCst);
|
||||||
let mut media = Media::new(index, "", false);
|
let mut media = Media::new(index, "", false);
|
||||||
media.begin = Some(time_in_seconds());
|
media.begin = Some(time_in_seconds());
|
||||||
@ -327,6 +331,20 @@ impl CurrentProgram {
|
|||||||
.current_index
|
.current_index
|
||||||
.fetch_add(1, Ordering::SeqCst);
|
.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
|
/// Build the playlist iterator
|
||||||
@ -334,7 +352,7 @@ impl Iterator for CurrentProgram {
|
|||||||
type Item = Media;
|
type Item = Media;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
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.last_node_ad = self.current_node.last_ad;
|
||||||
self.check_for_playlist(self.playout_stat.list_init.load(Ordering::SeqCst));
|
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");
|
trace!("Init playlist, from next iterator");
|
||||||
let mut init_clip_is_filler = false;
|
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();
|
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);
|
let (_, total_delta) = get_delta(&self.config, &self.start_sec);
|
||||||
|
|
||||||
if !self.config.playlist.infinit
|
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
|
&& total_delta.abs() > 1.0
|
||||||
{
|
{
|
||||||
// Playlist is to early finish,
|
// Playlist is to early finish,
|
||||||
@ -438,6 +456,10 @@ impl Iterator for CurrentProgram {
|
|||||||
|
|
||||||
drop(c_list);
|
drop(c_list);
|
||||||
|
|
||||||
|
if self.config.playlist.infinit {
|
||||||
|
self.recalculate_begin(false)
|
||||||
|
}
|
||||||
|
|
||||||
self.player_control.current_index.store(0, Ordering::SeqCst);
|
self.player_control.current_index.store(0, Ordering::SeqCst);
|
||||||
self.current_node = gen_source(
|
self.current_node = gen_source(
|
||||||
&self.config,
|
&self.config,
|
||||||
@ -502,6 +524,7 @@ fn timed_source(
|
|||||||
if (total_delta > node.out - node.seek && !last)
|
if (total_delta > node.out - node.seek && !last)
|
||||||
|| node.index.unwrap() < 2
|
|| node.index.unwrap() < 2
|
||||||
|| !config.playlist.length.contains(':')
|
|| !config.playlist.length.contains(':')
|
||||||
|
|| config.playlist.infinit
|
||||||
{
|
{
|
||||||
// when we are in the 24 hour range, get the clip
|
// when we are in the 24 hour range, get the clip
|
||||||
new_node.process = Some(true);
|
new_node.process = Some(true);
|
||||||
@ -742,7 +765,7 @@ fn handle_list_init(
|
|||||||
debug!("Playlist init");
|
debug!("Playlist init");
|
||||||
let (_, total_delta) = get_delta(config, &node.begin.unwrap());
|
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;
|
node.out = total_delta + node.seek;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,9 +103,12 @@ fn main() -> Result<(), ProcError> {
|
|||||||
let messages = Arc::new(Mutex::new(Vec::new()));
|
let messages = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
// try to create logging folder, if not exist
|
// 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) {
|
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);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use std::process::{self, Command, Stdio};
|
|||||||
|
|
||||||
use simplelog::*;
|
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
|
/// 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 {
|
pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
|
||||||
let mut enc_filter: Vec<String> = vec![];
|
let mut enc_filter: Vec<String> = vec![];
|
||||||
|
|
||||||
let mut enc_cmd = vec_strings![
|
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format];
|
||||||
"-hide_banner",
|
|
||||||
"-nostats",
|
if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd {
|
||||||
"-v",
|
enc_cmd.append(&mut encoder_input_cmd.clone());
|
||||||
log_format,
|
}
|
||||||
|
|
||||||
|
enc_cmd.append(&mut vec_strings![
|
||||||
"-i",
|
"-i",
|
||||||
"pipe:0",
|
"pipe:0",
|
||||||
"-window_title",
|
"-window_title",
|
||||||
"ffplayout"
|
"ffplayout"
|
||||||
];
|
]);
|
||||||
|
|
||||||
if let Some(mut cmd) = config.out.output_cmd.clone() {
|
if let Some(mut cmd) = config.out.output_cmd.clone() {
|
||||||
if !cmd.iter().any(|i| {
|
if !cmd.iter().any(|i| {
|
||||||
|
@ -34,7 +34,7 @@ use ffplayout_lib::{
|
|||||||
controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media,
|
controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media,
|
||||||
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
|
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
|
||||||
},
|
},
|
||||||
vec_strings,
|
vec_strings, ADVANCED_CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Ingest Server for HLS
|
/// 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 mut server_prefix = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"];
|
||||||
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
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);
|
let mut dummy_media = Media::new(0, "Live Stream", false);
|
||||||
dummy_media.unit = Ingest;
|
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;
|
let mut is_running;
|
||||||
|
|
||||||
if let Some(url) = stream_input.iter().find(|s| s.contains("://")) {
|
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];
|
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;
|
let mut read_rate = 1.0;
|
||||||
|
|
||||||
if let Some(begin) = &node.begin {
|
if let Some(begin) = &node.begin {
|
||||||
|
@ -19,11 +19,14 @@ pub use hls::write_hls;
|
|||||||
use crate::input::{ingest_server, source_generator};
|
use crate::input::{ingest_server, source_generator};
|
||||||
use crate::utils::task_runner;
|
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,
|
sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
|
||||||
ProcessControl, ProcessUnit::*,
|
ProcessControl, ProcessUnit::*,
|
||||||
|
},
|
||||||
|
ADVANCED_CONFIG,
|
||||||
};
|
};
|
||||||
use ffplayout_lib::vec_strings;
|
|
||||||
|
|
||||||
/// Player
|
/// Player
|
||||||
///
|
///
|
||||||
@ -104,8 +107,18 @@ pub fn player(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let c_index = if cfg!(debug_assertions) {
|
||||||
|
format!(
|
||||||
|
" ({}/{})",
|
||||||
|
node.index.unwrap() + 1,
|
||||||
|
play_control.current_list.lock().unwrap().len()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Play for <yellow>{}</>: <b><magenta>{} {}</></b>",
|
"Play for <yellow>{}</>{c_index}: <b><magenta>{} {}</></b>",
|
||||||
sec_to_time(node.out - node.seek),
|
sec_to_time(node.out - node.seek),
|
||||||
node.source,
|
node.source,
|
||||||
node.audio
|
node.audio
|
||||||
@ -130,6 +143,11 @@ pub fn player(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
|
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);
|
dec_cmd.append(&mut cmd);
|
||||||
|
|
||||||
if let Some(mut filter) = node.filter {
|
if let Some(mut filter) = node.filter {
|
||||||
|
@ -65,7 +65,7 @@ pub fn get_config(args: Args) -> Result<PlayoutConfig, ProcError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(log_path) = args.log {
|
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.log_to_file = true;
|
||||||
config.logging.path = log_path;
|
config.logging.path = log_path;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 737d86f901a9275b13e164378ad1c231c6370c1f
|
Subproject commit 3802e75c461eeea0febf20c1ea2c97cb89dd8d41
|
@ -14,13 +14,14 @@ crossbeam-channel = "0.5"
|
|||||||
derive_more = "0.99"
|
derive_more = "0.99"
|
||||||
ffprobe = "0.3"
|
ffprobe = "0.3"
|
||||||
file-rotate = "0.7"
|
file-rotate = "0.7"
|
||||||
|
lazy_static = "1.4"
|
||||||
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false }
|
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false }
|
||||||
lexical-sort = "0.3"
|
lexical-sort = "0.3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1"
|
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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
@ -11,8 +11,10 @@ mod custom;
|
|||||||
pub mod v_drawtext;
|
pub mod v_drawtext;
|
||||||
|
|
||||||
use crate::utils::{
|
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;
|
use super::vec_strings;
|
||||||
|
|
||||||
@ -184,7 +186,12 @@ impl Default for Filters {
|
|||||||
fn deinterlace(field_order: &Option<String>, chain: &mut Filters) {
|
fn deinterlace(field_order: &Option<String>, chain: &mut Filters) {
|
||||||
if let Some(order) = field_order {
|
if let Some(order) = field_order {
|
||||||
if order != "progressive" {
|
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 let (Some(w), Some(h)) = (v_stream.width, v_stream.height) {
|
||||||
if w > config.processing.width && aspect > config.processing.aspect {
|
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 {
|
} 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!(
|
let pad = match &ADVANCED_CONFIG.decoder.filters.pad_video {
|
||||||
"{scale}pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2",
|
Some(pad_video) => custom_format(
|
||||||
config.processing.width, config.processing.height
|
pad_video,
|
||||||
|
&[
|
||||||
|
&scale,
|
||||||
|
&config.processing.width.to_string(),
|
||||||
|
&config.processing.height.to_string(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
0,
|
None => format!(
|
||||||
Video,
|
"{}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) {
|
fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) {
|
||||||
if fps != config.processing.fps {
|
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
|
// width: i64, height: i64
|
||||||
if let (Some(w), Some(h)) = (width, height) {
|
if let (Some(w), Some(h)) = (width, height) {
|
||||||
if w != config.processing.width || h != config.processing.height {
|
if w != config.processing.width || h != config.processing.height {
|
||||||
chain.add_filter(
|
let scale = match &ADVANCED_CONFIG.decoder.filters.scale {
|
||||||
&format!(
|
Some(scale) => custom_format(
|
||||||
|
scale,
|
||||||
|
&[&config.processing.width, &config.processing.height],
|
||||||
|
),
|
||||||
|
None => format!(
|
||||||
"scale={}:{}",
|
"scale={}:{}",
|
||||||
config.processing.width, config.processing.height
|
config.processing.width, config.processing.height
|
||||||
),
|
),
|
||||||
0,
|
};
|
||||||
Video,
|
|
||||||
);
|
chain.add_filter(&scale, 0, Video);
|
||||||
} else {
|
} else {
|
||||||
chain.add_filter("null", 0, Video);
|
chain.add_filter("null", 0, Video);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_close(aspect, config.processing.aspect, 0.03) {
|
if !is_close(aspect, config.processing.aspect, 0.03) {
|
||||||
chain.add_filter(
|
let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar {
|
||||||
&format!("setdar=dar={}", config.processing.aspect),
|
Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]),
|
||||||
0,
|
None => format!("setdar=dar={}", config.processing.aspect),
|
||||||
Video,
|
};
|
||||||
)
|
|
||||||
|
chain.add_filter(&dar, 0, Video);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chain.add_filter(
|
let scale = match &ADVANCED_CONFIG.decoder.filters.scale {
|
||||||
&format!(
|
Some(scale) => custom_format(
|
||||||
|
scale,
|
||||||
|
&[&config.processing.width, &config.processing.height],
|
||||||
|
),
|
||||||
|
None => format!(
|
||||||
"scale={}:{}",
|
"scale={}:{}",
|
||||||
config.processing.width, config.processing.height
|
config.processing.width, config.processing.height
|
||||||
),
|
),
|
||||||
0,
|
};
|
||||||
Video,
|
chain.add_filter(&scale, 0, Video);
|
||||||
);
|
|
||||||
chain.add_filter(
|
let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar {
|
||||||
&format!("setdar=dar={}", config.processing.aspect),
|
Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]),
|
||||||
0,
|
None => format!("setdar=dar={}", config.processing.aspect),
|
||||||
Video,
|
};
|
||||||
)
|
|
||||||
|
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 {
|
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 {
|
if (node.out != node.duration && node.out - node.seek > 1.0) || fade_audio {
|
||||||
chain.add_filter(
|
let fade_out = match &ADVANCED_CONFIG.decoder.filters.fade_out {
|
||||||
&format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0)),
|
Some(fade) => custom_format(fade, &[t, &(node.out - node.seek - 1.0).to_string()]),
|
||||||
nr,
|
None => format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0)),
|
||||||
filter_type,
|
};
|
||||||
)
|
|
||||||
|
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();
|
let mut scale = String::new();
|
||||||
|
|
||||||
if !config.processing.logo_scale.is_empty() {
|
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",
|
"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
|
config.processing.logo.replace('\\', "/").replace(':', "\\\\:"), config.processing.logo_opacity, config.processing.logo_filter
|
||||||
);
|
)
|
||||||
|
};
|
||||||
|
|
||||||
if node.last_ad {
|
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 {
|
if node.next_ad {
|
||||||
logo_chain.push_str(&format!(
|
let length = node.out - node.seek - 1.0;
|
||||||
",fade=out:st={}:d=1.0:alpha=1",
|
|
||||||
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);
|
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())
|
.and_then(|v| v.parse::<f64>().ok())
|
||||||
{
|
{
|
||||||
if node.out - node.seek > video_duration - node.seek + 0.1 && node.duration >= node.out {
|
if node.out - node.seek > video_duration - node.seek + 0.1 && node.duration >= node.out {
|
||||||
chain.add_filter(
|
let duration = (node.out - node.seek) - (video_duration - node.seek);
|
||||||
&format!(
|
|
||||||
"tpad=stop_mode=add:stop_duration={}",
|
let tpad = match &ADVANCED_CONFIG.decoder.filters.tpad {
|
||||||
(node.out - node.seek) - (video_duration - node.seek)
|
Some(pad) => custom_format(pad, &[duration]),
|
||||||
),
|
None => format!("tpad=stop_mode=add:stop_duration={duration}"),
|
||||||
0,
|
};
|
||||||
Video,
|
|
||||||
)
|
chain.add_filter(&tpad, 0, Video)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -357,10 +416,14 @@ fn add_text(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_audio(node: &Media, chain: &mut Filters, nr: i32) {
|
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",
|
"aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000",
|
||||||
node.out - node.seek
|
node.out - node.seek
|
||||||
);
|
),
|
||||||
|
};
|
||||||
|
|
||||||
chain.add_filter(&audio, nr, Audio);
|
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
|
if node.out - node.seek > audio_duration - node.seek + 0.1 && node.duration >= node.out
|
||||||
{
|
{
|
||||||
chain.add_filter(
|
let apad = match &ADVANCED_CONFIG.decoder.filters.apad {
|
||||||
&format!("apad=whole_dur={}", node.out - node.seek),
|
Some(apad) => custom_format(apad, &[node.out - node.seek]),
|
||||||
nr,
|
None => format!("apad=whole_dur={}", node.out - node.seek),
|
||||||
Audio,
|
};
|
||||||
)
|
|
||||||
|
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) {
|
fn audio_volume(chain: &mut Filters, config: &PlayoutConfig, nr: i32) {
|
||||||
if config.processing.volume != 1.0 {
|
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(""));
|
let split = match &ADVANCED_CONFIG.decoder.filters.split {
|
||||||
chain.add_filter(&split_filter, nr, filter_type);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ use std::{
|
|||||||
|
|
||||||
use regex::Regex;
|
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(
|
pub fn filter_node(
|
||||||
config: &PlayoutConfig,
|
config: &PlayoutConfig,
|
||||||
@ -43,7 +44,11 @@ pub fn filter_node(
|
|||||||
.replace('\'', "'\\\\\\''")
|
.replace('\'', "'\\\\\\''")
|
||||||
.replace('%', "\\\\\\%")
|
.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 {
|
} else if let Some(socket) = zmq_socket {
|
||||||
let mut filter_cmd = format!("text=''{font}");
|
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}",
|
"zmq=b=tcp\\\\://'{}',drawtext@dyntext={filter_cmd}",
|
||||||
socket.replace(':', "\\:")
|
socket.replace(':', "\\:")
|
||||||
)
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
filter
|
filter
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
extern crate log;
|
extern crate log;
|
||||||
extern crate simplelog;
|
extern crate simplelog;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
use utils::advanced_config::AdvancedConfig;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref ADVANCED_CONFIG: Arc<AdvancedConfig> = Arc::new(AdvancedConfig::new());
|
||||||
|
}
|
||||||
|
102
lib/src/utils/advanced_config.rs
Normal file
102
lib/src/utils/advanced_config.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ use log::LevelFilter;
|
|||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use shlex::split;
|
use shlex::split;
|
||||||
|
|
||||||
|
use crate::ADVANCED_CONFIG;
|
||||||
|
|
||||||
use super::vec_strings;
|
use super::vec_strings;
|
||||||
use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*};
|
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) {
|
let f = match File::open(&config_path) {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!(
|
eprintln!(
|
||||||
"{config_path:?} doesn't exists!\nPut \"ffplayout.yml\" in \"/etc/playout/\" or beside the executable!"
|
"{config_path:?} doesn't exists!\nPut \"ffplayout.yml\" in \"/etc/playout/\" or beside the executable!"
|
||||||
);
|
);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
@ -424,6 +426,19 @@ impl PlayoutConfig {
|
|||||||
config.processing.audio_tracks = 1
|
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!(
|
let bitrate = format!(
|
||||||
"{}k",
|
"{}k",
|
||||||
config.processing.width * config.processing.height / 16
|
config.processing.width * config.processing.height / 16
|
||||||
@ -434,13 +449,6 @@ impl PlayoutConfig {
|
|||||||
(config.processing.width * config.processing.height / 16) / 2
|
(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![
|
process_cmd.append(&mut vec_strings![
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p",
|
"yuv420p",
|
||||||
@ -463,7 +471,7 @@ impl PlayoutConfig {
|
|||||||
|
|
||||||
if config.processing.copy_audio {
|
if config.processing.copy_audio {
|
||||||
process_cmd.append(&mut vec_strings!["-c:a", "copy"]);
|
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(
|
process_cmd.append(&mut pre_audio_codec(
|
||||||
&config.processing.custom_filter,
|
&config.processing.custom_filter,
|
||||||
&config.ingest.custom_filter,
|
&config.ingest.custom_filter,
|
||||||
|
@ -272,8 +272,9 @@ pub fn generate_playlist(
|
|||||||
let mut playlist = JsonPlaylist {
|
let mut playlist = JsonPlaylist {
|
||||||
channel: channel.clone(),
|
channel: channel.clone(),
|
||||||
date,
|
date,
|
||||||
current_file: None,
|
path: None,
|
||||||
start_sec: None,
|
start_sec: None,
|
||||||
|
length: None,
|
||||||
modified: None,
|
modified: None,
|
||||||
program: vec![],
|
program: vec![],
|
||||||
};
|
};
|
||||||
|
@ -19,8 +19,9 @@ pub fn import_file(
|
|||||||
let mut playlist = JsonPlaylist {
|
let mut playlist = JsonPlaylist {
|
||||||
channel: channel_name.unwrap_or_else(|| "Channel 1".to_string()),
|
channel: channel_name.unwrap_or_else(|| "Channel 1".to_string()),
|
||||||
date: date.to_string(),
|
date: date.to_string(),
|
||||||
current_file: None,
|
path: None,
|
||||||
start_sec: None,
|
start_sec: None,
|
||||||
|
length: None,
|
||||||
modified: None,
|
modified: None,
|
||||||
program: vec![],
|
program: vec![],
|
||||||
};
|
};
|
||||||
|
@ -9,8 +9,8 @@ use std::{
|
|||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
controller::ProcessUnit::*, get_date, is_remote, modified_time, time_from_header,
|
get_date, is_remote, modified_time, time_from_header, validate_playlist, Media, PlayerControl,
|
||||||
validate_playlist, Media, PlayerControl, PlayoutConfig, DUMMY_LEN,
|
PlayoutConfig, DUMMY_LEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This is our main playlist object, it holds all necessary information for the current day.
|
/// 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>,
|
pub start_sec: Option<f64>,
|
||||||
|
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[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)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub modified: Option<String>,
|
pub modified: Option<String>,
|
||||||
@ -33,7 +36,7 @@ pub struct JsonPlaylist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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);
|
let mut media = Media::new(0, "", false);
|
||||||
media.begin = Some(start);
|
media.begin = Some(start);
|
||||||
media.duration = DUMMY_LEN;
|
media.duration = DUMMY_LEN;
|
||||||
@ -42,7 +45,8 @@ impl JsonPlaylist {
|
|||||||
channel: "Channel 1".into(),
|
channel: "Channel 1".into(),
|
||||||
date,
|
date,
|
||||||
start_sec: Some(start),
|
start_sec: Some(start),
|
||||||
current_file: None,
|
length: Some(86400.0),
|
||||||
|
path: None,
|
||||||
modified: None,
|
modified: None,
|
||||||
program: vec![media],
|
program: vec![media],
|
||||||
}
|
}
|
||||||
@ -61,13 +65,9 @@ fn default_channel() -> String {
|
|||||||
"Channel 1".to_string()
|
"Channel 1".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_defaults(
|
pub fn set_defaults(playlist: &mut JsonPlaylist) {
|
||||||
mut playlist: JsonPlaylist,
|
let mut start_sec = playlist.start_sec.unwrap();
|
||||||
current_file: String,
|
let mut length = 0.0;
|
||||||
mut start_sec: f64,
|
|
||||||
) -> JsonPlaylist {
|
|
||||||
playlist.current_file = Some(current_file);
|
|
||||||
playlist.start_sec = Some(start_sec);
|
|
||||||
|
|
||||||
// Add extra values to every media clip
|
// Add extra values to every media clip
|
||||||
for (i, item) in playlist.program.iter_mut().enumerate() {
|
for (i, item) in playlist.program.iter_mut().enumerate() {
|
||||||
@ -78,69 +78,18 @@ fn set_defaults(
|
|||||||
item.process = Some(true);
|
item.process = Some(true);
|
||||||
item.filter = None;
|
item.filter = None;
|
||||||
|
|
||||||
start_sec += item.out - item.seek;
|
let dur = item.out - item.seek;
|
||||||
|
start_sec += dur;
|
||||||
|
length += dur;
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist
|
playlist.length = Some(length)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read json playlist file, fills JsonPlaylist struct and set some extra values,
|
/// Read json playlist file, fills JsonPlaylist struct and set some extra values,
|
||||||
/// which we need to process.
|
/// which we need to process.
|
||||||
pub fn read_json(
|
pub fn read_json(
|
||||||
config: &PlayoutConfig,
|
config: &mut PlayoutConfig,
|
||||||
player_control: &PlayerControl,
|
player_control: &PlayerControl,
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
@ -179,6 +128,8 @@ pub fn read_json(
|
|||||||
if let Ok(body) = resp.text() {
|
if let Ok(body) = resp.text() {
|
||||||
let mut playlist: JsonPlaylist =
|
let mut playlist: JsonPlaylist =
|
||||||
serde_json::from_str(&body).expect("Could't read remote json playlist.");
|
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) {
|
if let Some(time) = time_from_header(&headers) {
|
||||||
playlist.modified = Some(time.to_string());
|
playlist.modified = Some(time.to_string());
|
||||||
@ -197,10 +148,9 @@ pub fn read_json(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
match config.playlist.infinit {
|
set_defaults(&mut playlist);
|
||||||
true => return loop_playlist(config, current_file, playlist),
|
|
||||||
false => return set_defaults(playlist, current_file, start_sec),
|
return playlist;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,6 +175,8 @@ pub fn read_json(
|
|||||||
playlist = JsonPlaylist::new(date, start_sec)
|
playlist = JsonPlaylist::new(date, start_sec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playlist.path = Some(current_file);
|
||||||
|
playlist.start_sec = Some(start_sec);
|
||||||
playlist.modified = modified;
|
playlist.modified = modified;
|
||||||
|
|
||||||
let list_clone = playlist.clone();
|
let list_clone = playlist.clone();
|
||||||
@ -235,10 +187,9 @@ pub fn read_json(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
match config.playlist.infinit {
|
set_defaults(&mut playlist);
|
||||||
true => return loop_playlist(config, current_file, playlist),
|
|
||||||
false => return set_defaults(playlist, current_file, start_sec),
|
return playlist;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error!("Playlist <b><magenta>{current_file}</></b> not exist!");
|
error!("Playlist <b><magenta>{current_file}</></b> not exist!");
|
||||||
|
@ -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())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ pub fn init_logging(
|
|||||||
} else if app_config.path.is_file() {
|
} else if app_config.path.is_file() {
|
||||||
log_path = app_config.path
|
log_path = app_config.path
|
||||||
} else {
|
} else {
|
||||||
println!("Logging path not exists!")
|
eprintln!("Logging path not exists!")
|
||||||
}
|
}
|
||||||
|
|
||||||
let log_file = FileRotate::new(
|
let log_file = FileRotate::new(
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
|
fmt,
|
||||||
fs::{self, metadata, File},
|
fs::{self, metadata, File},
|
||||||
io::{BufRead, BufReader, Error},
|
io::{BufRead, BufReader, Error},
|
||||||
net::TcpListener,
|
net::TcpListener,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{exit, ChildStderr, Command, Stdio},
|
process::{exit, ChildStderr, Command, Stdio},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::{self, UNIX_EPOCH},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use chrono::{prelude::*, Duration};
|
use chrono::{prelude::*, TimeDelta};
|
||||||
use ffprobe::{ffprobe, Stream as FFStream};
|
use ffprobe::{ffprobe, Stream as FFStream};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@ -21,6 +21,7 @@ use serde::{de::Deserializer, Deserialize, Serialize};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
|
pub mod advanced_config;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod controller;
|
pub mod controller;
|
||||||
pub mod errors;
|
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();
|
let local: DateTime<Local> = time_now();
|
||||||
|
|
||||||
if seek && start > time_in_seconds() {
|
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 {
|
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()
|
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.
|
/// Convert floating number (seconds) to a formatted time string.
|
||||||
pub fn sec_to_time(sec: f64) -> String {
|
pub fn sec_to_time(sec: f64) -> String {
|
||||||
let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64);
|
format!(
|
||||||
// Create DateTime from SystemTime
|
"{:0>2}:{:0>2}:{:06.3}",
|
||||||
let date_time = DateTime::<Utc>::from(d);
|
(sec / 60.0 / 60.0) as i32,
|
||||||
|
(sec / 60.0 % 60.0) as i32,
|
||||||
date_time.format("%H:%M:%S%.3f").to_string()
|
(sec % 60.0),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get file extension
|
/// 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) {
|
pub fn get_delta(config: &PlayoutConfig, begin: &f64) -> (f64, f64) {
|
||||||
let mut current_time = time_in_seconds();
|
let mut current_time = time_in_seconds();
|
||||||
let start = config.playlist.start_sec.unwrap();
|
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;
|
let mut target_length = 86400.0;
|
||||||
|
|
||||||
if length > 0.0 && length != target_length {
|
if length > 0.0 && length != target_length {
|
||||||
target_length = length
|
target_length = length
|
||||||
}
|
}
|
||||||
|
|
||||||
if begin == &start && start == 0.0 && 86400.0 - current_time < 4.0 {
|
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 {
|
} else if start >= current_time && begin != &start {
|
||||||
current_time += target_length
|
current_time += 86400.0
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut current_delta = begin - current_time;
|
let mut current_delta = begin - current_time;
|
||||||
|
|
||||||
if is_close(current_delta, 86400.0, config.general.stop_threshold) {
|
if is_close(
|
||||||
current_delta -= 86400.0
|
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 {
|
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;
|
let days = duration.num_days() + 1;
|
||||||
|
|
||||||
for day in 0..days {
|
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
|
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> {
|
pub fn home_dir() -> Option<PathBuf> {
|
||||||
home_dir_inner()
|
home_dir_inner()
|
||||||
}
|
}
|
||||||
@ -954,7 +1009,7 @@ pub mod mock_time {
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
thread_local! {
|
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> {
|
pub fn time_now() -> DateTime<Local> {
|
||||||
|
@ -36,7 +36,7 @@ for target in "${targets[@]}"; do
|
|||||||
|
|
||||||
cp ./target/${target}/release/ffpapi.exe .
|
cp ./target/${target}/release/ffpapi.exe .
|
||||||
cp ./target/${target}/release/ffplayout.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
|
rm -f ffplayout.exe ffpapi.exe
|
||||||
elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then
|
elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then
|
||||||
if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; 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/ffpapi .
|
||||||
cp ./target/${target}/release/ffplayout .
|
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
|
rm -f ffplayout ffpapi
|
||||||
else
|
else
|
||||||
if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then
|
if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then
|
||||||
@ -72,11 +72,11 @@ for target in "${targets[@]}"; do
|
|||||||
echo ""
|
echo ""
|
||||||
done
|
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 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
|
cargo generate-rpm --payload-compress none --target=x86_64-unknown-linux-musl -p ffplayout-engine -o ffplayout-${version}-1.x86_64.rpm
|
||||||
fi
|
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
|
cargo deb --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_arm64.deb
|
||||||
fi
|
fi
|
||||||
|
@ -20,7 +20,7 @@ lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transpor
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1"
|
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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
288
tests/assets/playlists/2024/03/2024-03-19.json
Normal file
288
tests/assets/playlists/2024/03/2024-03-19.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user