auto create types for playout config, switch to real playout config form

This commit is contained in:
Jonathan Baecker 2024-10-13 22:30:13 +02:00
parent e3955faaad
commit 7f7ca6a237
15 changed files with 786 additions and 176 deletions

2
.cargo/config.toml Normal file
View File

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

34
Cargo.lock generated
View File

@ -1266,6 +1266,7 @@ dependencies = [
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"toml_edit", "toml_edit",
"ts-rs",
"uuid", "uuid",
"walkdir", "walkdir",
"zeromq", "zeromq",
@ -3663,6 +3664,15 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "tests" name = "tests"
version = "0.24.0-rc2" version = "0.24.0-rc2"
@ -3900,6 +3910,30 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 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]] [[package]]
name = "typeid" name = "typeid"
version = "1.0.2" version = "1.0.2"

View File

@ -1,7 +1,7 @@
## Closed Captions ## Closed Captions
#### Note: #### 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 ### Usage
**ffplayout** can handle closed captions in WebVTT format for HLS streaming. **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 \ -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 \ -muxpreload 0 -muxdelay 0 -f hls -hls_time 6 -hls_list_size 600 \
-hls_flags append_list+delete_segments+omit_endlist \ -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 \ -master_pl_name master.m3u8 \
-hls_segment_filename \ -hls_segment_filename \
live/stream-%d.ts live/stream.m3u8 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 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 ## Setup Frontend
Make sure to install the dependencies: Make sure to install the dependencies:

View File

@ -61,6 +61,7 @@ time = { version = "0.3", features = ["formatting", "macros"] }
tokio = { version = "1.29", features = ["full"] } tokio = { version = "1.29", features = ["full"] }
tokio-stream = "0.1" 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" uuid = "1.8"
walkdir = "2" walkdir = "2"
zeromq = { version = "0.4", default-features = false, features = [ zeromq = { version = "0.4", default-features = false, features = [

View File

@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
use shlex::split; use shlex::split;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use tokio::{fs, io::AsyncReadExt}; use tokio::{fs, io::AsyncReadExt};
use ts_rs::TS;
use crate::db::{handles, models}; use crate::db::{handles, models};
use crate::utils::{files::norm_abs_path, free_tcp_socket, time_to_sec}; 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", "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")] #[serde(rename_all = "lowercase")]
pub enum OutputMode { pub enum OutputMode {
Desktop, 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")] #[serde(rename_all = "lowercase")]
pub enum ProcessMode { pub enum ProcessMode {
Folder, 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 struct Template {
pub sources: Vec<Source>, pub sources: Vec<Source>,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
pub struct Source { pub struct Source {
#[ts(type = "string")]
pub start: NaiveTime, pub start: NaiveTime,
#[ts(type = "string")]
pub duration: NaiveTime, pub duration: NaiveTime,
pub shuffle: bool, pub shuffle: bool,
pub paths: Vec<PathBuf>, pub paths: Vec<PathBuf>,
@ -157,10 +162,14 @@ pub struct Source {
/// Channel Config /// Channel Config
/// ///
/// This we init ones, when ffplayout is starting and use them globally in the hole program. /// 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 { pub struct PlayoutConfig {
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub channel: Channel, pub channel: Channel,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub advanced: AdvancedConfig, pub advanced: AdvancedConfig,
pub general: General, pub general: General,
@ -176,7 +185,7 @@ pub struct PlayoutConfig {
pub output: Output, pub output: Output,
} }
#[derive(Debug, Default, Clone, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
pub struct Channel { pub struct Channel {
pub logs: PathBuf, pub logs: PathBuf,
pub public: PathBuf, pub public: PathBuf,
@ -197,23 +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 struct General {
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub id: i32, pub id: i32,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub channel_id: i32, pub channel_id: i32,
pub stop_threshold: f64, pub stop_threshold: f64,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub generate: Option<Vec<String>>, pub generate: Option<Vec<String>>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub ffmpeg_filters: Vec<String>, pub ffmpeg_filters: Vec<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub ffmpeg_libs: Vec<String>, pub ffmpeg_libs: Vec<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub template: Option<Template>, pub template: Option<Template>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub skip_validation: bool, pub skip_validation: bool,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub validate: bool, pub validate: bool,
} }
@ -234,18 +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 struct Mail {
pub subject: String, pub subject: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub smtp_server: String, pub smtp_server: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub starttls: bool, pub starttls: bool,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub sender_addr: String, pub sender_addr: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub sender_pass: String, pub sender_pass: String,
pub recipient: String, pub recipient: String,
#[ts(type = "string")]
pub mail_level: Level, pub mail_level: Level,
pub interval: i64, pub interval: i64,
} }
@ -280,7 +304,8 @@ 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 struct Logging {
pub ffmpeg_level: String, pub ffmpeg_level: String,
pub ingest_level: String, pub ingest_level: String,
@ -303,7 +328,8 @@ 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 struct Processing {
pub mode: ProcessMode, pub mode: ProcessMode,
pub audio_only: bool, pub audio_only: bool,
@ -315,6 +341,7 @@ pub struct Processing {
pub fps: f64, pub fps: f64,
pub add_logo: bool, pub add_logo: bool,
pub logo: String, pub logo: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub logo_path: String, pub logo_path: String,
pub logo_scale: String, pub logo_scale: String,
@ -330,6 +357,7 @@ pub struct Processing {
pub vtt_enable: bool, pub vtt_enable: bool,
#[serde(default)] #[serde(default)]
pub vtt_dummy: Option<String>, pub vtt_dummy: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub cmd: Option<Vec<String>>, pub cmd: Option<Vec<String>>,
} }
@ -363,11 +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 struct Ingest {
pub enable: bool, pub enable: bool,
pub input_param: String, pub input_param: String,
pub custom_filter: String, pub custom_filter: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>, pub input_cmd: Option<Vec<String>>,
} }
@ -383,12 +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 struct Playlist {
pub day_start: String, pub day_start: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub start_sec: Option<f64>, pub start_sec: Option<f64>,
pub length: String, pub length: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub length_sec: Option<f64>, pub length_sec: Option<f64>,
pub infinit: bool, pub infinit: bool,
@ -406,17 +439,22 @@ 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 struct Storage {
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub path: PathBuf, pub path: PathBuf,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub paths: Vec<PathBuf>, pub paths: Vec<PathBuf>,
pub filler: String, pub filler: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub filler_path: PathBuf, pub filler_path: PathBuf,
pub extensions: Vec<String>, pub extensions: Vec<String>,
pub shuffle: bool, pub shuffle: bool,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub shared_storage: bool, pub shared_storage: bool,
} }
@ -439,17 +477,23 @@ 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 struct Text {
pub add_text: bool, pub add_text: bool,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub node_pos: Option<usize>, pub node_pos: Option<usize>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub zmq_stream_socket: Option<String>, pub zmq_stream_socket: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub zmq_server_socket: Option<String>, pub zmq_server_socket: Option<String>,
#[ts(rename = "font")]
#[serde(alias = "fontfile")] #[serde(alias = "fontfile")]
pub font: String, pub font: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub font_path: String, pub font_path: String,
pub text_from_filename: bool, pub text_from_filename: bool,
@ -473,7 +517,8 @@ 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 struct Task {
pub enable: bool, pub enable: bool,
pub path: PathBuf, pub path: PathBuf,
@ -488,14 +533,18 @@ impl Task {
} }
} }
#[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 struct Output {
pub mode: OutputMode, pub mode: OutputMode,
pub output_param: String, pub output_param: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub output_count: usize, pub output_count: usize,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub output_filter: Option<String>, pub output_filter: Option<String>,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub output_cmd: Option<Vec<String>>, pub output_cmd: Option<Vec<String>>,
} }

View File

@ -6,80 +6,614 @@
class="mt-10 grid md:grid-cols-[180px_auto] gap-5" class="mt-10 grid md:grid-cols-[180px_auto] gap-5"
@submit.prevent="onSubmitPlayout" @submit.prevent="onSubmitPlayout"
> >
<template v-for="(item, key) in configStore.playout" :key="key"> <div class="text-xl pt-3 md:text-right">{{ t('config.general') }}:</div>
<div class="text-xl pt-3 text-right">{{ setTitle(key.toString()) }}:</div> <div class="md:pt-4">
<div class="md:pt-4"> <label class="form-control mb-2">
<label class="form-control mb-2"> <div class="whitespace-pre-line">
<div class="whitespace-pre-line"> {{ t('config.generalHelp') }}
{{ setHelp(key.toString()) }} </div>
</div> </label>
</label> <label class="form-control w-full mt-2">
<label <div class="label">
v-for="(prop, name) in (item as Record<string, any>)" <span class="label-text !text-md font-bold">Stop Threshold</span>
:key="name" </div>
class="form-control w-full" <input
:class="[typeof prop === 'boolean' && 'flex-row', name.toString() !== 'help_text' && 'mt-2']" 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="label-text-alt">{{ 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 <option v-for="level in logLevels" :key="level" :value="level">{{ level }}</option>
v-if=" </select>
name.toString() !== 'startInSec' && </label>
name.toString() !== 'lengthInSec' && <label class="form-control w-full mt-2">
!(name.toString() === 'path' && key.toString() === 'storage') <div class="label">
" <span class="label-text !text-md font-bold">Interval</span>
> </div>
<div class="label"> <input
<span class="label-text !text-md font-bold">{{ name }}</span> v-model="configStore.playout.mail.interval"
</div> type="number"
<input min="30"
v-if="name.toString() === 'sender_pass'" step="10"
v-model="item[name]" class="input input-sm input-bordered w-full max-w-36"
type="password" />
:placeholder="t('config.placeholderPass')" <div class="label">
class="input input-sm input-bordered w-full" <span class="label-text-alt">{{ t('config.mailInterval') }}</span>
/> </div>
<textarea </label>
v-else-if="name.toString() === 'output_param' || name.toString() === 'custom_filter'" </div>
v-model="item[name]"
class="textarea textarea-bordered" <div class="text-xl pt-3 md:text-right">{{ t('config.logging') }}:</div>
rows="3" <div class="md:pt-4">
/> <label class="form-control mb-2">
<input <div class="whitespace-pre-line">
v-else-if="typeof prop === 'number' && prop % 1 === 0" {{ t('config.logHelp') }}
v-model="item[name]" </div>
type="number" </label>
class="input input-sm input-bordered w-full" <label class="form-control w-full mt-2">
/> <div class="label">
<input <span class="label-text !text-md font-bold">ffmpeg Level</span>
v-else-if="typeof prop === 'number'" </div>
v-model="item[name]" <select
type="number" v-model="configStore.playout.logging.ffmpeg_level"
class="input input-sm input-bordered w-full" class="select select-sm select-bordered w-full max-w-xs"
step="0.0001" >
style="max-width: 250px" <option v-for="level in logLevels" :key="level" :value="level">{{ level }}</option>
/> </select>
<input </label>
v-else-if="typeof prop === 'boolean'" <label class="form-control w-full mt-2">
v-model="item[name]" <div class="label">
type="checkbox" <span class="label-text !text-md font-bold">Ingest Level</span>
class="checkbox checkbox-sm ms-2 mt-2" </div>
/> <select
<input v-model="configStore.playout.logging.ingest_level"
v-else-if="name === 'ignore_lines'" class="select select-sm select-bordered w-full max-w-xs"
v-model="formatIgnoreLines" >
type="text" <option v-for="level in logLevels" :key="level" :value="level">{{ level }}</option>
class="input input-sm input-bordered w-full" </select>
/> </label>
<input <label class="form-control w-full mt-2">
v-else <div class="flex flex-row">
:id="name" <input
v-model="item[name]" v-model="configStore.playout.logging.detect_silence"
type="text" type="checkbox"
class="input input-sm input-bordered w-full" class="checkbox checkbox-sm me-1 mt-2"
/> />
</template> <div class="label">
</label> <span class="label-text !text-md font-bold">Detect Silence</span>
</div> </div>
</template> </div>
<div class="label py-0">
<span class="label-text-alt">{{ 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="label-text-alt">{{ 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"
/>
</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"
/>
</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"
/>
</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"
/>
</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"
/>
</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"
/>
</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"
/>
</label>
<label class="form-control w-full flex-row mt-2">
<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>
</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"
/>
</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"
/>
</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 max-w-xs">
<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]"
/>
</label>
<label class="form-control w-full max-w-xs">
<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]"
/>
</label>
<label class="form-control w-full flex-row mt-2">
<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">Enable</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"
/>
</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"
/>
</label>
<label class="form-control w-full flex-row mt-2">
<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>
</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"
/>
</label>
<label class="form-control w-full flex-row mt-2">
<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>
</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"
/>
</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"
/>
</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"
/>
</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"> <div class="mt-5 mb-10">
<button class="btn btn-primary" type="submit">{{ t('config.save') }}</button> <button class="btn btn-primary" type="submit">{{ t('config.save') }}</button>
</div> </div>
@ -104,6 +638,10 @@ const indexStore = useIndex()
const showModal = ref(false) const showModal = ref(false)
const logLevels = ['INFO', 'WARNING', 'ERROR']
const processingMode = ['folder', 'playlist']
const outputMode = ['desktop', 'hls', 'stream', 'null']
const formatIgnoreLines = computed({ const formatIgnoreLines = computed({
get() { get() {
return configStore.playout.logging.ignore_lines.join(';') return configStore.playout.logging.ignore_lines.join(';')
@ -114,65 +652,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): string {
console.log('key:', key)
switch (key) {
case 'general':
return t('config.generalHelp')
case 'rpc_server':
return t('config.rpcHelp')
case 'mail':
return t('config.mailHelp')
case 'logging':
return t('config.logHelp')
case 'processing':
return t('config.processingHelp')
case 'ingest':
return t('config.ingestHelp')
case 'playlist':
return t('config.playlistHelp')
case 'storage':
return t('config.storageHelp')
case 'text':
return t('config.textHelp')
case 'task':
return t('config.taskHelp')
case 'output':
return t('config.outputHelp')
default:
return ''
}
}
async function onSubmitPlayout() { async function onSubmitPlayout() {
const update = await configStore.setPlayoutConfig(configStore.playout) const update = await configStore.setPlayoutConfig(configStore.playout)
configStore.onetimeInfo = true configStore.onetimeInfo = true

View File

@ -183,10 +183,9 @@ export default {
placeholderPass: 'Passwort', placeholderPass: 'Passwort',
help: 'Hilfe', help: 'Hilfe',
generalHelp: `Manchmal kann es vorkommen, dass eine Datei beschädigt ist, aber dennoch abspielbar. Dies kann einen Streaming-Fehler für alle folgenden Dateien verursachen. Die einzige Lösung in diesem Fall ist, ffplayout zu stoppen und es erneut zu starten. generalHelp: `Manchmal kann es vorkommen, dass eine Datei beschädigt ist, aber dennoch abspielbar. Dies kann einen Streaming-Fehler für alle folgenden Dateien verursachen. Die einzige Lösung in diesem Fall ist, ffplayout zu stoppen und es erneut zu starten.
'stop_threshold' stoppt ffplayout, wenn es asynchron in der Zeit über diesen Wert hinaus ist. Ein Wert unter 3 kann unerwartete Fehler verursachen.`, 'Stop Threshold' stoppt ffplayout, wenn es asynchron in der Zeit über diesen Wert hinaus ist. Ein Wert 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 dies nicht benötigt wird. 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 dies nicht benötigt wird.
'mail_level' kann INFO, WARNING oder ERROR sein. 'Interval' bezieht sich auf die Anzahl der Sekunden bis zur nächsten E-Mail; der Wert muss in Schritten von 10 erfolgen und darf nicht weniger als 30 Sekunden betragen.`,
'interval' bezieht sich auf die Anzahl der Sekunden bis zur nächsten E-Mail; der Wert muss in Schritten von 10 erfolgen und darf nicht weniger als 30 Sekunden betragen.`,
logHelp: `'ffmpeg_level/ingest_level' kann INFO, WARNING oder ERROR sein. logHelp: `'ffmpeg_level/ingest_level' kann INFO, WARNING oder ERROR sein.
'detect_silence' protokolliert eine Fehlermeldung, wenn die Audioleitung während des Validierungsprozesses 15 Sekunden lang stumm ist. 'detect_silence' protokolliert eine Fehlermeldung, wenn die Audioleitung während des Validierungsprozesses 15 Sekunden lang stumm ist.
'ignore' erlaubt dem Protokoll, Zeichenfolgen zu ignorieren, die übereinstimmende Zeilen enthalten; das Format ist eine durch Semikolons getrennte Liste.`, 'ignore' erlaubt dem Protokoll, Zeichenfolgen zu ignorieren, die übereinstimmende Zeilen enthalten; das Format ist eine durch Semikolons getrennte Liste.`,

View File

@ -182,23 +182,23 @@ export default {
output: 'Output', output: 'Output',
placeholderPass: 'Password', placeholderPass: 'Password',
help: 'Help', help: 'Help',
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. 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.',
'stop_threshold' stops ffplayout if it is asynchronous in time above this value. A number below 3 can cause unexpected errors.`, 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. 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.`,
'mail_level' can be INFO, WARNING, or ERROR. 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.',
'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.',
logHelp: `'ffmpeg_level/ingest_level' can be INFO, WARNING, or ERROR. logDetect: 'Logs an error message if the audio line is silent for 15 seconds during the validation process.',
'detect_silence' 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.',
'ignore' allows logging to ignore strings that contain matched lines; the format is a semicolon-separated list.`,
processingHelp: `Default processing for all clips ensures uniqueness. The mode can be either 'playlist' or 'folder'. processingHelp: `Default processing for all clips ensures uniqueness. The mode can be either 'playlist' or 'folder'.
The 'aspect' parameter must be a float number. The 'Aspect' parameter must be a float number.
The 'audio_tracks' parameter specifies how many audio tracks should be processed.'audio_channels' can be used if the audio has more channels than stereo. The 'Audio Tracks' parameter specifies how many audio tracks should be processed. 'Audio Channels' can be used if the audio has more channels than stereo.
The 'logo' is used only if the path exists; the path is relative to your storage folder. The 'Logo' is used only if the path exists; the path is relative to your storage folder.
'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. '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.
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. 'Logo Position' is specified in the format 'x:y', which sets the logo's position.
'vtt_enable' can only be used in HLS mode, and only when *.vtt files with the same filename as the video file exist.`, 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.
'Enable VTT' can only be used in HLS mode, and only when *.vtt files with the same filename as the video file exist.`,
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. 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.
'custom_filter' can be used in the same way as the one in the process section.`, 'Custom Filter' can be used in the same way as the one in the process section.`,
playlistHelp: `'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. playlistHelp: `'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.
'infinite: true' works with a single playlist file and loops it infinitely.`, 'infinite: true' works with a single playlist file and loops it infinitely.`,
storageHelp: `'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. storageHelp: `'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.

View File

@ -183,10 +183,9 @@ export default {
placeholderPass: 'Senha', placeholderPass: 'Senha',
help: 'Ajuda', help: 'Ajuda',
generalHelp: `Às vezes, pode acontecer que um arquivo esteja corrompido, mas ainda seja reproduzível. Isso pode produzir um erro de streaming para todos os arquivos seguintes. A única solução nesse caso é parar o ffplayout e iniciá-lo novamente. generalHelp: `Às vezes, pode acontecer que um arquivo esteja corrompido, mas ainda seja reproduzível. Isso pode produzir um erro de streaming para todos os arquivos seguintes. A única solução nesse caso é parar o ffplayout e iniciá-lo novamente.
'stop_threshold' para o ffplayout se ele estiver fora de sincronia no tempo acima desse valor. Um número abaixo de 3 pode causar erros inesperados.`, 'Stop Threshold' para o ffplayout se ele estiver fora de sincronia no tempo acima desse 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 você não precisar disso. 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 você não precisar disso.
'mail_level' pode ser INFO, WARNING ou ERROR. 'Interval' refere-se ao número de segundos até que um novo e-mail seja enviado; o valor deve ser em incrementos de 10 e não inferior a 30 segundos.`,
'interval' refere-se ao número de segundos até que um novo e-mail seja enviado; o valor deve ser em incrementos de 10 e não inferior a 30 segundos.`,
logHelp: `'ffmpeg_level/ingest_level' pode ser INFO, WARNING ou ERROR. logHelp: `'ffmpeg_level/ingest_level' pode ser INFO, WARNING ou ERROR.
'detect_silence' registra uma mensagem de erro se a linha de áudio estiver em silêncio por 15 segundos durante o processo de validação. 'detect_silence' registra uma mensagem de erro se a linha de áudio estiver em silêncio por 15 segundos durante o processo de validação.
'ignore' permite que o log ignore cadeias de caracteres que contenham linhas correspondentes; o formato é uma lista separada por ponto e vírgula.`, 'ignore' permite que o log ignore cadeias de caracteres que contenham linhas correspondentes; o formato é uma lista separada por ponto e vírgula.`,

View File

@ -183,10 +183,9 @@ export default {
placeholderPass: 'Password', placeholderPass: 'Password',
help: 'Help', help: 'Help',
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. 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.
'stop_threshold' stops ffplayout if it is asynchronous in time above this value. A number below 3 can cause unexpected errors.`, 'Stop 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. 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.
'mail_level' can be INFO, WARNING, or ERROR. '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.`,
'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: `'ffmpeg_level/ingest_level' can be INFO, WARNING, or ERROR. logHelp: `'ffmpeg_level/ingest_level' can be INFO, WARNING, or ERROR.
'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process. 'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process.
'ignore' allows logging to ignore strings that contain matched lines; the format is a semicolon-separated list.`, 'ignore' allows logging to ignore strings that contain matched lines; the format is a semicolon-separated list.`,

View File

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

View File

@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export const useConfig = defineStore('config', { export const useConfig = defineStore('config', {
state: () => ({ state: () => ({
i: 0, i: 0,
@ -10,7 +11,7 @@ export const useConfig = defineStore('config', {
channelsRaw: [] as Channel[], channelsRaw: [] as Channel[],
playlistLength: 86400.0, playlistLength: 86400.0,
advanced: {} as any, advanced: {} as any,
playout: {} as any, playout: {} as PlayoutConfigExt,
currentUser: 0, currentUser: 0,
configUser: {} as User, configUser: {} as User,
utcOffset: 0, utcOffset: 0,

View File

@ -1,4 +1,5 @@
import type { JwtPayload } from 'jwt-decode' import type { JwtPayload } from 'jwt-decode'
import type { PlayoutConfig, Playlist as Ply } from '~/types/playout_config'
export {} export {}
@ -9,6 +10,15 @@ declare global {
role: string role: string
} }
interface PlaylistExt extends Ply {
startInSec: number,
lengthInSec: number
}
interface PlayoutConfigExt extends PlayoutConfig {
playlist: PlaylistExt
}
interface LoginObj { interface LoginObj {
message: string message: string
status: number 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, };
export type Task = { enable: boolean, path: string, };
export type Text = { add_text: boolean, font: string, text_from_filename: boolean, style: string, regex: string, };