Merge pull request #806 from jb-alvarado/master

auto create types for playout config, switch to real playout config form
This commit is contained in:
jb-alvarado 2024-10-16 21:04:53 +02:00 committed by GitHub
commit a89365da10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1564 additions and 401 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[env]
TS_RS_EXPORT_DIR = { value = "frontend/types", relative = true }

167
Cargo.lock generated
View File

@ -403,9 +403,9 @@ dependencies = [
[[package]]
name = "addr2line"
version = "0.24.1"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
@ -735,9 +735,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.24"
version = "1.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1"
dependencies = [
"jobserver",
"libc",
@ -793,9 +793,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.19"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
"clap_derive",
@ -803,9 +803,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.19"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
"anstream",
"anstyle",
@ -1215,7 +1215,7 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "ffplayout"
version = "0.24.0-rc2"
version = "0.24.0-rc3"
dependencies = [
"actix-files",
"actix-multipart",
@ -1266,6 +1266,7 @@ dependencies = [
"tokio",
"tokio-stream",
"toml_edit",
"ts-rs",
"uuid",
"walkdir",
"zeromq",
@ -1324,9 +1325,9 @@ dependencies = [
[[package]]
name = "flexi_logger"
version = "0.29.0"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a250587a211932896a131f214a4f64c047b826ce072d2018764e5ff5141df8fa"
checksum = "6c6e500462d7c5ee8b974423b55bd47f3e09c8306050e5bbeaccaf2b17992f70"
dependencies = [
"chrono",
"glob",
@ -1373,9 +1374,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
@ -1388,9 +1389,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
@ -1398,15 +1399,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
@ -1426,15 +1427,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
@ -1443,21 +1444,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
@ -1496,9 +1497,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.31.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
@ -1975,9 +1976,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.70"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [
"wasm-bindgen",
]
@ -2408,21 +2409,18 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.4"
version = "0.36.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.20.1"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
dependencies = [
"portable-atomic",
]
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "paris"
@ -2567,12 +2565,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "portable-atomic"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -2590,9 +2582,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.86"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [
"unicode-ident",
]
@ -2916,9 +2908,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.13"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
dependencies = [
"log",
"once_cell",
@ -3103,9 +3095,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.10.0"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9720086b3357bcb44fce40117d769a4d068c70ecfa190850a980a71755f66fcc"
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
dependencies = [
"base64 0.22.1",
"chrono",
@ -3121,9 +3113,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.10.0"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f1abbfe725f27678f4663bcacb75a83e829fd464c25d78dd038a3a29e307cec"
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
dependencies = [
"darling",
"proc-macro2",
@ -3672,9 +3664,18 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "tests"
version = "0.24.0-rc2"
version = "0.24.0-rc3"
dependencies = [
"actix-rt",
"actix-test",
@ -3909,6 +3910,30 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9"
dependencies = [
"chrono",
"lazy_static",
"thiserror",
"ts-rs-macros",
]
[[package]]
name = "ts-rs-macros"
version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"termcolor",
]
[[package]]
name = "typeid"
version = "1.0.2"
@ -4100,9 +4125,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [
"cfg-if",
"once_cell",
@ -4111,9 +4136,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [
"bumpalo",
"log",
@ -4126,9 +4151,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
dependencies = [
"cfg-if",
"js-sys",
@ -4138,9 +4163,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -4148,9 +4173,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
@ -4161,15 +4186,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "web-sys"
version = "0.3.70"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -4560,9 +4585,9 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zeromq"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb0560d00172817b7f7c2265060783519c475702ae290b154115ca75e976d4d0"
checksum = "6a4528179201f6eecf211961a7d3276faa61554c82651ecc66387f68fc3004bd"
dependencies = [
"async-trait",
"asynchronous-codec",

View File

@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
description = "24/7 playout based on rust and ffmpeg"
readme = "README.md"
version = "0.24.0-rc2"
version = "0.24.0-rc3"
license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout"
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]

View File

@ -1,6 +1,6 @@
FROM alpine:latest
ARG FFPLAYOUT_VERSION=0.24.0-rc1
ARG FFPLAYOUT_VERSION=0.24.0-rc3
ARG SHARED_STORAGE=false
ENV DB=/db

View File

@ -1,6 +1,6 @@
FROM alpine:latest
ARG FFPLAYOUT_VERSION=0.24.0-rc1
ARG FFPLAYOUT_VERSION=0.24.0-rc3
ARG SHARED_STORAGE=false
ENV DB=/db

View File

@ -1,6 +1,6 @@
FROM nvidia/cuda:12.5.0-runtime-rockylinux9
ARG FFPLAYOUT_VERSION=0.24.0-rc1
ARG FFPLAYOUT_VERSION=0.24.0-rc3
ARG SHARED_STORAGE=false
ENV DB=/db

View File

@ -1,7 +1,7 @@
## Closed Captions
#### Note:
**This is only an _experimental feature_. Please be aware that bugs and unexpected behavior may occur. To utilize this feature, a [special patched](https://github.com/jb-alvarado/compile-ffmpeg-osx-linux) version of FFmpeg is required. Importantly, there is currently no official support for this functionality.**
**This is only an _experimental feature_. Please be aware that bugs and unexpected behavior may occur. To utilize this feature, a version after 7.1 of FFmpeg is required. Importantly, there is currently no official support for this functionality.**
### Usage
**ffplayout** can handle closed captions in WebVTT format for HLS streaming.
@ -16,7 +16,7 @@ To encode the closed captions, the **hls** mode needs to be enabled, and specifi
-profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop \
-muxpreload 0 -muxdelay 0 -f hls -hls_time 6 -hls_list_size 600 \
-hls_flags append_list+delete_segments+omit_endlist \
-var_stream_map v:0,a:0,s:0,sgroup:subs,name:English,language:en-US,default:YES \
-var_stream_map v:0,a:0,s:0,sgroup:subs,sname:English,language:en-US,default:YES \
-master_pl_name master.m3u8 \
-hls_segment_filename \
live/stream-%d.ts live/stream.m3u8

View File

@ -69,6 +69,11 @@ cargo deb --no-build --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffpla
cargo generate-rpm --target=x86_64-unknown-linux-musl
```
## Generate types for Frontend
The frontend uses TypeScript, to generate types for the rust structs run: `cargo test`.
The generated types are then in [types folder](/frontend/types).
## Setup Frontend
Make sure to install the dependencies:

View File

@ -27,7 +27,7 @@ crossbeam-channel = "0.5"
derive_more = { version = "1", features = ["display"] }
faccess = "0.2"
ffprobe = "0.4"
flexi_logger = { version = "=0.29.0", features = ["kv", "colors"] }
flexi_logger = { version = "0.29", features = ["kv", "colors"] }
futures-util = { version = "0.3", default-features = false, features = ["std"] }
home = "0.5"
jsonwebtoken = "9"
@ -60,7 +60,8 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
time = { version = "0.3", features = ["formatting", "macros"] }
tokio = { version = "1.29", features = ["full"] }
tokio-stream = "0.1"
toml_edit = {version ="0.22", features = ["serde"]}
toml_edit = {version = "0.22", features = ["serde"]}
ts-rs = { version = "10", features = ["chrono-impl", "no-serde-warnings"] }
uuid = "1.8"
walkdir = "2"
zeromq = { version = "0.4", default-features = false, features = [

View File

@ -869,10 +869,22 @@ pub async fn control_playout(
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
match control_state(&pool, manager, &control.control).await {
if manager.is_processing.load(Ordering::SeqCst) {
return Err(ServiceError::Conflict(
"A command is already being processed, please wait".to_string(),
));
}
manager.is_processing.store(true, Ordering::SeqCst);
let resp = match control_state(&pool, &manager, &control.control).await {
Ok(res) => Ok(web::Json(res)),
Err(e) => Err(e),
}
};
manager.is_processing.store(false, Ordering::SeqCst);
resp
}
/// **Get current Clip**

View File

@ -267,23 +267,19 @@ where
pub struct Configuration {
pub id: i32,
pub channel_id: i32,
pub general_help: String,
pub general_stop_threshold: f64,
pub mail_help: String,
pub mail_subject: String,
pub mail_recipient: String,
pub mail_level: String,
pub mail_interval: i64,
pub logging_help: String,
pub logging_ffmpeg_level: String,
pub logging_ingest_level: String,
pub logging_detect_silence: bool,
#[serde(default)]
pub logging_ignore: String,
pub processing_help: String,
pub processing_mode: String,
pub processing_audio_only: bool,
pub processing_copy_audio: bool,
@ -311,34 +307,28 @@ pub struct Configuration {
#[serde(default)]
pub processing_vtt_dummy: Option<String>,
pub ingest_help: String,
pub ingest_enable: bool,
pub ingest_param: String,
#[serde(default)]
pub ingest_filter: String,
pub playlist_help: String,
pub playlist_day_start: String,
pub playlist_length: String,
pub playlist_infinit: bool,
pub storage_help: String,
pub storage_filler: String,
pub storage_extensions: String,
pub storage_shuffle: bool,
pub text_help: String,
pub text_add: bool,
pub text_from_filename: bool,
pub text_font: String,
pub text_style: String,
pub text_regex: String,
pub task_help: String,
pub task_enable: bool,
pub task_path: String,
pub output_help: String,
pub output_mode: String,
pub output_param: String,
}
@ -348,19 +338,15 @@ impl Configuration {
Self {
id,
channel_id,
general_help: config.general.help_text,
general_stop_threshold: config.general.stop_threshold,
mail_help: config.mail.help_text,
mail_subject: config.mail.subject,
mail_recipient: config.mail.recipient,
mail_level: config.mail.mail_level.to_string(),
mail_interval: config.mail.interval,
logging_help: config.logging.help_text,
logging_ffmpeg_level: config.logging.ffmpeg_level,
logging_ingest_level: config.logging.ingest_level,
logging_detect_silence: config.logging.detect_silence,
logging_ignore: config.logging.ignore_lines.join(";"),
processing_help: config.processing.help_text,
processing_mode: config.processing.mode.to_string(),
processing_audio_only: config.processing.audio_only,
processing_audio_track_index: config.processing.audio_track_index,
@ -381,28 +367,22 @@ impl Configuration {
processing_filter: config.processing.custom_filter,
processing_vtt_enable: config.processing.vtt_enable,
processing_vtt_dummy: config.processing.vtt_dummy,
ingest_help: config.ingest.help_text,
ingest_enable: config.ingest.enable,
ingest_param: config.ingest.input_param,
ingest_filter: config.ingest.custom_filter,
playlist_help: config.playlist.help_text,
playlist_day_start: config.playlist.day_start,
playlist_length: config.playlist.length,
playlist_infinit: config.playlist.infinit,
storage_help: config.storage.help_text,
storage_filler: config.storage.filler,
storage_extensions: config.storage.extensions.join(";"),
storage_shuffle: config.storage.shuffle,
text_help: config.text.help_text,
text_add: config.text.add_text,
text_font: config.text.font,
text_from_filename: config.text.text_from_filename,
text_style: config.text.style,
text_regex: config.text.regex,
task_help: config.task.help_text,
task_enable: config.task.enable,
task_path: config.task.path.to_string_lossy().to_string(),
output_help: config.output.help_text,
output_mode: config.output.mode.to_string(),
output_param: config.output.output_param,
}

View File

@ -5,11 +5,13 @@ use serde_with::{serde_as, NoneAsEmptyString};
use shlex::split;
use sqlx::{Pool, Sqlite};
use tokio::io::AsyncReadExt;
use ts_rs::TS;
use crate::db::{handles, models::AdvancedConfiguration};
use crate::utils::ServiceError;
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, TS)]
#[ts(export, export_to = "advanced_config.d.ts")]
pub struct AdvancedConfig {
pub decoder: DecoderConfig,
pub encoder: EncoderConfig,
@ -18,81 +20,115 @@ pub struct AdvancedConfig {
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, TS)]
#[ts(export, export_to = "advanced_config.d.ts")]
pub struct DecoderConfig {
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub input_param: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub output_param: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub output_cmd: Option<Vec<String>>,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, TS)]
#[ts(export, export_to = "advanced_config.d.ts")]
pub struct EncoderConfig {
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub input_param: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, TS)]
#[ts(export, export_to = "advanced_config.d.ts")]
pub struct IngestConfig {
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub input_param: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, TS)]
#[ts(export, export_to = "advanced_config.d.ts")]
pub struct FilterConfig {
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub deinterlace: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub pad_scale_w: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub pad_scale_h: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub pad_video: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub fps: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub scale: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub set_dar: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub fade_in: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub fade_out: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo_scale: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo_fade_in: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo_fade_out: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub tpad: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub drawtext_from_file: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub drawtext_from_zmq: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub aevalsrc: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub afade_in: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub afade_out: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub apad: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub volume: Option<String>,
#[ts(type = "string")]
#[serde_as(as = "NoneAsEmptyString")]
pub split: Option<String>,
}

View File

@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
use shlex::split;
use sqlx::{Pool, Sqlite};
use tokio::{fs, io::AsyncReadExt};
use ts_rs::TS;
use crate::db::{handles, models};
use crate::utils::{files::norm_abs_path, free_tcp_socket, time_to_sec};
@ -52,7 +53,8 @@ pub const FFMPEG_UNRECOVERABLE_ERRORS: [&str; 6] = [
"Unrecognized option",
];
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
#[serde(rename_all = "lowercase")]
pub enum OutputMode {
Desktop,
@ -103,7 +105,8 @@ impl fmt::Display for OutputMode {
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
#[serde(rename_all = "lowercase")]
pub enum ProcessMode {
Folder,
@ -141,14 +144,16 @@ impl FromStr for ProcessMode {
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
pub struct Template {
pub sources: Vec<Source>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
pub struct Source {
#[ts(type = "string")]
pub start: NaiveTime,
#[ts(type = "string")]
pub duration: NaiveTime,
pub shuffle: bool,
pub paths: Vec<PathBuf>,
@ -157,10 +162,14 @@ pub struct Source {
/// Channel Config
///
/// This we init ones, when ffplayout is starting and use them globally in the hole program.
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct PlayoutConfig {
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub channel: Channel,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub advanced: AdvancedConfig,
pub general: General,
@ -176,7 +185,7 @@ pub struct PlayoutConfig {
pub output: Output,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
pub struct Channel {
pub logs: PathBuf,
pub public: PathBuf,
@ -197,24 +206,32 @@ impl Channel {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct General {
pub help_text: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub id: i32,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub channel_id: i32,
pub stop_threshold: f64,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub generate: Option<Vec<String>>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub ffmpeg_filters: Vec<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub ffmpeg_libs: Vec<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub template: Option<Template>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub skip_validation: bool,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub validate: bool,
}
@ -222,7 +239,6 @@ pub struct General {
impl General {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.general_help.clone(),
id: config.id,
channel_id: config.channel_id,
stop_threshold: config.general_stop_threshold,
@ -236,19 +252,24 @@ impl General {
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Mail {
pub help_text: String,
pub subject: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub smtp_server: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub starttls: bool,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub sender_addr: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub sender_pass: String,
pub recipient: String,
#[ts(type = "string")]
pub mail_level: Level,
pub interval: i64,
}
@ -256,7 +277,6 @@ pub struct Mail {
impl Mail {
fn new(global: &models::GlobalSettings, config: &models::Configuration) -> Self {
Self {
help_text: config.mail_help.clone(),
subject: config.mail_subject.clone(),
smtp_server: global.mail_smtp.clone(),
starttls: global.mail_starttls,
@ -272,7 +292,6 @@ impl Mail {
impl Default for Mail {
fn default() -> Self {
Mail {
help_text: String::default(),
subject: String::default(),
smtp_server: String::default(),
starttls: bool::default(),
@ -285,9 +304,9 @@ impl Default for Mail {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Logging {
pub help_text: String,
pub ffmpeg_level: String,
pub ingest_level: String,
pub detect_silence: bool,
@ -297,7 +316,6 @@ pub struct Logging {
impl Logging {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.logging_help.clone(),
ffmpeg_level: config.logging_ffmpeg_level.clone(),
ingest_level: config.logging_ingest_level.clone(),
detect_silence: config.logging_detect_silence,
@ -310,9 +328,9 @@ impl Logging {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Processing {
pub help_text: String,
pub mode: ProcessMode,
pub audio_only: bool,
pub copy_audio: bool,
@ -323,6 +341,7 @@ pub struct Processing {
pub fps: f64,
pub add_logo: bool,
pub logo: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub logo_path: String,
pub logo_scale: String,
@ -338,6 +357,7 @@ pub struct Processing {
pub vtt_enable: bool,
#[serde(default)]
pub vtt_dummy: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub cmd: Option<Vec<String>>,
}
@ -345,7 +365,6 @@ pub struct Processing {
impl Processing {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.processing_help.clone(),
mode: ProcessMode::new(&config.processing_mode.clone()),
audio_only: config.processing_audio_only,
audio_track_index: config.processing_audio_track_index,
@ -372,12 +391,13 @@ impl Processing {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Ingest {
pub help_text: String,
pub enable: bool,
pub input_param: String,
pub custom_filter: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
@ -385,7 +405,6 @@ pub struct Ingest {
impl Ingest {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.ingest_help.clone(),
enable: config.ingest_enable,
input_param: config.ingest_param.clone(),
custom_filter: config.ingest_filter.clone(),
@ -394,13 +413,15 @@ impl Ingest {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Playlist {
pub help_text: String,
pub day_start: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub start_sec: Option<f64>,
pub length: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub length_sec: Option<f64>,
pub infinit: bool,
@ -409,7 +430,6 @@ pub struct Playlist {
impl Playlist {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.playlist_help.clone(),
day_start: config.playlist_day_start.clone(),
start_sec: None,
length: config.playlist_length.clone(),
@ -419,26 +439,28 @@ impl Playlist {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Storage {
pub help_text: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub path: PathBuf,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub paths: Vec<PathBuf>,
pub filler: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub filler_path: PathBuf,
pub extensions: Vec<String>,
pub shuffle: bool,
#[serde(skip_serializing, skip_deserializing)]
#[serde(skip_deserializing)]
pub shared_storage: bool,
}
impl Storage {
fn new(config: &models::Configuration, path: PathBuf, shared_storage: bool) -> Self {
Self {
help_text: config.storage_help.clone(),
path,
paths: vec![],
filler: config.storage_filler.clone(),
@ -454,18 +476,22 @@ impl Storage {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Text {
pub help_text: String,
pub add_text: bool,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub node_pos: Option<usize>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub zmq_stream_socket: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub zmq_server_socket: Option<String>,
#[serde(alias = "fontfile")]
pub font: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub font_path: String,
pub text_from_filename: bool,
@ -476,7 +502,6 @@ pub struct Text {
impl Text {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.text_help.clone(),
add_text: config.text_add,
node_pos: None,
zmq_stream_socket: None,
@ -490,9 +515,9 @@ impl Text {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Task {
pub help_text: String,
pub enable: bool,
pub path: PathBuf,
}
@ -500,22 +525,24 @@ pub struct Task {
impl Task {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.task_help.clone(),
enable: config.task_enable,
path: PathBuf::from(config.task_path.clone()),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[ts(export, export_to = "playout_config.d.ts")]
pub struct Output {
pub help_text: String,
pub mode: OutputMode,
pub output_param: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub output_count: usize,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub output_filter: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub output_cmd: Option<Vec<String>>,
}
@ -523,7 +550,6 @@ pub struct Output {
impl Output {
fn new(config: &models::Configuration) -> Self {
Self {
help_text: config.output_help.clone(),
mode: OutputMode::new(&config.output_mode),
output_param: config.output_param.clone(),
output_count: 0,

View File

@ -132,7 +132,7 @@ pub async fn send_message(
pub async fn control_state(
conn: &Pool<Sqlite>,
manager: ChannelManager,
manager: &ChannelManager,
command: &str,
) -> Result<Map<String, Value>, ServiceError> {
let config = manager.config.lock().unwrap().clone();

View File

@ -1,5 +1,5 @@
<template>
<div class="min-w-[200px] pe-8 w-[768px]">
<div class="max-w-[1200px] xs:pe-8">
<h2 class="pt-3 text-3xl">{{ t('advanced.title') }}</h2>
<p class="mt-5 font-bold text-orange-500">{{ t('advanced.warning') }}</p>
<form
@ -7,26 +7,417 @@
class="mt-10 grid md:grid-cols-[180px_auto] gap-5"
@submit.prevent="onSubmitAdvanced"
>
<template v-for="(item, key) in configStore.advanced" :key="key">
<div class="text-xl pt-3 text-right">{{ setTitle(key.toString()) }}:</div>
<div class="md:pt-4">
<label
v-for="(_, name) in (item as Record<string, any>)"
:key="name"
class="form-control w-full"
>
<div class="label">
<span class="label-text !text-md font-bold">{{ name }}</span>
</div>
<input
:id="name"
v-model="item[name]"
type="text"
class="input input-sm input-bordered w-full"
/>
</label>
</div>
</template>
<div class="text-xl pt-3 md:text-right">{{ t('advanced.decoder') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
In streaming mode, the decoder settings are responsible for unifying the media files.
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Input Parameter</span>
</div>
<input
v-model="configStore.advanced.decoder.input_param"
type="text"
name="input_param"
class="input input-sm input-bordered w-full"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Output Parameter</span>
</div>
<input
v-model="configStore.advanced.decoder.output_param"
type="text"
name="output_param"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">
Default: -c:v mpeg2video -g 1 -b:v 57600k -minrate 57600k -maxrate 57600k -bufsize 28800k
-mpegts_flags initial_discontinuity -c:a s302m -strict -2 -sample_fmt s16 -ar 48000 -ac 2 -f
mpegts
</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('advanced.encoder') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">Encoder settings representing the streaming output.</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Input Parameter</span>
</div>
<input
v-model="configStore.advanced.encoder.input_param"
type="text"
name="input_param"
class="input input-sm input-bordered w-full"
/>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('advanced.filter') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
The filters are mainly there to transform audio and video into the correct format, but also to
place text and logo over the video, create in/out fade etc.<br />
If curly brackets are included in the default values, these must be adopted.<br />
If a filter is not compatible and you know that it is not absolutely necessary to use it, add
null/anull.
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Deinterlace</span>
</div>
<input
v-model="configStore.advanced.filter.deinterlace"
type="text"
name="deinterlace"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">Default: yadif=0:-1:0 </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Pad Scaling Width</span>
</div>
<input
v-model="configStore.advanced.filter.pad_scale_w"
type="text"
name="pad_scale_w"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: scale={}:-1 </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Pad Scaling Height</span>
</div>
<input
v-model="configStore.advanced.filter.pad_scale_h"
type="text"
name="pad_scale_h"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: scale=-1:{} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Pad Video</span>
</div>
<input
v-model="configStore.advanced.filter.pad_video"
type="text"
name="pad_video"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">
Default: pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2
</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">FPS</span>
</div>
<input
v-model="configStore.advanced.filter.fps"
type="text"
name="fps"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: fps={} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Scale</span>
</div>
<input
v-model="configStore.advanced.filter.scale"
type="text"
name="scale"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: scale={}:{} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Set Dar</span>
</div>
<input
v-model="configStore.advanced.filter.set_dar"
type="text"
name="set_dar"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: setdar=dar={} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Fade In</span>
</div>
<input
v-model="configStore.advanced.filter.fade_in"
type="text"
name="fade_in"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: fade=in:st=0:d=0.5 </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Fade Out</span>
</div>
<input
v-model="configStore.advanced.filter.fade_out"
type="text"
name="fade_out"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: fade=out:st={}:d=1.0 </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo Scale</span>
</div>
<input
v-model="configStore.advanced.filter.overlay_logo_scale"
type="text"
name="overlay_logo_scale"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: scale={} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo Fade In</span>
</div>
<input
v-model="configStore.advanced.filter.overlay_logo_fade_in"
type="text"
name="overlay_logo_fade_in"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">
Default: fade=in:st=0:d=1.0:alpha=1
</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo Fade Out</span>
</div>
<input
v-model="configStore.advanced.filter.overlay_logo_fade_out"
type="text"
name="overlay_logo_fade_out"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">
Default: fade=out:st={}:d=1.0:alpha=1
</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo Overlay</span>
</div>
<input
v-model="configStore.advanced.filter.overlay_logo"
type="text"
name="overlay_logo"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">
Default: null[l];[v][l]overlay={}:shortest=1
</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">TPad</span>
</div>
<input
v-model="configStore.advanced.filter.tpad"
type="text"
name="tpad"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">
Default: tpad=stop_mode=add:stop_duration={}
</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Drawtext from File</span>
</div>
<input
v-model="configStore.advanced.filter.drawtext_from_file"
type="text"
name="drawtext_from_file"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: drawtext=text='{}':{}{} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Drawtext from ZMQ</span>
</div>
<input
v-model="configStore.advanced.filter.drawtext_from_zmq"
type="text"
name="drawtext_from_zmq"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">
Default: zmq=b=tcp\\\\://'{}',drawtext@dyntext={}
</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Audio Source</span>
</div>
<input
v-model="configStore.advanced.filter.aevalsrc"
type="text"
name="aevalsrc"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80 break-all">
Default: aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000
</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Audio Fade In</span>
</div>
<input
v-model="configStore.advanced.filter.afade_in"
type="text"
name="afade_in"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: afade=in:st=0:d=0.5 </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Audio Fade Out</span>
</div>
<input
v-model="configStore.advanced.filter.afade_out"
type="text"
name="afade_out"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: afade=out:st={}:d=1.0 </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Audio Pad</span>
</div>
<input
v-model="configStore.advanced.filter.apad"
type="text"
name="apad"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: apad=whole_dur={} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Volumen</span>
</div>
<input
v-model="configStore.advanced.filter.volume"
type="text"
name="volume"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: volume={} </span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Split</span>
</div>
<input
v-model="configStore.advanced.filter.split"
type="text"
name="split"
class="input input-sm input-bordered w-full"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80"> Default: split={}{} </span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('advanced.ingest') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">Ingest settings are for live streaming input.</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Input Parameter</span>
</div>
<input
v-model="configStore.advanced.ingest.input_param"
type="text"
name="input_param"
class="input input-sm input-bordered w-full"
/>
</label>
</div>
<div class="mt-5 mb-10">
<button class="btn btn-primary" type="submit">{{ t('config.save') }}</button>
</div>
@ -50,21 +441,6 @@ const indexStore = useIndex()
const showModal = ref(false)
function setTitle(input: string): string {
switch (input) {
case 'decoder':
return t('advanced.decoder')
case 'encoder':
return t('advanced.encoder')
case 'filter':
return t('advanced.filter')
case 'ingest':
return t('advanced.ingest')
default:
return input
}
}
async function onSubmitAdvanced() {
const update = await configStore.setAdvancedConfig()
configStore.onetimeInfo = true

View File

@ -1,83 +1,710 @@
<template>
<div class="max-w-[1200px] pe-8">
<div class="max-w-[1200px] xs:pe-8">
<h2 class="pt-3 text-3xl">{{ t('config.playoutConf') }}</h2>
<form
v-if="configStore.playout"
class="mt-10 grid md:grid-cols-[180px_auto] gap-5"
@submit.prevent="onSubmitPlayout"
>
<template v-for="(item, key) in configStore.playout" :key="key">
<div class="text-xl pt-3 text-right">{{ setTitle(key.toString()) }}:</div>
<div class="md:pt-4">
<label
v-for="(prop, name) in (item as Record<string, any>)"
:key="name"
class="form-control w-full"
:class="[typeof prop === 'boolean' && 'flex-row', name.toString() !== 'help_text' && 'mt-2']"
<div class="text-xl pt-3 md:text-right">{{ t('config.general') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.generalHelp') }}
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Stop Threshold</span>
</div>
<input
v-model="configStore.playout.general.stop_threshold"
type="number"
min="3"
class="input input-sm input-bordered w-full max-w-36"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.stopThreshold') }}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.mail') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.mailHelp') }}
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Subject</span>
</div>
<input
v-model="configStore.playout.mail.subject"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Recipient</span>
</div>
<input
v-model="configStore.playout.mail.recipient"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Mail Level</span>
</div>
<select
v-model="configStore.playout.mail.mail_level"
class="select select-sm select-bordered w-full max-w-xs"
>
<template
v-if="
name.toString() !== 'startInSec' &&
name.toString() !== 'lengthInSec' &&
!(name.toString() === 'path' && key.toString() === 'storage')
"
>
<div v-if="name.toString() !== 'help_text'" class="label">
<span class="label-text !text-md font-bold">{{ name }}</span>
</div>
<div v-if="name.toString() === 'help_text'" class="whitespace-pre-line">
{{ setHelp(key.toString(), prop) }}
</div>
<input
v-else-if="name.toString() === 'sender_pass'"
v-model="item[name]"
type="password"
:placeholder="t('config.placeholderPass')"
class="input input-sm input-bordered w-full"
/>
<textarea
v-else-if="name.toString() === 'output_param' || name.toString() === 'custom_filter'"
v-model="item[name]"
class="textarea textarea-bordered"
rows="3"
/>
<input
v-else-if="typeof prop === 'number' && prop % 1 === 0"
v-model="item[name]"
type="number"
class="input input-sm input-bordered w-full"
/>
<input
v-else-if="typeof prop === 'number'"
v-model="item[name]"
type="number"
class="input input-sm input-bordered w-full"
step="0.0001"
style="max-width: 250px"
/>
<input
v-else-if="typeof prop === 'boolean'"
v-model="item[name]"
type="checkbox"
class="checkbox checkbox-sm ms-2 mt-2"
/>
<input
v-else-if="name === 'ignore_lines'"
v-model="formatIgnoreLines"
type="text"
class="input input-sm input-bordered w-full"
/>
<input
v-else
:id="name"
v-model="item[name]"
type="text"
class="input input-sm input-bordered w-full"
/>
</template>
</label>
</div>
</template>
<option v-for="level in logLevels" :key="level" :value="level">{{ level }}</option>
</select>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Interval</span>
</div>
<input
v-model="configStore.playout.mail.interval"
type="number"
min="30"
step="10"
class="input input-sm input-bordered w-full max-w-36"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.mailInterval') }}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.logging') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.logHelp') }}
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">ffmpeg Level</span>
</div>
<select
v-model="configStore.playout.logging.ffmpeg_level"
class="select select-sm select-bordered w-full max-w-xs"
>
<option v-for="level in logLevels" :key="level" :value="level">{{ level }}</option>
</select>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Ingest Level</span>
</div>
<select
v-model="configStore.playout.logging.ingest_level"
class="select select-sm select-bordered w-full max-w-xs"
>
<option v-for="level in logLevels" :key="level" :value="level">{{ level }}</option>
</select>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.logging.detect_silence"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Detect Silence</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{ t('config.logDetect') }}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Ignore Lines</span>
</div>
<input
v-model="formatIgnoreLines"
type="text"
class="input input-sm input-bordered w-full truncate"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.logIgnore') }}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.processing') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.processingHelp') }}
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Mode</span>
</div>
<select
v-model="configStore.playout.processing.mode"
class="select select-sm select-bordered w-full max-w-xs"
>
<option v-for="mode in processingMode" :key="mode" :value="mode">{{ mode }}</option>
</select>
</label>
<label class="form-control w-full flex-row mt-2">
<input
v-model="configStore.playout.processing.audio_only"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Audio Only</span>
</div>
</label>
<label class="form-control w-full flex-row mt-2">
<input
v-model="configStore.playout.processing.copy_audio"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Copy Audio</span>
</div>
</label>
<label class="form-control w-full flex-row mt-2">
<input
v-model="configStore.playout.processing.copy_video"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Copy Video</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Width</span>
</div>
<input
v-model="configStore.playout.processing.width"
type="number"
min="-1"
step="1"
class="input input-sm input-bordered w-full max-w-36"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Height</span>
</div>
<input
v-model="configStore.playout.processing.height"
type="number"
min="-1"
step="1"
class="input input-sm input-bordered w-full max-w-36"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Aspect</span>
</div>
<input
v-model="configStore.playout.processing.aspect"
type="number"
min="1"
step="0.001"
class="input input-sm input-bordered w-full max-w-36"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">FPS</span>
</div>
<input
v-model="configStore.playout.processing.fps"
type="number"
min="1"
step="0.01"
class="input input-sm input-bordered w-full max-w-36"
/>
</label>
<label class="form-control w-full flex-row mt-2">
<input
v-model="configStore.playout.processing.add_logo"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Add Logo</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo</span>
</div>
<input
v-model="configStore.playout.processing.logo"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingLogoPath')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo Opacity</span>
</div>
<input
v-model="configStore.playout.processing.logo_opacity"
type="number"
min="0"
max="1"
step="0.01"
class="input input-sm input-bordered w-full max-w-36"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo Scale</span>
</div>
<input
v-model="configStore.playout.processing.logo_scale"
type="text"
class="input input-sm input-bordered w-full max-w-md"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingLogoScale')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Logo Position</span>
</div>
<input
v-model="configStore.playout.processing.logo_position"
type="text"
class="input input-sm input-bordered w-full max-w-md"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingLogoPosition')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Audio Tracks</span>
</div>
<input
v-model="configStore.playout.processing.audio_tracks"
type="number"
min="1"
max="255"
step="1"
class="input input-sm input-bordered w-full max-w-36"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingAudioTracks')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Audio Track Index</span>
</div>
<input
v-model="configStore.playout.processing.audio_track_index"
type="number"
min="-1"
max="255"
step="1"
class="input input-sm input-bordered w-full max-w-36"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingAudioIndex')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Audio Channels</span>
</div>
<input
v-model="configStore.playout.processing.audio_channels"
type="number"
min="1"
max="255"
step="1"
class="input input-sm input-bordered w-full max-w-36"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingAudioChannels')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Volumen</span>
</div>
<input
v-model="configStore.playout.processing.volume"
type="number"
min="0"
max="1"
step="0.001"
class="input input-sm input-bordered w-full max-w-36"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Custom Filter</span>
</div>
<textarea
v-model="configStore.playout.processing.custom_filter"
class="textarea textarea-bordered"
rows="3"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingCustomFilter')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.processing.vtt_enable"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Enable VTT</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingVTTEnable')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">VTT Dummy</span>
</div>
<input
v-model="configStore.playout.processing.vtt_dummy"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingVTTDummy')
}}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.ingest') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.ingestHelp') }}
</div>
</label>
<label class="form-control w-full flex-row mt-2">
<input
v-model="configStore.playout.ingest.enable"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Enable</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Input Param</span>
</div>
<input
v-model="configStore.playout.ingest.input_param"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Custom Filter</span>
</div>
<textarea
v-model="configStore.playout.ingest.custom_filter"
class="textarea textarea-bordered"
rows="3"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.ingestCustomFilter')
}}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.playlist') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.playlistHelp') }}
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Day Start</span>
</div>
<input
v-model="configStore.playout.playlist.day_start"
type="text"
class="input input-sm input-bordered w-full max-w-xs"
pattern="([01]?[0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9]"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.playlistDayStart') }}</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Length</span>
</div>
<input
v-model="configStore.playout.playlist.length"
type="text"
class="input input-sm input-bordered w-full max-w-xs"
pattern="([01]?[0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9]"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.playlistLength') }}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.playlist.infinit"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Infinit</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{ t('config.playlistInfinit') }}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.storage') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.storageHelp') }}
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Filler</span>
</div>
<input
v-model="configStore.playout.storage.filler"
type="text"
name="filler"
class="input input-sm input-bordered w-full max-w-lg"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.storageFiller') }}</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Extensions</span>
</div>
<input
v-model="configStore.playout.storage.extensions"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.storageExtension') }}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.storage.shuffle"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Shuffle</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{ t('config.storageShuffle') }}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.text') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.textHelp') }}
</div>
</label>
<label class="form-control w-full flex-row mt-2">
<input
v-model="configStore.playout.text.add_text"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Add Text</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Font</span>
</div>
<input
v-model="configStore.playout.text.font"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.textFont') }}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.text.text_from_filename"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Text from File</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{ t('config.textFromFile') }}</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Style</span>
</div>
<input
v-model="configStore.playout.text.style"
type="text"
class="input input-sm input-bordered w-full truncate"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.textStyle') }}</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Regex</span>
</div>
<input
v-model="configStore.playout.text.regex"
type="text"
class="input input-sm input-bordered w-full max-w-lg"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.textRegex') }}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.task') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.taskHelp') }}
</div>
</label>
<label class="form-control w-full flex-row mt-2">
<input
v-model="configStore.playout.task.enable"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Enable</span>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-base font-bold">Path</span>
</div>
<input
v-model="configStore.playout.task.path"
type="text"
name="task_path"
class="input input-sm input-bordered w-full max-w-lg"
/>
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.taskPath') }}</span>
</div>
</label>
</div>
<div class="text-xl pt-3 md:text-right">{{ t('config.output') }}:</div>
<div class="md:pt-4">
<label class="form-control mb-2">
<div class="whitespace-pre-line">
{{ t('config.outputHelp') }}
</div>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Mode</span>
</div>
<select
v-model="configStore.playout.output.mode"
class="select select-sm select-bordered w-full max-w-xs"
>
<option v-for="mode in outputMode" :key="mode" :value="mode">{{ mode }}</option>
</select>
</label>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">Output Parameter</span>
</div>
<textarea
v-model="configStore.playout.output.output_param"
class="textarea textarea-bordered"
rows="6"
/>
</label>
</div>
<div class="mt-5 mb-10">
<button class="btn btn-primary" type="submit">{{ t('config.save') }}</button>
</div>
@ -102,6 +729,10 @@ const indexStore = useIndex()
const showModal = ref(false)
const logLevels = ['INFO', 'WARNING', 'ERROR']
const processingMode = ['folder', 'playlist']
const outputMode = ['desktop', 'hls', 'stream', 'null']
const formatIgnoreLines = computed({
get() {
return configStore.playout.logging.ignore_lines.join(';')
@ -112,64 +743,6 @@ const formatIgnoreLines = computed({
},
})
function setTitle(input: string): string {
switch (input) {
case 'general':
return t('config.general')
case 'rpc_server':
return t('config.rpcServer')
case 'mail':
return t('config.mail')
case 'logging':
return t('config.logging')
case 'processing':
return t('config.processing')
case 'ingest':
return t('config.ingest')
case 'playlist':
return t('config.playlist')
case 'storage':
return t('config.storage')
case 'text':
return t('config.text')
case 'task':
return t('config.task')
case 'output':
return t('config.output')
default:
return input
}
}
function setHelp(key: string, text: string): string {
switch (key) {
case 'general':
return t('config.generalText')
case 'rpc_server':
return t('config.rpcText')
case 'mail':
return t('config.mailText')
case 'logging':
return t('config.logText')
case 'processing':
return t('config.processingText')
case 'ingest':
return t('config.ingestText')
case 'playlist':
return t('config.playlistText')
case 'storage':
return t('config.storageText')
case 'text':
return t('config.textText')
case 'task':
return t('config.taskText')
case 'output':
return t('config.outputText')
default:
return text
}
}
async function onSubmitPlayout() {
const update = await configStore.setPlayoutConfig(configStore.playout)
configStore.onetimeInfo = true

View File

@ -1,5 +1,5 @@
<template>
<div class="w-full max-w-[800px] pe-8">
<div class="w-full max-w-[800px] xs:pe-8">
<h2 class="pt-3 text-3xl">{{ t('user.title') }}</h2>
<div v-if="authStore.role === 'GlobalAdmin'" class="flex flex-col xs:flex-row gap-2 w-full mb-5 mt-10">
<div class="grow">
@ -234,12 +234,12 @@ async function addUser(add: boolean) {
await getUsers()
await getUserConfig()
} else {
indexStore.msgAlert('error', t('user.addFailed'), 2)
indexStore.msgAlert('error', t('user.addFailed'), 3)
}
clearUser()
} else {
indexStore.msgAlert('error', t('user.mismatch'), 2)
indexStore.msgAlert('error', t('user.mismatch'), 3)
}
} else {
showUserModal.value = false
@ -248,8 +248,13 @@ async function addUser(add: boolean) {
}
async function onSubmitUser() {
if (newPass.value && newPass.value === confirmPass.value) {
configStore.configUser.password = newPass.value
if (newPass.value) {
if (newPass.value === confirmPass.value) {
configStore.configUser.password = newPass.value
} else {
indexStore.msgAlert('error', t('user.mismatch'), 3)
return
}
}
authStore.inspectToken()

View File

@ -182,23 +182,41 @@ export default {
output: 'Ausgabe',
placeholderPass: 'Passwort',
help: 'Hilfe',
generalText: `Manchmal kann es vorkommen, dass eine Datei beschädigt, aber noch abspielbar ist, was einen Streaming-Fehler bei allen folgenden Dateien verursachen kann. Die einzige Möglichkeit, dies zu beheben, besteht darin, ffplayout anzuhalten und neu zu starten. Wir sagen hier nur, wann es gestoppt werden muss, der Startvorgang ist Ihnen überlassen. Der beste Weg ist ein systemd-Dienst unter Linux.
'stop_threshold' wird ffplayout stoppen, wenn es zeitlich über diesem Wert liegt. Eine Zahl kleiner als 3 kann zu unerwarteten Fehlern führen.`,
rpcText:
'Führe einen JSON-RPC-Server aus, um Informationen über die Wiedergabe und einige Kontrollfunktionen zu erhalten.',
mailText: `Senden Sie Fehlermeldungen an die E-Mail-Adresse, z. B. fehlende Playlist, ungültiges json-Format, fehlender Clip-Pfad. Lassen Sie den Empfänger leer, wenn Sie keine Benachrichtigung benötigen. 'mail_level' kann INFO, WARNING oder ERROR sein. 'interval' bedeutet Sekunden, bis eine neue E-Mail gesendet wird.`,
logText: `Wenn 'log_to_file' true ist, wird in eine Datei protokolliert, wenn false, in die Konsole. 'local_time' auf false setzt die Log-Zeitstempel auf UTC. Pfad zu /var/log/ nur, wenn Sie dies als Daemon ausführen.
'level' kann DEBUG, INFO, WARNING, ERROR sein. 'ffmpeg_level' kann INFO, WARNING, ERROR sein. 'detect_silence' protokolliert eine Fehlermeldung, wenn die Audiospur während des Validierungsprozesses 15 Sekunden lang still ist.`,
processingText: `Standardverarbeitung für alle Clips, um sie einheitlich zu machen. Modus kann "playlist" oder "folder" sein. 'aspect' muss eine Fließkommazahl sein. 'logo' wird nur verwendet, wenn der Pfad existiert.
'logo_scale' skaliert das Logo auf die Zielgröße, leer lassen, wenn keine Skalierung erforderlich ist, Format ist 'width:height', zum Beispiel '100:-1' für proportionale Skalierung. Mit 'logo_opacity' können Sie das Logo transparent machen.
Mit 'audio_tracks' kann man einstellen, wie viele Audiospuren verarbeitet werden sollen. 'audio_channels' kann verwendet werden, wenn das Audio mehr Kanäle als nur Stereo hat. Mit 'logo_position' im Format 'x:y' wird die Position des Logos festgelegt. Mit 'custom_filter' ist es möglich, zusätzliche Filter anzuwenden. Die Filterausgänge sollten mit [c_v_out] für Videofilter und [c_a_out] für Audiofilter enden.`,
ingestText: `Startet einen Server für einen Live-Eingangsstream. Dieser Stream hat Vorrang vor dem normalen Streaming, bis er fertig ist. Es gibt nur einen sehr einfachen Authentifizierungsmechanismus, um zu prüfen, ob der Streamname korrekt ist. 'custom_filter' kann auf die gleiche Weise wie im Abschnitt Verarbeitung verwendet werden.`,
playlistText: `'path' kann ein Pfad zu einer einzelnen Datei oder einem Verzeichnis sein. Bei Verzeichnissen geben Sie nur das Stammverzeichnis an, z. B. '/playlists', Unterverzeichnisse werden vom Program gelesen. Unterverzeichnisse benötigen diese Struktur '/playlists/2018/01'.
'day_start' ist die Uhrzeit, zu der die Wiedergabeliste beginnen soll; lassen Sie 'day_start' leer, wenn die Wiedergabeliste immer am Anfang beginnen soll. 'length' steht für die Ziellänge der Wiedergabeliste; wenn leer, wird die tatsächliche Länge nicht berücksichtigt. 'infinit: true' arbeitet mit einer einzelnen Wiedergabelistendatei und spielt diese in einer Endlosschleife.`,
storageText: `Spielt geordnete oder zufällige Dateien aus dem Pfad ab. 'filler_clip' ist zum Füllen des Endes, um 24 Stunden zu erreichen, es wird eine Schleife abgespielt, wenn nötig. Setzen Sie 'extensions', um nur nach Dateien mit dieser Erweiterung zu suchen. Setzen Sie 'shuffle' auf 'true', um Dateien zufällig auszuwählen.`,
textText: `Überlagernder Text in Kombination mit libzmq für die Remote-Textmanipulation. Unter Windows muss der Pfad der Schriftartdatei 'C\\:/WINDOWS/fonts/DejaVuSans.ttf' sein. 'text_from_filename' aktiviert die Textextraktion aus einem Dateinamen. Mit 'style' können Sie die Zeichentext-Parameter wie Position, Farbe, etc. festlegen. Post Text via API wird dies überschreiben. Mit 'regex' können Sie den Dateinamen formatieren, um einen Titel zu erhalten.`,
taskText: `Führt ein externes Programm mit einem bestimmten Medienobjekt aus. Das Medienobjekt liegt im json-Format vor und enthält alle Informationen über den aktuellen Clip. Das externe Programm kann ein Skript oder eine Binärdatei sein. oder eine Binärdatei, sollte aber nur für kurze Zeit laufen.`,
outputText: `Die endgültige Playout-Kompression. Passen Sie die Einstellungen entsprechend Ihren Bedürfnissen an. 'mode' hat die Optionen 'desktop', 'hls', 'null', 'stream'. Verwenden Sie 'stream' und passen Sie die 'output_param:'-Einstellungen an, wenn Sie zu einem rtmp/rtsp/srt/... Server streamen wollen. In der Produktion sollten Sie die hls-Wiedergabeliste nicht mit ffpapi ausliefern, sondern nginx oder einen anderen Webserver verwenden!`,
generalHelp: 'Manchmal kann es passieren, dass eine Datei beschädigt ist, aber dennoch abgespielt werden kann. Dies kann zu einem Streaming-Fehler für alle folgenden Dateien führen. Die einzige Lösung in diesem Fall ist, ffplayout zu stoppen und erneut zu starten.',
stopThreshold: 'Der Schwellenwert stoppt ffplayout, wenn es zeitlich asynchron über diesem Wert ist. Eine Zahl unter 3 kann unerwartete Fehler verursachen.',
mailHelp: `Sende Fehlermeldungen an eine E-Mail-Adresse, wie z.B. fehlende Clips, fehlendes oder ungültiges Playlist-Format usw. Lass den Empfänger leer, wenn du dies nicht benötigst.`,
mailInterval: 'Das Intervall bezieht sich auf die Anzahl der Sekunden, bis eine neue E-Mail gesendet wird; der Wert muss in 10er-Schritten und nicht unter 30 Sekunden liegen.',
logHelp: 'Passen Sie das Verhalten des Loggings an.',
logDetect: 'Protokolliert eine Fehlermeldung, wenn die Audioleitung während des Validierungsprozesses 15 Sekunden lang stumm ist.',
logIgnore: 'Ignoriere Zeichenfolgen, die übereinstimmende Zeilen enthalten; das Format ist eine durch Semikolon getrennte Liste.',
processingHelp: 'Die Standardverarbeitung für alle Clips stellt die Einzigartigkeit sicher.',
processingLogoPath: 'Das Logo wird nur verwendet, wenn der Pfad existiert; der Pfad ist relativ zum Speicherordner.',
processingLogoScale: `Lass die Skalierung des Logos leer, wenn keine Skalierung erforderlich ist. Das Format lautet 'Breite:Höhe', zum Beispiel: '100:-1' für proportionale Skalierung.`,
processingLogoPosition: `Die Position wird im Format 'x:y' angegeben.`,
processingAudioTracks: 'Gib an, wie viele Audiospuren verarbeitet werden sollen.',
processingAudioIndex: 'Welche Audiospur verwendet werden soll, -1 für alle.',
processingAudioChannels: 'Stelle die Anzahl der Audiokanäle ein, wenn das Audio mehr Kanäle als Stereo hat.',
processingCustomFilter: 'Füge benutzerdefinierte Filter zur Verarbeitung hinzu. Die Filterausgaben müssen mit [c_v_out] für Video-Filter und [c_a_out] für Audio-Filter enden.',
processingVTTEnable: 'VTT kann nur im HLS-Modus verwendet werden und nur, wenn *.vtt-Dateien mit demselben Namen wie die Videodatei vorhanden sind.',
processingVTTDummy: 'Ein Platzhalter wird benötigt, wenn keine vtt-Datei vorhanden ist.',
ingestHelp: `Starte einen Server für einen Ingest-Stream. Dieser Stream wird den normalen Stream überschreiben, bis er beendet ist. Es gibt nur einen sehr einfachen Authentifizierungsmechanismus, der überprüft, ob der Streamname korrekt ist.`,
ingestCustomFilter: 'Wende einen benutzerdefinierten Filter auf den Ingest-Stream auf dieselbe Weise wie im Abschnitt Verarbeitung an.',
playlistHelp: 'Playlist-Verwaltung.',
playlistDayStart: 'Zu welcher Zeit die Playlist starten soll; lasse es leer, wenn die Playlist immer von Anfang an starten soll.',
playlistLength: 'Ziel-Länge der Playlist; wenn es leer ist, wird die reale Länge nicht berücksichtigt.',
playlistInfinit: 'Eine einzelne Playlist-Datei endlos wiederholen.',
storageHelp: 'Speichereinstellungen, die Standorte sind relativ zum Kanal-Speicher.',
storageFiller: 'Verwende Füllmaterial, um anstelle einer fehlenden Datei abzuspielen oder um die verbleibende Zeit zu füllen, um eine Gesamtdauer von 24 Stunden zu erreichen. Es kann eine Datei oder ein Ordner sein und wird bei Bedarf wiederholt.',
storageExtension: 'Gib an, welche Dateien gesucht und verwendet werden sollen.',
storageShuffle: 'Wähle Dateien zufällig aus (im Ordner-Modus und bei der Playlist-Erstellung).',
textHelp: 'Texteinblendung in Kombination mit libzmq für die Fernmanipulation von Text.',
textFont: 'Relativer Pfad zum Kanal-Speicher.',
textFromFile: 'Extrahiere Text aus einem Dateinamen.',
textStyle: 'Definiere die Parameter für drawtext, wie Position, Farbe usw. Das Posten von Text über die API überschreibt dies.',
textRegex: 'Formatiere Dateinamen, um einen Titel daraus zu extrahieren.',
taskHelp: 'Führe ein externes Programm mit einem gegebenen Medienobjekt aus. Das Medienobjekt ist im JSON-Format und enthält alle Informationen über den aktuellen Clip. Das externe Programm kann ein Skript oder eine Binärdatei sein, sollte aber nur für kurze Zeit laufen.',
taskPath: 'Pfad zur ausführbaren Datei.',
outputHelp: `Die endgültige Playout-Codierung, passe die Einstellungen nach deinen Bedürfnissen an. Verwende den 'stream'-Modus und passe den 'Ausgabe-Parameter' an, wenn du zu einem RTMP/RTSP/SRT/...-Server streamen möchtest. Im Produktionsbetrieb verwende kein HLS mit ffplayout; nutze Nginx oder einen anderen Webserver!`,
restartTile: 'Playout neustarten',
restartText: 'ffplayout neustarten um Einstellungen anzuwenden?',
updatePlayoutSuccess: 'Update der Playout-Konfiguration erfolgreich!',

View File

@ -182,22 +182,42 @@ export default {
output: 'Output',
placeholderPass: 'Password',
help: 'Help',
generalText: `Sometimes it can happen that a file is corrupted but still playable, this can cause a streaming error on all following files. The only way to fix this is to stop and restart ffplayout. Here we only say when to stop, the starting process is up to you. The best way is a systemd service on Linux.
'stop_threshold' will stop ffplayout if it is async in time above this value. A number less than 3 may cause unexpected errors.`,
rpcText: 'Run a JSON RPC server to get information about what is playing and for some control functions.',
mailText: `Send error messages to the email address, such as missing playlist; invalid json format; missing clip path. Leave the recipient blank if you don't need it. 'mail_level' can be INFO, WARNING or ERROR. 'interval' means seconds until a new mail is sent.`,
logText: `If 'log_to_file' is true, log to file, if false, log to console. 'local_time' to false will set log timestamps to UTC. Path to /var/log/ only if you are running this as a daemon.
'level' can be DEBUG, INFO, WARNING, ERROR. 'ffmpeg_level' can be INFO, WARNING, ERROR. 'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process.`,
processingText: `Default processing for all clips to make them unique. Mode can be Playlist or Folder. 'aspect' must be a float number. 'logo' is only used if the path exists.
'logo_scale' scales the logo to the target size, leave empty if no scaling is needed, format is 'width:height', for example '100:-1' for proportional scaling. With 'logo_opacity' you can make the logo transparent.
With 'audio_tracks' it is possible to configure how many audio tracks should be processed. 'audio_channels' can be used if the audio has more channels than just stereo. With 'logo_position' in 'x:y' format you set the logo position. With 'custom_filter' it is possible to apply additional filters. The filter outputs should end with [c_v_out] for video filters and [c_a_out] for audio filters.`,
ingestText: `Run a server for an ingest stream. This stream will override the normal streaming until it is done. There is only a very simple authentication mechanism to check if the stream name is correct. 'custom_filter' can be used in the same way as in the process section.`,
playlistText: `'path' can be a path to a single file or a directory. For directory specify only the root folder, for example '/playlists', subdirectories will be read by the program. Subdirectories need this structure '/playlists/2018/01'.
'day_start' is the time at which the playlist should start, leave 'day_start' empty if the playlist should always start at the beginning. 'length' represents the target length of the playlist, if empty, real length will not be considered. 'infinit: true' works with single playlist file and loops it infinitely.`,
storageText: `Play ordered or random files from path. 'filler_clip' is for filling the end to reach 24 hours, it will loop when necessary. Set 'extensions' to search only for files with that extension. Set 'shuffle' to 'true' to select files randomly.`,
textText: `Overlay text in combination with libzmq for remote text manipulation. On Windows, the font file path must be 'C\\:/WINDOWS/fonts/DejaVuSans.ttf'. 'text_from_filename' activates text extraction from a filename. With 'style' you can set the drawtext parameters like position, color, etc. Post Text via API will override this. With 'regex' you can format the filename to get a title.`,
taskText: `Run an external program with a given media object. The media object is in json format and contains all the information about the current clip. The external program can be a script or a binary. or a binary, but should only run for a short time.`,
outputText: `The final playout compression. Adjust the settings according to your needs. 'mode' has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust the 'output_param:' settings if you want to stream to an rtmp/rtsp/srt/... server. In production don't serve hls playlist with ffpapi, use nginx or another web server!`,
generalHelp: 'Sometimes it can happen that a file is corrupt but still playable. This can produce a streaming error for all following files. The only solution in this case is to stop ffplayout and start it again.',
stopThreshold: 'The threshold stops ffplayout if it is asynchronous in time above this value. A number below 3 can cause unexpected errors.',
mailHelp: `Send error messages to an email address, such as missing clips, missing or invalid playlist format, etc.. Leave the recipient blank if you don't need this.`,
mailInterval: 'The interval refers to the number of seconds until a new email is sent; the value must be in increments of 10 and not lower then 30 seconds.',
logHelp: 'Adjust logging behavior.',
logDetect: 'Logs an error message if the audio line is silent for 15 seconds during the validation process.',
logIgnore: 'Ignore strings that contain matched lines; the format is a semicolon-separated list.',
processingHelp: 'Default processing for all clips ensures uniqueness.',
processingLogoPath: 'The logo is used only if the path exists; the path is relative to the storage folder.',
processingLogoScale: `Leave logo scale blank if no scaling is needed. The format is 'width:height', for example: '100:-1' for proportional scaling.`,
processingLogoPosition: `Position is specified in the format 'x:y'`,
processingAudioTracks: 'Specify how many audio tracks should be processed.',
processingAudioIndex: 'Which audio line to use, -1 for all.',
processingAudioChannels: 'Set the audio channel count, if audio has more channels than stereo.',
processingCustomFilter: 'Add custom filters to the processing. The filter outputs must end with [c_v_out] for video filters and [c_a_out] for audio filters.',
processingVTTEnable: 'VTT can only be used in HLS mode and only if there are *.vtt files with the same name as the video file.',
processingVTTDummy: 'A placeholder is needed if there is no vtt file.',
ingestHelp: `Run a server for an ingest stream. This stream will override the normal streaming until it is finished. There is only a very simple authentication mechanism, which checks if the stream name is correct.`,
ingestCustomFilter: 'Apply a custom filter to the Ingest stream in the same way as in the Processing section.',
playlistHelp: 'Playlist handling.',
playlistDayStart: 'At what time the playlist should start; leave it blank if the playlist should always start at the beginning.',
playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.',
playlistInfinit: 'Loop a single playlist file infinitely.',
storageHelp: 'Storage settings, locations are relative to channel storage.',
storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder and will loop when necessary.',
storageExtension: 'Specify which files to search and use.',
storageShuffle: 'Pick files randomly (in folder mode and playlist generation).',
textHelp: 'Overlay text in combination with libzmq for remote text manipulation.',
textFont: 'Relative path to channel storage.',
textFromFile: 'Extraction of text from a filename.',
textStyle: 'Define the drawtext parameters, such as position, color, etc. Posting text over the API will override this.',
textRegex: 'Format file names to extract a title from them.',
taskHelp: 'Run an external program with a given media object. The media object is in JSON format and contains all the information about the current clip. The external program can be a script or a binary, but it should only run for a short time.',
taskPath: 'Path to executable.',
outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server.
In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`,
restartTile: 'Restart Playout',
restartText: 'Restart ffplayout to apply changes?',
updatePlayoutSuccess: 'Update playout config success!',

View File

@ -182,22 +182,41 @@ export default {
output: 'Saída',
placeholderPass: 'Senha',
help: 'Ajuda',
generalText: `Às vezes pode acontecer que um arquivo esteja corrompido mas ainda assim ele pode ser reproduzido, isso pode causar um erro de transmissão em todos os arquivos seguintes. A única maneira de corrigir isso é parar e reiniciar o ffplayout. Aqui só dizemos quando parar, o processo de início é por sua conta. A melhor maneira é um serviço systemd no Linux.
'stop_threshold' vai parar o ffplayout se estiver assíncrono em um tempo acima deste valor. Um número menor que 3 pode causar erros inesperados.`,
rpcText: 'Execute um servidor JSON RPC para obter informações sobre o que está sendo reproduzido e para algumas funções de controle.',
mailText: `Envie mensagens de erro para o endereço de email, como lista de reprodução faltando; formato JSON inválido; caminho do clipe ausente. Deixe o destinatário em branco se não for necessário. 'mail_level' pode ser INFO, WARNING ou ERROR. 'interval' é o tempo em segundos até que um novo e-mail seja enviado.`,
logText: `Se 'log_to_file' estiver ativado (true), o registro de log será salvo em um arquivo de texto, e se desativado (false), registrará o log no console. Se 'local_time' estiver desativado (false) o sistema definirá os carimbos de data/hora do registro para UTC. Defina o caminho para /var/log/ apenas se você estiver executando o programa como daemon.
'level' pode ser DEBUG, INFO, WARNING, ERROR. 'ffmpeg_level' pode ser INFO, WARNING, ERROR. 'detect_silence' registra uma mensagem de erro se a linha de áudio ficar silenciosa por 15 segundos durante o processo de validação.`,
processingText: `Processamento padrão para todos os clipes para torná-los únicos. O modo ('mode') pode ser Playlist ou Folder. 'aspect' deve ser um número de ponto flutuante. 'logo' é usado apenas se o caminho existir.
'logo_scale' dimensiona o logo para o tamanho desejado, deixe esse campo de configuração vazio se nenhum dimensionamento for necessário, o formato é 'largura:altura', por exemplo '100:-1' para dimensionamento proporcional. Com 'logo_opacity' você pode tornar o logo transparente.
Com 'audio_tracks' é possível configurar quantas faixas de áudio devem ser processadas. 'audio_channels' pode ser usado se o áudio tiver mais canais do que apenas estéreo. Com 'logo_position' no formato 'x:y' você define a posição do logo. Com 'custom_filter' é possível aplicar filtros adicionais. As saídas do filtro devem terminar com [c_v_out] para filtros de vídeo e [c_a_out] para filtros de áudio.`,
ingestText: `Execute um servidor de ingestão para poder receber um fluxo de stream a partir de uma fonte externa, como por exemplo um software de streaming como o OBS Studio. Este stream substituirá o streaming normal até que seja concluído. Existe apenas um mecanismo de autenticação muito simples para verificar se o nome do stream está correto. 'custom_filter' pode ser usado da mesma forma que na seção de processo.`,
playlistText: `'path' é o caminho para o arquivo de playlist. 'path' pode ser um caminho para um único arquivo ou um diretório. Se um diretório for especificado, defina apenas a pasta raiz desse diretório, por exemplo '/playlists', subdiretórios serão lidos pelo programa. Subdiretórios precisam seguir a seguinte estrutura de diretórios: '/playlists/2018/01'.
'day_start' é o horário fixo, inicial, em que a playlist será reproduzida, deixe 'day_start' vazio se a lista de reprodução sempre deve começar do início. 'length' representa a duração desejada da playlist, se vazio, a duração real não será considerada. 'infinit: true' funciona com um arquivo de lista de reprodução único e o repete infinitamente.`,
storageText: `Reproduza arquivos de mídia de maneira ordenada ou aleatória a partir de um caminho de diretório ('path') especificado. 'filler_clip' define um "clipe de preenchimento", cuja função é preencher a lacuna de tempo de uma playlist com duração inferior a 24 horas completas, esse clipe ou conjunto de clipes também será repetido quando algum arquivo de mídia não puder ser reproduzido, quando estiver indisponível ou se nenhuma playlist válida estiver sendo reproduzida. Defina 'extensions' para delimitar a busca interna de arquivos de mídia apenas por arquivos com a extensão adicionada. Defina 'shuffle' como 'true' para selecionar arquivos aleatoriamente.`,
textText: `Sobreponha texto (legenda) em combinação com o libzmq, para manipulação remota de texto. No Windows, o caminho do arquivo de fonte deve ser 'C\\:/WINDOWS/fonts/DejaVuSans.ttf'. 'text_from_filename' ativa a extração de texto (legenda) a partir de um arquivo de texto especificado. Com 'style' você pode definir os parâmetros drawtext como posição, cor, etc. Postar Texto via API irá substituir isso. Com 'regex' você pode formatar o nome do arquivo para obter um título.`,
taskText: `Execute um programa externo em conjunto com um determinado objeto de mídia. O objeto de mídia está no formato JSON e contém todas as informações sobre o clipe atual. O programa externo pode ser um script ou um binário. ou um binário, mas deve ser executado apenas por um curto período de tempo.`,
outputText: `A compressão final do stream gerado pelo playout. Ajuste as configurações de acordo com suas necessidades. 'mode' possui as opções 'desktop', 'hls', 'null', 'stream'. Use 'stream' e ajuste as configurações de 'output_param:' se você desejar retransmitir o stream para um servidor rtmp/rtsp/srt/... Em ambiente de produção não sirva a playlist hls com ffpapi, use nginx ou outro servidor web!`,
generalHelp: 'Às vezes pode acontecer de um arquivo estar corrompido, mas ainda ser reproduzível. Isso pode causar um erro de streaming para todos os arquivos seguintes. A única solução nesse caso é parar o ffplayout e reiniciá-lo.',
stopThreshold: 'O limite para o ffplayout se ele estiver fora de sincronia acima deste valor. Um número abaixo de 3 pode causar erros inesperados.',
mailHelp: `Envie mensagens de erro para um endereço de e-mail, como clipes ausentes, formato de playlist ausente ou inválido, etc. Deixe o destinatário em branco se não precisar disso.`,
mailInterval: 'O intervalo se refere ao número de segundos até o envio de um novo e-mail; o valor deve ser em incrementos de 10 e não inferior a 30 segundos.',
logHelp: 'Ajuste o comportamento de log.',
logDetect: 'Registra uma mensagem de erro se a linha de áudio estiver em silêncio por 15 segundos durante o processo de validação.',
logIgnore: 'Ignorar strings que contenham linhas correspondentes; o formato é uma lista separada por ponto e vírgula.',
processingHelp: 'O processamento padrão para todos os clipes garante a exclusividade.',
processingLogoPath: 'O logotipo só é usado se o caminho existir; o caminho é relativo à pasta de armazenamento.',
processingLogoScale: `Deixe a escala do logotipo em branco se não for necessário escalonamento. O formato é 'largura:altura', por exemplo: '100:-1' para escalonamento proporcional.`,
processingLogoPosition: `A posição é especificada no formato 'x:y'.`,
processingAudioTracks: 'Especifique quantas faixas de áudio devem ser processadas.',
processingAudioIndex: 'Qual linha de áudio usar, -1 para todas.',
processingAudioChannels: 'Defina a contagem de canais de áudio, se o áudio tiver mais canais do que estéreo.',
processingCustomFilter: 'Adicione filtros personalizados ao processamento. As saídas de filtro devem terminar com [c_v_out] para filtros de vídeo e [c_a_out] para filtros de áudio.',
processingVTTEnable: 'VTT só pode ser usado no modo HLS e apenas se houver arquivos *.vtt com o mesmo nome do arquivo de vídeo.',
processingVTTDummy: 'Um espaço reservado é necessário se não houver arquivo vtt.',
ingestHelp: `Execute um servidor para um fluxo de ingestão. Este fluxo substituirá o streaming normal até que termine. Há apenas um mecanismo de autenticação simples que verifica se o nome do fluxo está correto.`,
ingestCustomFilter: 'Aplique um filtro personalizado ao fluxo de ingestão da mesma forma que na seção de Processamento.',
playlistHelp: 'Gerenciamento de playlist.',
playlistDayStart: 'A que horas a playlist deve começar; deixe em branco se a playlist sempre começar do início.',
playlistLength: 'Duração alvo da playlist; quando estiver em branco, o comprimento real não será considerado.',
playlistInfinit: 'Reproduza infinitamente um único arquivo de playlist.',
storageHelp: 'Configurações de armazenamento, os locais são relativos ao armazenamento do canal.',
storageFiller: 'Use preenchimento para tocar no lugar de um arquivo ausente ou para preencher o tempo restante para atingir um total de 24 horas. Pode ser um arquivo ou pasta e será repetido quando necessário.',
storageExtension: 'Especifique quais arquivos procurar e usar.',
storageShuffle: 'Escolha arquivos aleatoriamente (no modo de pasta e geração de playlist).',
textHelp: 'Sobrepor texto em combinação com libzmq para manipulação remota de texto.',
textFont: 'Caminho relativo ao armazenamento do canal.',
textFromFile: 'Extração de texto a partir de um nome de arquivo.',
textStyle: 'Defina os parâmetros drawtext, como posição, cor, etc. Postar texto pela API substituirá isso.',
textRegex: 'Formate nomes de arquivos para extrair um título deles.',
taskHelp: 'Execute um programa externo com um objeto de mídia fornecido. O objeto de mídia está em formato JSON e contém todas as informações sobre o clipe atual. O programa externo pode ser um script ou binário, mas deve ser executado apenas por um curto período de tempo.',
taskPath: 'Caminho para o executável.',
outputHelp: `A codificação final do playout, ajuste as configurações de acordo com suas necessidades. Use o modo 'stream' e ajuste o 'Parâmetro de Saída' quando quiser fazer streaming para um servidor RTMP/RTSP/SRT/... No ambiente de produção, não sirva playlists HLS com ffplayout; use Nginx ou outro servidor web!`,
restartTile: 'Reiniciar Playout',
restartText: 'Reiniciar o ffplayout para aplicar as alterações?',
updatePlayoutSuccess: 'Sucesso na atualização da configuração do playout!',

View File

@ -182,22 +182,42 @@ export default {
output: 'Out',
placeholderPass: 'Password',
help: 'Help',
generalText: `Sometimes it can happen that a file is corrupted but still playable, this can cause a streaming error on all following files. The only way to fix this is to stop and restart ffplayout. Here we only say when to stop, the starting process is up to you. The best way is a systemd service on Linux.
'stop_threshold' will stop ffplayout if it is async in time above this value. A number less than 3 may cause unexpected errors.`,
rpcText: 'Запустите сервер JSON RPC, чтобы получить информацию о том, что воспроизводится, а также о некоторых функциях управления.',
mailText: `Отправлять сообщения об ошибках на адрес электронной почты, например об отсутствии списка воспроизведения; неверный формат json; отсутствует путь к клипу. Оставьте получателя пустым, если он вам не нужен. 'mail_level' может быть ИНФОРМАЦИЯ, ПРЕДУПРЕЖДЕНИЕ или ОШИБКА. 'interval' означает секунды до отправки нового письма.`,
logText: `If 'log_to_file' is true, log to file, if false, log to console. 'local_time' to false will set log timestamps to UTC. Path to /var/log/ only if you are running this as a daemon.
'level' can be DEBUG, INFO, WARNING, ERROR. 'ffmpeg_level' can be INFO, WARNING, ERROR. 'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process.`,
processingText: `Default processing for all clips to make them unique. Mode can be Playlist or Folder. 'aspect' must be a float number. 'logo' is only used if the path exists.
'logo_scale' scales the logo to the target size, leave empty if no scaling is needed, format is 'width:height', for example '100:-1' for proportional scaling. With 'logo_opacity' you can make the logo transparent.
With 'audio_tracks' it is possible to configure how many audio tracks should be processed. 'audio_channels' can be used if the audio has more channels than just stereo. With 'logo_position' in 'x:y' format you set the logo position. With 'custom_filter' it is possible to apply additional filters. The filter outputs should end with [c_v_out] for video filters and [c_a_out] for audio filters.`,
ingestText: `Запустите сервер для получения входящего потока. Этот поток будет переопределять обычную потоковую передачу до тех пор, пока она не будет завершена. Существует только очень простой механизм аутентификации, позволяющий проверить правильность имени потока. 'custom_filter' можно использовать так же, как в разделе "Настройки эфира".`,
playlistText: `'path' can be a path to a single file or a directory. For directory specify only the root folder, for example '/playlists', subdirectories will be read by the program. Subdirectories need this structure '/playlists/2018/01'.
'day_start' is the time at which the playlist should start, leave 'day_start' empty if the playlist should always start at the beginning. 'length' represents the target length of the playlist, if empty, real length will not be considered. 'infinit: true' works with single playlist file and loops it infinitely.`,
storageText: `Play ordered or random files from path. 'filler_clip' is for filling the end to reach 24 hours, it will loop when necessary. Set 'extensions' to search only for files with that extension. Set 'shuffle' to 'true' to select files randomly.`,
textText: `Overlay text in combination with libzmq for remote text manipulation. On Windows, the font file path must be 'C\\:/WINDOWS/fonts/DejaVuSans.ttf'. 'text_from_filename' activates text extraction from a filename. With 'style' you can set the drawtext parameters like position, color, etc. Post Text via API will override this. With 'regex' you can format the filename to get a title.`,
taskText: `Run an external program with a given media object. The media object is in json format and contains all the information about the current clip. The external program can be a script or a binary. or a binary, but should only run for a short time.`,
outputText: `The final playout compression. Adjust the settings according to your needs. 'mode' has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust the 'output_param:' settings if you want to stream to an rtmp/rtsp/srt/... server. In production don't serve hls playlist with ffpapi, use nginx or another web server!`,
generalHelp: 'Sometimes it can happen that a file is corrupt but still playable. This can produce a streaming error for all following files. The only solution in this case is to stop ffplayout and start it again.',
stopThreshold: 'The threshold stops ffplayout if it is asynchronous in time above this value. A number below 3 can cause unexpected errors.',
mailHelp: `Send error messages to an email address, such as missing clips, missing or invalid playlist format, etc.. Leave the recipient blank if you don't need this.`,
mailInterval: 'The interval refers to the number of seconds until a new email is sent; the value must be in increments of 10 and not lower then 30 seconds.',
logHelp: 'Adjust logging behavior.',
logDetect: 'Logs an error message if the audio line is silent for 15 seconds during the validation process.',
logIgnore: 'Ignore strings that contain matched lines; the format is a semicolon-separated list.',
processingHelp: 'Default processing for all clips ensures uniqueness.',
processingLogoPath: 'The logo is used only if the path exists; the path is relative to the storage folder.',
processingLogoScale: `Leave logo scale blank if no scaling is needed. The format is 'width:height', for example: '100:-1' for proportional scaling.`,
processingLogoPosition: `Position is specified in the format 'x:y'`,
processingAudioTracks: 'Specify how many audio tracks should be processed.',
processingAudioIndex: 'Which audio line to use, -1 for all.',
processingAudioChannels: 'Set the audio channel count, if audio has more channels than stereo.',
processingCustomFilter: 'Add custom filters to the processing. The filter outputs must end with [c_v_out] for video filters and [c_a_out] for audio filters.',
processingVTTEnable: 'VTT can only be used in HLS mode and only if there are *.vtt files with the same name as the video file.',
processingVTTDummy: 'A placeholder is needed if there is no vtt file.',
ingestHelp: `Run a server for an ingest stream. This stream will override the normal streaming until it is finished. There is only a very simple authentication mechanism, which checks if the stream name is correct.`,
ingestCustomFilter: 'Apply a custom filter to the Ingest stream in the same way as in the Processing section.',
playlistHelp: 'Playlist handling.',
playlistDayStart: 'At what time the playlist should start; leave it blank if the playlist should always start at the beginning.',
playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.',
playlistInfinit: 'Loop a single playlist file infinitely.',
storageHelp: 'Storage settings, locations are relative to channel storage.',
storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder and will loop when necessary.',
storageExtension: 'Specify which files to search and use.',
storageShuffle: 'Pick files randomly (in folder mode and playlist generation).',
textHelp: 'Overlay text in combination with libzmq for remote text manipulation.',
textFont: 'Relative path to channel storage.',
textFromFile: 'Extraction of text from a filename.',
textStyle: 'Define the drawtext parameters, such as position, color, etc. Posting text over the API will override this.',
textRegex: 'Format file names to extract a title from them.',
taskHelp: 'Run an external program with a given media object. The media object is in JSON format and contains all the information about the current clip. The external program can be a script or a binary, but it should only run for a short time.',
taskPath: 'Path to executable.',
outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server.
In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`,
restartTile: 'Перезапуск Playout',
restartText: 'Перезапустить ffplayout для применения изменений?',
updatePlayoutSuccess: 'Обновление конфигурации воспроизведения прошло успешно!',

View File

@ -1,8 +1,8 @@
<template>
<div class="flex w-full h-[calc(100vh-60px)] ps-1">
<div class="flex-none w-[70px] join join-vertical me-3 pt-7">
<div class="flex flex-wrap xs:flex-nowrap w-full xs:h-[calc(100vh-60px)] ps-1">
<div class="xs:flex-none w-full xs:w-[68px] join join-horizontal xs:join-vertical me-1 pt-7">
<button
class="join-item w-full btn btn-sm btn-primary duration-500"
class="join-item btn btn-sm btn-primary duration-500"
:class="activeConf === 1 && 'btn-secondary'"
@click="activeConf = 1"
>
@ -10,28 +10,28 @@
</button>
<button
v-if="authStore.role === 'GlobalAdmin'"
class="join-item w-full btn btn-sm btn-primary duration-500"
class="join-item btn btn-sm btn-primary duration-500"
:class="activeConf === 2 && 'btn-secondary'"
@click="activeConf = 2"
>
Advanced
</button>
<button
class="join-item w-full btn btn-sm btn-primary mt-1 duration-500"
class="join-item btn btn-sm btn-primary mt-1 duration-500"
:class="activeConf === 3 && 'btn-secondary'"
@click="activeConf = 3"
>
Playout
</button>
<button
class="join-item w-full btn btn-sm btn-primary mt-1 duration-500"
class="join-item btn btn-sm btn-primary mt-1 duration-500"
:class="activeConf === 4 && 'btn-secondary'"
@click="activeConf = 4"
>
{{ t('config.user') }}
</button>
</div>
<div class="w-[calc(100%-70px)] mt-6 px-6 overflow-auto">
<div class="w-full xs:w-[calc(100%-70px)] mt-6 px-3 xs:px-6 overflow-auto">
<div>
<div v-if="activeConf === 1" class="w-full flex justify-center">
<ConfigChannel />

View File

@ -57,7 +57,7 @@
<i class="bi-files" />
</button>
<button
v-if="!configStore.playout.playlist.loop"
v-if="!configStore.playout.playlist.infinit"
class="btn btn-sm btn-primary join-item"
:title="t('player.loop')"
@click="loopClips()"

View File

@ -1,5 +1,6 @@
import { cloneDeep } from 'lodash-es'
import { defineStore } from 'pinia'
import type { AdvancedConfig } from '~/types/advanced_config'
export const useConfig = defineStore('config', {
state: () => ({
@ -9,8 +10,8 @@ export const useConfig = defineStore('config', {
channels: [] as Channel[],
channelsRaw: [] as Channel[],
playlistLength: 86400.0,
advanced: {} as any,
playout: {} as any,
advanced: {} as AdvancedConfig,
playout: {} as PlayoutConfigExt,
currentUser: 0,
configUser: {} as User,
utcOffset: 0,

11
frontend/types/advanced_config.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AdvancedConfig = { decoder: DecoderConfig, encoder: EncoderConfig, filter: FilterConfig, ingest: IngestConfig, };
export type DecoderConfig = { input_param: string, output_param: string, };
export type EncoderConfig = { input_param: string, };
export type FilterConfig = { deinterlace: string, pad_scale_w: string, pad_scale_h: string, pad_video: string, fps: string, scale: string, set_dar: string, fade_in: string, fade_out: string, overlay_logo_scale: string, overlay_logo_fade_in: string, overlay_logo_fade_out: string, overlay_logo: string, tpad: string, drawtext_from_file: string, drawtext_from_zmq: string, aevalsrc: string, afade_in: string, afade_out: string, apad: string, volume: string, split: string, };
export type IngestConfig = { input_param: string, };

View File

@ -1,4 +1,6 @@
import type { JwtPayload } from 'jwt-decode'
import type { AdvancedConfig } from '~/types/advanced_config'
import type { PlayoutConfig, Playlist as Ply } from '~/types/playout_config'
export {}
@ -9,6 +11,15 @@ declare global {
role: string
}
interface PlaylistExt extends Ply {
startInSec: number,
lengthInSec: number
}
interface PlayoutConfigExt extends PlayoutConfig {
playlist: PlaylistExt
}
interface LoginObj {
message: string
status: number

32
frontend/types/playout_config.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type General = { stop_threshold: number, };
export type Ingest = { enable: boolean, input_param: string, custom_filter: string, };
export type Logging = { ffmpeg_level: string, ingest_level: string, detect_silence: boolean, ignore_lines: Array<string>, };
export type Mail = { subject: string, recipient: string, mail_level: string, interval: bigint, };
export type Output = { mode: OutputMode, output_param: string, };
export type OutputMode = "desktop" | "hls" | "null" | "stream";
export type Playlist = { day_start: string, length: string, infinit: boolean, };
/**
* Channel Config
*
* This we init ones, when ffplayout is starting and use them globally in the hole program.
*/
export type PlayoutConfig = { general: General, mail: Mail, logging: Logging, processing: Processing, ingest: Ingest, playlist: Playlist, storage: Storage, text: Text, task: Task, output: Output, };
export type ProcessMode = "folder" | "playlist";
export type Processing = { mode: ProcessMode, audio_only: boolean, copy_audio: boolean, copy_video: boolean, width: bigint, height: bigint, aspect: number, fps: number, add_logo: boolean, logo: string, logo_scale: string, logo_opacity: number, logo_position: string, audio_tracks: number, audio_track_index: number, audio_channels: number, volume: number, custom_filter: string, vtt_enable: boolean, vtt_dummy: string | null, };
export type Storage = { filler: string, extensions: Array<string>, shuffle: boolean, shared_storage: boolean, };
export type Task = { enable: boolean, path: string, };
export type Text = { add_text: boolean, font: string, text_from_filename: boolean, style: string, regex: string, };

View File

@ -82,19 +82,15 @@ CREATE TABLE
configurations (
id INTEGER PRIMARY KEY,
channel_id INTEGER NOT NULL DEFAULT 1,
general_help TEXT NOT NULL DEFAULT "Sometimes it can happen that a file is corrupt but still playable. This can produce a streaming error for all following files. The only solution in this case is to stop ffplayout and start it again.\n'stop_threshold' stops ffplayout if it is asynchronous in time above this value. A number below 3 can cause unexpected errors.",
general_stop_threshold REAL NOT NULL DEFAULT 11.0,
mail_help TEXT NOT NULL DEFAULT "Send error messages to an email address, such as missing clips, missing or invalid playlist format, etc.. Leave the recipient blank if you don't need this.\n'mail_level' can be INFO, WARNING, or ERROR.\n'interval' refers to the number of seconds until a new email is sent; the value must be in increments of 10 and not lower then 30 seconds.",
mail_subject TEXT NOT NULL DEFAULT "Playout Error",
mail_recipient TEXT NOT NULL DEFAULT "",
mail_level TEXT NOT NULL DEFAULT "ERROR",
mail_interval INTEGER NOT NULL DEFAULT 120,
logging_help TEXT NOT NULL DEFAULT "'ffmpeg_level/ingest_level' can be INFO, WARNING, or ERROR.\n'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process.\n'ignore' allows logging to ignore strings that contain matched lines; the format is a semicolon-separated list.",
logging_ffmpeg_level TEXT NOT NULL DEFAULT "ERROR",
logging_ingest_level TEXT NOT NULL DEFAULT "ERROR",
logging_detect_silence INTEGER NOT NULL DEFAULT 0,
logging_ignore TEXT NOT NULL DEFAULT "P sub_mb_type 4 out of range at;error while decoding MB;negative number of zero coeffs at;out of range intra chroma pred mode;non-existing SPS 0 referenced in buffering period",
processing_help TEXT NOT NULL DEFAULT "Default processing for all clips ensures uniqueness. The mode can be either 'playlist' or 'folder'.\nThe 'aspect' parameter must be a float number.\nThe 'audio_tracks' parameter specifies how many audio tracks should be processed.'audio_channels' can be used if the audio has more channels than stereo.\nThe 'logo' is used only if the path exists; the path is relative to your storage folder.\n'logo_scale' scales the logo to the target size. Leave it blank if no scaling is needed. The format is 'width:height', for example, '100:-1' for proportional scaling. The 'logo_opacity' option allows the logo to become transparent.'logo_position' is specified in the format 'x:y', which sets the logo's position.\nWith 'custom_filter', it is possible to apply additional filters. The filter outputs should end with [c_v_out] for video filters and [c_a_out] for audio filters.\n'vtt_enable' can only be used in HLS mode, and only when *.vtt files with the same filename as the video file exist.",
processing_mode TEXT NOT NULL DEFAULT "playlist",
processing_audio_only INTEGER NOT NULL DEFAULT 0,
processing_copy_audio INTEGER NOT NULL DEFAULT 0,
@ -115,28 +111,22 @@ CREATE TABLE
processing_filter TEXT NOT NULL DEFAULT "",
processing_vtt_enable INTEGER NOT NULL DEFAULT 0,
processing_vtt_dummy TEXT NULL DEFAULT "00-assets/dummy.vtt",
ingest_help "Run a server for an ingest stream. This stream will override the normal streaming until it is finished. There is only a very simple authentication mechanism, which checks if the stream name is correct.\n'custom_filter' can be used in the same way as the one in the process section.",
ingest_enable INTEGER NOT NULL DEFAULT 0,
ingest_param TEXT NOT NULL DEFAULT "-f live_flv -listen 1 -i rtmp://127.0.0.1:1936/live/stream",
ingest_filter TEXT NOT NULL DEFAULT "",
playlist_help TEXT NOT NULL DEFAULT "'day_start' indicates at what time the playlist should start; leave 'day_start' blank if the playlist should always start at the beginning. 'length' represents the target length of the playlist; when it is blank, the real length will not be considered.\n'infinite: true' works with a single playlist file and loops it infinitely.",
playlist_day_start TEXT NOT NULL DEFAULT "05:59:25",
playlist_length TEXT NOT NULL DEFAULT "24:00:00",
playlist_infinit INTEGER NOT NULL DEFAULT 0,
storage_help TEXT NOT NULL DEFAULT "'filler' is used to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder and will loop when necessary.\n'extensions' specifies which files to search for by this extension. Activate 'shuffle' to pick files randomly.",
storage_filler TEXT NOT NULL DEFAULT "filler/filler.mp4",
storage_extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm",
storage_shuffle INTEGER NOT NULL DEFAULT 1,
text_help TEXT NOT NULL DEFAULT "Overlay text in combination with libzmq for remote text manipulation. 'font' is a relative path to your storage folder.\n'text_from_filename' activates the extraction of text from a filename. With 'style', you can define the drawtext parameters, such as position, color, etc. Posting text over the API will override this. With 'regex', you can format file names to extract a title from them.",
text_add INTEGER NOT NULL DEFAULT 1,
text_from_filename INTEGER NOT NULL DEFAULT 0,
text_font TEXT NOT NULL DEFAULT "00-assets/DejaVuSans.ttf",
text_style TEXT NOT NULL DEFAULT "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4",
text_regex TEXT NOT NULL DEFAULT "^.+[/\\](.*)(.mp4|.mkv|.webm)$",
task_help TEXT NOT NULL DEFAULT "Run an external program with a given media object. The media object is in JSON format and contains all the information about the current clip. The external program can be a script or a binary, but it should only run for a short time.",
task_enable INTEGER NOT NULL DEFAULT 0,
task_path TEXT NOT NULL DEFAULT "",
output_help TEXT NOT NULL DEFAULT "The final playout encoding, set the settings according to your needs. 'mode' has the options 'desktop', 'hls', 'null', and 'stream'. Use 'stream' and adjust the 'output_param:' settings when you want to stream to an RTMP/RTSP/SRT/... server.\nIn production, don't serve HLS playlists with ffplayout; use Nginx or another web server!",
output_mode TEXT NOT NULL DEFAULT "hls",
output_param TEXT NOT NULL DEFAULT "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -muxpreload 0 -muxdelay 0 -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename live/stream-%d.ts live/stream.m3u8",
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE CASCADE ON DELETE CASCADE