Merge pull request #122 from jb-alvarado/master
no global static config
This commit is contained in:
commit
6da197002c
96
Cargo.lock
generated
96
Cargo.lock
generated
@ -94,9 +94,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "3.1.15"
|
version = "3.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85a35a599b11c089a7f49105658d089b8f2cf0882993c17daf6de15285c2c35d"
|
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
@ -111,9 +111,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "3.1.7"
|
version = "3.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1"
|
checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
@ -207,7 +207,6 @@ dependencies = [
|
|||||||
"lettre",
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
"once_cell",
|
|
||||||
"openssl",
|
"openssl",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
@ -233,7 +232,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "file-rotate"
|
name = "file-rotate"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "git+https://github.com/jb-alvarado/file-rotate.git#ae5062a5b82626b4d1f9fea2a17325fe1d160d4c"
|
source = "git+https://github.com/jb-alvarado/file-rotate.git#ee1dc1cea05885b8cb472191b50a044869da7e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"flate2",
|
"flate2",
|
||||||
@ -761,7 +760,7 @@ dependencies = [
|
|||||||
"kernel32-sys",
|
"kernel32-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"miow 0.2.2",
|
"miow",
|
||||||
"net2",
|
"net2",
|
||||||
"slab",
|
"slab",
|
||||||
"winapi 0.2.8",
|
"winapi 0.2.8",
|
||||||
@ -769,16 +768,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
|
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"miow 0.3.7",
|
|
||||||
"ntapi",
|
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"winapi 0.3.9",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -805,15 +802,6 @@ dependencies = [
|
|||||||
"ws2_32-sys",
|
"ws2_32-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miow"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
|
||||||
dependencies = [
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
@ -871,15 +859,6 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ntapi"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
|
|
||||||
dependencies = [
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.45"
|
version = "0.1.45"
|
||||||
@ -1066,9 +1045,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.37"
|
version = "1.0.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
|
checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
@ -1298,9 +1277,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.92"
|
version = "1.0.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52"
|
checksum = "04066589568b72ec65f42d65a1a52436e954b168773148893c020269563decf2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1381,14 +1360,14 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.18.1"
|
version = "1.18.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dce653fb475565de9f6fb0614b28bca8df2c430c0cf84bcd9c843f15de5414cc"
|
checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio 0.8.2",
|
"mio 0.8.3",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@ -1571,6 +1550,49 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ws2_32-sys"
|
name = "ws2_32-sys"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -17,7 +17,6 @@ jsonrpc-http-server = "18.0"
|
|||||||
lettre = "0.10.0-rc.6"
|
lettre = "0.10.0-rc.6"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
notify = "4.0"
|
notify = "4.0"
|
||||||
once_cell = "1.10"
|
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@ -51,7 +50,11 @@ suggests = "ffmpeg"
|
|||||||
copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved."
|
copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved."
|
||||||
conf-files = ["/etc/ffplayout/ffplayout.yml"]
|
conf-files = ["/etc/ffplayout/ffplayout.yml"]
|
||||||
assets = [
|
assets = [
|
||||||
["target/x86_64-unknown-linux-musl/release/ffplayout", "/usr/bin/ffplayout", "755"],
|
[
|
||||||
|
"target/x86_64-unknown-linux-musl/release/ffplayout",
|
||||||
|
"/usr/bin/ffplayout",
|
||||||
|
"755"
|
||||||
|
],
|
||||||
["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"],
|
["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"],
|
||||||
["assets/logo.png", "/usr/share/ffplayout/logo.png", "644"],
|
["assets/logo.png", "/usr/share/ffplayout/logo.png", "644"],
|
||||||
["README.md", "/usr/share/doc/ffplayout-engine/README", "644"],
|
["README.md", "/usr/share/doc/ffplayout-engine/README", "644"],
|
||||||
|
@ -21,9 +21,7 @@ fn audio_filter(config: &GlobalConfig) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create filter nodes for ingest live stream.
|
/// Create filter nodes for ingest live stream.
|
||||||
pub fn filter_cmd() -> Vec<String> {
|
pub fn filter_cmd(config: &GlobalConfig) -> Vec<String> {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
|
|
||||||
let mut filter = format!(
|
let mut filter = format!(
|
||||||
"[0:v]fps={},scale={}:{},setdar=dar={},fade=in:st=0:d=0.5",
|
"[0:v]fps={},scale={}:{},setdar=dar={},fade=in:st=0:d=0.5",
|
||||||
config.processing.fps,
|
config.processing.fps,
|
||||||
|
@ -182,7 +182,7 @@ fn extend_video(node: &mut Media, chain: &mut Filters) {
|
|||||||
/// add drawtext filter for lower thirds messages
|
/// add drawtext filter for lower thirds messages
|
||||||
fn add_text(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
fn add_text(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
||||||
if config.text.add_text && config.text.over_pre {
|
if config.text.add_text && config.text.over_pre {
|
||||||
let filter = v_drawtext::filter_node(node);
|
let filter = v_drawtext::filter_node(config, node);
|
||||||
|
|
||||||
chain.add_filter(&filter, "video");
|
chain.add_filter(&filter, "video");
|
||||||
|
|
||||||
@ -277,7 +277,7 @@ fn realtime_filter(node: &mut Media, chain: &mut Filters, config: &GlobalConfig,
|
|||||||
|
|
||||||
if &config.out.mode.to_lowercase() == "hls" {
|
if &config.out.mode.to_lowercase() == "hls" {
|
||||||
let mut speed_filter = format!("{t}realtime=speed=1");
|
let mut speed_filter = format!("{t}realtime=speed=1");
|
||||||
let (delta, _) = get_delta(&node.begin.unwrap());
|
let (delta, _) = get_delta(config, &node.begin.unwrap());
|
||||||
let duration = node.out - node.seek;
|
let duration = node.out - node.seek;
|
||||||
|
|
||||||
if delta < 0.0 {
|
if delta < 0.0 {
|
||||||
@ -292,9 +292,7 @@ fn realtime_filter(node: &mut Media, chain: &mut Filters, config: &GlobalConfig,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filter_chains(node: &mut Media) -> Vec<String> {
|
pub fn filter_chains(config: &GlobalConfig, node: &mut Media) -> Vec<String> {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
|
|
||||||
let mut filters = Filters::new();
|
let mut filters = Filters::new();
|
||||||
let mut audio_map = "1:a".to_string();
|
let mut audio_map = "1:a".to_string();
|
||||||
filters.audio_map = Some(audio_map);
|
filters.audio_map = Some(audio_map);
|
||||||
|
@ -4,8 +4,7 @@ use regex::Regex;
|
|||||||
|
|
||||||
use crate::utils::{GlobalConfig, Media};
|
use crate::utils::{GlobalConfig, Media};
|
||||||
|
|
||||||
pub fn filter_node(node: &mut Media) -> String {
|
pub fn filter_node(config: &GlobalConfig, node: &mut Media) -> String {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let mut filter = String::new();
|
let mut filter = String::new();
|
||||||
let mut font = String::new();
|
let mut font = String::new();
|
||||||
|
|
||||||
|
@ -33,8 +33,11 @@ pub struct FolderSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FolderSource {
|
impl FolderSource {
|
||||||
pub fn new(current_list: Arc<Mutex<Vec<Media>>>, global_index: Arc<AtomicUsize>) -> Self {
|
pub fn new(
|
||||||
let config = GlobalConfig::global();
|
config: &GlobalConfig,
|
||||||
|
current_list: Arc<Mutex<Vec<Media>>>,
|
||||||
|
global_index: Arc<AtomicUsize>,
|
||||||
|
) -> Self {
|
||||||
let mut media_list = vec![];
|
let mut media_list = vec![];
|
||||||
let mut index: usize = 0;
|
let mut index: usize = 0;
|
||||||
|
|
||||||
@ -120,7 +123,7 @@ impl Iterator for FolderSource {
|
|||||||
let i = self.index.load(Ordering::SeqCst);
|
let i = self.index.load(Ordering::SeqCst);
|
||||||
self.current_node = self.nodes.lock().unwrap()[i].clone();
|
self.current_node = self.nodes.lock().unwrap()[i].clone();
|
||||||
self.current_node.add_probe();
|
self.current_node.add_probe();
|
||||||
self.current_node.add_filter();
|
self.current_node.add_filter(&self.config);
|
||||||
self.current_node.begin = Some(get_sec());
|
self.current_node.begin = Some(get_sec());
|
||||||
|
|
||||||
self.index.fetch_add(1, Ordering::SeqCst);
|
self.index.fetch_add(1, Ordering::SeqCst);
|
||||||
@ -143,7 +146,7 @@ impl Iterator for FolderSource {
|
|||||||
|
|
||||||
self.current_node = self.nodes.lock().unwrap()[0].clone();
|
self.current_node = self.nodes.lock().unwrap()[0].clone();
|
||||||
self.current_node.add_probe();
|
self.current_node.add_probe();
|
||||||
self.current_node.add_filter();
|
self.current_node.add_filter(&self.config);
|
||||||
self.current_node.begin = Some(get_sec());
|
self.current_node.begin = Some(get_sec());
|
||||||
|
|
||||||
self.index.store(1, Ordering::SeqCst);
|
self.index.store(1, Ordering::SeqCst);
|
||||||
@ -160,11 +163,10 @@ fn file_extension(filename: &Path) -> Option<&str> {
|
|||||||
/// Create a watcher, which monitor file changes.
|
/// Create a watcher, which monitor file changes.
|
||||||
/// When a change is register, update the current file list.
|
/// When a change is register, update the current file list.
|
||||||
/// This makes it possible, to play infinitely and and always new files to it.
|
/// This makes it possible, to play infinitely and and always new files to it.
|
||||||
pub fn watchman(sources: Arc<Mutex<Vec<Media>>>) {
|
pub fn watchman(config: GlobalConfig, sources: Arc<Mutex<Vec<Media>>>) {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
let path = config.storage.path.clone();
|
let path = config.storage.path;
|
||||||
|
|
||||||
if !Path::new(&path).exists() {
|
if !Path::new(&path).exists() {
|
||||||
error!("Folder path not exists: '{path}'");
|
error!("Folder path not exists: '{path}'");
|
||||||
|
@ -10,33 +10,30 @@ use simplelog::*;
|
|||||||
|
|
||||||
use crate::filter::ingest_filter::filter_cmd;
|
use crate::filter::ingest_filter::filter_cmd;
|
||||||
use crate::utils::{stderr_reader, GlobalConfig, Ingest, ProcessControl};
|
use crate::utils::{stderr_reader, GlobalConfig, Ingest, ProcessControl};
|
||||||
|
use crate::vec_strings;
|
||||||
|
|
||||||
/// ffmpeg Ingest Server
|
/// ffmpeg Ingest Server
|
||||||
///
|
///
|
||||||
/// Start ffmpeg in listen mode, and wait for input.
|
/// Start ffmpeg in listen mode, and wait for input.
|
||||||
pub fn ingest_server(
|
pub fn ingest_server(
|
||||||
|
config: GlobalConfig,
|
||||||
log_format: String,
|
log_format: String,
|
||||||
ingest_sender: Sender<(usize, [u8; 65088])>,
|
ingest_sender: Sender<(usize, [u8; 65088])>,
|
||||||
mut proc_control: ProcessControl,
|
mut proc_control: ProcessControl,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let mut buffer: [u8; 65088] = [0; 65088];
|
let mut buffer: [u8; 65088] = [0; 65088];
|
||||||
let filter_list = filter_cmd();
|
let mut server_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format];
|
||||||
|
|
||||||
let mut server_cmd = vec!["-hide_banner", "-nostats", "-v", log_format.as_str()];
|
|
||||||
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
||||||
let stream_settings = config.processing.settings.clone().unwrap();
|
|
||||||
|
|
||||||
server_cmd.append(&mut stream_input.iter().map(String::as_str).collect());
|
server_cmd.append(&mut stream_input.clone());
|
||||||
server_cmd.append(&mut filter_list.iter().map(String::as_str).collect());
|
server_cmd.append(&mut filter_cmd(&config));
|
||||||
server_cmd.append(&mut stream_settings.iter().map(String::as_str).collect());
|
server_cmd.append(&mut config.processing.settings.unwrap());
|
||||||
|
|
||||||
let mut is_running;
|
let mut is_running;
|
||||||
|
|
||||||
info!(
|
if let Some(url) = stream_input.iter().find(|s| s.contains("://")) {
|
||||||
"Start ingest server, listening on: <b><magenta>{}</></b>",
|
info!("Start ingest server, listening on: <b><magenta>{url}</></b>",);
|
||||||
stream_input.last().unwrap()
|
};
|
||||||
);
|
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Server CMD: <bright-blue>\"ffmpeg {}\"</>",
|
"Server CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||||
|
@ -35,17 +35,19 @@ pub fn source_generator(
|
|||||||
&config.storage.path
|
&config.storage.path
|
||||||
);
|
);
|
||||||
|
|
||||||
let folder_source = FolderSource::new(current_list, index);
|
let config_clone = config.clone();
|
||||||
|
let folder_source = FolderSource::new(&config, current_list, index);
|
||||||
let node_clone = folder_source.nodes.clone();
|
let node_clone = folder_source.nodes.clone();
|
||||||
|
|
||||||
// Spawn a thread to monitor folder for file changes.
|
// Spawn a thread to monitor folder for file changes.
|
||||||
thread::spawn(move || watchman(node_clone));
|
thread::spawn(move || watchman(config_clone, node_clone));
|
||||||
|
|
||||||
Box::new(folder_source) as Box<dyn Iterator<Item = Media>>
|
Box::new(folder_source) as Box<dyn Iterator<Item = Media>>
|
||||||
}
|
}
|
||||||
"playlist" => {
|
"playlist" => {
|
||||||
info!("Playout in playlist mode");
|
info!("Playout in playlist mode");
|
||||||
let program = CurrentProgram::new(playout_stat, is_terminated, current_list, index);
|
let program =
|
||||||
|
CurrentProgram::new(&config, playout_stat, is_terminated, current_list, index);
|
||||||
|
|
||||||
Box::new(program) as Box<dyn Iterator<Item = Media>>
|
Box::new(program) as Box<dyn Iterator<Item = Media>>
|
||||||
}
|
}
|
||||||
|
@ -34,13 +34,13 @@ pub struct CurrentProgram {
|
|||||||
|
|
||||||
impl CurrentProgram {
|
impl CurrentProgram {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
config: &GlobalConfig,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
current_list: Arc<Mutex<Vec<Media>>>,
|
current_list: Arc<Mutex<Vec<Media>>>,
|
||||||
global_index: Arc<AtomicUsize>,
|
global_index: Arc<AtomicUsize>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let config = GlobalConfig::global();
|
let json = read_json(config, None, is_terminated.clone(), true, 0.0);
|
||||||
let json = read_json(None, is_terminated.clone(), true, 0.0);
|
|
||||||
|
|
||||||
*current_list.lock().unwrap() = json.program;
|
*current_list.lock().unwrap() = json.program;
|
||||||
*playout_stat.current_date.lock().unwrap() = json.date.clone();
|
*playout_stat.current_date.lock().unwrap() = json.date.clone();
|
||||||
@ -72,7 +72,7 @@ impl CurrentProgram {
|
|||||||
// Check if playlist file got updated, and when yes we reload it and setup everything in place.
|
// Check if playlist file got updated, and when yes we reload it and setup everything in place.
|
||||||
fn check_update(&mut self, seek: bool) {
|
fn check_update(&mut self, seek: bool) {
|
||||||
if self.json_path.is_none() {
|
if self.json_path.is_none() {
|
||||||
let json = read_json(None, self.is_terminated.clone(), seek, 0.0);
|
let json = read_json(&self.config, None, self.is_terminated.clone(), seek, 0.0);
|
||||||
|
|
||||||
self.json_path = json.current_file;
|
self.json_path = json.current_file;
|
||||||
self.json_mod = json.modified;
|
self.json_mod = json.modified;
|
||||||
@ -92,6 +92,7 @@ impl CurrentProgram {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let json = read_json(
|
let json = read_json(
|
||||||
|
&self.config,
|
||||||
self.json_path.clone(),
|
self.json_path.clone(),
|
||||||
self.is_terminated.clone(),
|
self.is_terminated.clone(),
|
||||||
false,
|
false,
|
||||||
@ -127,20 +128,30 @@ impl CurrentProgram {
|
|||||||
let current_time = get_sec();
|
let current_time = get_sec();
|
||||||
let start_sec = self.config.playlist.start_sec.unwrap();
|
let start_sec = self.config.playlist.start_sec.unwrap();
|
||||||
let target_length = self.config.playlist.length_sec.unwrap();
|
let target_length = self.config.playlist.length_sec.unwrap();
|
||||||
let (delta, total_delta) = get_delta(¤t_time);
|
let (delta, total_delta) = get_delta(&self.config, ¤t_time);
|
||||||
let mut duration = self.current_node.out;
|
let mut duration = self.current_node.out;
|
||||||
|
|
||||||
if self.current_node.duration > self.current_node.out {
|
if self.current_node.duration > self.current_node.out {
|
||||||
duration = self.current_node.duration
|
duration = self.current_node.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_start = self.current_node.begin.unwrap() - start_sec + duration + delta;
|
let mut next_start = self.current_node.begin.unwrap() - start_sec + duration + delta;
|
||||||
|
|
||||||
|
if self.index.load(Ordering::SeqCst) == self.nodes.lock().unwrap().len() - 1 {
|
||||||
|
next_start += self.config.general.stop_threshold;
|
||||||
|
}
|
||||||
|
|
||||||
if next_start >= target_length
|
if next_start >= target_length
|
||||||
|| is_close(total_delta, 0.0, 2.0)
|
|| is_close(total_delta, 0.0, 2.0)
|
||||||
|| is_close(total_delta, target_length, 2.0)
|
|| is_close(total_delta, target_length, 2.0)
|
||||||
{
|
{
|
||||||
let json = read_json(None, self.is_terminated.clone(), false, next_start);
|
let json = read_json(
|
||||||
|
&self.config,
|
||||||
|
None,
|
||||||
|
self.is_terminated.clone(),
|
||||||
|
false,
|
||||||
|
next_start,
|
||||||
|
);
|
||||||
|
|
||||||
let data = json!({
|
let data = json!({
|
||||||
"time_shift": 0.0,
|
"time_shift": 0.0,
|
||||||
@ -232,7 +243,7 @@ impl CurrentProgram {
|
|||||||
let mut node_clone = self.nodes.lock().unwrap()[index].clone();
|
let mut node_clone = self.nodes.lock().unwrap()[index].clone();
|
||||||
|
|
||||||
node_clone.seek = time_sec - node_clone.begin.unwrap();
|
node_clone.seek = time_sec - node_clone.begin.unwrap();
|
||||||
self.current_node = handle_list_init(node_clone);
|
self.current_node = handle_list_init(&self.config, node_clone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,7 +254,6 @@ impl Iterator for CurrentProgram {
|
|||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.playout_stat.list_init.load(Ordering::SeqCst) {
|
if self.playout_stat.list_init.load(Ordering::SeqCst) {
|
||||||
debug!("Playlist init");
|
|
||||||
self.check_update(true);
|
self.check_update(true);
|
||||||
|
|
||||||
if self.json_path.is_some() {
|
if self.json_path.is_some() {
|
||||||
@ -268,7 +278,7 @@ impl Iterator for CurrentProgram {
|
|||||||
self.init_clip();
|
self.init_clip();
|
||||||
} else {
|
} else {
|
||||||
let mut current_time = get_sec();
|
let mut current_time = get_sec();
|
||||||
let (_, total_delta) = get_delta(¤t_time);
|
let (_, total_delta) = get_delta(&self.config, ¤t_time);
|
||||||
let mut duration = DUMMY_LEN;
|
let mut duration = DUMMY_LEN;
|
||||||
|
|
||||||
if DUMMY_LEN > total_delta {
|
if DUMMY_LEN > total_delta {
|
||||||
@ -285,7 +295,7 @@ impl Iterator for CurrentProgram {
|
|||||||
media.duration = duration;
|
media.duration = duration;
|
||||||
media.out = duration;
|
media.out = duration;
|
||||||
|
|
||||||
self.current_node = gen_source(media);
|
self.current_node = gen_source(&self.config, media);
|
||||||
self.nodes.lock().unwrap().push(self.current_node.clone());
|
self.nodes.lock().unwrap().push(self.current_node.clone());
|
||||||
self.index
|
self.index
|
||||||
.store(self.nodes.lock().unwrap().len(), Ordering::SeqCst);
|
.store(self.nodes.lock().unwrap().len(), Ordering::SeqCst);
|
||||||
@ -323,7 +333,8 @@ impl Iterator for CurrentProgram {
|
|||||||
let last_playlist = self.json_path.clone();
|
let last_playlist = self.json_path.clone();
|
||||||
let last_ad = self.current_node.last_ad;
|
let last_ad = self.current_node.last_ad;
|
||||||
self.check_for_next_playlist();
|
self.check_for_next_playlist();
|
||||||
let (_, total_delta) = get_delta(&self.config.playlist.start_sec.unwrap());
|
let (_, total_delta) =
|
||||||
|
get_delta(&self.config, &self.config.playlist.start_sec.unwrap());
|
||||||
|
|
||||||
if last_playlist == self.json_path
|
if last_playlist == self.json_path
|
||||||
&& total_delta.abs() > self.config.general.stop_threshold
|
&& total_delta.abs() > self.config.general.stop_threshold
|
||||||
@ -340,12 +351,12 @@ impl Iterator for CurrentProgram {
|
|||||||
}
|
}
|
||||||
self.current_node.duration = duration;
|
self.current_node.duration = duration;
|
||||||
self.current_node.out = duration;
|
self.current_node.out = duration;
|
||||||
self.current_node = gen_source(self.current_node.clone());
|
self.current_node = gen_source(&self.config, self.current_node.clone());
|
||||||
self.nodes.lock().unwrap().push(self.current_node.clone());
|
self.nodes.lock().unwrap().push(self.current_node.clone());
|
||||||
self.last_next_ad();
|
self.last_next_ad();
|
||||||
|
|
||||||
self.current_node.last_ad = last_ad;
|
self.current_node.last_ad = last_ad;
|
||||||
self.current_node.add_filter();
|
self.current_node.add_filter(&self.config);
|
||||||
|
|
||||||
self.index.fetch_add(1, Ordering::SeqCst);
|
self.index.fetch_add(1, Ordering::SeqCst);
|
||||||
|
|
||||||
@ -353,7 +364,7 @@ impl Iterator for CurrentProgram {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.index.store(0, Ordering::SeqCst);
|
self.index.store(0, Ordering::SeqCst);
|
||||||
self.current_node = gen_source(self.nodes.lock().unwrap()[0].clone());
|
self.current_node = gen_source(&self.config, self.nodes.lock().unwrap()[0].clone());
|
||||||
self.last_next_ad();
|
self.last_next_ad();
|
||||||
self.current_node.last_ad = last_ad;
|
self.current_node.last_ad = last_ad;
|
||||||
|
|
||||||
@ -374,7 +385,7 @@ fn timed_source(
|
|||||||
last: bool,
|
last: bool,
|
||||||
playout_stat: &PlayoutStatus,
|
playout_stat: &PlayoutStatus,
|
||||||
) -> Media {
|
) -> Media {
|
||||||
let (delta, total_delta) = get_delta(&node.begin.unwrap());
|
let (delta, total_delta) = get_delta(config, &node.begin.unwrap());
|
||||||
let mut shifted_delta = delta;
|
let mut shifted_delta = delta;
|
||||||
let mut new_node = node.clone();
|
let mut new_node = node.clone();
|
||||||
new_node.process = Some(false);
|
new_node.process = Some(false);
|
||||||
@ -394,7 +405,7 @@ fn timed_source(
|
|||||||
|
|
||||||
debug!("Total time remaining: <yellow>{total_delta:.3}</>");
|
debug!("Total time remaining: <yellow>{total_delta:.3}</>");
|
||||||
|
|
||||||
let sync = check_sync(shifted_delta);
|
let sync = check_sync(config, shifted_delta);
|
||||||
|
|
||||||
if !sync {
|
if !sync {
|
||||||
new_node.cmd = None;
|
new_node.cmd = None;
|
||||||
@ -408,7 +419,7 @@ fn timed_source(
|
|||||||
|| !config.playlist.length.contains(':')
|
|| !config.playlist.length.contains(':')
|
||||||
{
|
{
|
||||||
// when we are in the 24 hour range, get the clip
|
// when we are in the 24 hour range, get the clip
|
||||||
new_node = gen_source(node);
|
new_node = gen_source(config, node);
|
||||||
new_node.process = Some(true);
|
new_node.process = Some(true);
|
||||||
} else if total_delta <= 0.0 {
|
} else if total_delta <= 0.0 {
|
||||||
info!("Begin is over play time, skip: {}", node.source);
|
info!("Begin is over play time, skip: {}", node.source);
|
||||||
@ -420,7 +431,7 @@ fn timed_source(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the source CMD, or when clip not exist, get a dummy.
|
/// Generate the source CMD, or when clip not exist, get a dummy.
|
||||||
fn gen_source(mut node: Media) -> Media {
|
fn gen_source(config: &GlobalConfig, mut node: Media) -> Media {
|
||||||
if Path::new(&node.source).is_file() {
|
if Path::new(&node.source).is_file() {
|
||||||
node.add_probe();
|
node.add_probe();
|
||||||
node.cmd = Some(seek_and_length(
|
node.cmd = Some(seek_and_length(
|
||||||
@ -429,7 +440,7 @@ fn gen_source(mut node: Media) -> Media {
|
|||||||
node.out,
|
node.out,
|
||||||
node.duration,
|
node.duration,
|
||||||
));
|
));
|
||||||
node.add_filter();
|
node.add_filter(config);
|
||||||
} else {
|
} else {
|
||||||
if node.source.chars().count() == 0 {
|
if node.source.chars().count() == 0 {
|
||||||
warn!(
|
warn!(
|
||||||
@ -439,10 +450,10 @@ fn gen_source(mut node: Media) -> Media {
|
|||||||
} else {
|
} else {
|
||||||
error!("File not found: <b><magenta>{}</></b>", node.source);
|
error!("File not found: <b><magenta>{}</></b>", node.source);
|
||||||
}
|
}
|
||||||
let (source, cmd) = gen_dummy(node.out - node.seek);
|
let (source, cmd) = gen_dummy(config, node.out - node.seek);
|
||||||
node.source = source;
|
node.source = source;
|
||||||
node.cmd = Some(cmd);
|
node.cmd = Some(cmd);
|
||||||
node.add_filter();
|
node.add_filter(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
node
|
node
|
||||||
@ -450,8 +461,9 @@ fn gen_source(mut node: Media) -> Media {
|
|||||||
|
|
||||||
/// Handle init clip, but this clip can be the last one in playlist,
|
/// Handle init clip, but this clip can be the last one in playlist,
|
||||||
/// this we have to figure out and calculate the right length.
|
/// this we have to figure out and calculate the right length.
|
||||||
fn handle_list_init(mut node: Media) -> Media {
|
fn handle_list_init(config: &GlobalConfig, mut node: Media) -> Media {
|
||||||
let (_, total_delta) = get_delta(&node.begin.unwrap());
|
debug!("Playlist init");
|
||||||
|
let (_, total_delta) = get_delta(config, &node.begin.unwrap());
|
||||||
let mut out = node.out;
|
let mut out = node.out;
|
||||||
|
|
||||||
if node.out - node.seek > total_delta {
|
if node.out - node.seek > total_delta {
|
||||||
@ -459,7 +471,7 @@ fn handle_list_init(mut node: Media) -> Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node.out = out;
|
node.out = out;
|
||||||
gen_source(node)
|
gen_source(config, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// when we come to last clip in playlist,
|
/// when we come to last clip in playlist,
|
||||||
|
6
src/macros/mod.rs
Normal file
6
src/macros/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! vec_strings {
|
||||||
|
($($str:expr),*) => ({
|
||||||
|
vec![$(String::from($str),)*] as Vec<String>
|
||||||
|
});
|
||||||
|
}
|
24
src/main.rs
24
src/main.rs
@ -14,14 +14,17 @@ use simplelog::*;
|
|||||||
|
|
||||||
mod filter;
|
mod filter;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod macros;
|
||||||
mod output;
|
mod output;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use crate::output::{player, write_hls};
|
use crate::output::{player, write_hls};
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
generate_playlist, init_config, init_logging, validate_ffmpeg, GlobalConfig, PlayerControl,
|
generate_playlist, init_logging, validate_ffmpeg, GlobalConfig, PlayerControl, PlayoutStatus,
|
||||||
PlayoutStatus, ProcessControl,
|
ProcessControl,
|
||||||
};
|
};
|
||||||
use rpc::json_rpc_server;
|
use rpc::json_rpc_server;
|
||||||
|
|
||||||
@ -62,22 +65,21 @@ fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Init the config, set process controller, create logging.
|
let config = GlobalConfig::new();
|
||||||
init_config();
|
let config_clone = config.clone();
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let play_control = PlayerControl::new();
|
let play_control = PlayerControl::new();
|
||||||
let playout_stat = PlayoutStatus::new();
|
let playout_stat = PlayoutStatus::new();
|
||||||
let proc_control = ProcessControl::new();
|
let proc_control = ProcessControl::new();
|
||||||
|
|
||||||
let logging = init_logging();
|
let logging = init_logging(&config);
|
||||||
CombinedLogger::init(logging).unwrap();
|
CombinedLogger::init(logging).unwrap();
|
||||||
|
|
||||||
validate_ffmpeg();
|
validate_ffmpeg(&config);
|
||||||
status_file(&config.general.stat_file, &playout_stat);
|
status_file(&config.general.stat_file, &playout_stat);
|
||||||
|
|
||||||
if let Some(range) = config.general.generate.clone() {
|
if let Some(range) = config.general.generate.clone() {
|
||||||
// run a simple playlist generator and save them to disk
|
// run a simple playlist generator and save them to disk
|
||||||
generate_playlist(range);
|
generate_playlist(&config, range);
|
||||||
|
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
@ -88,15 +90,15 @@ fn main() {
|
|||||||
|
|
||||||
if config.rpc_server.enable {
|
if config.rpc_server.enable {
|
||||||
// If RPC server is enable we also fire up a JSON RPC server.
|
// If RPC server is enable we also fire up a JSON RPC server.
|
||||||
thread::spawn(move || json_rpc_server(play_ctl, play_stat, proc_ctl));
|
thread::spawn(move || json_rpc_server(config_clone, play_ctl, play_stat, proc_ctl));
|
||||||
}
|
}
|
||||||
|
|
||||||
if &config.out.mode.to_lowercase() == "hls" {
|
if &config.out.mode.to_lowercase() == "hls" {
|
||||||
// write files/playlist to HLS m3u8 playlist
|
// write files/playlist to HLS m3u8 playlist
|
||||||
write_hls(play_control, playout_stat, proc_control);
|
write_hls(&config, play_control, playout_stat, proc_control);
|
||||||
} else {
|
} else {
|
||||||
// play on desktop or stream to a remote target
|
// play on desktop or stream to a remote target
|
||||||
player(play_control, playout_stat, proc_control);
|
player(&config, play_control, playout_stat, proc_control);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Playout done...");
|
info!("Playout done...");
|
||||||
|
@ -4,16 +4,15 @@ use simplelog::*;
|
|||||||
|
|
||||||
use crate::filter::v_drawtext;
|
use crate::filter::v_drawtext;
|
||||||
use crate::utils::{GlobalConfig, Media};
|
use crate::utils::{GlobalConfig, Media};
|
||||||
|
use crate::vec_strings;
|
||||||
|
|
||||||
/// Desktop Output
|
/// Desktop Output
|
||||||
///
|
///
|
||||||
/// Instead of streaming, we run a ffplay instance and play on desktop.
|
/// Instead of streaming, we run a ffplay instance and play on desktop.
|
||||||
pub fn output(log_format: &str) -> process::Child {
|
pub fn output(config: &GlobalConfig, log_format: &str) -> process::Child {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
|
|
||||||
let mut enc_filter: Vec<String> = vec![];
|
let mut enc_filter: Vec<String> = vec![];
|
||||||
|
|
||||||
let mut enc_cmd = vec!["-hide_banner", "-nostats", "-v", log_format, "-i", "pipe:0"];
|
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format, "-i", "pipe:0"];
|
||||||
|
|
||||||
if config.text.add_text && !config.text.over_pre {
|
if config.text.add_text && !config.text.over_pre {
|
||||||
info!(
|
info!(
|
||||||
@ -22,11 +21,13 @@ pub fn output(log_format: &str) -> process::Child {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut filter: String = "null,".to_string();
|
let mut filter: String = "null,".to_string();
|
||||||
filter.push_str(v_drawtext::filter_node(&mut Media::new(0, String::new(), false)).as_str());
|
filter.push_str(
|
||||||
|
v_drawtext::filter_node(config, &mut Media::new(0, String::new(), false)).as_str(),
|
||||||
|
);
|
||||||
enc_filter = vec!["-vf".to_string(), filter];
|
enc_filter = vec!["-vf".to_string(), filter];
|
||||||
}
|
}
|
||||||
|
|
||||||
enc_cmd.append(&mut enc_filter.iter().map(String::as_str).collect());
|
enc_cmd.append(&mut enc_filter);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Encoder CMD: <bright-blue>\"ffplay {}\"</>",
|
"Encoder CMD: <bright-blue>\"ffplay {}\"</>",
|
||||||
|
@ -33,6 +33,7 @@ use crate::utils::{
|
|||||||
sec_to_time, stderr_reader, Decoder, GlobalConfig, Ingest, PlayerControl, PlayoutStatus,
|
sec_to_time, stderr_reader, Decoder, GlobalConfig, Ingest, PlayerControl, PlayoutStatus,
|
||||||
ProcessControl,
|
ProcessControl,
|
||||||
};
|
};
|
||||||
|
use crate::vec_strings;
|
||||||
|
|
||||||
fn format_line(line: String, level: &str) -> String {
|
fn format_line(line: String, level: &str) -> String {
|
||||||
line.replace(&format!("[{level: >5}] "), "")
|
line.replace(&format!("[{level: >5}] "), "")
|
||||||
@ -40,27 +41,24 @@ fn format_line(line: String, level: &str) -> String {
|
|||||||
|
|
||||||
/// Ingest Server for HLS
|
/// Ingest Server for HLS
|
||||||
fn ingest_to_hls_server(
|
fn ingest_to_hls_server(
|
||||||
|
config: GlobalConfig,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
mut proc_control: ProcessControl,
|
mut proc_control: ProcessControl,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let dec_settings = config.out.clone().output_cmd.unwrap();
|
|
||||||
let playlist_init = playout_stat.list_init;
|
let playlist_init = playout_stat.list_init;
|
||||||
let filter_list = filter_cmd();
|
|
||||||
|
|
||||||
let mut server_cmd = vec!["-hide_banner", "-nostats", "-v", "level+info"];
|
let mut server_cmd = 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_cmd.append(&mut stream_input.iter().map(String::as_str).collect());
|
server_cmd.append(&mut stream_input.clone());
|
||||||
server_cmd.append(&mut filter_list.iter().map(String::as_str).collect());
|
server_cmd.append(&mut filter_cmd(&config));
|
||||||
server_cmd.append(&mut dec_settings.iter().map(String::as_str).collect());
|
server_cmd.append(&mut config.out.clone().output_cmd.unwrap());
|
||||||
|
|
||||||
let mut is_running;
|
let mut is_running;
|
||||||
|
|
||||||
info!(
|
if let Some(url) = stream_input.iter().find(|s| s.contains("://")) {
|
||||||
"Start ingest server, listening on: <b><magenta>{}</></b>",
|
info!("Start ingest server, listening on: <b><magenta>{url}</></b>");
|
||||||
stream_input.last().unwrap()
|
};
|
||||||
);
|
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Server CMD: <bright-blue>\"ffmpeg {}\"</>",
|
"Server CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||||
@ -132,12 +130,12 @@ fn ingest_to_hls_server(
|
|||||||
///
|
///
|
||||||
/// Write with single ffmpeg instance directly to a HLS playlist.
|
/// Write with single ffmpeg instance directly to a HLS playlist.
|
||||||
pub fn write_hls(
|
pub fn write_hls(
|
||||||
|
config: &GlobalConfig,
|
||||||
play_control: PlayerControl,
|
play_control: PlayerControl,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
mut proc_control: ProcessControl,
|
mut proc_control: ProcessControl,
|
||||||
) {
|
) {
|
||||||
let config = GlobalConfig::global();
|
let config_clone = config.clone();
|
||||||
let dec_settings = config.out.clone().output_cmd.unwrap();
|
|
||||||
let ff_log_format = format!("level+{}", config.logging.ffmpeg_level.to_lowercase());
|
let ff_log_format = format!("level+{}", config.logging.ffmpeg_level.to_lowercase());
|
||||||
let play_stat = playout_stat.clone();
|
let play_stat = playout_stat.clone();
|
||||||
let proc_control_c = proc_control.clone();
|
let proc_control_c = proc_control.clone();
|
||||||
@ -152,13 +150,13 @@ pub fn write_hls(
|
|||||||
|
|
||||||
// spawn a thread for ffmpeg ingest server and create a channel for package sending
|
// spawn a thread for ffmpeg ingest server and create a channel for package sending
|
||||||
if config.ingest.enable {
|
if config.ingest.enable {
|
||||||
thread::spawn(move || ingest_to_hls_server(play_stat, proc_control_c));
|
thread::spawn(move || ingest_to_hls_server(config_clone, play_stat, proc_control_c));
|
||||||
}
|
}
|
||||||
|
|
||||||
for node in get_source {
|
for node in get_source {
|
||||||
*play_control.current_media.lock().unwrap() = Some(node.clone());
|
*play_control.current_media.lock().unwrap() = Some(node.clone());
|
||||||
|
|
||||||
let cmd = match node.cmd {
|
let mut cmd = match node.cmd {
|
||||||
Some(cmd) => cmd,
|
Some(cmd) => cmd,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
@ -173,15 +171,15 @@ pub fn write_hls(
|
|||||||
node.source
|
node.source
|
||||||
);
|
);
|
||||||
|
|
||||||
let filter = node.filter.unwrap();
|
let mut filter = node.filter.unwrap();
|
||||||
let mut dec_cmd = vec!["-hide_banner", "-nostats", "-v", ff_log_format.as_str()];
|
let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
|
||||||
dec_cmd.append(&mut cmd.iter().map(String::as_str).collect());
|
dec_cmd.append(&mut cmd);
|
||||||
|
|
||||||
if filter.len() > 1 {
|
if filter.len() > 1 {
|
||||||
dec_cmd.append(&mut filter.iter().map(String::as_str).collect());
|
dec_cmd.append(&mut filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
dec_cmd.append(&mut dec_settings.iter().map(String::as_str).collect());
|
dec_cmd.append(&mut config.out.clone().output_cmd.unwrap());
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"HLS writer CMD: <bright-blue>\"ffmpeg {}\"</>",
|
"HLS writer CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||||
|
@ -20,6 +20,7 @@ use crate::utils::{
|
|||||||
sec_to_time, stderr_reader, Decoder, Encoder, GlobalConfig, PlayerControl, PlayoutStatus,
|
sec_to_time, stderr_reader, Decoder, Encoder, GlobalConfig, PlayerControl, PlayoutStatus,
|
||||||
ProcessControl,
|
ProcessControl,
|
||||||
};
|
};
|
||||||
|
use crate::vec_strings;
|
||||||
|
|
||||||
/// Player
|
/// Player
|
||||||
///
|
///
|
||||||
@ -31,12 +32,12 @@ use crate::utils::{
|
|||||||
/// When a live ingest arrive, it stops the current playing and switch to the live source.
|
/// When a live ingest arrive, it stops the current playing and switch to the live source.
|
||||||
/// When ingest stops, it switch back to playlist/folder mode.
|
/// When ingest stops, it switch back to playlist/folder mode.
|
||||||
pub fn player(
|
pub fn player(
|
||||||
|
config: &GlobalConfig,
|
||||||
play_control: PlayerControl,
|
play_control: PlayerControl,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
mut proc_control: ProcessControl,
|
mut proc_control: ProcessControl,
|
||||||
) {
|
) {
|
||||||
let config = GlobalConfig::global();
|
let config_clone = config.clone();
|
||||||
let dec_settings = config.processing.clone().settings.unwrap();
|
|
||||||
let ff_log_format = format!("level+{}", config.logging.ffmpeg_level.to_lowercase());
|
let ff_log_format = format!("level+{}", config.logging.ffmpeg_level.to_lowercase());
|
||||||
let mut buffer = [0; 65088];
|
let mut buffer = [0; 65088];
|
||||||
let mut live_on = false;
|
let mut live_on = false;
|
||||||
@ -53,8 +54,8 @@ pub fn player(
|
|||||||
|
|
||||||
// get ffmpeg output instance
|
// get ffmpeg output instance
|
||||||
let mut enc_proc = match config.out.mode.as_str() {
|
let mut enc_proc = match config.out.mode.as_str() {
|
||||||
"desktop" => desktop::output(&ff_log_format),
|
"desktop" => desktop::output(config, &ff_log_format),
|
||||||
"stream" => stream::output(&ff_log_format),
|
"stream" => stream::output(config, &ff_log_format),
|
||||||
_ => panic!("Output mode doesn't exists!"),
|
_ => panic!("Output mode doesn't exists!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ pub fn player(
|
|||||||
// spawn a thread to log ffmpeg output error messages
|
// spawn a thread to log ffmpeg output error messages
|
||||||
let error_encoder_thread = thread::spawn(move || stderr_reader(enc_err, "Encoder"));
|
let error_encoder_thread = thread::spawn(move || stderr_reader(enc_err, "Encoder"));
|
||||||
|
|
||||||
*proc_control.decoder_term.lock().unwrap() = Some(enc_proc);
|
*proc_control.encoder_term.lock().unwrap() = Some(enc_proc);
|
||||||
|
|
||||||
let ff_log_format_c = ff_log_format.clone();
|
let ff_log_format_c = ff_log_format.clone();
|
||||||
let proc_control_c = proc_control.clone();
|
let proc_control_c = proc_control.clone();
|
||||||
@ -74,13 +75,15 @@ pub fn player(
|
|||||||
if config.ingest.enable {
|
if config.ingest.enable {
|
||||||
let (ingest_sender, rx) = bounded(96);
|
let (ingest_sender, rx) = bounded(96);
|
||||||
ingest_receiver = Some(rx);
|
ingest_receiver = Some(rx);
|
||||||
thread::spawn(move || ingest_server(ff_log_format_c, ingest_sender, proc_control_c));
|
thread::spawn(move || {
|
||||||
|
ingest_server(config_clone, ff_log_format_c, ingest_sender, proc_control_c)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
'source_iter: for node in get_source {
|
'source_iter: for node in get_source {
|
||||||
*play_control.current_media.lock().unwrap() = Some(node.clone());
|
*play_control.current_media.lock().unwrap() = Some(node.clone());
|
||||||
|
|
||||||
let cmd = match node.cmd {
|
let mut cmd = match node.cmd {
|
||||||
Some(cmd) => cmd,
|
Some(cmd) => cmd,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
@ -95,15 +98,15 @@ pub fn player(
|
|||||||
node.source
|
node.source
|
||||||
);
|
);
|
||||||
|
|
||||||
let filter = node.filter.unwrap();
|
let mut filter = node.filter.unwrap();
|
||||||
let mut dec_cmd = vec!["-hide_banner", "-nostats", "-v", ff_log_format.as_str()];
|
let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
|
||||||
dec_cmd.append(&mut cmd.iter().map(String::as_str).collect());
|
dec_cmd.append(&mut cmd);
|
||||||
|
|
||||||
if filter.len() > 1 {
|
if filter.len() > 1 {
|
||||||
dec_cmd.append(&mut filter.iter().map(String::as_str).collect());
|
dec_cmd.append(&mut filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
dec_cmd.append(&mut dec_settings.iter().map(String::as_str).collect());
|
dec_cmd.append(&mut config.processing.clone().settings.unwrap());
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Decoder CMD: <bright-blue>\"ffmpeg {}\"</>",
|
"Decoder CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||||
|
@ -7,25 +7,25 @@ use simplelog::*;
|
|||||||
|
|
||||||
use crate::filter::v_drawtext;
|
use crate::filter::v_drawtext;
|
||||||
use crate::utils::{GlobalConfig, Media};
|
use crate::utils::{GlobalConfig, Media};
|
||||||
|
use crate::vec_strings;
|
||||||
|
|
||||||
/// Streaming Output
|
/// Streaming Output
|
||||||
///
|
///
|
||||||
/// Prepare the ffmpeg command for streaming output
|
/// Prepare the ffmpeg command for streaming output
|
||||||
pub fn output(log_format: &str) -> process::Child {
|
pub fn output(config: &GlobalConfig, log_format: &str) -> process::Child {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let mut enc_filter: Vec<String> = vec![];
|
let mut enc_filter: Vec<String> = vec![];
|
||||||
let mut preview: Vec<&str> = vec![];
|
let mut preview: Vec<String> = vec_strings![];
|
||||||
let preview_cmd = config.out.preview_cmd.as_ref().unwrap().clone();
|
let mut preview_cmd = config.out.preview_cmd.as_ref().unwrap().clone();
|
||||||
let output_cmd = config.out.output_cmd.as_ref().unwrap().clone();
|
let mut output_cmd = config.out.output_cmd.as_ref().unwrap().clone();
|
||||||
|
|
||||||
let mut enc_cmd = vec![
|
let mut enc_cmd = vec_strings![
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-nostats",
|
"-nostats",
|
||||||
"-v",
|
"-v",
|
||||||
log_format,
|
log_format,
|
||||||
"-re",
|
"-re",
|
||||||
"-i",
|
"-i",
|
||||||
"pipe:0",
|
"pipe:0"
|
||||||
];
|
];
|
||||||
|
|
||||||
if config.text.add_text && !config.text.over_pre {
|
if config.text.add_text && !config.text.over_pre {
|
||||||
@ -34,25 +34,27 @@ pub fn output(log_format: &str) -> process::Child {
|
|||||||
config.text.bind_address
|
config.text.bind_address
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut filter: String = "[0:v]null,".to_string();
|
let mut filter = "[0:v]null,".to_string();
|
||||||
filter.push_str(v_drawtext::filter_node(&mut Media::new(0, String::new(), false)).as_str());
|
filter.push_str(
|
||||||
|
v_drawtext::filter_node(config, &mut Media::new(0, String::new(), false)).as_str(),
|
||||||
|
);
|
||||||
|
|
||||||
if config.out.preview {
|
if config.out.preview {
|
||||||
filter.push_str(",split=2[v_out1][v_out2]");
|
filter.push_str(",split=2[v_out1][v_out2]");
|
||||||
|
|
||||||
preview = vec!["-map", "[v_out1]", "-map", "0:a"];
|
preview = vec_strings!["-map", "[v_out1]", "-map", "0:a"];
|
||||||
preview.append(&mut preview_cmd.iter().map(String::as_str).collect());
|
preview.append(&mut preview_cmd);
|
||||||
preview.append(&mut vec!["-map", "[v_out2]", "-map", "0:a"]);
|
preview.append(&mut vec_strings!["-map", "[v_out2]", "-map", "0:a"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
enc_filter = vec!["-filter_complex".to_string(), filter];
|
enc_filter = vec!["-filter_complex".to_string(), filter];
|
||||||
} else if config.out.preview {
|
} else if config.out.preview {
|
||||||
preview = preview_cmd.iter().map(String::as_str).collect()
|
preview = preview_cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
enc_cmd.append(&mut enc_filter.iter().map(String::as_str).collect());
|
enc_cmd.append(&mut enc_filter);
|
||||||
enc_cmd.append(&mut preview);
|
enc_cmd.append(&mut preview);
|
||||||
enc_cmd.append(&mut output_cmd.iter().map(String::as_str).collect());
|
enc_cmd.append(&mut output_cmd);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Encoder CMD: <bright-blue>\"ffmpeg {}\"</>",
|
"Encoder CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||||
|
@ -56,11 +56,13 @@ fn get_data_map(config: &GlobalConfig, media: Media) -> Map<String, Value> {
|
|||||||
/// - get last clip
|
/// - get last clip
|
||||||
/// - reset player state to original clip
|
/// - reset player state to original clip
|
||||||
pub fn json_rpc_server(
|
pub fn json_rpc_server(
|
||||||
|
config: GlobalConfig,
|
||||||
play_control: PlayerControl,
|
play_control: PlayerControl,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
proc_control: ProcessControl,
|
proc_control: ProcessControl,
|
||||||
) {
|
) {
|
||||||
let config = GlobalConfig::global();
|
let addr = config.rpc_server.address.clone();
|
||||||
|
let auth = config.rpc_server.authorization.clone();
|
||||||
let mut io = IoHandler::default();
|
let mut io = IoHandler::default();
|
||||||
let proc = proc_control.clone();
|
let proc = proc_control.clone();
|
||||||
|
|
||||||
@ -90,10 +92,10 @@ pub fn json_rpc_server(
|
|||||||
let mut media = play_control.current_list.lock().unwrap()[index].clone();
|
let mut media = play_control.current_list.lock().unwrap()[index].clone();
|
||||||
media.add_probe();
|
media.add_probe();
|
||||||
|
|
||||||
let (delta, _) = get_delta(&media.begin.unwrap_or(0.0));
|
let (delta, _) = get_delta(&config, &media.begin.unwrap_or(0.0));
|
||||||
*time_shift = delta;
|
*time_shift = delta;
|
||||||
*date = current_date.clone();
|
*date = current_date.clone();
|
||||||
write_status(¤t_date, delta);
|
write_status(&config, ¤t_date, delta);
|
||||||
|
|
||||||
data_map.insert("operation".to_string(), json!("move_to_next"));
|
data_map.insert("operation".to_string(), json!("move_to_next"));
|
||||||
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
||||||
@ -129,10 +131,10 @@ pub fn json_rpc_server(
|
|||||||
play_control.index.fetch_sub(2, Ordering::SeqCst);
|
play_control.index.fetch_sub(2, Ordering::SeqCst);
|
||||||
media.add_probe();
|
media.add_probe();
|
||||||
|
|
||||||
let (delta, _) = get_delta(&media.begin.unwrap_or(0.0));
|
let (delta, _) = get_delta(&config, &media.begin.unwrap_or(0.0));
|
||||||
*time_shift = delta;
|
*time_shift = delta;
|
||||||
*date = current_date.clone();
|
*date = current_date.clone();
|
||||||
write_status(¤t_date, delta);
|
write_status(&config, ¤t_date, delta);
|
||||||
|
|
||||||
data_map.insert("operation".to_string(), json!("move_to_last"));
|
data_map.insert("operation".to_string(), json!("move_to_last"));
|
||||||
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
||||||
@ -164,7 +166,7 @@ pub fn json_rpc_server(
|
|||||||
*date = current_date.clone();
|
*date = current_date.clone();
|
||||||
playout_stat.list_init.store(true, Ordering::SeqCst);
|
playout_stat.list_init.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
write_status(¤t_date, 0.0);
|
write_status(&config, ¤t_date, 0.0);
|
||||||
|
|
||||||
data_map.insert("operation".to_string(), json!("reset_playout_state"));
|
data_map.insert("operation".to_string(), json!("reset_playout_state"));
|
||||||
|
|
||||||
@ -177,7 +179,7 @@ pub fn json_rpc_server(
|
|||||||
// get infos about current clip
|
// get infos about current clip
|
||||||
if map.contains_key("media") && &map["media"] == "current" {
|
if map.contains_key("media") && &map["media"] == "current" {
|
||||||
if let Some(media) = play_control.current_media.lock().unwrap().clone() {
|
if let Some(media) = play_control.current_media.lock().unwrap().clone() {
|
||||||
let data_map = get_data_map(config, media);
|
let data_map = get_data_map(&config, media);
|
||||||
|
|
||||||
return Ok(Value::Object(data_map));
|
return Ok(Value::Object(data_map));
|
||||||
};
|
};
|
||||||
@ -190,7 +192,7 @@ pub fn json_rpc_server(
|
|||||||
if index < play_control.current_list.lock().unwrap().len() {
|
if index < play_control.current_list.lock().unwrap().len() {
|
||||||
let media = play_control.current_list.lock().unwrap()[index].clone();
|
let media = play_control.current_list.lock().unwrap()[index].clone();
|
||||||
|
|
||||||
let data_map = get_data_map(config, media);
|
let data_map = get_data_map(&config, media);
|
||||||
|
|
||||||
return Ok(Value::Object(data_map));
|
return Ok(Value::Object(data_map));
|
||||||
}
|
}
|
||||||
@ -205,7 +207,7 @@ pub fn json_rpc_server(
|
|||||||
if index > 1 && index - 2 < play_control.current_list.lock().unwrap().len() {
|
if index > 1 && index - 2 < play_control.current_list.lock().unwrap().len() {
|
||||||
let media = play_control.current_list.lock().unwrap()[index - 2].clone();
|
let media = play_control.current_list.lock().unwrap()[index - 2].clone();
|
||||||
|
|
||||||
let data_map = get_data_map(config, media);
|
let data_map = get_data_map(&config, media);
|
||||||
|
|
||||||
return Ok(Value::Object(data_map));
|
return Ok(Value::Object(data_map));
|
||||||
}
|
}
|
||||||
@ -223,9 +225,9 @@ pub fn json_rpc_server(
|
|||||||
AccessControlAllowOrigin::Null,
|
AccessControlAllowOrigin::Null,
|
||||||
]))
|
]))
|
||||||
// add middleware, for authentication
|
// add middleware, for authentication
|
||||||
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
.request_middleware(move |request: hyper::Request<hyper::Body>| {
|
||||||
if request.headers().contains_key("authorization")
|
if request.headers().contains_key("authorization")
|
||||||
&& request.headers()["authorization"] == config.rpc_server.authorization
|
&& request.headers()["authorization"] == auth
|
||||||
{
|
{
|
||||||
if request.uri() == "/status" {
|
if request.uri() == "/status" {
|
||||||
println!("{:?}", request.headers().contains_key("authorization"));
|
println!("{:?}", request.headers().contains_key("authorization"));
|
||||||
@ -238,7 +240,7 @@ pub fn json_rpc_server(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.rest_api(RestApi::Secure)
|
.rest_api(RestApi::Secure)
|
||||||
.start_http(&config.rpc_server.address.parse().unwrap())
|
.start_http(&addr.parse().unwrap())
|
||||||
.expect("Unable to start RPC server");
|
.expect("Unable to start RPC server");
|
||||||
|
|
||||||
*proc_control.rpc_handle.lock().unwrap() = Some(server.close_handle());
|
*proc_control.rpc_handle.lock().unwrap() = Some(server.close_handle());
|
||||||
|
69
src/tests/mod.rs
Normal file
69
src/tests/mod.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use std::{
|
||||||
|
thread::{self, sleep},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::output::player;
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::utils::*;
|
||||||
|
#[cfg(test)]
|
||||||
|
use simplelog::*;
|
||||||
|
|
||||||
|
fn timed_kill(sec: u64, mut proc_ctl: ProcessControl) {
|
||||||
|
sleep(Duration::from_secs(sec));
|
||||||
|
|
||||||
|
proc_ctl.kill_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn playlist_change_at_midnight() {
|
||||||
|
let mut config = GlobalConfig::new();
|
||||||
|
config.mail.recipient = "".into();
|
||||||
|
config.processing.mode = "playlist".into();
|
||||||
|
config.playlist.day_start = "00:00:00".into();
|
||||||
|
config.playlist.length = "24:00:00".into();
|
||||||
|
config.logging.log_to_file = false;
|
||||||
|
|
||||||
|
let play_control = PlayerControl::new();
|
||||||
|
let playout_stat = PlayoutStatus::new();
|
||||||
|
let proc_control = ProcessControl::new();
|
||||||
|
let proc_ctl = proc_control.clone();
|
||||||
|
|
||||||
|
let logging = init_logging(&config);
|
||||||
|
CombinedLogger::init(logging).unwrap();
|
||||||
|
|
||||||
|
mock_time::set_mock_time("2022-05-09T23:59:45");
|
||||||
|
|
||||||
|
thread::spawn(move || timed_kill(30, proc_ctl));
|
||||||
|
|
||||||
|
player(&config, play_control, playout_stat, proc_control);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn playlist_change_at_six() {
|
||||||
|
let mut config = GlobalConfig::new();
|
||||||
|
config.mail.recipient = "".into();
|
||||||
|
config.processing.mode = "playlist".into();
|
||||||
|
config.playlist.day_start = "06:00:00".into();
|
||||||
|
config.playlist.length = "24:00:00".into();
|
||||||
|
config.logging.log_to_file = false;
|
||||||
|
|
||||||
|
let play_control = PlayerControl::new();
|
||||||
|
let playout_stat = PlayoutStatus::new();
|
||||||
|
let proc_control = ProcessControl::new();
|
||||||
|
let proc_ctl = proc_control.clone();
|
||||||
|
|
||||||
|
let logging = init_logging(&config);
|
||||||
|
CombinedLogger::init(logging).unwrap();
|
||||||
|
|
||||||
|
mock_time::set_mock_time("2022-05-09T05:59:45");
|
||||||
|
|
||||||
|
thread::spawn(move || timed_kill(30, proc_ctl));
|
||||||
|
|
||||||
|
player(&config, play_control, playout_stat, proc_control);
|
||||||
|
}
|
52
src/tests/utils/mod.rs
Normal file
52
src/tests/utils/mod.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
use chrono::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::utils::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mock_date_time() {
|
||||||
|
let time_str = "2022-05-20T06:00:00";
|
||||||
|
let date_obj = NaiveDateTime::parse_from_str(time_str, "%Y-%m-%dT%H:%M:%S");
|
||||||
|
let time = Local.from_local_datetime(&date_obj.unwrap()).unwrap();
|
||||||
|
|
||||||
|
mock_time::set_mock_time(time_str);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
time.format("%Y-%m-%dT%H:%M:%S.2f").to_string(),
|
||||||
|
time_now().format("%Y-%m-%dT%H:%M:%S.2f").to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_date_yesterday() {
|
||||||
|
mock_time::set_mock_time("2022-05-20T05:59:24");
|
||||||
|
|
||||||
|
let date = get_date(true, 21600.0, 86400.0);
|
||||||
|
|
||||||
|
assert_eq!("2022-05-19".to_string(), date);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_date_tomorrow() {
|
||||||
|
mock_time::set_mock_time("2022-05-20T23:59:30");
|
||||||
|
|
||||||
|
let date = get_date(false, 0.0, 86400.01);
|
||||||
|
|
||||||
|
assert_eq!("2022-05-21".to_string(), date);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delta() {
|
||||||
|
let mut config = GlobalConfig::new();
|
||||||
|
config.mail.recipient = "".into();
|
||||||
|
config.processing.mode = "playlist".into();
|
||||||
|
config.playlist.day_start = "00:00:00".into();
|
||||||
|
config.playlist.length = "24:00:00".into();
|
||||||
|
config.logging.log_to_file = false;
|
||||||
|
|
||||||
|
mock_time::set_mock_time("2022-05-09T23:59:59");
|
||||||
|
let (delta, _) = get_delta(&config, &86401.0);
|
||||||
|
|
||||||
|
assert!(delta < 2.0);
|
||||||
|
}
|
@ -55,6 +55,12 @@ pub struct Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get arguments from command line, and return them.
|
/// Get arguments from command line, and return them.
|
||||||
|
#[cfg(not(test))]
|
||||||
pub fn get_args() -> Args {
|
pub fn get_args() -> Args {
|
||||||
Args::parse()
|
Args::parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn get_args() -> Args {
|
||||||
|
Args::parse_from(["-o desktop"].iter())
|
||||||
|
}
|
||||||
|
@ -5,12 +5,11 @@ use std::{
|
|||||||
process,
|
process,
|
||||||
};
|
};
|
||||||
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::{self};
|
|
||||||
use shlex::split;
|
use shlex::split;
|
||||||
|
|
||||||
use crate::utils::{get_args, time_to_sec};
|
use crate::utils::{get_args, time_to_sec};
|
||||||
|
use crate::vec_strings;
|
||||||
|
|
||||||
/// Global Config
|
/// Global Config
|
||||||
///
|
///
|
||||||
@ -137,7 +136,7 @@ pub struct Out {
|
|||||||
|
|
||||||
impl GlobalConfig {
|
impl GlobalConfig {
|
||||||
/// Read config from YAML file, and set some extra config values.
|
/// Read config from YAML file, and set some extra config values.
|
||||||
fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let args = get_args();
|
let args = get_args();
|
||||||
let mut config_path = match env::current_exe() {
|
let mut config_path = match env::current_exe() {
|
||||||
Ok(path) => path.parent().unwrap().join("ffplayout.yml"),
|
Ok(path) => path.parent().unwrap().join("ffplayout.yml"),
|
||||||
@ -167,8 +166,15 @@ impl GlobalConfig {
|
|||||||
.join("ffplayout_status.json")
|
.join("ffplayout_status.json")
|
||||||
.display()
|
.display()
|
||||||
.to_string();
|
.to_string();
|
||||||
let fps = config.processing.fps.to_string();
|
let bitrate = format!(
|
||||||
let bitrate = config.processing.width * config.processing.height / 10;
|
"{}k",
|
||||||
|
config.processing.width * config.processing.height / 10
|
||||||
|
);
|
||||||
|
let buf_size = format!(
|
||||||
|
"{}k",
|
||||||
|
(config.processing.width * config.processing.height / 10) / 2
|
||||||
|
);
|
||||||
|
|
||||||
config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start));
|
config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start));
|
||||||
|
|
||||||
if config.playlist.length.contains(':') {
|
if config.playlist.length.contains(':') {
|
||||||
@ -178,35 +184,29 @@ impl GlobalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We set the decoder settings here, so we only define them ones.
|
// We set the decoder settings here, so we only define them ones.
|
||||||
let mut settings: Vec<String> = vec![
|
let mut settings = vec_strings![
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p",
|
"yuv420p",
|
||||||
"-r",
|
"-r",
|
||||||
&fps,
|
&config.processing.fps.to_string(),
|
||||||
"-c:v",
|
"-c:v",
|
||||||
"mpeg2video",
|
"mpeg2video",
|
||||||
"-g",
|
"-g",
|
||||||
"1",
|
"1",
|
||||||
"-b:v",
|
"-b:v",
|
||||||
format!("{bitrate}k").as_str(),
|
&bitrate,
|
||||||
"-minrate",
|
"-minrate",
|
||||||
format!("{bitrate}k").as_str(),
|
&bitrate,
|
||||||
"-maxrate",
|
"-maxrate",
|
||||||
format!("{bitrate}k").as_str(),
|
&bitrate,
|
||||||
"-bufsize",
|
"-bufsize",
|
||||||
format!("{}k", bitrate / 2).as_str(),
|
&buf_size
|
||||||
]
|
];
|
||||||
.iter()
|
|
||||||
.map(|&s| s.into())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
settings.append(&mut pre_audio_codec(config.processing.add_loudnorm));
|
settings.append(&mut pre_audio_codec(config.processing.add_loudnorm));
|
||||||
settings.append(
|
settings.append(&mut vec_strings![
|
||||||
&mut vec!["-ar", "48000", "-ac", "2", "-f", "mpegts", "-"]
|
"-ar", "48000", "-ac", "2", "-f", "mpegts", "-"
|
||||||
.iter()
|
]);
|
||||||
.map(|&s| s.into())
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
|
|
||||||
config.processing.settings = Some(settings);
|
config.processing.settings = Some(settings);
|
||||||
|
|
||||||
@ -221,6 +221,9 @@ impl GlobalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(log_path) = args.log {
|
if let Some(log_path) = args.log {
|
||||||
|
if Path::new(&log_path).is_dir() {
|
||||||
|
config.logging.log_to_file = true;
|
||||||
|
}
|
||||||
config.logging.log_path = log_path;
|
config.logging.log_path = log_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,28 +268,17 @@ impl GlobalConfig {
|
|||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn global() -> &'static GlobalConfig {
|
|
||||||
INSTANCE.get().expect("Config is not initialized")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static INSTANCE: OnceCell<GlobalConfig> = OnceCell::new();
|
|
||||||
|
|
||||||
/// When add_loudnorm is False we use a different audio encoder,
|
/// When add_loudnorm is False we use a different audio encoder,
|
||||||
/// s302m has higher quality, but is experimental
|
/// s302m has higher quality, but is experimental
|
||||||
/// and works not well together with the loudnorm filter.
|
/// and works not well together with the loudnorm filter.
|
||||||
fn pre_audio_codec(add_loudnorm: bool) -> Vec<String> {
|
fn pre_audio_codec(add_loudnorm: bool) -> Vec<String> {
|
||||||
let mut codec = vec!["-c:a", "s302m", "-strict", "-2"];
|
let mut codec = vec_strings!["-c:a", "s302m", "-strict", "-2"];
|
||||||
|
|
||||||
if add_loudnorm {
|
if add_loudnorm {
|
||||||
codec = vec!["-c:a", "mp2", "-b:a", "384k"];
|
codec = vec_strings!["-c:a", "mp2", "-b:a", "384k"];
|
||||||
}
|
}
|
||||||
|
|
||||||
codec.iter().map(|&s| s.into()).collect()
|
codec
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_config() {
|
|
||||||
let config = GlobalConfig::new();
|
|
||||||
INSTANCE.set(config).unwrap();
|
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,9 @@ impl ProcessControl {
|
|||||||
|
|
||||||
for unit in [Decoder, Encoder, Ingest] {
|
for unit in [Decoder, Encoder, Ingest] {
|
||||||
if let Err(e) = self.kill(unit) {
|
if let Err(e) = self.kill(unit) {
|
||||||
error!("{e}")
|
if !e.contains("exited process") {
|
||||||
|
error!("{e}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,8 +50,7 @@ fn get_date_range(date_range: &[String]) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate playlists
|
/// Generate playlists
|
||||||
pub fn generate_playlist(mut date_range: Vec<String>) {
|
pub fn generate_playlist(config: &GlobalConfig, mut date_range: Vec<String>) {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let total_length = config.playlist.length_sec.unwrap();
|
let total_length = config.playlist.length_sec.unwrap();
|
||||||
let current_list = Arc::new(Mutex::new(vec![Media::new(0, "".to_string(), false)]));
|
let current_list = Arc::new(Mutex::new(vec![Media::new(0, "".to_string(), false)]));
|
||||||
let index = Arc::new(AtomicUsize::new(0));
|
let index = Arc::new(AtomicUsize::new(0));
|
||||||
@ -70,7 +69,7 @@ pub fn generate_playlist(mut date_range: Vec<String>) {
|
|||||||
date_range = get_date_range(&date_range)
|
date_range = get_date_range(&date_range)
|
||||||
}
|
}
|
||||||
|
|
||||||
let media_list = FolderSource::new(current_list, index);
|
let media_list = FolderSource::new(config, current_list, index);
|
||||||
let list_length = media_list.nodes.lock().unwrap().len();
|
let list_length = media_list.nodes.lock().unwrap().len();
|
||||||
|
|
||||||
for date in date_range {
|
for date in date_range {
|
||||||
|
@ -48,13 +48,13 @@ impl Playlist {
|
|||||||
/// Read json playlist file, fills Playlist struct and set some extra values,
|
/// Read json playlist file, fills Playlist 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: &GlobalConfig,
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
seek: bool,
|
seek: bool,
|
||||||
next_start: f64,
|
next_start: f64,
|
||||||
) -> Playlist {
|
) -> Playlist {
|
||||||
let config = GlobalConfig::global();
|
let config_clone = config.clone();
|
||||||
|
|
||||||
let mut playlist_path = Path::new(&config.playlist.path).to_owned();
|
let mut playlist_path = Path::new(&config.playlist.path).to_owned();
|
||||||
let mut start_sec = config.playlist.start_sec.unwrap();
|
let mut start_sec = config.playlist.start_sec.unwrap();
|
||||||
let date = get_date(seek, start_sec, next_start);
|
let date = get_date(seek, start_sec, next_start);
|
||||||
@ -113,7 +113,7 @@ pub fn read_json(
|
|||||||
|
|
||||||
let list_clone = playlist.clone();
|
let list_clone = playlist.clone();
|
||||||
|
|
||||||
thread::spawn(move || validate_playlist(list_clone, is_terminated, config.clone()));
|
thread::spawn(move || validate_playlist(list_clone, is_terminated, config_clone));
|
||||||
|
|
||||||
playlist
|
playlist
|
||||||
}
|
}
|
||||||
|
@ -25,26 +25,21 @@ use simplelog::*;
|
|||||||
use crate::utils::GlobalConfig;
|
use crate::utils::GlobalConfig;
|
||||||
|
|
||||||
/// send log messages to mail recipient
|
/// send log messages to mail recipient
|
||||||
fn send_mail(msg: String) {
|
fn send_mail(cfg: &GlobalConfig, msg: String) {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
|
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from(config.mail.sender_addr.parse().unwrap())
|
.from(cfg.mail.sender_addr.parse().unwrap())
|
||||||
.to(config.mail.recipient.parse().unwrap())
|
.to(cfg.mail.recipient.parse().unwrap())
|
||||||
.subject(config.mail.subject.clone())
|
.subject(cfg.mail.subject.clone())
|
||||||
.header(header::ContentType::TEXT_PLAIN)
|
.header(header::ContentType::TEXT_PLAIN)
|
||||||
.body(clean_string(&msg))
|
.body(clean_string(&msg))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let credentials = Credentials::new(
|
let credentials = Credentials::new(cfg.mail.sender_addr.clone(), cfg.mail.sender_pass.clone());
|
||||||
config.mail.sender_addr.clone(),
|
|
||||||
config.mail.sender_pass.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut transporter = SmtpTransport::relay(config.mail.smtp_server.clone().as_str());
|
let mut transporter = SmtpTransport::relay(cfg.mail.smtp_server.clone().as_str());
|
||||||
|
|
||||||
if config.mail.starttls {
|
if cfg.mail.starttls {
|
||||||
transporter = SmtpTransport::starttls_relay(config.mail.smtp_server.clone().as_str())
|
transporter = SmtpTransport::starttls_relay(cfg.mail.smtp_server.clone().as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
let mailer = transporter.unwrap().credentials(credentials).build();
|
let mailer = transporter.unwrap().credentials(credentials).build();
|
||||||
@ -59,11 +54,11 @@ fn send_mail(msg: String) {
|
|||||||
/// Basic Mail Queue
|
/// Basic Mail Queue
|
||||||
///
|
///
|
||||||
/// Check every give seconds for messages and send them.
|
/// Check every give seconds for messages and send them.
|
||||||
fn mail_queue(messages: Arc<Mutex<Vec<String>>>, interval: u64) {
|
fn mail_queue(cfg: GlobalConfig, messages: Arc<Mutex<Vec<String>>>, interval: u64) {
|
||||||
loop {
|
loop {
|
||||||
if messages.lock().unwrap().len() > 0 {
|
if messages.lock().unwrap().len() > 0 {
|
||||||
let msg = messages.lock().unwrap().join("\n");
|
let msg = messages.lock().unwrap().join("\n");
|
||||||
send_mail(msg);
|
send_mail(&cfg, msg);
|
||||||
|
|
||||||
messages.lock().unwrap().clear();
|
messages.lock().unwrap().clear();
|
||||||
}
|
}
|
||||||
@ -131,7 +126,7 @@ impl SharedLogger for LogMailer {
|
|||||||
///
|
///
|
||||||
/// ToDo: maybe in next version from simplelog this is not necessary anymore.
|
/// ToDo: maybe in next version from simplelog this is not necessary anymore.
|
||||||
fn clean_string(text: &str) -> String {
|
fn clean_string(text: &str) -> String {
|
||||||
let regex: Regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap();
|
let regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap();
|
||||||
|
|
||||||
regex.replace_all(text, "").to_string()
|
regex.replace_all(text, "").to_string()
|
||||||
}
|
}
|
||||||
@ -141,8 +136,8 @@ fn clean_string(text: &str) -> String {
|
|||||||
/// - console logger
|
/// - console logger
|
||||||
/// - file logger
|
/// - file logger
|
||||||
/// - mail logger
|
/// - mail logger
|
||||||
pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
pub fn init_logging(config: &GlobalConfig) -> Vec<Box<dyn SharedLogger>> {
|
||||||
let config = GlobalConfig::global();
|
let config_clone = config.clone();
|
||||||
let app_config = config.logging.clone();
|
let app_config = config.logging.clone();
|
||||||
let mut time_level = LevelFilter::Off;
|
let mut time_level = LevelFilter::Off;
|
||||||
let mut app_logger: Vec<Box<dyn SharedLogger>> = vec![];
|
let mut app_logger: Vec<Box<dyn SharedLogger>> = vec![];
|
||||||
@ -169,7 +164,7 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
let file_config = log_config
|
let file_config = log_config
|
||||||
.clone()
|
.clone()
|
||||||
.set_time_format_custom(format_description!(
|
.set_time_format_custom(format_description!(
|
||||||
"[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]]"
|
"[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:5]]"
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
let mut log_path = "logs/ffplayout.log".to_string();
|
let mut log_path = "logs/ffplayout.log".to_string();
|
||||||
@ -185,20 +180,18 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
println!("Logging path not exists!")
|
println!("Logging path not exists!")
|
||||||
}
|
}
|
||||||
|
|
||||||
let log = || {
|
let log_file = FileRotate::new(
|
||||||
FileRotate::new(
|
log_path,
|
||||||
log_path,
|
AppendTimestamp::with_format(
|
||||||
AppendTimestamp::with_format(
|
"%Y-%m-%d",
|
||||||
"%Y-%m-%d",
|
FileLimit::MaxFiles(app_config.backup_count),
|
||||||
FileLimit::MaxFiles(app_config.backup_count),
|
DateFrom::DateYesterday,
|
||||||
DateFrom::DateYesterday,
|
),
|
||||||
),
|
ContentLimit::Time(TimeFrequency::Daily),
|
||||||
ContentLimit::Time(TimeFrequency::Daily),
|
Compression::None,
|
||||||
Compression::None,
|
);
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
app_logger.push(WriteLogger::new(LevelFilter::Debug, file_config, log()));
|
app_logger.push(WriteLogger::new(LevelFilter::Debug, file_config, log_file));
|
||||||
} else {
|
} else {
|
||||||
let term_config = log_config
|
let term_config = log_config
|
||||||
.clone()
|
.clone()
|
||||||
@ -207,7 +200,7 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
.set_level_color(Level::Warn, Some(Color::Ansi256(208)))
|
.set_level_color(Level::Warn, Some(Color::Ansi256(208)))
|
||||||
.set_level_color(Level::Error, Some(Color::Ansi256(9)))
|
.set_level_color(Level::Error, Some(Color::Ansi256(9)))
|
||||||
.set_time_format_custom(format_description!(
|
.set_time_format_custom(format_description!(
|
||||||
"\x1b[[30;1m[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:4]]\x1b[[0m"
|
"\x1b[[30;1m[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:5]]\x1b[[0m"
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -225,7 +218,7 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
let messages_clone = messages.clone();
|
let messages_clone = messages.clone();
|
||||||
let interval = config.mail.interval;
|
let interval = config.mail.interval;
|
||||||
|
|
||||||
thread::spawn(move || mail_queue(messages_clone, interval));
|
thread::spawn(move || mail_queue(config_clone, messages_clone, interval));
|
||||||
|
|
||||||
let mail_config = log_config.build();
|
let mail_config = log_config.build();
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ mod json_validate;
|
|||||||
mod logging;
|
mod logging;
|
||||||
|
|
||||||
pub use arg_parse::get_args;
|
pub use arg_parse::get_args;
|
||||||
pub use config::{init_config, GlobalConfig};
|
pub use config::GlobalConfig;
|
||||||
pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*};
|
pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*};
|
||||||
pub use generator::generate_playlist;
|
pub use generator::generate_playlist;
|
||||||
pub use json_serializer::{read_json, Playlist, DUMMY_LEN};
|
pub use json_serializer::{read_json, Playlist, DUMMY_LEN};
|
||||||
@ -119,9 +119,9 @@ impl Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_filter(&mut self) {
|
pub fn add_filter(&mut self, config: &GlobalConfig) {
|
||||||
let mut node = self.clone();
|
let mut node = self.clone();
|
||||||
self.filter = Some(filter_chains(&mut node))
|
self.filter = Some(filter_chains(config, &mut node))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,8 +190,7 @@ impl MediaProbe {
|
|||||||
/// Write current status to status file in temp folder.
|
/// Write current status to status file in temp folder.
|
||||||
///
|
///
|
||||||
/// The status file is init in main function and mostly modified in RPC server.
|
/// The status file is init in main function and mostly modified in RPC server.
|
||||||
pub fn write_status(date: &str, shift: f64) {
|
pub fn write_status(config: &GlobalConfig, date: &str, shift: f64) {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let stat_file = config.general.stat_file.clone();
|
let stat_file = config.general.stat_file.clone();
|
||||||
|
|
||||||
let data = json!({
|
let data = json!({
|
||||||
@ -206,14 +205,14 @@ pub fn write_status(date: &str, shift: f64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pub fn get_timestamp() -> i64 {
|
// pub fn get_timestamp() -> i64 {
|
||||||
// let local: DateTime<Local> = Local::now();
|
// let local: DateTime<Local> = time_now();
|
||||||
|
|
||||||
// local.timestamp_millis() as i64
|
// local.timestamp_millis() as i64
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/// Get current time in seconds.
|
/// Get current time in seconds.
|
||||||
pub fn get_sec() -> f64 {
|
pub fn get_sec() -> f64 {
|
||||||
let local: DateTime<Local> = Local::now();
|
let local: DateTime<Local> = time_now();
|
||||||
|
|
||||||
(local.hour() * 3600 + local.minute() * 60 + local.second()) as f64
|
(local.hour() * 3600 + local.minute() * 60 + local.second()) as f64
|
||||||
+ (local.nanosecond() as f64 / 1000000000.0)
|
+ (local.nanosecond() as f64 / 1000000000.0)
|
||||||
@ -224,7 +223,7 @@ pub fn get_sec() -> f64 {
|
|||||||
/// - When time is before playlist start, get date from yesterday.
|
/// - When time is before playlist start, get date from yesterday.
|
||||||
/// - When given next_start is over target length (normally a full day), get date from tomorrow.
|
/// - When given next_start is over target length (normally a full day), get date from tomorrow.
|
||||||
pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
|
pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
|
||||||
let local: DateTime<Local> = Local::now();
|
let local: DateTime<Local> = time_now();
|
||||||
|
|
||||||
if seek && start > get_sec() {
|
if seek && start > get_sec() {
|
||||||
return (local - Duration::days(1)).format("%Y-%m-%d").to_string();
|
return (local - Duration::days(1)).format("%Y-%m-%d").to_string();
|
||||||
@ -286,8 +285,7 @@ pub fn is_close(a: f64, b: f64, to: f64) -> bool {
|
|||||||
/// if we still in sync.
|
/// if we still in sync.
|
||||||
///
|
///
|
||||||
/// We also get here the global delta between clip start and time when a new playlist should start.
|
/// We also get here the global delta between clip start and time when a new playlist should start.
|
||||||
pub fn get_delta(begin: &f64) -> (f64, f64) {
|
pub fn get_delta(config: &GlobalConfig, begin: &f64) -> (f64, f64) {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let mut current_time = get_sec();
|
let mut current_time = get_sec();
|
||||||
let start = config.playlist.start_sec.unwrap();
|
let start = config.playlist.start_sec.unwrap();
|
||||||
let length = time_to_sec(&config.playlist.length);
|
let length = time_to_sec(&config.playlist.length);
|
||||||
@ -318,9 +316,7 @@ pub fn get_delta(begin: &f64) -> (f64, f64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if clip in playlist is in sync with global time.
|
/// Check if clip in playlist is in sync with global time.
|
||||||
pub fn check_sync(delta: f64) -> bool {
|
pub fn check_sync(config: &GlobalConfig, delta: f64) -> bool {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
|
|
||||||
if delta.abs() > config.general.stop_threshold && config.general.stop_threshold > 0.0 {
|
if delta.abs() > config.general.stop_threshold && config.general.stop_threshold > 0.0 {
|
||||||
error!("Clip begin out of sync for <yellow>{}</> seconds", delta);
|
error!("Clip begin out of sync for <yellow>{}</> seconds", delta);
|
||||||
return false;
|
return false;
|
||||||
@ -330,8 +326,7 @@ pub fn check_sync(delta: f64) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a dummy clip as a placeholder for missing video files.
|
/// Create a dummy clip as a placeholder for missing video files.
|
||||||
pub fn gen_dummy(duration: f64) -> (String, Vec<String>) {
|
pub fn gen_dummy(config: &GlobalConfig, duration: f64) -> (String, Vec<String>) {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
let color = "#121212";
|
let color = "#121212";
|
||||||
let source = format!(
|
let source = format!(
|
||||||
"color=c={color}:s={}x{}:d={duration}",
|
"color=c={color}:s={}x{}:d={duration}",
|
||||||
@ -475,9 +470,7 @@ fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
|
|||||||
/// Validate ffmpeg/ffprobe/ffplay.
|
/// Validate ffmpeg/ffprobe/ffplay.
|
||||||
///
|
///
|
||||||
/// Check if they are in system and has all filters and codecs we need.
|
/// Check if they are in system and has all filters and codecs we need.
|
||||||
pub fn validate_ffmpeg() {
|
pub fn validate_ffmpeg(config: &GlobalConfig) {
|
||||||
let config = GlobalConfig::global();
|
|
||||||
|
|
||||||
is_in_system("ffmpeg");
|
is_in_system("ffmpeg");
|
||||||
is_in_system("ffprobe");
|
is_in_system("ffprobe");
|
||||||
|
|
||||||
@ -505,3 +498,37 @@ pub fn validate_ffmpeg() {
|
|||||||
warn!("ffmpeg contains no zmq filter! Text messages will not work...");
|
warn!("ffmpeg contains no zmq filter! Text messages will not work...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get system time, in non test case.
|
||||||
|
#[cfg(not(test))]
|
||||||
|
pub fn time_now() -> DateTime<Local> {
|
||||||
|
Local::now()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mocked system time, in test case.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod mock_time {
|
||||||
|
use super::*;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static DATE_TIME_DIFF: RefCell<Option<Duration>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_now() -> DateTime<Local> {
|
||||||
|
DATE_TIME_DIFF.with(|cell| match cell.borrow().as_ref().cloned() {
|
||||||
|
Some(diff) => Local::now() - diff,
|
||||||
|
None => Local::now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mock_time(date_time: &str) {
|
||||||
|
let date_obj = NaiveDateTime::parse_from_str(date_time, "%Y-%m-%dT%H:%M:%S");
|
||||||
|
let time = Local.from_local_datetime(&date_obj.unwrap()).unwrap();
|
||||||
|
|
||||||
|
DATE_TIME_DIFF.with(|cell| *cell.borrow_mut() = Some(Local::now() - time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub use mock_time::time_now;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user