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-stream",
"toml_edit",
"ts-rs",
"uuid",
"walkdir",
"zeromq",
@ -3663,6 +3664,15 @@ 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"
@ -3900,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"

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

@ -61,6 +61,7 @@ time = { version = "0.3", features = ["formatting", "macros"] }
tokio = { version = "1.29", features = ["full"] }
tokio-stream = "0.1"
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

@ -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,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 {
#[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,
}
@ -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 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,
}
@ -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 ffmpeg_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 mode: ProcessMode,
pub audio_only: bool,
@ -315,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,
@ -330,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>>,
}
@ -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 enable: bool,
pub input_param: String,
pub custom_filter: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
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 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,
@ -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 {
#[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,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
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 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>,
#[ts(rename = "font")]
#[serde(alias = "fontfile")]
pub font: String,
#[ts(skip)]
#[serde(skip_serializing, skip_deserializing)]
pub font_path: String,
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 enable: bool,
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 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>>,
}

View File

@ -6,80 +6,614 @@
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="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">
{{ setHelp(key.toString()) }}
{{ t('config.generalHelp') }}
</div>
</label>
<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']"
>
<template
v-if="
name.toString() !== 'startInSec' &&
name.toString() !== 'lengthInSec' &&
!(name.toString() === 'path' && key.toString() === 'storage')
"
>
<label class="form-control w-full mt-2">
<div class="label">
<span class="label-text !text-md font-bold">{{ name }}</span>
<span class="label-text !text-md font-bold">Stop Threshold</span>
</div>
<input
v-if="name.toString() === 'sender_pass'"
v-model="item[name]"
type="password"
:placeholder="t('config.placeholderPass')"
class="input input-sm input-bordered w-full"
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"
>
<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="label-text-alt">{{ 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="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-else-if="name.toString() === 'output_param' || name.toString() === 'custom_filter'"
v-model="item[name]"
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-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]"
v-model="configStore.playout.processing.vtt_enable"
type="checkbox"
class="checkbox checkbox-sm ms-2 mt-2"
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-else-if="name === 'ignore_lines'"
v-model="formatIgnoreLines"
v-model="configStore.playout.processing.vtt_dummy"
type="text"
class="input input-sm input-bordered w-full"
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"
/>
<input
v-else
:id="name"
v-model="item[name]"
type="text"
class="input input-sm input-bordered w-full"
/>
</template>
</label>
</div>
</template>
<div class="mt-5 mb-10">
<button class="btn btn-primary" type="submit">{{ t('config.save') }}</button>
</div>
@ -104,6 +638,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(';')
@ -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() {
const update = await configStore.setPlayoutConfig(configStore.playout)
configStore.onetimeInfo = true

View File

@ -183,10 +183,9 @@ export default {
placeholderPass: 'Passwort',
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.
'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.
'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.
'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.`,

View File

@ -182,23 +182,23 @@ export default {
output: 'Output',
placeholderPass: 'Password',
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.
'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.
'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.`,
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.
'ignore' allows logging to ignore strings that contain matched lines; the format is a semicolon-separated list.`,
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. The mode can be either 'playlist' or 'folder'.
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 '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.
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.
'vtt_enable' can only be used in HLS mode, and only when *.vtt files with the same filename as the video file exist.`,
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 '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.
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.
'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.
'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.

View File

@ -183,10 +183,9 @@ export default {
placeholderPass: 'Senha',
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.
'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.
'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.
'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.`,

View File

@ -183,10 +183,9 @@ export default {
placeholderPass: 'Password',
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.
'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.
'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.
'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.`,

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

View File

@ -1,4 +1,5 @@
import type { JwtPayload } from 'jwt-decode'
import type { PlayoutConfig, Playlist as Ply } from '~/types/playout_config'
export {}
@ -9,6 +10,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, };
export type Task = { enable: boolean, path: string, };
export type Text = { add_text: boolean, font: string, text_from_filename: boolean, style: string, regex: string, };