better advanced config form

This commit is contained in:
Jonathan Baecker 2024-10-14 12:27:55 +02:00
parent 7f7ca6a237
commit 53b2fd442b
14 changed files with 749 additions and 189 deletions

View File

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

View File

@ -454,8 +454,7 @@ pub struct Storage {
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_deserializing)]
#[serde(skip_serializing, skip_deserializing)]
pub shared_storage: bool, pub shared_storage: bool,
} }
@ -490,7 +489,6 @@ pub struct Text {
#[ts(skip)] #[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)] #[ts(skip)]

View File

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

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="max-w-[1200px] pe-8"> <div class="max-w-[1200px] xs:pe-8">
<h2 class="pt-3 text-3xl">{{ t('config.playoutConf') }}</h2> <h2 class="pt-3 text-3xl">{{ t('config.playoutConf') }}</h2>
<form <form
v-if="configStore.playout" v-if="configStore.playout"
@ -24,7 +24,7 @@
class="input input-sm input-bordered w-full max-w-36" class="input input-sm input-bordered w-full max-w-36"
/> />
<div class="label"> <div class="label">
<span class="label-text-alt">{{ t('config.stopThreshold') }}</span> <span class="text-sm select-text text-base-content/80">{{ t('config.stopThreshold') }}</span>
</div> </div>
</label> </label>
</div> </div>
@ -79,7 +79,7 @@
class="input input-sm input-bordered w-full max-w-36" class="input input-sm input-bordered w-full max-w-36"
/> />
<div class="label"> <div class="label">
<span class="label-text-alt">{{ t('config.mailInterval') }}</span> <span class="text-sm select-text text-base-content/80">{{ t('config.mailInterval') }}</span>
</div> </div>
</label> </label>
</div> </div>
@ -125,7 +125,7 @@
</div> </div>
</div> </div>
<div class="label py-0"> <div class="label py-0">
<span class="label-text-alt">{{ t('config.logDetect') }}</span> <span class="text-sm select-text text-base-content/80">{{ t('config.logDetect') }}</span>
</div> </div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
@ -138,7 +138,7 @@
class="input input-sm input-bordered w-full truncate" class="input input-sm input-bordered w-full truncate"
/> />
<div class="label"> <div class="label">
<span class="label-text-alt">{{ t('config.logIgnore') }}</span> <span class="text-sm select-text text-base-content/80">{{ t('config.logIgnore') }}</span>
</div> </div>
</label> </label>
</div> </div>
@ -258,6 +258,11 @@
type="text" type="text"
class="input input-sm input-bordered w-full max-w-lg" class="input input-sm input-bordered w-full max-w-lg"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingLogoPath')
}}</span>
</div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
<div class="label"> <div class="label">
@ -281,6 +286,11 @@
type="text" type="text"
class="input input-sm input-bordered w-full max-w-md" class="input input-sm input-bordered w-full max-w-md"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingLogoScale')
}}</span>
</div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
<div class="label"> <div class="label">
@ -291,6 +301,11 @@
type="text" type="text"
class="input input-sm input-bordered w-full max-w-md" class="input input-sm input-bordered w-full max-w-md"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingLogoPosition')
}}</span>
</div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
<div class="label"> <div class="label">
@ -304,6 +319,11 @@
step="1" step="1"
class="input input-sm input-bordered w-full max-w-36" class="input input-sm input-bordered w-full max-w-36"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingAudioTracks')
}}</span>
</div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
<div class="label"> <div class="label">
@ -317,6 +337,11 @@
step="1" step="1"
class="input input-sm input-bordered w-full max-w-36" class="input input-sm input-bordered w-full max-w-36"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingAudioIndex')
}}</span>
</div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
<div class="label"> <div class="label">
@ -330,6 +355,11 @@
step="1" step="1"
class="input input-sm input-bordered w-full max-w-36" class="input input-sm input-bordered w-full max-w-36"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingAudioChannels')
}}</span>
</div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
<div class="label"> <div class="label">
@ -353,15 +383,27 @@
class="textarea textarea-bordered" class="textarea textarea-bordered"
rows="3" 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"> <div class="label">
<span class="label-text !text-md font-bold">Enable VTT</span> <span class="text-sm select-text text-base-content/80">{{
t('config.processingCustomFilter')
}}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.processing.vtt_enable"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Enable VTT</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingVTTEnable')
}}</span>
</div> </div>
</label> </label>
<label class="form-control w-full mt-2"> <label class="form-control w-full mt-2">
@ -373,6 +415,11 @@
type="text" type="text"
class="input input-sm input-bordered w-full max-w-lg" class="input input-sm input-bordered w-full max-w-lg"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.processingVTTDummy')
}}</span>
</div>
</label> </label>
</div> </div>
@ -412,6 +459,11 @@
class="textarea textarea-bordered" class="textarea textarea-bordered"
rows="3" rows="3"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{
t('config.ingestCustomFilter')
}}</span>
</div>
</label> </label>
</div> </div>
@ -422,7 +474,7 @@
{{ t('config.playlistHelp') }} {{ t('config.playlistHelp') }}
</div> </div>
</label> </label>
<label class="form-control w-full max-w-xs"> <label class="form-control w-full">
<div class="label"> <div class="label">
<span class="label-text text-base font-bold">Day Start</span> <span class="label-text text-base font-bold">Day Start</span>
</div> </div>
@ -432,8 +484,11 @@
class="input input-sm input-bordered w-full max-w-xs" 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]" pattern="([01]?[0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9]"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.playlistDayStart') }}</span>
</div>
</label> </label>
<label class="form-control w-full max-w-xs"> <label class="form-control w-full">
<div class="label"> <div class="label">
<span class="label-text text-base font-bold">Length</span> <span class="label-text text-base font-bold">Length</span>
</div> </div>
@ -443,15 +498,23 @@
class="input input-sm input-bordered w-full max-w-xs" 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]" 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"> <div class="label">
<span class="label-text !text-md font-bold">Enable</span> <span class="text-sm select-text text-base-content/80">{{ t('config.playlistLength') }}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.playlist.infinit"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Infinit</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{ t('config.playlistInfinit') }}</span>
</div> </div>
</label> </label>
</div> </div>
@ -473,6 +536,9 @@
name="filler" name="filler"
class="input input-sm input-bordered w-full max-w-lg" class="input input-sm input-bordered w-full max-w-lg"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.storageFiller') }}</span>
</div>
</label> </label>
<label class="form-control w-full"> <label class="form-control w-full">
<div class="label"> <div class="label">
@ -483,15 +549,23 @@
type="text" type="text"
class="input input-sm input-bordered w-full max-w-lg" 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"> <div class="label">
<span class="label-text !text-md font-bold">Shuffle</span> <span class="text-sm select-text text-base-content/80">{{ t('config.storageExtension') }}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.storage.shuffle"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Shuffle</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{ t('config.storageShuffle') }}</span>
</div> </div>
</label> </label>
</div> </div>
@ -522,15 +596,23 @@
type="text" type="text"
class="input input-sm input-bordered w-full max-w-lg" 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"> <div class="label">
<span class="label-text !text-md font-bold">Text from File</span> <span class="text-sm select-text text-base-content/80">{{ t('config.textFont') }}</span>
</div>
</label>
<label class="form-control w-full mt-2">
<div class="flex flex-row">
<input
v-model="configStore.playout.text.text_from_filename"
type="checkbox"
class="checkbox checkbox-sm me-1 mt-2"
/>
<div class="label">
<span class="label-text !text-md font-bold">Text from File</span>
</div>
</div>
<div class="label py-0">
<span class="text-sm select-text text-base-content/80">{{ t('config.textFromFile') }}</span>
</div> </div>
</label> </label>
<label class="form-control w-full"> <label class="form-control w-full">
@ -542,6 +624,9 @@
type="text" type="text"
class="input input-sm input-bordered w-full truncate" class="input input-sm input-bordered w-full truncate"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.textStyle') }}</span>
</div>
</label> </label>
<label class="form-control w-full"> <label class="form-control w-full">
<div class="label"> <div class="label">
@ -552,6 +637,9 @@
type="text" type="text"
class="input input-sm input-bordered w-full max-w-lg" class="input input-sm input-bordered w-full max-w-lg"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.textRegex') }}</span>
</div>
</label> </label>
</div> </div>
@ -582,6 +670,9 @@
name="task_path" name="task_path"
class="input input-sm input-bordered w-full max-w-lg" class="input input-sm input-bordered w-full max-w-lg"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">{{ t('config.taskPath') }}</span>
</div>
</label> </label>
</div> </div>

View File

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

View File

@ -182,31 +182,41 @@ export default {
output: 'Ausgabe', output: 'Ausgabe',
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 passieren, dass eine Datei beschädigt ist, aber dennoch abgespielt werden kann. Dies kann zu einem Streaming-Fehler für alle folgenden Dateien führen. Die einzige Lösung in diesem Fall ist, ffplayout zu stoppen und erneut zu starten.',
'Stop Threshold' stoppt ffplayout, wenn es asynchron in der Zeit über diesen Wert hinaus ist. Ein Wert unter 3 kann unerwartete Fehler verursachen.`, stopThreshold: 'Der Schwellenwert stoppt ffplayout, wenn es zeitlich asynchron über diesem Wert ist. Eine Zahl unter 3 kann unerwartete Fehler verursachen.',
mailHelp: `Sende Fehlermeldungen an eine E-Mail-Adresse, wie z.B. fehlende Clips, fehlendes oder ungültiges Playlist-Format usw. Lass den Empfänger leer, wenn 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 du dies nicht benötigst.`,
'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.`, mailInterval: 'Das Intervall bezieht sich auf die Anzahl der Sekunden, bis eine neue E-Mail gesendet wird; der Wert muss in 10er-Schritten und nicht unter 30 Sekunden liegen.',
logHelp: `'ffmpeg_level/ingest_level' kann INFO, WARNING oder ERROR sein. logHelp: 'Passen Sie das Verhalten des Loggings an.',
'detect_silence' protokolliert eine Fehlermeldung, wenn die Audioleitung während des Validierungsprozesses 15 Sekunden lang stumm ist. logDetect: '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.`, logIgnore: 'Ignoriere Zeichenfolgen, die übereinstimmende Zeilen enthalten; das Format ist eine durch Semikolon getrennte Liste.',
processingHelp: `Die Standardverarbeitung für alle Clips sorgt für Einzigartigkeit. Der Modus kann entweder 'playlist' oder 'folder' sein. processingHelp: 'Die Standardverarbeitung für alle Clips stellt die Einzigartigkeit sicher.',
Der Parameter 'aspect' muss eine Gleitkommazahl sein. processingLogoPath: 'Das Logo wird nur verwendet, wenn der Pfad existiert; der Pfad ist relativ zum Speicherordner.',
Der Parameter 'audio_tracks' gibt an, wie viele Audiotracks verarbeitet werden sollen. 'audio_channels' kann verwendet werden, wenn das Audio mehr als Stereo-Kanäle hat. processingLogoScale: `Lass die Skalierung des Logos leer, wenn keine Skalierung erforderlich ist. Das Format lautet 'Breite:Höhe', zum Beispiel: '100:-1' für proportionale Skalierung.`,
Das 'logo' wird nur verwendet, wenn der Pfad existiert; der Pfad ist relativ zu deinem Speicherordner. processingLogoPosition: `Die Position wird im Format 'x:y' angegeben.`,
'logo_scale' skaliert das Logo auf die Zielgröße. Lasse es leer, wenn keine Skalierung erforderlich ist. Das Format ist 'Breite:Höhe', z.B. '100:-1' für proportionale Skalierung. Die Option 'logo_opacity' ermöglicht es, das Logo transparent zu machen. Die 'logo_position' wird im Format 'x:y' angegeben, was die Position des Logos festlegt. processingAudioTracks: 'Gib an, wie viele Audiospuren verarbeitet werden sollen.',
Mit 'custom_filter' ist es möglich, zusätzliche Filter anzuwenden. Die Filterausgaben sollten mit [c_v_out] für Video-Filter und [c_a_out] für Audio-Filter enden. processingAudioIndex: 'Welche Audiospur verwendet werden soll, -1 für alle.',
'vtt_enable' kann nur im HLS-Modus verwendet werden, und nur wenn *.vtt-Dateien mit demselben Dateinamen wie die Videodatei existieren.`, processingAudioChannels: 'Stelle die Anzahl der Audiokanäle ein, wenn das Audio mehr Kanäle als Stereo hat.',
ingestHelp: `Starte einen Server für einen Eingabestream. Dieser Stream überschreibt den normalen Stream, bis er beendet ist. Es gibt nur einen sehr einfachen Authentifizierungsmechanismus, der überprüft, ob der Stream-Name korrekt ist. processingCustomFilter: 'Füge benutzerdefinierte Filter zur Verarbeitung hinzu. Die Filterausgaben müssen mit [c_v_out] für Video-Filter und [c_a_out] für Audio-Filter enden.',
'custom_filter' kann auf die gleiche Weise verwendet werden wie der im Verarbeitungsabschnitt.`, processingVTTEnable: 'VTT kann nur im HLS-Modus verwendet werden und nur, wenn *.vtt-Dateien mit demselben Namen wie die Videodatei vorhanden sind.',
playlistHelp: `'day_start' gibt an, zu welcher Zeit die Playlist starten soll; lass 'day_start' leer, wenn die Playlist immer am Anfang starten soll. 'length' stellt die Ziellänge der Playlist dar; wenn es leer ist, wird die reale Länge nicht berücksichtigt. processingVTTDummy: 'Ein Platzhalter wird benötigt, wenn keine vtt-Datei vorhanden ist.',
'infinite: true' funktioniert mit einer einzigen Playlist-Datei und schleift sie unendlich.`, ingestHelp: `Starte einen Server für einen Ingest-Stream. Dieser Stream wird den normalen Stream überschreiben, bis er beendet ist. Es gibt nur einen sehr einfachen Authentifizierungsmechanismus, der überprüft, ob der Streamname korrekt ist.`,
storageHelp: `'filler' wird verwendet, um anstelle einer fehlenden Datei oder zur Auffüllung der verbleibenden Zeit auf insgesamt 24 Stunden abzuspielen. Es kann eine Datei oder ein Ordner sein und wird bei Bedarf wiederholt. ingestCustomFilter: 'Wende einen benutzerdefinierten Filter auf den Ingest-Stream auf dieselbe Weise wie im Abschnitt Verarbeitung an.',
'extensions' gibt an, nach welchen Dateien anhand dieser Erweiterung gesucht werden soll. Aktiviere 'shuffle', um Dateien zufällig auszuwählen.`, playlistHelp: 'Playlist-Verwaltung.',
textHelp: `Überlagere Text in Kombination mit libzmq zur Fernmanipulation von Text. 'font' ist ein relativer Pfad zu deinem Speicherordner. playlistDayStart: 'Zu welcher Zeit die Playlist starten soll; lasse es leer, wenn die Playlist immer von Anfang an starten soll.',
'text_from_filename' aktiviert die Extraktion von Text aus einem Dateinamen. Mit 'style' kannst du die Drawtext-Parameter wie Position, Farbe usw. definieren. Die Übermittlung von Text über die API überschreibt dies. Mit 'regex' kannst du Dateinamen formatieren, um einen Titel daraus zu extrahieren.`, playlistLength: 'Ziel-Länge der Playlist; wenn es leer ist, wird die reale Länge nicht berücksichtigt.',
taskHelp: `Führe ein externes Programm mit einem angegebenen Medienobjekt aus. Das Medienobjekt ist im JSON-Format und enthält alle Informationen über den aktuellen Clip. Das externe Programm kann ein Skript oder eine Binärdatei sein, aber es sollte nur für kurze Zeit ausgeführt werden.`, playlistInfinit: 'Eine einzelne Playlist-Datei endlos wiederholen.',
outputHelp: `Das endgültige Playout-Encoding, passe die Einstellungen nach deinen Bedürfnissen an. 'mode' hat die Optionen 'desktop', 'hls', 'null' und 'stream'. Verwende 'stream' und passe die 'output_param:'-Einstellungen an, wenn du an einen RTMP/RTSP/SRT/...-Server streamen möchtest. storageHelp: 'Speichereinstellungen, die Standorte sind relativ zum Kanal-Speicher.',
In der Produktion solltest du keine HLS-Playlists mit ffplayout bereitstellen; verwende Nginx oder einen anderen Webserver!`, storageFiller: 'Verwende Füllmaterial, um anstelle einer fehlenden Datei abzuspielen oder um die verbleibende Zeit zu füllen, um eine Gesamtdauer von 24 Stunden zu erreichen. Es kann eine Datei oder ein Ordner sein und wird bei Bedarf wiederholt.',
storageExtension: 'Gib an, welche Dateien gesucht und verwendet werden sollen.',
storageShuffle: 'Wähle Dateien zufällig aus (im Ordner-Modus und bei der Playlist-Erstellung).',
textHelp: 'Texteinblendung in Kombination mit libzmq für die Fernmanipulation von Text.',
textFont: 'Relativer Pfad zum Kanal-Speicher.',
textFromFile: 'Extrahiere Text aus einem Dateinamen.',
textStyle: 'Definiere die Parameter für drawtext, wie Position, Farbe usw. Das Posten von Text über die API überschreibt dies.',
textRegex: 'Formatiere Dateinamen, um einen Titel daraus zu extrahieren.',
taskHelp: 'Führe ein externes Programm mit einem gegebenen Medienobjekt aus. Das Medienobjekt ist im JSON-Format und enthält alle Informationen über den aktuellen Clip. Das externe Programm kann ein Skript oder eine Binärdatei sein, sollte aber nur für kurze Zeit laufen.',
taskPath: 'Pfad zur ausführbaren Datei.',
outputHelp: `Die endgültige Playout-Codierung, passe die Einstellungen nach deinen Bedürfnissen an. Verwende den 'stream'-Modus und passe den 'Ausgabe-Parameter' an, wenn du zu einem RTMP/RTSP/SRT/...-Server streamen möchtest. Im Produktionsbetrieb verwende kein HLS mit ffplayout; nutze Nginx oder einen anderen Webserver!`,
restartTile: 'Playout neustarten', restartTile: 'Playout neustarten',
restartText: 'ffplayout neustarten um Einstellungen anzuwenden?', restartText: 'ffplayout neustarten um Einstellungen anzuwenden?',
updatePlayoutSuccess: 'Update der Playout-Konfiguration erfolgreich!', updatePlayoutSuccess: 'Update der Playout-Konfiguration erfolgreich!',

View File

@ -189,24 +189,34 @@ export default {
logHelp: 'Adjust logging behavior.', logHelp: 'Adjust logging behavior.',
logDetect: 'Logs an error message if the audio line is silent for 15 seconds during the validation process.', 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.', 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'. processingHelp: 'Default processing for all clips ensures uniqueness.',
The 'Aspect' parameter must be a float number. processingLogoPath: 'The logo is used only if the path exists; the path is relative to the storage folder.',
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. processingLogoScale: `Leave logo scale blank if no scaling is needed. The format is 'width:height', for example: '100:-1' for proportional scaling.`,
The 'Logo' is used only if the path exists; the path is relative to your storage folder. processingLogoPosition: `Position is specified in the format 'x:y'`,
'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. processingAudioTracks: 'Specify how many audio tracks should be processed.',
'Logo Position' is specified in the format 'x:y', which sets the logo's position. processingAudioIndex: 'Which audio line to use, -1 for all.',
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. processingAudioChannels: 'Set the audio channel count, if audio has more channels than stereo.',
'Enable VTT' can only be used in HLS mode, and only when *.vtt files with the same filename as the video file exist.`, processingCustomFilter: 'Add custom filters to the processing. The filter outputs must end with [c_v_out] for video filters and [c_a_out] for audio filters.',
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. processingVTTEnable: 'VTT can only be used in HLS mode and only if there are *.vtt files with the same name as the video file.',
'Custom Filter' can be used in the same way as the one in the process section.`, processingVTTDummy: 'A placeholder is needed if there is no vtt file.',
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. 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.`,
'infinite: true' works with a single playlist file and loops it infinitely.`, ingestCustomFilter: 'Apply a custom filter to the Ingest stream in the same way as in the Processing section.',
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. playlistHelp: 'Playlist handling.',
'extensions' specifies which files to search for by this extension. Activate 'shuffle' to pick files randomly.`, playlistDayStart: 'At what time the playlist should start; leave it blank if the playlist should always start at the beginning.',
textHelp: `Overlay text in combination with libzmq for remote text manipulation. 'font' is a relative path to your storage folder. playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.',
'text_from_filename' activates the extraction of text from a filename. With 'style', you can define the drawtext parameters, such as position, color, etc. Posting text over the API will override this. With 'regex', you can format file names to extract a title from them.`, playlistInfinit: 'Loop a single playlist file infinitely.',
taskHelp: `Run an external program with a given media object. The media object is in JSON format and contains all the information about the current clip. The external program can be a script or a binary, but it should only run for a short time.`, storageHelp: 'Storage settings, locations are relative to channel storage.',
outputHelp: `The final playout encoding, set the settings according to your needs. 'mode' has the options 'desktop', 'hls', 'null', and 'stream'. Use 'stream' and adjust the 'output_param:' settings when you want to stream to an RTMP/RTSP/SRT/... server. storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder and will loop when necessary.',
storageExtension: 'Specify which files to search and use.',
storageShuffle: 'Pick files randomly (in folder mode and playlist generation).',
textHelp: 'Overlay text in combination with libzmq for remote text manipulation.',
textFont: 'Relative path to channel storage.',
textFromFile: 'Extraction of text from a filename.',
textStyle: 'Define the drawtext parameters, such as position, color, etc. Posting text over the API will override this.',
textRegex: 'Format file names to extract a title from them.',
taskHelp: 'Run an external program with a given media object. The media object is in JSON format and contains all the information about the current clip. The external program can be a script or a binary, but it should only run for a short time.',
taskPath: 'Path to executable.',
outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server.
In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`, In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`,
restartTile: 'Restart Playout', restartTile: 'Restart Playout',
restartText: 'Restart ffplayout to apply changes?', restartText: 'Restart ffplayout to apply changes?',

View File

@ -182,31 +182,41 @@ export default {
output: 'Saída', output: 'Saída',
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 de um arquivo estar corrompido, mas ainda ser reproduzível. Isso pode causar um erro de streaming para todos os arquivos seguintes. A única solução nesse caso é parar o ffplayout e reiniciá-lo.',
'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.`, stopThreshold: 'O limite para o ffplayout se ele estiver fora de sincronia acima deste valor. Um número abaixo de 3 pode causar erros inesperados.',
mailHelp: `Envie mensagens de erro para um endereço de e-mail, como clipes ausentes, formato de playlist ausente ou inválido, etc. Deixe o destinatário em branco se 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 não precisar disso.`,
'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.`, mailInterval: 'O intervalo se refere ao número de segundos até o envio de um novo e-mail; o valor deve ser em incrementos de 10 e não inferior a 30 segundos.',
logHelp: `'ffmpeg_level/ingest_level' pode ser INFO, WARNING ou ERROR. logHelp: 'Ajuste o comportamento de log.',
'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. logDetect: '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.`, logIgnore: 'Ignorar strings que contenham linhas correspondentes; o formato é uma lista separada por ponto e vírgula.',
processingHelp: `O processamento padrão para todos os clipes garante a exclusividade. O modo pode ser 'playlist' ou 'folder'. processingHelp: 'O processamento padrão para todos os clipes garante a exclusividade.',
O parâmetro 'aspect' deve ser um número de ponto flutuante. processingLogoPath: 'O logotipo só é usado se o caminho existir; o caminho é relativo à pasta de armazenamento.',
O parâmetro 'audio_tracks' especifica quantas trilhas de áudio devem ser processadas. 'audio_channels' pode ser usado se o áudio tiver mais canais que estéreo. processingLogoScale: `Deixe a escala do logotipo em branco se não for necessário escalonamento. O formato é 'largura:altura', por exemplo: '100:-1' para escalonamento proporcional.`,
O 'logo' é usado apenas se o caminho existir; o caminho é relativo à sua pasta de armazenamento. processingLogoPosition: `A posição é especificada no formato 'x:y'.`,
'logo_scale' redimensiona o logotipo para o tamanho desejado. Deixe em branco se não for necessário redimensionar. O formato é 'largura:altura', por exemplo, '100:-1' para redimensionamento proporcional. A opção 'logo_opacity' permite que o logotipo fique transparente. A 'logo_position' é especificada no formato 'x:y', que define a posição do logotipo. processingAudioTracks: 'Especifique quantas faixas de áudio devem ser processadas.',
Com 'custom_filter', é possível aplicar filtros adicionais. As saídas de filtro devem terminar com [c_v_out] para filtros de vídeo e [c_a_out] para filtros de áudio. processingAudioIndex: 'Qual linha de áudio usar, -1 para todas.',
'vtt_enable' pode ser usado no modo HLS e somente quando existirem arquivos *.vtt com o mesmo nome do arquivo de vídeo.`, processingAudioChannels: 'Defina a contagem de canais de áudio, se o áudio tiver mais canais do que estéreo.',
ingestHelp: `Execute um servidor para um fluxo de ingestão. Esse fluxo substituirá o streaming normal até que seja concluído. Existe apenas um mecanismo de autenticação muito simples, que verifica se o nome do fluxo está correto. processingCustomFilter: 'Adicione filtros personalizados ao processamento. As saídas de filtro devem terminar com [c_v_out] para filtros de vídeo e [c_a_out] para filtros de áudio.',
'custom_filter' pode ser usado da mesma forma que o da seção de processamento.`, processingVTTEnable: 'VTT só pode ser usado no modo HLS e apenas se houver arquivos *.vtt com o mesmo nome do arquivo de vídeo.',
playlistHelp: `'day_start' indica a que horas a playlist deve começar; deixe 'day_start' em branco se a playlist sempre deve começar do início. 'length' representa a duração alvo da playlist; quando está em branco, o comprimento real não será considerado. processingVTTDummy: 'Um espaço reservado é necessário se não houver arquivo vtt.',
'infinite: true' funciona com um único arquivo de playlist e o repete infinitamente.`, ingestHelp: `Execute um servidor para um fluxo de ingestão. Este fluxo substituirá o streaming normal até que termine. Há apenas um mecanismo de autenticação simples que verifica se o nome do fluxo está correto.`,
storageHelp: `'filler' é usado para tocar no lugar de um arquivo ausente ou para preencher o tempo restante para atingir um total de 24 horas. Pode ser um arquivo ou pasta e será repetido quando necessário. ingestCustomFilter: 'Aplique um filtro personalizado ao fluxo de ingestão da mesma forma que na seção de Processamento.',
'extensions' especifica quais arquivos procurar com base nessa extensão. Ative 'shuffle' para selecionar arquivos aleatoriamente.`, playlistHelp: 'Gerenciamento de playlist.',
textHelp: `Sobreponha texto em combinação com libzmq para manipulação remota de texto. 'font' é um caminho relativo à sua pasta de armazenamento. playlistDayStart: 'A que horas a playlist deve começar; deixe em branco se a playlist sempre começar do início.',
'text_from_filename' ativa a extração de texto a partir de um nome de arquivo. Com 'style', você pode definir os parâmetros do drawtext, como posição, cor, etc. Enviar texto pela API substituirá isso. Com 'regex', você pode formatar nomes de arquivos para extrair um título deles.`, playlistLength: 'Duração alvo da playlist; quando estiver em branco, o comprimento real não será considerado.',
taskHelp: `Execute um programa externo com um objeto de mídia fornecido. O objeto de mídia está no formato JSON e contém todas as informações sobre o clipe atual. O programa externo pode ser um script ou um binário, mas deve ser executado por um curto período de tempo.`, playlistInfinit: 'Reproduza infinitamente um único arquivo de playlist.',
outputHelp: `A codificação final do playout, ajuste as configurações conforme suas necessidades. 'mode' tem as opções 'desktop', 'hls', 'null' e 'stream'. Use 'stream' e ajuste as configurações 'output_param:' quando quiser transmitir para um servidor RTMP/RTSP/SRT/... . storageHelp: 'Configurações de armazenamento, os locais são relativos ao armazenamento do canal.',
Em produção, não sirva playlists HLS com ffplayout; use Nginx ou outro servidor web!`, storageFiller: 'Use preenchimento para tocar no lugar de um arquivo ausente ou para preencher o tempo restante para atingir um total de 24 horas. Pode ser um arquivo ou pasta e será repetido quando necessário.',
storageExtension: 'Especifique quais arquivos procurar e usar.',
storageShuffle: 'Escolha arquivos aleatoriamente (no modo de pasta e geração de playlist).',
textHelp: 'Sobrepor texto em combinação com libzmq para manipulação remota de texto.',
textFont: 'Caminho relativo ao armazenamento do canal.',
textFromFile: 'Extração de texto a partir de um nome de arquivo.',
textStyle: 'Defina os parâmetros drawtext, como posição, cor, etc. Postar texto pela API substituirá isso.',
textRegex: 'Formate nomes de arquivos para extrair um título deles.',
taskHelp: 'Execute um programa externo com um objeto de mídia fornecido. O objeto de mídia está em formato JSON e contém todas as informações sobre o clipe atual. O programa externo pode ser um script ou binário, mas deve ser executado apenas por um curto período de tempo.',
taskPath: 'Caminho para o executável.',
outputHelp: `A codificação final do playout, ajuste as configurações de acordo com suas necessidades. Use o modo 'stream' e ajuste o 'Parâmetro de Saída' quando quiser fazer streaming para um servidor RTMP/RTSP/SRT/... No ambiente de produção, não sirva playlists HLS com ffplayout; use Nginx ou outro servidor web!`,
restartTile: 'Reiniciar Playout', restartTile: 'Reiniciar Playout',
restartText: 'Reiniciar o ffplayout para aplicar as alterações?', restartText: 'Reiniciar o ffplayout para aplicar as alterações?',
updatePlayoutSuccess: 'Sucesso na atualização da configuração do playout!', updatePlayoutSuccess: 'Sucesso na atualização da configuração do playout!',

View File

@ -182,30 +182,41 @@ export default {
output: 'Out', output: 'Out',
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.`,
'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.`, 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: `'ffmpeg_level/ingest_level' can be INFO, WARNING, or ERROR. logHelp: 'Adjust logging behavior.',
'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process. logDetect: '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.`, 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'. processingHelp: 'Default processing for all clips ensures uniqueness.',
The 'aspect' parameter must be a float number. processingLogoPath: 'The logo is used only if the path exists; the path is relative to the storage folder.',
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. processingLogoScale: `Leave logo scale blank if no scaling is needed. The format is 'width:height', for example: '100:-1' for proportional scaling.`,
The 'logo' is used only if the path exists; the path is relative to your storage folder. processingLogoPosition: `Position is specified in the format 'x:y'`,
'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. processingAudioTracks: 'Specify how many audio tracks should be processed.',
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. processingAudioIndex: 'Which audio line to use, -1 for all.',
'vtt_enable' can only be used in HLS mode, and only when *.vtt files with the same filename as the video file exist.`, processingAudioChannels: 'Set the audio channel count, if audio has more channels than stereo.',
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. processingCustomFilter: 'Add custom filters to the processing. The filter outputs must end with [c_v_out] for video filters and [c_a_out] for audio filters.',
'custom_filter' can be used in the same way as the one in the process section.`, processingVTTEnable: 'VTT can only be used in HLS mode and only if there are *.vtt files with the same name as the video file.',
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. processingVTTDummy: 'A placeholder is needed if there is no vtt file.',
'infinite: true' works with a single playlist file and loops it infinitely.`, 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.`,
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. ingestCustomFilter: 'Apply a custom filter to the Ingest stream in the same way as in the Processing section.',
'extensions' specifies which files to search for by this extension. Activate 'shuffle' to pick files randomly.`, playlistHelp: 'Playlist handling.',
textHelp: `Overlay text in combination with libzmq for remote text manipulation. 'font' is a relative path to your storage folder. playlistDayStart: 'At what time the playlist should start; leave it blank if the playlist should always start at the beginning.',
'text_from_filename' activates the extraction of text from a filename. With 'style', you can define the drawtext parameters, such as position, color, etc. Posting text over the API will override this. With 'regex', you can format file names to extract a title from them.`, playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.',
taskHelp: `Run an external program with a given media object. The media object is in JSON format and contains all the information about the current clip. The external program can be a script or a binary, but it should only run for a short time.`, playlistInfinit: 'Loop a single playlist file infinitely.',
outputHelp: `The final playout encoding, set the settings according to your needs. 'mode' has the options 'desktop', 'hls', 'null', and 'stream'. Use 'stream' and adjust the 'output_param:' settings when you want to stream to an RTMP/RTSP/SRT/... server. storageHelp: 'Storage settings, locations are relative to channel storage.',
storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder and will loop when necessary.',
storageExtension: 'Specify which files to search and use.',
storageShuffle: 'Pick files randomly (in folder mode and playlist generation).',
textHelp: 'Overlay text in combination with libzmq for remote text manipulation.',
textFont: 'Relative path to channel storage.',
textFromFile: 'Extraction of text from a filename.',
textStyle: 'Define the drawtext parameters, such as position, color, etc. Posting text over the API will override this.',
textRegex: 'Format file names to extract a title from them.',
taskHelp: 'Run an external program with a given media object. The media object is in JSON format and contains all the information about the current clip. The external program can be a script or a binary, but it should only run for a short time.',
taskPath: 'Path to executable.',
outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server.
In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`, In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`,
restartTile: 'Перезапуск Playout', restartTile: 'Перезапуск Playout',
restartText: 'Перезапустить ffplayout для применения изменений?', restartText: 'Перезапустить ffplayout для применения изменений?',

View File

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

View File

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

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

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

View File

@ -1,4 +1,5 @@
import type { JwtPayload } from 'jwt-decode' import type { JwtPayload } from 'jwt-decode'
import type { AdvancedConfig } from '~/types/advanced_config'
import type { PlayoutConfig, Playlist as Ply } from '~/types/playout_config' import type { PlayoutConfig, Playlist as Ply } from '~/types/playout_config'
export {} export {}

View File

@ -25,7 +25,7 @@ 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 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 Storage = { filler: string, extensions: Array<string>, shuffle: boolean, shared_storage: boolean, };
export type Task = { enable: boolean, path: string, }; export type Task = { enable: boolean, path: string, };