Merge pull request #788 from jb-alvarado/master

unify init process, simplify argument, revert ad color, bug fixes
This commit is contained in:
jb-alvarado 2024-10-02 18:46:52 +02:00 committed by GitHub
commit 2b149884f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 962 additions and 876 deletions

View File

@ -51,9 +51,13 @@
"canonicalize",
"ffpengine",
"flexi",
"fontfile",
"httpauth",
"lettre",
"libc",
"maxrate",
"minrate",
"muxer",
"neli",
"nuxt",
"paris",
@ -61,6 +65,7 @@
"reqwest",
"rsplit",
"rustls",
"sqlite",
"sqlx",
"starttls",
"tokio",

67
Cargo.lock generated
View File

@ -735,9 +735,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.22"
version = "1.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0"
checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
dependencies = [
"jobserver",
"libc",
@ -793,9 +793,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.18"
version = "4.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
dependencies = [
"clap_builder",
"clap_derive",
@ -803,9 +803,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.18"
version = "4.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
dependencies = [
"anstream",
"anstyle",
@ -1518,7 +1518,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.5.0",
"indexmap 2.6.0",
"slab",
"tokio",
"tokio-util",
@ -1541,6 +1541,12 @@ dependencies = [
"allocator-api2",
]
[[package]]
name = "hashbrown"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
[[package]]
name = "hashlink"
version = "0.9.1"
@ -1648,9 +1654,9 @@ checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.9.4"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "httpdate"
@ -1902,12 +1908,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
"hashbrown 0.15.0",
"serde",
]
@ -2727,9 +2733,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags 2.6.0",
]
@ -2777,9 +2783,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "reqwest"
version = "0.12.7"
version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"base64 0.22.1",
"bytes",
@ -2925,11 +2931,10 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
version = "2.1.3"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"base64 0.22.1",
"rustls-pki-types",
]
@ -2977,9 +2982,9 @@ dependencies = [
[[package]]
name = "scc"
version = "2.1.17"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c947adb109a8afce5fc9c7bf951f87f146e9147b3a6a58413105628fb1d1e66"
checksum = "836f1e0f4963ef5288b539b643b35e043e76a32d0f4e47e67febf69576527f50"
dependencies = [
"sdd",
]
@ -3038,7 +3043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5"
dependencies = [
"form_urlencoded",
"indexmap 2.5.0",
"indexmap 2.6.0",
"itoa",
"ryu",
"serde",
@ -3098,15 +3103,15 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.9.0"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
checksum = "9720086b3357bcb44fce40117d769a4d068c70ecfa190850a980a71755f66fcc"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.5.0",
"indexmap 2.6.0",
"serde",
"serde_derive",
"serde_json",
@ -3116,9 +3121,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.9.0"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
checksum = "5f1abbfe725f27678f4663bcacb75a83e829fd464c25d78dd038a3a29e307cec"
dependencies = [
"darling",
"proc-macro2",
@ -3307,7 +3312,7 @@ dependencies = [
"hashbrown 0.14.5",
"hashlink",
"hex",
"indexmap 2.5.0",
"indexmap 2.6.0",
"log",
"memchr",
"once_cell",
@ -3853,7 +3858,7 @@ version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.5.0",
"indexmap 2.6.0",
"serde",
"serde_spanned",
"toml_datetime",
@ -3948,9 +3953,9 @@ dependencies = [
[[package]]
name = "unicode-properties"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-xid"

View File

@ -3,6 +3,8 @@ members = ["engine", "tests"]
resolver = "2"
[workspace.package]
description = "24/7 playout based on rust and ffmpeg"
readme = "README.md"
version = "0.24.0-beta5"
license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout"

View File

@ -12,7 +12,7 @@ COPY <<-EOT /run.sh
#!/bin/sh
if [ ! -f /db/ffplayout.db ]; then
ffplayout -u admin -p admin -m contact@example.com --storage "/tv-media" --playlist "/playlists" --public "/public" --log-path "/logging" --shared-storage
ffplayout -i -u admin -p admin -m contact@example.com --storage "/tv-media" --playlist "/playlists" --public "/public" --log-path "/logging" --shared
fi
/usr/bin/ffplayout -l "0.0.0.0:8787"

View File

@ -22,9 +22,6 @@ How to build the image:\
# build default
docker build -t ffplayout-image .
# build with shared storage (same storage for all channels)
docker build --build-arg SHARED_STORAGE=true .
# build from root folder, to copy local *.rpm package
docker build -f docker/Dockerfile -t ffplayout-image .

View File

@ -14,7 +14,7 @@ COPY <<-EOT /run.sh
#!/bin/sh
if [ ! -f /db/ffplayout.db ]; then
ffplayout -u admin -p admin -m contact@example.com --storage "/tv-media" --playlist "/playlists" --public "/public" --log-path "/logging" --shared-storage
ffplayout -i -u admin -p admin -m contact@example.com --storage "/tv-media" --playlist "/playlists" --public "/public" --log-path "/logging" --shared
fi
/usr/bin/ffplayout -l "0.0.0.0:8787"

View File

@ -204,7 +204,7 @@ COPY <<-EOT /run.sh
#!/bin/sh
if [ ! -f /db/ffplayout.db ]; then
ffplayout -u admin -p admin -m contact@example.com --storage "/tv-media" --playlist "/playlists" --public "/public" --log-path "/logging" --shared-storage
ffplayout -i -u admin -p admin -m contact@example.com --storage "/tv-media" --playlist "/playlists" --public "/public" --log-path "/logging" --shared
fi
/usr/bin/ffplayout -l "0.0.0.0:8787"

View File

@ -1,6 +1,6 @@
## Advanced settings
With **advanced settings** you can control all ffmpeg inputs/decoder output and filters.
With **advanced settings** you can control all ffmpeg inputs/decoder/output and filters.
> **_Note:_** Changing these settings is for advanced users only! There will be no support or guarantee that it will work and be stable after changing them!

View File

@ -1,10 +1,10 @@
## Custom filter
ffplayout allows it to define a custom filter string. For that is the parameter **custom_filter** in the **ffplayout.yml** config file under **processing**. The playlist can also contain a **custom_filter** parameter for every clip, with the same usage.
ffplayout allows the definition of a custom filter string. For this, the parameter **custom_filter** is available in the playout configuration under **processing**. The playlist can also contain a **custom_filter** parameter for each clip, with the same usage.
The filter outputs should end with `[c_v_out]` for video filter, and `[c_a_out]` for audio filter. The filters will be apply on every clip and after the filters which unify the clips.
The filter outputs should end with `[c_v_out]` for video filters and `[c_a_out]` for audio filters. The filters will be applied to every clip and after the filters that unify the clips.
It is possible to apply only video or audio filters, or both. For a better understanding here some examples:
It is possible to apply only video filters, only audio filters, or both. For a better understanding, here are some examples:
#### Apply Gaussian blur and volume filter:
@ -51,7 +51,7 @@ The **custom filter** from **config -> processing** and from **playlist** got ap
```mermaid
flowchart LR
subgraph fileloop["file loop"]
direction LR
Input --> dec
@ -84,7 +84,6 @@ custom_filter: "[v_in];movie=image_input.png:s=v,loop=loop=250.0:size=1:start=0,
And here are the explanation for each filter:
```PYTHON
# get input from video
[v_in];

View File

@ -107,13 +107,3 @@ npm run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
### Experimental Frontend Features
To use experimental frontend features, add `NUXT_BUILD_EXPERIMENTAL=true` tu run and build command, like:
```
NUXT_BUILD_EXPERIMENTAL=true npm run dev
```
**Note:** This function is only for developers and testers who can do without support.

View File

@ -1,10 +1,9 @@
### Folder Mode
ffplayout can play files from a folder, no playlists are required for this mode. This folder is monitored for changes, and when new files are added or deleted, this is registered and updated accordingly.
ffplayout can play files from a folder; no playlists are required for this mode. This folder is monitored for changes, and when new files are added or deleted, they are registered and updated accordingly.
You just have to set `mode: folder` in the config under `processing:` and under `storage:` you have to enter the correct folder and the file extensions you want to scan for.
You just need to set `mode: folder` in the config under `processing:`, and under `storage:`, you have to specify the correct folder and the file extensions you want to scan for.
Additionally there is a **shuffle** mode, if this is activated, the files will be played randomly.
Additionally, there is a **shuffle** mode. If this is activated, the files will be played randomly.
If shuffle mode is off, the clips will be played in sorted order.

View File

@ -1,12 +1,12 @@
In some situations, application closure may occur in conjunction with Live Ingest.
Here is an example, in combination with SRS:
Here is an example in combination with SRS:
When a live stream is sent, it is forwarded to ffplayout, which then switches the TV program to the live stream.
Problems now occur if the internet connection for the live stream is not stable. Then timeouts can occur, SRS breaks the connection to the playout and the whole ffplayout process has to be restarted. The default timeout is 5000ms, i.e. 5 seconds.
Problems can occur if the internet connection for the live stream is not stable. In such cases, timeouts can occur, SRS breaks the connection to the playout, and the entire ffplayout process has to be restarted. The default timeout is 5000ms, or 5 seconds.
The timeout can be heard in SRS in the respective vhosts with:
The timeout can be changed in SRS in the respective vhosts with:
```NGINX
publish {

View File

@ -1,32 +1,28 @@
### Install ffplayout
ffplayout provides ***.deb** and ***.rpm** packages, which makes it more easy to install and use, but there is still some steps to do.
**Note:** This is the official and supported way.
1. download the latest ffplayout from [release](https://github.com/ffplayout/ffplayout/releases/latest) page and place the package in the **/tmp** folder.
2. install it with `apt install /tmp/ffplayout_<VERSION>_amd64.deb`
3. install ffmpeg/ffprobe, or compile and copy it to **/usr/local/bin/**
4. activate systemd services:
- `systemctl enable ffplayout`
5. initial defaults and add global admin user:
- `sudo -u ffpu ffplayout -i`
6. start ffplayout:
- `systemctl start ffplayout`
7. use a revers proxy for SSL, Port is **8787**.
8. login with your browser, address without proxy would be: **http://[IP ADDRESS]:8787**
ffplayout provides ***.deb** and ***.rpm** packages, which makes it easier to install and use, but there are still some steps to follow.
Default location for playlists and media files are: **/var/lib/ffplayout/**.
1. Download the latest ffplayout from the [release](https://github.com/ffplayout/ffplayout/releases/latest) page and place the package in the **/tmp** folder
2. Install it with `apt install /tmp/ffplayout_<VERSION>_amd64.deb`
3. Install ffmpeg/ffprobe, or compile and copy them to **/usr/local/bin/**
4. Initialize the defaults and add a global admin user: `sudo -u ffpu ffplayout -i`
5. Use a reverse proxy for SSL; the port is **8787**
6. Log in with your browser. The address without a proxy would be: **http://[IP ADDRESS]:8787**
### Manual Install
-----
- install ffmpeg/ffprobe, or compile and copy it to **/usr/local/bin/**
- download the latest archive from [release](https://github.com/ffplayout/ffplayout/releases/latest) page
- copy the ffplayout binary to `/usr/bin/`
- copy **assets/ffplayout.yml** to `/etc/ffplayout`
- create folder `/var/log/ffplayout`
- create system user **ffpu**
- give ownership from `/etc/ffplayout` and `/var/log/ffplayout` to **ffpu**
- copy **assets/ffplayout.service** to `/etc/systemd/system`
- copy **assets/ffplayout.1.gz** to `/usr/share/man/man1/`
- copy **public** folder to `/usr/share/ffplayout/`
- activate service and run it: `systemctl enable --now ffplayout`
**Note:** This is for advanced users only.
- Install ffmpeg/ffprobe, or compile and copy them to **/usr/local/bin/**
- Download the latest archive from the [release](https://github.com/ffplayout/ffplayout/releases/latest) page
- Copy the ffplayout binary to `/usr/bin/`
- Copy **assets/ffplayout.yml** to `/etc/ffplayout`
- Create the folder `/var/log/ffplayout`
- Create the system user **ffpu**
- Give ownership of `/etc/ffplayout` and `/var/log/ffplayout` to **ffpu**
- Copy **assets/ffplayout.service** to `/etc/systemd/system`
- Copy **assets/ffplayout.1.gz** to `/usr/share/man/man1/`
- Copy the **public** folder to `/usr/share/ffplayout/`
- Activate the service and run it: `systemctl enable --now ffplayout`

View File

@ -1,8 +1,8 @@
### Live Ingest
With live ingest you have the possibility to switch from playlist, or folder mode to a live stream.
With live ingest, you have the possibility to switch from playlist or folder mode to a live stream.
It works in a way, that it create a ffmpeg instance in _listen_ (_server_) mode. For example when you stream over RTMP to it, you can set the ingest input parameters to:
It works by creating an ffmpeg instance in _listen_ (_server_) mode. For example, when streaming over RTMP, you can set the ingest input parameters to:
```
-f live_flv -listen 1 -i rtmp://0.0.0.0:1936/live/my-secrete-streaming-key
@ -14,14 +14,14 @@ For SRT you could use:
-f mpegts -i 'srt://0.0.0.0:40077?mode=listener&passphrase=12345abcde'
```
Have in mind, that the ingest mode **can't** pull from a server, it only can act as its own server and listen for income.
Keep in mind that the ingest mode **can't** pull from a server; it can only act as its own server and listen for incoming streams.
When it notice a incoming stream, it will stop the current playing and continue the live source. The output will not interrupt, so you have a continuously output stream.
When it detects an incoming stream, it will stop the currently playing content and switch to the live source. The output will not be interrupted, so you will have a continuous output stream.
In rare cases it can happen, that for a short moment after switching the image freezes, but then it will continue. Also a short frame flickering can happen.
In rare cases, it may happen that, for a short moment after switching, the image freezes, but then it will continue. Also, a brief frame flicker might occur.
You need to know, that **ffmpeg in current version has no authentication mechanism and it just listen to the protocol and port (no app and stream name).**
You should know that **ffmpeg, in its current version, has no authentication mechanism and simply listens to the protocol and port (no app and stream name).**
ffplayout catches this problem with monitoring the output from ffmpeg. When the input is **rtmp** and the app or stream name differs to the config it stops the ingest process. So in a way we have a bit control, which stream we let come in and which not.
ffplayout addresses this issue by monitoring the output from ffmpeg. When the input is **rtmp** and the app or stream name differs from the configuration, it stops the ingest process. So, in a way, we have some control over which streams are accepted and which are not.
In theory you can use every [protocol](https://ffmpeg.org/ffmpeg-protocols.html) from ffmpeg which support a **listen** mode.
In theory, you can use any [protocol](https://ffmpeg.org/ffmpeg-protocols.html) from ffmpeg that supports a **listen** mode.

View File

@ -2,15 +2,15 @@
**\* This is an experimental feature and more intended for advanced users. Use it with caution!**
With _ffplayout_ you can output streams with multiple audio tracks, with some limitations:
* Not all formats support multiple audio tracks. For example _flv/rtmp_ doesn't support it.
* In your output parameters you need to set the correct mapping.
With _ffplayout_, you can output streams with multiple audio tracks, with some limitations:
* Not all formats support multiple audio tracks. For example, _flv/rtmp_ doesn't support it.
* In your output parameters, you need to set the correct mapping.
ffmpeg filter usage and encoding parameters can become very complex, so it can happen that not every combination works out of the box.
ffmpeg filter usage and encoding parameters can become very complex, so it may happen that not every combination works out of the box.
To get e better idea of what works, you can examine [engin_cmd](../tests/src/engine_cmd.rs).
To get a better idea of what works, you can examine [engine_cmd](../tests/src/engine_cmd.rs).
If you just output a single video stream with multiple audio tracks, let's say with `srt://` protocol, you only need to set in you config under `processing:` the correct `audio_tracks:` count.
If you are outputting a single video stream with multiple audio tracks, for example with the `srt://` protocol, you only need to set the correct `audio_tracks:` count in your config under `processing:`.
For multiple video resolutions and multiple audio tracks, the parameters could look like:

View File

@ -2,11 +2,11 @@ ffplayout supports different types of outputs, let's explain them a bit:
## Stream
The streaming output can be used for ever kind of classical streaming. For example for **rtmp, srt, rtp** etc. Any streaming type supported by ffmpeg should work.
The streaming output can be used for any kind of classical streaming, such as **rtmp, srt, rtp**, etc. Any streaming type supported by ffmpeg should work.
**Remember that you need a streaming server as a destination if you want to use this mode.**
You can use for example:
For example, you can use:
- [SRS](https://github.com/ossrs/srs)
- [OvenMediaEngine](https://www.ovenmediaengine.com/ome)
@ -17,9 +17,9 @@ Of course, you can also use media platforms that support streaming input.
### Multiple Outputs:
ffplayout supports multiple outputs in a way, that it can output the same stream to multiple targets with different encoding settings.
ffplayout supports multiple outputs in such a way that it can send the same stream to multiple targets with different encoding settings.
For example you want to stream different resolutions, you could apply this output parameters:
For example, if you want to stream at different resolutions, you could apply these output parameters:
```YAML
...
@ -58,21 +58,21 @@ For example you want to stream different resolutions, you could apply this outpu
When you are using the text overlay filter, it will apply to all outputs.
The same works to for HLS output.
The same applies to HLS output.
If you want to use different resolution, you should apply them in order from biggest to smallest. Use the biggest resolution in config under `processing:` and the smaller ones in `output_params:`.
If you want to use different resolutions, you should apply them in order from largest to smallest. Use the largest resolution in the config under `processing:` and the smaller ones in `output_params:`.
## Desktop
In desktop mode you will get your picture on screen. For this you need a desktop system, theoretical all platforms should work here. ffplayout will need for that **ffplay**.
In desktop mode, you will get your picture on the screen. For this, you need a desktop system; theoretically, all platforms should work here. ffplayout will require **ffplay** for that.
## HLS
In this mode you can output directly to a hls playlist. The nice thing here is, that ffplayout need less resources then in streaming mode.
In this mode, you can output directly to an HLS playlist. The nice thing here is that ffplayout requires fewer resources than in streaming mode.
HLS output is currently the default, mostly because it works out of the box and don't need a streaming target. In default settings it saves the segments to **/usr/share/ffplayout/public/live/**.
HLS output is currently the default, mostly because it works out of the box and doesn't need a streaming target. By default, it saves the segments to **/usr/share/ffplayout/public/live/**.
**It is recommend to serve the HLS stream with nginx or another web server, and not with ffplayout (which is more meant for previewing).**
**It is recommended to serve the HLS stream with nginx or another web server, and not with ffplayout (which is more meant for previewing).**
**HLS multiple outputs example:**
@ -141,7 +141,7 @@ The tee pseudo-muxer in FFmpeg is crucial in live streaming scenarios where a si
**FFmpeg's Tee Pseudo-Muxer Parameter Configuration:**
The configuration of the tee pseudo-muxer in FFmpeg allows the broadcasting of a single input to multiple outputs simultaneously, each with specific settings. This is accomplished by specifying distinct formats and protocols for each output within a single command line, thus minimizing computational load by avoiding re-encoding for each target.
The configuration of the tee pseudo-muxer in FFmpeg allows for the broadcasting of a single input to multiple outputs simultaneously, each with specific settings. This is accomplished by specifying distinct formats and protocols for each output within a single command line, thus minimizing computational load by avoiding re-encoding for each target.
### Parameters and Syntax:
@ -160,9 +160,9 @@ The configuration of the tee pseudo-muxer in FFmpeg allows the broadcasting of a
-b:a 128k
-flags +cgop
-flags +global_header
-f tee
-f tee
[f=flv:onfail=ignore]rtmp://127.0.0.1:1935/798e3a9e-47b5-4cd5-8079-76a20e03fee6.stream|[f=mpegts:onfail=ignore]udp://127.0.0.1:1234?pkt_size=1316|[f=hls:hls_time=6:hls_list_size=600:hls_flags=append_list+delete_segments+omit_endlist:hls_segment_filename=/usr/share/ffplayout/public/live/stream-%d.ts]/usr/share/ffplayout/public/live/stream.m3u8
```
```
**1. `-f tee`**: Specifies the use of the tee pseudo-muxer, which facilitates the multiplexing of the broadcast.
@ -173,7 +173,7 @@ The configuration of the tee pseudo-muxer in FFmpeg allows the broadcasting of a
- **First Output**: `[f=flv:onfail=ignore]rtmp://127.0.0.1:1935/798e3a9e-47b5-4cd5-8079-76a20e03fee6.stream`
- **f=flv**: Sets the output format to FLV (Flash Video).
- **onfail=ignore**: Directs FFmpeg to continue operating even if this output fails.
- **Second Output**: `[f=mpegts:onfail=ignore]udp://127.0.0.1:1234?pkt_size=1316`
- **f=mpegts**: Sets the output format to MPEG-TS (MPEG Transport Stream).
- **udp://...**: Uses the UDP protocol to send the stream with a specified packet size (`pkt_size=1316`).
@ -181,4 +181,4 @@ The configuration of the tee pseudo-muxer in FFmpeg allows the broadcasting of a
- **Third Output**: `[f=hls:hls_time=6:hls_list_size=600:hls_flags=append_list+delete_segments+omit_endlist:hls_segment_filename=/usr/share/ffplayout/public/live/stream-%d.ts]/usr/share/ffplayout/public/live/stream.m3u8`
- **f=hls**: Sets the output format to HLS (HTTP Live Streaming).
Each stream is processed by the tee pseudo-muxer, which encodes the input just once, directing it to various outputs as per the specifications, thereby allowing for an efficient and less resource-intensive operation.
Each stream is processed by the tee pseudo-muxer, which encodes the input only once, directing it to various outputs as specified, thereby allowing for efficient and less resource-intensive operation.

View File

@ -1,4 +1,4 @@
## Playlist generation template
## Playlist Generation Template
It is possible to generate playlists based on templates. A template could look like:

View File

@ -1,12 +1,12 @@
### Preview Stream
When you are using the web frontend, maybe you wonder how you get a preview in the player. The default installation creates a HLS playlist and the player using this one, but most of the time the HLS mode is not used, instead the stream output mode is activated.
When you are using the web frontend, you may wonder how to get a preview in the player. The default installation creates an HLS playlist, and the player uses this, but the HLS mode is not always utilized; instead, the stream output mode is activated.
So if you stream to a external server, you have different options to get a preview stream for you player. The simplest one would be, if you get a m3u8 playlist address from your external target, like: https://example.org/live/stream.m3u8 this you can use in the configuration section from the frontend.
So if you stream to an external server, you have different options to get a preview stream for your player. The simplest option would be to obtain an m3u8 playlist address from your external target, such as: https://example.org/live/stream.m3u8. You can use this in the configuration section of the frontend.
Another option would be (which is not testet), to add a HLS output option to your streaming parameters.
Another option (which has not been tested) is to add an HLS output option to your streaming parameters.
The next option can be, that you install a rtmp server locally and create here your preview stream. In the following lines this is described in more detail.
The next option is to install an RTMP server locally and create your preview stream there. In the following lines, this is described in more detail.
The ffplayout engine has no special preview config parameters, but you can add your settings to the **output_param**, like:
@ -29,11 +29,11 @@ The ffplayout engine has no special preview config parameters, but you can add y
...
```
In this documentation we suspect, that you are using [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend) and that you using [SRS](https://github.com/ossrs/srs) at least for the preview stream. The most stable solution is previewing over HLS, but it is also possible to use [HTTP-FLV](https://github.com/ossrs/srs/wiki/v4_EN_DeliveryHttpStream) for less latency.
In this documentation, we assume that you are using [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend) and that you are using [SRS](https://github.com/ossrs/srs) at least for the preview stream. The most stable solution is previewing over HLS, but it is also possible to use [HTTP-FLV](https://github.com/ossrs/srs/wiki/v4_EN_DeliveryHttpStream) for lower latency.
To get this working we have to follow some steps.
To get this working, we need to follow some steps.
#### First step is to compile and install SRS:
#### The first step is to compile and install SRS:
```BASH
# install some tool for compiling
@ -58,7 +58,7 @@ make install
```
Now we need a systemd service, to startup SRS automatically. Create the file:
Now we need a systemd service to start SRS automatically. Create the file:
**/etc/systemd/system/srs.service**
@ -134,11 +134,11 @@ vhost __defaultVhost__ {
```
Now you can enable and start SRS with: `systemctl enable --now srs` and check if it is running: `systemctl status srs`
Now you can enable and start SRS with: `systemctl enable --now srs` and check if it is running: `systemctl status srs`.
#### Configure Nginx
We assume that you have already installed nginx and you are using it already for the frontend. So open the frontend config **/etc/nginx/sites-enabled/ffplayout.conf** and add a new location to it:
We assume that you have already installed Nginx and are using it for the frontend. Open the frontend config **/etc/nginx/sites-enabled/ffplayout.conf** and add a new location to it:
```NGINX
location /live/stream.flv {
@ -192,10 +192,10 @@ server {
}
```
Of course in production you should have a HTTPS directive to, but this step is up to you.
Of course, in production, you should have an HTTPS directive as well, but this step is up to you.
Restart Nginx.
You can (re)start ffplayout and when you setup everything correct it should run without errors.
You can (re)start ffplayout, and when you have set everything up correctly, it should run without errors.
You can go now in your frontend configuration and change the `player_url` to: `http://[domain or IP]/live/stream.flv` or `http://[domain or IP]/live/stream.m3u8`, save and reload the page. When you go now to the player tap you should see the preview video.
You can now go to your frontend configuration and change the `player_url` to: `http://[domain or IP]/live/stream.flv` or `http://[domain or IP]/live/stream.m3u8`. Save and reload the page. When you go to the player tab, you should see the preview video.

View File

@ -1,5 +1,6 @@
### Video from URL
Videos from URL are videos where you can watch directly in browser or download, for example:
Videos from a URL are videos that you can watch directly in your browser or download. For example:
```json
{
@ -10,8 +11,8 @@ Videos from URL are videos where you can watch directly in browser or download,
}
```
This should work in general, because most time it have a duration information and it is faster playable then a real live stream source. Avoid seeking because it can take to much time.
This should work in general because most of the time it has duration information and is faster to play than a real live stream source. Avoid seeking, as it can take too much time.
**Live streams as input in playlist, like rtmp is not supported.**
**Live streams as input in playlists, such as RTMP, are not supported.**
Be careful with it, better test it multiple times!
Be careful with this; it's better to test it multiple times!

View File

@ -1,10 +1,10 @@
### Stream Copy
ffplayout supports a stream copy mode since v0.20.0. A separate copy mode for video and audio is possible. This mode uses less CPU and RAM, but has some drawbacks:
ffplayout has supported a stream copy mode. A separate copy mode for video and audio is possible. This mode uses less CPU and RAM but has some drawbacks:
- All files must have exactly the same resolution, framerate, color depth, audio channels and kHz.
- All files must have exactly the same resolution, framerate, color depth, audio channels, and kHz.
- All files must use the same codecs and settings.
- The video and audio lines of a file must be the same length.
- The codecs and A/V settings must be supported by mpegts and the output destination.
- The codecs and A/V settings must be supported by MPEG-TS and the output destination.
**This mode is experimental and will not have the same stability as the stream mode.**

View File

@ -1,7 +1,7 @@
[package]
name = "ffplayout"
description = "24/7 playout based on rust and ffmpeg"
readme = "../README.md"
description.workspace = true
readme.workspace = true
version.workspace = true
license.workspace = true
authors.workspace = true
@ -135,7 +135,7 @@ assets = [
],
]
maintainer-scripts = "../debian/"
systemd-units = { enable = false, unit-scripts = "../assets" }
systemd-units = { enable = true, unit-scripts = "../assets" }
[package.metadata.deb.variants.arm64]
assets = [

View File

@ -490,9 +490,9 @@ async fn patch_channel(
if !role.has_authority(&Role::GlobalAdmin) {
let channel = handles::select_channel(&pool, &id).await?;
data.hls_path = channel.hls_path;
data.playlist_path = channel.playlist_path;
data.storage_path = channel.storage_path;
data.public = channel.public;
data.playlists = channel.playlists;
data.storage = channel.storage;
}
handles::update_channel(&pool, *id, data).await?;
@ -669,13 +669,13 @@ async fn update_playout_config(
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let p = manager.channel.lock().unwrap().storage_path.clone();
let storage_path = Path::new(&p);
let p = manager.channel.lock().unwrap().storage.clone();
let storage = Path::new(&p);
let config_id = manager.config.lock().unwrap().general.id;
let (_, _, logo) = norm_abs_path(storage_path, &data.processing.logo)?;
let (_, _, filler) = norm_abs_path(storage_path, &data.storage.filler)?;
let (_, _, font) = norm_abs_path(storage_path, &data.text.font)?;
let (_, _, logo) = norm_abs_path(storage, &data.processing.logo)?;
let (_, _, filler) = norm_abs_path(storage, &data.storage.filler)?;
let (_, _, font) = norm_abs_path(storage, &data.text.font)?;
data.processing.logo = logo;
data.storage.filler = filler;
@ -955,8 +955,10 @@ pub async fn process_control(
}
}
ProcessCtl::Start => {
manager.channel.lock().unwrap().active = true;
manager.async_start().await;
if !manager.is_alive.load(Ordering::SeqCst) {
manager.channel.lock().unwrap().active = true;
manager.async_start().await;
}
}
ProcessCtl::Stop => {
manager.channel.lock().unwrap().active = false;
@ -965,7 +967,10 @@ pub async fn process_control(
ProcessCtl::Restart => {
manager.async_stop().await;
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
manager.async_start().await;
if !manager.is_alive.load(Ordering::SeqCst) {
manager.async_start().await;
}
}
}
@ -1064,14 +1069,14 @@ pub async fn gen_playlist(
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(params.0).unwrap();
manager.config.lock().unwrap().general.generate = Some(vec![params.1.clone()]);
let storage_path = manager.config.lock().unwrap().channel.storage_path.clone();
let storage = manager.config.lock().unwrap().channel.storage.clone();
if let Some(obj) = data {
if let Some(paths) = &obj.paths {
let mut path_list = vec![];
for path in paths {
let (p, _, _) = norm_abs_path(&storage_path, path)?;
let (p, _, _) = norm_abs_path(&storage, path)?;
path_list.push(p);
}
@ -1249,8 +1254,9 @@ pub async fn remove(
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
let recursive = data.recursive;
match remove_file_or_folder(&config, &data.into_inner().source).await {
match remove_file_or_folder(&config, &data.into_inner().source, recursive).await {
Ok(obj) => Ok(web::Json(obj)),
Err(e) => Err(e),
}
@ -1306,9 +1312,9 @@ async fn get_file(
let id: i32 = req.match_info().query("id").parse()?;
let manager = controllers.lock().unwrap().get(id).unwrap();
let config = manager.config.lock().unwrap();
let storage_path = config.channel.storage_path.clone();
let storage = config.channel.storage.clone();
let file_path = req.match_info().query("filename");
let (path, _, _) = norm_abs_path(&storage_path, file_path)?;
let (path, _, _) = norm_abs_path(&storage, file_path)?;
let file = actix_files::NamedFile::open(path)?;
Ok(file
@ -1339,7 +1345,7 @@ async fn get_public(
{
let manager = controllers.lock().unwrap().get(id).unwrap();
let config = manager.config.lock().unwrap();
config.channel.hls_path.join(public)
config.channel.public.join(public)
} else {
public_path()
}

View File

@ -36,7 +36,8 @@ pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<(), Box<dyn std::error::E
}
pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> {
let query = "SELECT id, secret, logging_path, playlist_root, public_root, storage_root, shared_storage FROM global WHERE id = 1";
let query =
"SELECT id, secret, logs, playlists, public, storage, shared FROM global WHERE id = 1";
sqlx::query_as(query).fetch_one(conn).await
}
@ -45,15 +46,15 @@ pub async fn update_global(
conn: &Pool<Sqlite>,
global: GlobalSettings,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "UPDATE global SET logging_path = $2, playlist_root = $3, public_root = $4, storage_root = $5, shared_storage = $6 WHERE id = 1";
let query = "UPDATE global SET logs = $2, playlists = $3, public = $4, storage = $5, shared = $6 WHERE id = 1";
sqlx::query(query)
.bind(global.id)
.bind(global.logging_path)
.bind(global.playlist_root)
.bind(global.public_root)
.bind(global.storage_root)
.bind(global.shared_storage)
.bind(global.logs)
.bind(global.playlists)
.bind(global.public)
.bind(global.storage)
.bind(global.shared)
.execute(conn)
.await
}
@ -73,7 +74,7 @@ pub async fn select_related_channels(
) -> Result<Vec<Channel>, sqlx::Error> {
let query = match user_id {
Some(id) => format!(
"SELECT c.id, c.name, c.preview_url, c.extra_extensions, c.active, c.hls_path, c.playlist_path, c.storage_path, c.last_date, c.time_shift FROM channels c
"SELECT c.id, c.name, c.preview_url, c.extra_extensions, c.active, c.public, c.playlists, c.storage, c.last_date, c.time_shift FROM channels c
left join user_channels uc on uc.channel_id = c.id
left join user u on u.id = uc.user_id
WHERE u.id = {id} ORDER BY c.id ASC;"
@ -110,16 +111,16 @@ pub async fn update_channel(
channel: Channel,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query =
"UPDATE channels SET name = $2, preview_url = $3, extra_extensions = $4, hls_path = $5, playlist_path = $6, storage_path = $7 WHERE id = $1";
"UPDATE channels SET name = $2, preview_url = $3, extra_extensions = $4, public = $5, playlists = $6, storage = $7 WHERE id = $1";
sqlx::query(query)
.bind(id)
.bind(channel.name)
.bind(channel.preview_url)
.bind(channel.extra_extensions)
.bind(channel.hls_path)
.bind(channel.playlist_path)
.bind(channel.storage_path)
.bind(channel.public)
.bind(channel.playlists)
.bind(channel.storage)
.execute(conn)
.await
}
@ -151,14 +152,14 @@ pub async fn update_player(
}
pub async fn insert_channel(conn: &Pool<Sqlite>, channel: Channel) -> Result<Channel, sqlx::Error> {
let query = "INSERT INTO channels (name, preview_url, extra_extensions, hls_path, playlist_path, storage_path) VALUES($1, $2, $3, $4, $5, $6)";
let query = "INSERT INTO channels (name, preview_url, extra_extensions, public, playlists, storage) VALUES($1, $2, $3, $4, $5, $6)";
let result = sqlx::query(query)
.bind(channel.name)
.bind(channel.preview_url)
.bind(channel.extra_extensions)
.bind(channel.hls_path)
.bind(channel.playlist_path)
.bind(channel.storage_path)
.bind(channel.public)
.bind(channel.playlists)
.bind(channel.storage)
.execute(conn)
.await?;

View File

@ -16,11 +16,11 @@ use crate::utils::config::PlayoutConfig;
pub struct GlobalSettings {
pub id: i32,
pub secret: Option<String>,
pub logging_path: String,
pub playlist_root: String,
pub public_root: String,
pub storage_root: String,
pub shared_storage: bool,
pub logs: String,
pub playlists: String,
pub public: String,
pub storage: String,
pub shared: bool,
}
impl GlobalSettings {
@ -32,11 +32,11 @@ impl GlobalSettings {
Err(_) => GlobalSettings {
id: 0,
secret: None,
logging_path: String::new(),
playlist_root: String::new(),
public_root: String::new(),
storage_root: String::new(),
shared_storage: false,
logs: String::new(),
playlists: String::new(),
public: String::new(),
storage: String::new(),
shared: false,
},
}
}
@ -61,9 +61,9 @@ pub struct Channel {
pub preview_url: String,
pub extra_extensions: String,
pub active: bool,
pub hls_path: String,
pub playlist_path: String,
pub storage_path: String,
pub public: String,
pub playlists: String,
pub storage: String,
pub last_date: Option<String>,
pub time_shift: f64,

View File

@ -243,7 +243,7 @@ async fn main() -> std::io::Result<()> {
exit(1);
};
} else if ARGS.validate {
let mut playlist_path = config.channel.playlist_path.clone();
let mut playlist_path = config.channel.playlists.clone();
let start_sec = config.playlist.start_sec.unwrap();
let date = get_date(false, start_sec, false);

View File

@ -349,7 +349,7 @@ pub fn start_channel(manager: ChannelManager) -> Result<(), ProcessError> {
let channel_id = config.general.channel_id;
drain_hls_path(
&config.channel.hls_path,
&config.channel.public,
&config.output.output_cmd.clone().unwrap_or_default(),
channel_id,
)?;

View File

@ -29,7 +29,7 @@ pub fn watchman(
sources: Arc<Mutex<Vec<Media>>>,
) {
let id = config.general.channel_id;
let path = Path::new(&config.channel.storage_path);
let path = Path::new(&config.channel.storage);
if !path.exists() {
error!(target: Target::file_mail(), channel = id; "Folder path not exists: '{path:?}'");

View File

@ -75,7 +75,7 @@ pub fn ingest_server(
let ingest_is_running = channel_mgr.ingest_is_running.clone();
let vtt_dummy = config
.channel
.storage_path
.storage
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if let Some(ingest_input_cmd) = config.advanced.ingest.input_cmd {

View File

@ -28,7 +28,7 @@ pub fn source_generator(manager: ChannelManager) -> Box<dyn Iterator<Item = Medi
info!(target: Target::file_mail(), channel = id; "Playout in folder mode");
debug!(target: Target::file_mail(), channel = id;
"Monitor folder: <b><magenta>{:?}</></b>",
config.channel.storage_path
config.channel.storage
);
let config_clone = config.clone();

View File

@ -314,14 +314,11 @@ impl CurrentProgram {
self.current_node =
handle_list_init(&self.config, node_clone, &self.manager, last_index);
if self.current_node.source.contains(
&self
.config
.channel
.storage_path
.to_string_lossy()
.to_string(),
) || self.current_node.source.contains("color=c=#121212")
if self
.current_node
.source
.contains(&self.config.channel.storage.to_string_lossy().to_string())
|| self.current_node.source.contains("color=c=#121212")
{
is_filler = true;
}

View File

@ -65,7 +65,7 @@ fn ingest_to_hls_server(manager: ChannelManager) -> Result<(), ProcessError> {
if config.processing.vtt_enable {
let vtt_dummy = config
.channel
.storage_path
.storage
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if vtt_dummy.is_file() {

View File

@ -40,7 +40,7 @@ impl FolderSource {
path_list.push(path)
}
} else {
path_list.push(&config.channel.storage_path)
path_list.push(&config.channel.storage)
}
for path in &path_list {

View File

@ -28,13 +28,13 @@ pub fn import_file(
program: vec![],
};
let playlist_root = &config.channel.playlist_path;
let playlist_root = &config.channel.playlists;
if !playlist_root.is_dir() {
return Err(Error::new(
ErrorKind::Other,
format!(
"Playlist folder <b><magenta>{:?}</></b> not exists!",
config.channel.playlist_path,
config.channel.playlists,
),
));
}

View File

@ -100,11 +100,11 @@ pub fn read_json(
) -> JsonPlaylist {
let id = config.general.channel_id;
let config_clone = config.clone();
let mut playlist_path = config.channel.playlist_path.clone();
let mut playlist_path = config.channel.playlists.clone();
let start_sec = config.playlist.start_sec.unwrap();
let date = get_date(seek, start_sec, get_next);
if playlist_path.is_dir() || is_remote(&config.channel.playlist_path.to_string_lossy()) {
if playlist_path.is_dir() || is_remote(&config.channel.playlists.to_string_lossy()) {
let d: Vec<&str> = date.split('-').collect();
playlist_path = playlist_path
.join(d[0])

View File

@ -69,7 +69,7 @@ pub fn prepare_output_cmd(
let re_v = Regex::new(r"\[?0:v(:0)?\]?").unwrap();
let vtt_dummy = config
.channel
.storage_path
.storage
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if let Some(mut filter) = filters.clone() {
@ -622,7 +622,7 @@ pub fn loop_image(config: &PlayoutConfig, node: &Media) -> Vec<String> {
let vtt_file = Path::new(&node.source).with_extension("vtt");
let vtt_dummy = config
.channel
.storage_path
.storage
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if node.seek > 0.5 {
@ -663,7 +663,7 @@ pub fn loop_filler(config: &PlayoutConfig, node: &Media) -> Vec<String> {
let vtt_file = Path::new(&node.source).with_extension("vtt");
let vtt_dummy = config
.channel
.storage_path
.storage
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if vtt_file.is_file() {
@ -737,7 +737,7 @@ pub fn seek_and_length(config: &PlayoutConfig, node: &mut Media) -> Vec<String>
let vtt_file = Path::new(&node.source).with_extension("vtt");
let vtt_dummy = config
.channel
.storage_path
.storage
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if node.seek > 0.5 {
@ -788,7 +788,7 @@ pub fn gen_dummy(config: &PlayoutConfig, duration: f64) -> (String, Vec<String>)
if config.processing.vtt_enable {
let vtt_dummy = config
.channel
.storage_path
.storage
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if vtt_dummy.is_file() {

View File

@ -35,7 +35,9 @@ Stream dynamic playlists or folder contents with the power of ffmpeg.
The target can be an HLS playlist, rtmp/srt/udp server, desktop player
or any other output supported by ffmpeg.\n
ffplayout also provides a web frontend and API to control streaming,
manage config, files, text overlay, etc. "))]
manage config, files, text overlay, etc."),
next_line_help = false,
)]
pub struct Args {
#[clap(
short,
@ -45,9 +47,6 @@ pub struct Args {
)]
pub init: bool,
#[clap(short, long, help_heading = Some("Initial Setup"), help = "Add a global admin user")]
pub add: bool,
#[clap(short, long, help_heading = Some("Initial Setup"), help = "Create admin user")]
pub username: Option<String>,
@ -66,16 +65,19 @@ pub struct Args {
help_heading = Some("Initial Setup"),
help = "Share storage across channels, important for running in Containers"
)]
pub shared_storage: bool,
pub shared: bool,
#[clap(long, env, help_heading = Some("Initial Setup / General"), help = "Logging path")]
pub log_path: Option<PathBuf>,
pub logs: Option<String>,
#[clap(long, env, help_heading = Some("Initial Setup / General"), help = "Path to public files, also HLS playlists")]
pub public: Option<String>,
#[clap(long, help_heading = Some("Initial Setup / Playlist"), help = "Path to playlist, or playlist root folder.")]
pub playlist: Option<String>,
pub playlists: Option<String>,
#[clap(short, long, help_heading = Some("General"), help = "Add a global admin")]
pub add: bool,
#[clap(long, env, help_heading = Some("General"), help = "Path to database file")]
pub db: Option<PathBuf>,
@ -129,7 +131,7 @@ pub struct Args {
long,
env,
help_heading = Some("General / Playout"),
help = "Channels by ids to process (for export config, foreground running, etc.)",
help = "Channels by ids to process (for export config, generate playlist, foreground running, etc.)",
num_args = 1..,
)]
pub channels: Option<Vec<i32>>,
@ -184,29 +186,35 @@ fn global_user(args: &mut Args) {
let mut user = String::new();
let mut mail = String::new();
print!("Global admin: ");
stdout().flush().unwrap();
if args.username.is_none() {
print!("Global admin: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut user)
.expect("Did not enter a correct name?");
stdin()
.read_line(&mut user)
.expect("Did not enter a correct name?");
args.username = Some(user.trim().to_string());
args.username = Some(user.trim().to_string());
}
print!("Password: ");
stdout().flush().unwrap();
let password = read_password();
if args.password.is_none() {
print!("Password: ");
stdout().flush().unwrap();
let password = read_password();
args.password = password.ok();
args.password = password.ok();
}
print!("Mail: ");
stdout().flush().unwrap();
if args.mail.is_none() {
print!("Mail: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut mail)
.expect("Did not enter a correct name?");
stdin()
.read_line(&mut mail)
.expect("Did not enter a correct name?");
args.mail = Some(mail.trim().to_string());
args.mail = Some(mail.trim().to_string());
}
}
pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
@ -230,94 +238,114 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
let mut storage = String::new();
let mut playlist = String::new();
let mut logging = String::new();
let mut hls = String::new();
let mut public = String::new();
let mut shared_store = String::new();
let mut global = GlobalSettings {
id: 0,
secret: None,
logging_path: String::new(),
playlist_root: String::new(),
public_root: String::new(),
storage_root: String::new(),
shared_storage: false,
logs: String::new(),
playlists: String::new(),
public: String::new(),
storage: String::new(),
shared: false,
};
if check_user.unwrap_or_default().is_empty() {
global_user(&mut args);
}
print!("Storage path [/var/lib/ffplayout/tv-media]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut storage)
.expect("Did not enter a correct path?");
if storage.trim().is_empty() {
global.storage_root = "/var/lib/ffplayout/tv-media".to_string();
if let Some(st) = args.storage {
global.storage = st;
} else {
global.storage_root = storage
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string();
print!("Storage path [/var/lib/ffplayout/tv-media]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut storage)
.expect("Did not enter a correct path?");
global.storage = if storage.trim().is_empty() {
"/var/lib/ffplayout/tv-media".to_string()
} else {
storage
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string()
};
}
print!("Playlist path [/var/lib/ffplayout/playlists]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut playlist)
.expect("Did not enter a correct path?");
if playlist.trim().is_empty() {
global.playlist_root = "/var/lib/ffplayout/playlists".to_string();
if let Some(pl) = args.playlists {
global.playlists = pl
} else {
global.playlist_root = playlist
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string();
print!("Playlist path [/var/lib/ffplayout/playlists]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut playlist)
.expect("Did not enter a correct path?");
global.playlists = if playlist.trim().is_empty() {
"/var/lib/ffplayout/playlists".to_string()
} else {
playlist
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string()
};
}
print!("Logging path [/var/log/ffplayout]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut logging)
.expect("Did not enter a correct path?");
if logging.trim().is_empty() {
global.logging_path = "/var/log/ffplayout".to_string();
if let Some(lp) = args.logs {
global.logs = lp;
} else {
global.logging_path = logging
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string();
print!("Logging path [/var/log/ffplayout]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut logging)
.expect("Did not enter a correct path?");
global.logs = if logging.trim().is_empty() {
"/var/log/ffplayout".to_string()
} else {
logging
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string()
}
}
print!("Public (HLS) path [/usr/share/ffplayout/public]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut hls)
.expect("Did not enter a correct path?");
if hls.trim().is_empty() {
global.public_root = "/usr/share/ffplayout/public".to_string();
if let Some(p) = args.public {
global.public = p;
} else {
global.public_root = hls
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string();
print!("Public (HLS) path [/usr/share/ffplayout/public]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut public)
.expect("Did not enter a correct path?");
global.public = if public.trim().is_empty() {
"/usr/share/ffplayout/public".to_string()
} else {
public
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string()
};
}
print!("Shared storage [Y/n]: ");
stdout().flush().unwrap();
if args.shared {
global.shared = true;
} else {
print!("Shared storage [Y/n]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut shared_store)
.expect("Did not enter a yes or no?");
stdin()
.read_line(&mut shared_store)
.expect("Did not enter a yes or no?");
global.shared_storage = shared_store.trim().to_lowercase().starts_with('y');
global.shared = shared_store.trim().to_lowercase().starts_with('y');
}
if let Err(e) = handles::update_global(pool, global.clone()).await {
eprintln!("{e}");
@ -325,25 +353,24 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
};
let mut channel = handles::select_channel(pool, &1).await.unwrap();
channel.hls_path = global.public_root;
channel.playlist_path = global.playlist_root;
channel.storage_path = global.storage_root;
channel.public = global.public;
channel.playlists = global.playlists;
channel.storage = global.storage;
let mut storage_path = PathBuf::from(channel.storage_path.clone());
let mut storage_path = PathBuf::from(channel.storage.clone());
if global.shared_storage {
if global.shared {
storage_path = storage_path.join("1");
channel.preview_url = "http://127.0.0.1:8787/1/stream.m3u8".to_string();
channel.hls_path = Path::new(&channel.hls_path)
channel.public = Path::new(&channel.public)
.join("1")
.to_string_lossy()
.to_string();
channel.playlist_path = Path::new(&channel.playlist_path)
channel.playlists = Path::new(&channel.playlists)
.join("1")
.to_string_lossy()
.to_string();
channel.storage_path = storage_path.to_string_lossy().to_string();
channel.storage = storage_path.to_string_lossy().to_string();
};
if let Err(e) = copy_assets(&storage_path).await {
@ -358,20 +385,13 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
}
println!("\nSet global settings done...");
}
if args.add {
} else if args.add {
global_user(&mut args);
}
if let Some(username) = args.username {
error_code = 0;
if args.mail.is_none() || args.password.is_none() {
eprintln!("Mail/password missing!");
error_code = 1;
}
let chl: Vec<i32> = channels.clone().iter().map(|c| c.id).collect();
let ff_user = User {
@ -392,73 +412,6 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
println!("Create global admin user \"{username}\" done...");
}
if !args.init
&& args.storage.is_some()
&& args.playlist.is_some()
&& args.public.is_some()
&& args.log_path.is_some()
{
error_code = 0;
let global = GlobalSettings {
id: 0,
secret: None,
logging_path: args.log_path.unwrap().to_string_lossy().to_string(),
playlist_root: args.playlist.unwrap(),
public_root: args.public.unwrap(),
storage_root: args.storage.unwrap(),
shared_storage: args.shared_storage,
};
let mut channel = handles::select_channel(pool, &1)
.await
.expect("Select Channel 1");
let mut storage_path = PathBuf::from(global.storage_root.clone());
if args.shared_storage {
storage_path = storage_path.join("1");
channel.hls_path = Path::new(&global.public_root)
.join("1")
.to_string_lossy()
.to_string();
channel.playlist_path = Path::new(&global.playlist_root)
.join("1")
.to_string_lossy()
.to_string();
channel.storage_path = storage_path.to_string_lossy().to_string();
} else {
channel.hls_path = global.public_root.clone();
channel.playlist_path = global.playlist_root.clone();
channel.storage_path = global.storage_root.clone();
}
if let Err(e) = copy_assets(&storage_path).await {
eprintln!("{e}");
};
match handles::update_global(pool, global.clone()).await {
Ok(_) => println!("Update globals done..."),
Err(e) => {
eprintln!("{e}");
error_code = 1;
}
};
match handles::update_channel(pool, 1, channel).await {
Ok(_) => println!("Update channel done..."),
Err(e) => {
eprintln!("{e}");
error_code = 1;
}
};
#[cfg(target_family = "unix")]
{
update_permissions().await;
}
}
if ARGS.list_channels {
let chl = channels
.iter()

View File

@ -35,7 +35,7 @@ pub async fn create_channel(
target_channel: Channel,
) -> Result<Channel, ServiceError> {
let channel = handles::insert_channel(conn, target_channel).await?;
let storage_path = PathBuf::from(channel.storage_path.clone());
let storage_path = PathBuf::from(channel.storage.clone());
handles::new_channel_presets(conn, channel.id).await?;

View File

@ -6,6 +6,7 @@ use std::{
use chrono::NaiveTime;
use flexi_logger::Level;
use regex::Regex;
use serde::{Deserialize, Serialize};
use shlex::split;
use sqlx::{Pool, Sqlite};
@ -176,21 +177,21 @@ pub struct PlayoutConfig {
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Channel {
pub logging_path: PathBuf,
pub hls_path: PathBuf,
pub playlist_path: PathBuf,
pub storage_path: PathBuf,
pub shared_storage: bool,
pub logs: PathBuf,
pub public: PathBuf,
pub playlists: PathBuf,
pub storage: PathBuf,
pub shared: bool,
}
impl Channel {
pub fn new(config: &models::GlobalSettings, channel: models::Channel) -> Self {
Self {
logging_path: PathBuf::from(config.logging_path.clone()),
hls_path: PathBuf::from(channel.hls_path.clone()),
playlist_path: PathBuf::from(channel.playlist_path.clone()),
storage_path: PathBuf::from(channel.storage_path.clone()),
shared_storage: config.shared_storage,
logs: PathBuf::from(config.logs.clone()),
public: PathBuf::from(channel.public.clone()),
playlists: PathBuf::from(channel.playlists.clone()),
storage: PathBuf::from(channel.storage.clone()),
shared: config.shared,
}
}
}
@ -588,27 +589,23 @@ impl PlayoutConfig {
let task = Task::new(&config);
let mut output = Output::new(&config);
if !channel.storage_path.is_dir() {
tokio::fs::create_dir_all(&channel.storage_path)
if !channel.storage.is_dir() {
tokio::fs::create_dir_all(&channel.storage)
.await
.unwrap_or_else(|_| {
panic!("Can't create storage folder: {:#?}", channel.storage_path)
});
.unwrap_or_else(|_| panic!("Can't create storage folder: {:#?}", channel.storage));
}
let mut storage =
Storage::new(&config, channel.storage_path.clone(), global.shared_storage);
let mut storage = Storage::new(&config, channel.storage.clone(), global.shared);
if !channel.playlist_path.is_dir() {
tokio::fs::create_dir_all(&channel.playlist_path).await?;
if !channel.playlists.is_dir() {
tokio::fs::create_dir_all(&channel.playlists).await?;
}
if !channel.logging_path.is_dir() {
tokio::fs::create_dir_all(&channel.logging_path).await?;
if !channel.logs.is_dir() {
tokio::fs::create_dir_all(&channel.logs).await?;
}
let (filler_path, _, filler) =
norm_abs_path(&channel.storage_path, &config.storage_filler)?;
let (filler_path, _, filler) = norm_abs_path(&channel.storage, &config.storage_filler)?;
storage.filler = filler;
storage.filler_path = filler_path;
@ -621,7 +618,7 @@ impl PlayoutConfig {
playlist.length_sec = Some(86400.0);
}
let (logo_path, _, logo) = norm_abs_path(&channel.storage_path, &processing.logo)?;
let (logo_path, _, logo) = norm_abs_path(&channel.storage, &processing.logo)?;
if processing.add_logo && !logo_path.is_file() {
processing.add_logo = false;
@ -707,15 +704,57 @@ impl PlayoutConfig {
cmd.remove(i);
}
let is_tee_muxer = cmd.contains(&"tee".to_string());
for item in cmd.iter_mut() {
if item.ends_with(".ts") || (item.ends_with(".m3u8") && item != "master.m3u8") {
if let Ok((hls_path, _, _)) = norm_abs_path(&channel.hls_path, item) {
let parent = hls_path.parent().ok_or("HLS parent path")?;
if is_tee_muxer {
// Processes the `item` string to replace `.ts` and `.m3u8` filenames with their absolute paths.
// Ensures that the corresponding directories exist.
//
// - Uses regular expressions to identify `.ts` and `.m3u8` filenames within the `item` string.
// - For each identified filename, normalizes its path and checks if the parent directory exists.
// - Creates the parent directory if it does not exist.
// - Replaces the original filename in the `item` string with the normalized absolute path.
let re_ts = Regex::new(r"filename=(\S+?\.ts)").unwrap();
let re_m3 = Regex::new(r"\](\S+?\.m3u8)").unwrap();
for s in item.clone().split('|') {
if let Some(ts) = re_ts.captures(s).and_then(|p| p.get(1)) {
let (segment_path, _, _) =
norm_abs_path(&channel.public, ts.as_str())?;
let parent = segment_path.parent().ok_or("HLS parent path")?;
if !parent.is_dir() {
fs::create_dir_all(parent).await?;
}
item.clone_from(
&item.replace(ts.as_str(), &segment_path.to_string_lossy()),
);
}
if let Some(m3) = re_m3.captures(s).and_then(|p| p.get(1)) {
let (m3u8_path, _, _) =
norm_abs_path(&channel.public, m3.as_str())?;
let parent = m3u8_path.parent().ok_or("HLS parent path")?;
if !parent.is_dir() {
fs::create_dir_all(parent).await?;
}
item.clone_from(
&item.replace(m3.as_str(), &m3u8_path.to_string_lossy()),
);
}
}
} else if let Ok((public, _, _)) = norm_abs_path(&channel.public, item) {
let parent = public.parent().ok_or("HLS parent path")?;
if !parent.is_dir() {
fs::create_dir_all(parent).await?;
}
item.clone_from(&hls_path.to_string_lossy().to_string());
item.clone_from(&public.to_string_lossy().to_string());
};
}
}
@ -736,7 +775,7 @@ impl PlayoutConfig {
text.node_pos = None;
}
let (font_path, _, font) = norm_abs_path(&channel.storage_path, &text.font)?;
let (font_path, _, font) = norm_abs_path(&channel.storage, &text.font)?;
text.font = font;
text.font_path = font_path.to_string_lossy().to_string();
@ -853,12 +892,12 @@ pub async fn get_config(
config.storage.paths = paths;
}
if let Some(playlist) = args.playlist {
config.channel.playlist_path = PathBuf::from(&playlist);
if let Some(playlist) = args.playlists {
config.channel.playlists = PathBuf::from(&playlist);
}
if let Some(folder) = args.folder {
config.channel.storage_path = folder;
config.channel.storage = folder;
config.processing.mode = ProcessMode::Folder;
}
@ -877,10 +916,10 @@ pub async fn get_config(
}
}
if args.shared_storage {
// config.channel.shared_storage could be true already,
// so should not be overridden with false when args.shared_storage is not set
config.channel.shared_storage = args.shared_storage
if args.shared {
// config.channel.shared could be true already,
// so should not be overridden with false when args.shared is not set
config.channel.shared = args.shared
}
if let Some(volume) = args.volume {

View File

@ -27,6 +27,8 @@ pub struct PathObject {
files: Option<Vec<VideoFile>>,
#[serde(default)]
pub folders_only: bool,
#[serde(default)]
pub recursive: bool,
}
impl PathObject {
@ -38,6 +40,7 @@ impl PathObject {
folders: Some(vec![]),
files: Some(vec![]),
folders_only: false,
recursive: false,
}
}
}
@ -115,13 +118,12 @@ pub async fn browser(
let mut extensions = config.storage.extensions.clone();
extensions.append(&mut channel_extensions);
let (path, parent, path_component) =
norm_abs_path(&config.channel.storage_path, &path_obj.source)?;
let (path, parent, path_component) = norm_abs_path(&config.channel.storage, &path_obj.source)?;
let parent_path = if !path_component.is_empty() {
path.parent().unwrap()
} else {
&config.channel.storage_path
&config.channel.storage
};
let mut obj = PathObject::new(path_component, Some(parent));
@ -212,7 +214,7 @@ pub async fn create_directory(
config: &PlayoutConfig,
path_obj: &PathObject,
) -> Result<HttpResponse, ServiceError> {
let (path, _, _) = norm_abs_path(&config.channel.storage_path, &path_obj.source)?;
let (path, _, _) = norm_abs_path(&config.channel.storage, &path_obj.source)?;
if let Err(e) = fs::create_dir_all(&path).await {
return Err(ServiceError::BadRequest(e.to_string()));
@ -281,8 +283,8 @@ pub async fn rename_file(
config: &PlayoutConfig,
move_object: &MoveObject,
) -> Result<MoveObject, ServiceError> {
let (source_path, _, _) = norm_abs_path(&config.channel.storage_path, &move_object.source)?;
let (mut target_path, _, _) = norm_abs_path(&config.channel.storage_path, &move_object.target)?;
let (source_path, _, _) = norm_abs_path(&config.channel.storage, &move_object.source)?;
let (mut target_path, _, _) = norm_abs_path(&config.channel.storage, &move_object.target)?;
if !source_path.exists() {
return Err(ServiceError::BadRequest("Source file not exist!".into()));
@ -313,23 +315,36 @@ pub async fn rename_file(
pub async fn remove_file_or_folder(
config: &PlayoutConfig,
source_path: &str,
recursive: bool,
) -> Result<(), ServiceError> {
let (source, _, _) = norm_abs_path(&config.channel.storage_path, source_path)?;
let (source, _, _) = norm_abs_path(&config.channel.storage, source_path)?;
if !source.exists() {
return Err(ServiceError::BadRequest("Source does not exists!".into()));
}
if source.is_dir() {
match fs::remove_dir(source).await {
Ok(_) => return Ok(()),
Err(e) => {
error!("{e}");
return Err(ServiceError::BadRequest(
"Delete folder failed! (Folder must be empty)".into(),
));
}
};
if recursive {
match fs::remove_dir_all(source).await {
Ok(_) => return Ok(()),
Err(e) => {
error!("{e}");
return Err(ServiceError::BadRequest(
"Delete folder and its content failed!".into(),
));
}
};
} else {
match fs::remove_dir(source).await {
Ok(_) => return Ok(()),
Err(e) => {
error!("{e}");
return Err(ServiceError::BadRequest(
"Delete folder failed! (Folder must be empty)".into(),
));
}
};
}
}
if source.is_file() {
@ -346,7 +361,7 @@ pub async fn remove_file_or_folder(
}
async fn valid_path(config: &PlayoutConfig, path: &str) -> Result<PathBuf, ServiceError> {
let (test_path, _, _) = norm_abs_path(&config.channel.storage_path, path)?;
let (test_path, _, _) = norm_abs_path(&config.channel.storage, path)?;
if !test_path.is_dir() {
return Err(ServiceError::BadRequest("Target folder not exists!".into()));

View File

@ -206,7 +206,7 @@ pub fn playlist_generator(manager: &ChannelManager) -> Result<Vec<JsonPlaylist>,
}
}
};
let playlist_root = &config.channel.playlist_path;
let playlist_root = &config.channel.playlists;
let mut playlists = vec![];
let mut date_range = vec![];
let mut from_template = false;
@ -215,7 +215,7 @@ pub fn playlist_generator(manager: &ChannelManager) -> Result<Vec<JsonPlaylist>,
error!(
target: Target::all(), channel = id;
"Playlist folder <b><magenta>{:?}</></b> not exists!",
config.channel.playlist_path
config.channel.playlists
);
}

View File

@ -299,11 +299,7 @@ fn file_formatter(
pub fn log_file_path() -> PathBuf {
let config = GlobalSettings::global();
let mut log_path = ARGS
.log_path
.clone()
.unwrap_or(PathBuf::from(&config.logging_path));
let mut log_path = PathBuf::from(&ARGS.logs.as_ref().unwrap_or(&config.logs));
if !log_path.is_absolute() {
log_path = env::current_dir().unwrap().join(log_path);

View File

@ -203,7 +203,7 @@ pub fn public_path() -> PathBuf {
let dev_path = env::current_dir()
.unwrap_or_default()
.join("frontend/.output/public/");
let mut public_path = PathBuf::from(&config.public_root);
let mut public_path = PathBuf::from(&config.public);
if let Some(p) = &ARGS.public {
// When public path is set as argument use this path for serving static files.

View File

@ -14,7 +14,7 @@ pub async fn read_playlist(
date: String,
) -> Result<JsonPlaylist, ServiceError> {
let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = config.channel.playlist_path.clone();
let mut playlist_path = config.channel.playlists.clone();
playlist_path = playlist_path
.join(d[0])
@ -34,7 +34,7 @@ pub async fn write_playlist(
) -> Result<String, ServiceError> {
let date = json_data.date.clone();
let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = config.channel.playlist_path.clone();
let mut playlist_path = config.channel.playlists.clone();
if !playlist_path
.extension()
@ -93,7 +93,7 @@ pub fn generate_playlist(manager: ChannelManager) -> Result<JsonPlaylist, Servic
for path in &source.paths {
let (safe_path, _, _) =
norm_abs_path(&config.channel.storage_path, &path.to_string_lossy())?;
norm_abs_path(&config.channel.storage, &path.to_string_lossy())?;
paths.push(safe_path);
}
@ -124,7 +124,7 @@ pub fn generate_playlist(manager: ChannelManager) -> Result<JsonPlaylist, Servic
pub async fn delete_playlist(config: &PlayoutConfig, date: &str) -> Result<String, ServiceError> {
let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = PathBuf::from(&config.channel.playlist_path);
let mut playlist_path = PathBuf::from(&config.channel.playlists);
playlist_path = playlist_path
.join(d[0])

View File

@ -118,7 +118,7 @@ pub fn stat(config: PlayoutConfig) -> SystemStat {
for disk in &*disks {
if disk.mount_point().to_string_lossy().len() > 1
&& config.channel.storage_path.starts_with(disk.mount_point())
&& config.channel.storage.starts_with(disk.mount_point())
{
storage.path = disk.name().to_string_lossy().to_string();
storage.total = disk.total_space();

View File

@ -72,7 +72,7 @@ async function onSubmitAdvanced() {
if (update.status === 200) {
indexStore.msgAlert('success', t('advanced.updateSuccess'), 2)
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
await $fetch(`/api/control/${channel}/process/`, {
method: 'POST',
@ -90,7 +90,7 @@ async function onSubmitAdvanced() {
async function restart(res: boolean) {
if (res) {
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
await $fetch(`/api/control/${channel}/process/`, {
method: 'POST',

View File

@ -1,6 +1,6 @@
<template>
<div v-if="configStore.channels && configStore.channels[configStore.id]" class="w-full max-w-[800px]">
<h2 class="pt-3 text-3xl">{{ t('config.channelConf') }} ({{ configStore.channels[configStore.id].id }})</h2>
<div v-if="channel" class="w-full max-w-[800px]">
<h2 class="pt-3 text-3xl">{{ t('config.channelConf') }} ({{ channel.id }})</h2>
<div class="w-full flex justify-end my-4">
<button v-if="authStore.role === 'GlobalAdmin'" class="btn btn-sm btn-primary" @click="newChannel()">
{{ t('config.addChannel') }}
@ -12,10 +12,11 @@
<span class="label-text">{{ t('config.name') }}</span>
</div>
<input
v-model="configStore.channels[configStore.id].name"
v-model="channel.name"
type="text"
placeholder="Type here"
class="input input-bordered w-full"
@keyup="isChanged"
/>
</label>
@ -24,9 +25,10 @@
<span class="label-text">{{ t('config.previewUrl') }}</span>
</div>
<input
v-model="configStore.channels[configStore.id].preview_url"
v-model="channel.preview_url"
type="text"
class="input input-bordered w-full"
@keyup="isChanged"
/>
</label>
@ -35,9 +37,10 @@
<span class="label-text">{{ t('config.extensions') }}</span>
</div>
<input
v-model="configStore.channels[configStore.id].extra_extensions"
v-model="channel.extra_extensions"
type="text"
class="input input-bordered w-full"
@keyup="isChanged"
/>
</label>
@ -50,12 +53,13 @@
</div>
<label class="form-control w-full mt-3">
<div class="label">
<span class="label-text">{{ t('config.hlsPath') }}</span>
<span class="label-text">{{ t('config.publicPath') }}</span>
</div>
<input
v-model="configStore.channels[configStore.id].hls_path"
v-model="channel.public"
type="text"
class="input input-bordered w-full"
@keyup="isChanged"
/>
</label>
@ -64,9 +68,10 @@
<span class="label-text">{{ t('config.playlistPath') }}</span>
</div>
<input
v-model="configStore.channels[configStore.id].playlist_path"
v-model="channel.playlists"
type="text"
class="input input-bordered w-full"
@keyup="isChanged"
/>
</label>
@ -75,94 +80,168 @@
<span class="label-text">{{ t('config.storagePath') }}</span>
</div>
<input
v-model="configStore.channels[configStore.id].storage_path"
v-model="channel.storage"
type="text"
class="input input-bordered w-full"
@keyup="isChanged"
/>
</label>
</template>
<div class="join my-4">
<button class="join-item btn btn-primary" @click="addUpdateChannel()">
<div class="my-4 flex gap-1">
<button class="btn" :class="saved ? 'btn-primary' : 'btn-error'" @click="addUpdateChannel()">
{{ t('config.save') }}
</button>
<button
v-if="
authStore.role === 'GlobalAdmin' &&
configStore.channels.length > 1 &&
configStore.channels[configStore.id].id > 1
authStore.role === 'GlobalAdmin' && configStore.channels.length > 1 && channel.id > 1 && saved
"
class="join-item btn btn-primary"
class="btn btn-primary"
@click="deleteChannel()"
>
{{ t('config.delete') }}
</button>
<button v-if="!saved" class="btn btn-primary text-xl" @click="resetChannel()">
<i class="bi-arrow-repeat" />
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { $_ } = useNuxtApp()
import { cloneDeep, isEqual } from 'lodash-es'
const { t } = useI18n()
const authStore = useAuth()
const configStore = useConfig()
const indexStore = useIndex()
const { i } = storeToRefs(useConfig())
const saved = ref(true)
const channel = ref({} as Channel)
const channelOrig = ref({} as Channel)
onMounted(() => {
channel.value = cloneDeep(configStore.channels[i.value])
channelOrig.value = cloneDeep(configStore.channels[i.value])
})
watch([i], () => {
if (configStore.channels[i.value]) {
channel.value = cloneDeep(configStore.channels[i.value])
}
})
function isChanged() {
if (isEqual(channel.value, channelOrig.value)) {
saved.value = true
} else {
saved.value = false
}
}
function rmId(path: string) {
return path.replace(/\/\d+$/, '')
}
function newChannel() {
const channels = $_.cloneDeep(configStore.channels)
const newChannel = $_.cloneDeep(configStore.channels[configStore.channels.length - 1])
channel.value.id = configStore.channels.length + 1
channel.value.name = `Channel ${channel.value.id}`
channel.value.preview_url = `${window.location.protocol}//${window.location.host}/${channel.value.id}/live/stream.m3u8`
channel.value.public = `${rmId(channel.value.public)}/${channel.value.id}`
channel.value.playlists = `${rmId(channel.value.playlists)}/${channel.value.id}`
channel.value.storage = `${rmId(channel.value.storage)}/${channel.value.id}`
newChannel.id = channels.length + 1
newChannel.name = `Channel ${newChannel.id}`
newChannel.preview_url = `${window.location.protocol}//${window.location.host}/${newChannel.id}/live/stream.m3u8`
newChannel.hls_path = `${rmId(newChannel.hls_path)}/${newChannel.id}`
newChannel.playlist_path = `${rmId(newChannel.playlist_path)}/${newChannel.id}`
newChannel.storage_path = `${rmId(newChannel.storage_path)}/${newChannel.id}`
saved.value = false
}
channels.push(newChannel)
configStore.channels = channels
configStore.id = configStore.channels.length - 1
async function addNewChannel() {
await $fetch('/api/channel/', {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify(channel.value),
})
.then((chl) => {
i.value = channel.value.id - 1
configStore.channels.push(cloneDeep(chl))
configStore.channelsRaw.push(chl)
configStore.configCount = configStore.channels.length
indexStore.msgAlert('success', t('config.updateChannelSuccess'), 2)
})
.catch(() => {
indexStore.msgAlert('error', t('config.updateChannelFailed'), 3)
})
}
async function updateChannel() {
await fetch(`/api/channel/${channel.value.id}`, {
method: 'PATCH',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify(channel.value),
})
.then(() => {
for (let i = 0; i < configStore.channels.length; i++) {
if (configStore.channels[i].id === channel.value.id) {
configStore.channels[i] = cloneDeep(channel.value)
break
}
}
for (let i = 0; i < configStore.channelsRaw.length; i++) {
if (configStore.channelsRaw[i].id === channel.value.id) {
configStore.channelsRaw[i] = cloneDeep(channel.value)
break
}
}
indexStore.msgAlert('success', t('config.updateChannelSuccess'), 2)
})
.catch(() => {
indexStore.msgAlert('error', t('config.updateChannelFailed'), 3)
})
}
async function addUpdateChannel() {
/*
Save channel settings.
Save or update channel settings.
*/
const update = await configStore.setChannelConfig(configStore.channels[configStore.id])
if (!saved.value) {
saved.value = true
if (update.status && update.status < 400) {
indexStore.msgAlert('success', t('config.updateChannelSuccess'), 2)
} else {
indexStore.msgAlert('error', t('config.updateChannelFailed'), 2)
if (configStore.channels[i.value].id !== channel.value.id) {
await addNewChannel()
} else {
await updateChannel()
}
await configStore.getPlayoutConfig()
await configStore.getUserConfig()
}
}
async function deleteChannel() {
const config = $_.cloneDeep(configStore.channels)
const id = config[configStore.id].id
function resetChannel() {
channel.value = cloneDeep(configStore.channels[i.value])
saved.value = true
}
if (id === 1) {
async function deleteChannel() {
if (channel.value.id === 1) {
indexStore.msgAlert('warning', t('config.errorChannelDelete'), 2)
return
}
const response = await fetch(`/api/channel/${id}`, {
const response = await fetch(`/api/channel/${channel.value.id}`, {
method: 'DELETE',
headers: authStore.authHeader,
})
config.splice(configStore.id, 1)
configStore.channelsRaw.splice(configStore.id, 1)
configStore.channels = config
configStore.id = configStore.channels.length - 1
i.value = configStore.i - 1
await configStore.getChannelConfig()
await configStore.getPlayoutConfig()
await configStore.getUserConfig()
if (response.status === 200) {
indexStore.msgAlert('success', t('config.errorChannelDelete'), 2)

View File

@ -19,7 +19,6 @@
v-if="
name.toString() !== 'startInSec' &&
name.toString() !== 'lengthInSec' &&
!(name.startsWith('vtt_') && !config.public.buildExperimental) &&
!(name.toString() === 'path' && key.toString() === 'storage')
"
>
@ -178,7 +177,7 @@ async function onSubmitPlayout() {
if (update.status === 200) {
indexStore.msgAlert('success', t('config.updatePlayoutSuccess'), 2)
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
await $fetch(`/api/control/${channel}/process/`, {
method: 'POST',
@ -198,7 +197,7 @@ async function onSubmitPlayout() {
async function restart(res: boolean) {
if (res) {
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
await $fetch(`/api/control/${channel}/process/`, {
method: 'POST',

View File

@ -144,7 +144,7 @@ const user = ref({
password: '',
confirm: '',
admin: false,
channel_ids: [1],
channel_ids: [configStore.channels[configStore.i]?.id ?? 1],
role_id: 3,
} as User)

View File

@ -31,7 +31,7 @@
<details tabindex="0" @focusout="closeDropdown">
<summary>
<div class="h-[19px] text-base">
<span> {{ configStore.channels[configStore.id].name }} </span>
<span> {{ configStore.channels[configStore.i].name }} </span>
</div>
</summary>
<ul class="p-2">
@ -68,7 +68,7 @@
<details tabindex="0" @focusout="closeDropdown">
<summary>
<div class="h-[19px] text-base">
<span> {{ configStore.channels[configStore.id].name }} </span>
<span> {{ configStore.channels[configStore.i].name }} </span>
</div>
</summary>
<ul class="p-2 bg-base-100 rounded-md !mt-1 w-36" tabindex="0">
@ -127,17 +127,15 @@ if (colorMode.value === 'dark') {
function closeMenu() {
setTimeout(() => {
isOpen.value = false
menuDropdown.value.removeAttribute('open')
console.log('close')
menuDropdown.value?.removeAttribute('open')
}, 200)
}
function clickMenu() {
console.log('close')
isOpen.value = !isOpen.value
if (!isOpen.value) {
menuDropdown.value.removeAttribute('open')
menuDropdown.value?.removeAttribute('open')
}
}
@ -146,14 +144,14 @@ function blurMenu() {
isOpen.value = !isOpen.value
} else {
setTimeout(() => {
menuDropdown.value.removeAttribute('open')
menuDropdown.value?.removeAttribute('open')
}, 200)
}
}
function closeDropdown($event: any) {
setTimeout(() => {
$event.target.parentNode.removeAttribute('open')
$event.target.parentNode?.removeAttribute('open')
}, 200)
}
@ -163,7 +161,7 @@ function logout() {
}
function selectChannel(index: number) {
configStore.id = index
configStore.i = index
configStore.getPlayoutConfig()
}

View File

@ -79,7 +79,7 @@ const { secToHMS, mediaType } = stringFormatter()
const configStore = useConfig()
const mediaStore = useMedia()
const { id } = storeToRefs(useConfig())
const { i } = storeToRefs(useConfig())
const browserSortOptions = {
group: { name: 'playlist', pull: 'clone', put: false },
@ -102,7 +102,7 @@ onMounted(async () => {
}
})
watch([id], () => {
watch([i], () => {
mediaStore.getTree('')
})
</script>

View File

@ -6,8 +6,8 @@
<div class="w-full aspect-video">
<video v-if="streamExtension === 'flv'" ref="httpStreamFlv" controls />
<VideoPlayer
v-else-if="configStore.showPlayer && configStore.channels[configStore.id]"
:key="configStore.id"
v-else-if="configStore.showPlayer && configStore.channels[configStore.i]"
:key="configStore.i"
class="live-player"
reference="httpStream"
:options="{
@ -19,7 +19,7 @@
sources: [
{
type: 'application/x-mpegURL',
src: configStore.channels[configStore.id].preview_url,
src: configStore.channels[configStore.i].preview_url,
},
],
}"
@ -56,7 +56,7 @@
<div
v-else
class="h-1/3 font-bold text truncate"
:class="{'text-base-content/60': playlistStore.current.category === 'advertisement'}"
:class="{ 'text-base-content/60': playlistStore.current.category === 'advertisement' }"
:title="playlistStore.current.title || filename(playlistStore.current.source)"
>
{{
@ -67,8 +67,8 @@
</div>
<div class="grow">
<strong>{{ t('player.duration') }}:</strong>
{{ secToHMS(playlistStore.current.duration) }} |
<strong>{{ t('player.in') }}:</strong> {{ secToHMS(playlistStore.current.in) }} |
{{ secToHMS(playlistStore.current.duration) }} | <strong>{{ t('player.in') }}:</strong>
{{ secToHMS(playlistStore.current.in) }} |
<strong>{{ t('player.out') }}:</strong>
{{ secToHMS(playlistStore.current.out) }}
@ -80,7 +80,11 @@
<div class="h-1/3">
<progress
class="progress progress-accent w-full"
:value="playlistStore.progressValue"
:value="
playlistStore.progressValue && playlistStore.progressValue <= 100
? playlistStore.progressValue
: 0
"
max="100"
/>
</div>
@ -165,6 +169,7 @@
</template>
<script setup lang="ts">
import { throttle } from 'lodash-es'
import { storeToRefs } from 'pinia'
import mpegts from 'mpegts.js'
@ -175,7 +180,7 @@ const configStore = useConfig()
const indexStore = useIndex()
const playlistStore = usePlaylist()
const { filename, secToHMS } = stringFormatter()
const { id } = storeToRefs(useConfig())
const { i } = storeToRefs(useConfig())
const currentDefault = {
uid: '',
@ -191,21 +196,19 @@ playlistStore.current = currentDefault
const timeStr = ref('00:00:00')
const timer = ref()
const errorCounter = ref(0)
const streamExtension = ref(configStore.channels[configStore.id].preview_url.split('.').pop())
const streamExtension = ref(configStore.channels[configStore.i].preview_url.split('.').pop())
const httpStreamFlv = ref(null)
const httpFlvSource = ref({
type: 'flv',
isLive: true,
url: configStore.channels[configStore.id].preview_url,
url: configStore.channels[configStore.i].preview_url,
})
const mpegtsOptions = ref({
lazyLoadMaxDuration: 3 * 60,
liveBufferLatencyChasing: true,
})
const streamUrl = ref(
`/data/event/${configStore.channels[configStore.id].id}?endpoint=playout&uuid=${authStore.uuid}`
)
const streamUrl = ref(`/data/event/${configStore.channels[configStore.i].id}?endpoint=playout&uuid=${authStore.uuid}`)
// 'http://127.0.0.1:8787/data/event/1?endpoint=playout&uuid=f2f8c29b-712a-48c5-8919-b535d3a05a3a'
const { status, data, error, close } = useEventSource(streamUrl, [], {
@ -256,9 +259,9 @@ watch([status, error], async () => {
if (errorCounter.value > 11) {
await authStore.obtainUuid()
streamUrl.value = `/data/event/${
configStore.channels[configStore.id].id
}?endpoint=playout&uuid=${authStore.uuid}`
streamUrl.value = `/data/event/${configStore.channels[configStore.i].id}?endpoint=playout&uuid=${
authStore.uuid
}`
errorCounter.value = 0
}
}
@ -277,12 +280,10 @@ watch([data], () => {
}
})
watch([id], () => {
watch([i], () => {
resetStatus()
streamUrl.value = `/data/event/${configStore.channels[configStore.id].id}?endpoint=playout&uuid=${
authStore.uuid
}`
streamUrl.value = `/data/event/${configStore.channels[configStore.i].id}?endpoint=playout&uuid=${authStore.uuid}`
if (timer.value) {
clearTimeout(timer.value)
@ -313,32 +314,31 @@ function resetStatus() {
playlistStore.current = currentDefault
}
async function controlProcess(state: string) {
const controlProcess = throttle(async (state: string) => {
/*
Control playout (start, stop, restart)
*/
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
await $fetch(`/api/control/${channel}/process/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify({ command: state }),
})
}
}, 800)
async function controlPlayout(state: string) {
const controlPlayout = throttle(async (state: string) => {
/*
Control playout:
- jump to next clip
- jump to last clip
- reset playout state
*/
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
await $fetch(`/api/control/${channel}/playout/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify({ control: state }),
})
}
}, 800)
</script>

View File

@ -347,7 +347,7 @@ function addFolderToTemplate(event: any, item: TemplateItem) {
event.item.remove()
const storagePath = configStore.channels[configStore.id].storage_path
const storagePath = configStore.channels[configStore.i].storage
const navPath = mediaStore.folderCrumbs[mediaStore.folderCrumbs.length - 1].path
const sourcePath = `${storagePath}/${navPath}/${mediaStore.folderList.folders[o].name}`.replace(/\/[/]+/g, '/')
@ -402,7 +402,7 @@ async function generatePlaylist() {
}
}
await $fetch(`/api/playlist/${configStore.channels[configStore.id].id}/generate/${playlistStore.listDate}`, {
await $fetch(`/api/playlist/${configStore.channels[configStore.i].id}/generate/${playlistStore.listDate}`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body,

View File

@ -4,11 +4,11 @@
ref="playlistContainer"
class="relative w-full h-full !bg-base-300 rounded-e overflow-auto"
>
<div v-if="playlistStore.isLoading" class="w-full h-full absolute z-10 flex justify-center bg-base-100/70">
<div v-if="playlistStore.isLoading" class="w-full h-full absolute z-2 flex justify-center bg-base-100/70">
<span class="loading loading-spinner loading-lg" />
</div>
<table class="table table-zebra table-fixed">
<thead class="top-0 sticky z-10">
<thead class="top-0 sticky z-2">
<tr class="bg-base-100 rounded-tr-lg">
<th v-if="!configStore.playout.playlist.infinit" class="w-[85px] p-0 text-left">
<div class="border-b border-my-gray px-4 py-3">
@ -78,7 +78,7 @@
'!bg-lime-500/30':
playlistStore.playoutIsRunning && listDate === todayDate && index === currentIndex,
'!bg-amber-600/40': element.overtime,
'text-blue-300': element.category === 'advertisement',
'text-base-content/60': element.category === 'advertisement',
}"
>
<td v-if="!configStore.playout.playlist.infinit" class="ps-4 py-2 text-left">
@ -139,7 +139,7 @@ const { processPlaylist, genUID } = playlistOperations()
const playlistContainer = ref()
const sortContainer = ref()
const todayDate = ref($dayjs().utcOffset(configStore.utcOffset).format('YYYY-MM-DD'))
const { id } = storeToRefs(useConfig())
const { i } = storeToRefs(useConfig())
const { currentIndex, listDate, playoutIsRunning } = storeToRefs(usePlaylist())
const playlistSortOptions = {
@ -169,7 +169,7 @@ onMounted(() => {
}, 150)
})
watch([listDate, id], () => {
watch([listDate, i], () => {
setTimeout(() => {
getPlaylist()
}, 800)
@ -259,7 +259,7 @@ function addClip(event: any) {
event.item?.remove()
const storagePath = configStore.channels[configStore.id].storage_path
const storagePath = configStore.channels[configStore.i].storage
const sourcePath = `${storagePath}/${mediaStore.folderTree.source}/${mediaStore.folderTree.files[o].name}`.replace(
/\/[/]+/g,
'/'

View File

@ -1,5 +1,5 @@
<template>
<div class="grid grid-cols-1 xs:grid-cols-2 border-4 rounded-md border-primary text-left shadow min-w-[320px] md:min-w-[728px] max-w-[960px] mt-5 xs:mt-0">
<div class="grid grid-cols-1 xs:grid-cols-2 border-4 rounded-md border-primary text-left shadow min-w-[320px] md:min-w-[728px] max-w-[960px] mt-5">
<div class="p-4 bg-base-100">
<span class="text-3xl">{{ sysStat.system.name }} {{ sysStat.system.version }}</span>
<span v-if="sysStat.system.kernel">
@ -96,7 +96,7 @@ const configStore = useConfig()
const indexStore = useIndex()
const streamUrl = ref(
`/data/event/${configStore.channels[configStore.id].id}?endpoint=system&uuid=${authStore.uuid}`
`/data/event/${configStore.channels[configStore.i].id}?endpoint=system&uuid=${authStore.uuid}`
)
// 'http://127.0.0.1:8787/data/event/1?endpoint=system&uuid=f2f8c29b-712a-48c5-8919-b535d3a05a3a'
@ -139,7 +139,7 @@ watch([status, error], async () => {
if (errorCounter.value > 15) {
await authStore.obtainUuid()
streamUrl.value = `/data/event/${configStore.channels[configStore.id].id}?endpoint=system&uuid=${
streamUrl.value = `/data/event/${configStore.channels[configStore.i].id}?endpoint=system&uuid=${
authStore.uuid
}`
errorCounter.value = 0

View File

@ -39,12 +39,12 @@ export const stringFormatter = () => {
.replace(/\[Validator\]/g, '<span class="log-server">[Validator]</span>')
}
function timeToSeconds(time: string) {
function timeToSeconds(time: string): number {
const t = time.split(':')
return parseInt(t[0]) * 3600 + parseInt(t[1]) * 60 + parseInt(t[2])
}
function secToHMS(sec: number) {
function secToHMS(sec: number): string {
const sign = Math.sign(sec)
sec = Math.abs(sec)
@ -62,7 +62,7 @@ export const stringFormatter = () => {
return `${hString}:${m}:${s}`
}
function numberToHex(num: number) {
function numberToHex(num: number): string {
return '0x' + Math.round(num * 255).toString(16)
}
@ -70,7 +70,7 @@ export const stringFormatter = () => {
return parseFloat((parseFloat(parseInt(num, 16).toString()) / 255).toFixed(2))
}
function filename(path: string) {
function filename(path: string): string {
if (path) {
const pathArr = path.split('/')
const name = pathArr[pathArr.length - 1]
@ -85,7 +85,7 @@ export const stringFormatter = () => {
}
}
function parent(path: string) {
function parent(path: string): string {
if (path) {
const pathArr = path.split('/')
pathArr.pop()
@ -100,7 +100,7 @@ export const stringFormatter = () => {
}
}
function toMin(sec: number) {
function toMin(sec: number): string {
if (sec) {
const minutes = Math.floor(sec / 60)
const seconds = Math.round(sec - minutes * 60)
@ -167,6 +167,14 @@ export const stringFormatter = () => {
return null
}
function dir_file(path: string): {dir: string, file: string} {
const index = path.lastIndexOf('/')
const dir = path.substring(0, index + 1) || '/'
const file = path.substring(index + 1)
return {dir, file}
}
return {
fileSize,
formatLog,
@ -179,6 +187,7 @@ export const stringFormatter = () => {
toMin,
secondsToTime,
mediaType,
dir_file,
}
}

View File

@ -1,7 +1,7 @@
export const useVariables = () => {
const multiSelectClasses = {
container: 'relative input input-bordered w-full flex items-center justify-end px-0 min-h-[32px]',
containerDisabled: 'cursor-default !bg-base-100',
container: 'relative input input-bordered w-full h-auto flex items-center justify-end px-0 min-h-[32px]',
containerDisabled: '[&>div]:cursor-default !bg-base-100 [&>div>div]:pr-2',
containerOpen: 'rounded-b-none',
containerOpenTop: 'rounded-t-none',
containerActive: 'ring ring-green-500 ring-opacity-30',

View File

@ -93,7 +93,9 @@ export default {
notExists: 'Speicher existiert nicht!',
create: 'Ordner erstellen',
upload: 'Dateien hochladen',
deleteTitle: 'Datei/Ordner löschen',
delete: 'Lösche',
file: 'Datei',
folder: 'Ordner',
deleteQuestion: 'Sind Sie sicher, dass Sie löschen möchten',
preview: 'Vorschau',
rename: 'Datei umbenennen',
@ -110,6 +112,7 @@ export default {
folderError: 'Fehler beim Erstellen des Ordners',
uploadError: 'Fehler beim Hochladen',
fileExists: 'Datei existiert bereits!',
recursive: 'Rekursiv',
},
message: {
savePreset: 'Voreinstellung speichern',
@ -200,7 +203,7 @@ export default {
updatePlayoutFailed: 'Update playout config fehlgeschlagen!',
forbiddenPlaylistPath: 'Zugriff untersagt: Playlist-Ordner kann nicht geöffnet werden.',
noPlayoutConfig: 'Keine Playout-Konfiguration gefunden!',
hlsPath: 'HLS-Pfad',
publicPath: 'Public (HLS) Pfad',
playlistPath: 'Wiedergabelistenpfad',
storagePath: 'Speicherpfad',
sharedStorage: 'Gemeinsamer Speicher ist aktiviert, verwende denselben Speicherstamm für alle Kanäle!',

View File

@ -93,7 +93,9 @@ export default {
notExists: 'Storage not exist!',
create: 'Create Folder',
upload: 'Upload Files',
deleteTitle: 'Delete File/Folder',
delete: 'Delete',
file: 'File',
folder: 'Folder',
deleteQuestion: 'Are you sure that you want to delete',
preview: 'Preview',
rename: 'Rename File',
@ -110,6 +112,7 @@ export default {
folderError: 'Folder create error',
uploadError: 'Upload error',
fileExists: 'File exists already!',
recursive: 'Recursive',
},
message: {
savePreset: 'Save Preset',
@ -199,7 +202,7 @@ export default {
updatePlayoutFailed: 'Update playout config failed!',
forbiddenPlaylistPath: 'Access forbidden: Playlist folder cannot be opened.',
noPlayoutConfig: 'No playout config found!',
hlsPath: 'HLS Path',
publicPath: 'Public (HLS) Path',
playlistPath: 'Playlist Path',
storagePath: 'Storage Path',
sharedStorage: 'Shared storage is enabled, use the same storage root for all channels!',

View File

@ -93,7 +93,9 @@ export default {
notExists: 'O armazenamento não existe!',
create: 'Criar Pasta',
upload: 'Enviar Arquivos',
deleteTitle: 'Deletar Arquivo/Pasta',
delete: 'Deletar',
file: 'Arquivo',
folder: 'Pasta',
deleteQuestion: 'Tem certeza que deseja deletar?',
preview: 'Visualizar',
rename: 'Renomear Arquivo',
@ -110,6 +112,7 @@ export default {
folderError: 'Erro ao criar pasta',
uploadError: 'Erro ao carregar',
fileExists: 'O arquivo já existe!',
recursive: 'Recursivo',
},
message: {
savePreset: 'Salvar predefinição',
@ -199,7 +202,7 @@ export default {
updatePlayoutFailed: 'Falha na atualização da configuração do playout!',
forbiddenPlaylistPath: 'Acesso proibido: A pasta da lista de reprodução não pode ser aberta',
noPlayoutConfig: 'Nenhuma configuração de playout encontrada!',
hlsPath: 'HLS Path',
publicPath: 'Public (HLS) Path',
playlistPath: 'Playlist Path',
storagePath: 'Storage Path',
sharedStorage: 'O armazenamento compartilhado está ativado, use a mesma raiz de armazenamento para todos os canais',

View File

@ -93,7 +93,9 @@ export default {
notExists: 'Папки не существует!',
create: 'Сделать папку',
upload: 'Загрузить файлы',
deleteTitle: 'Удалить Файл/Папку',
delete: 'Удалить',
file: 'Файл',
folder: 'Папку',
deleteQuestion: 'Вы уверены что хотите это удалить',
preview: 'Просмотр',
rename: 'Переименовать файл',
@ -110,6 +112,7 @@ export default {
folderError: 'Ошибка Создания папки',
uploadError: 'Ошибка Загрузки',
fileExists: 'Файл уже имеется!',
recursive: 'Рекурсивный',
},
message: {
savePreset: 'Сохранить шаблон',
@ -199,7 +202,7 @@ export default {
updatePlayoutFailed: 'Обновление конфигурации воспроизведения не удалось!',
forbiddenPlaylistPath: 'Доступ запрещен: Папка плейлиста не может быть открыта.',
noPlayoutConfig: 'Конфигурация воспроизведения не найдена!',
hlsPath: 'HLS Path',
publicPath: 'Public (HLS) Path',
playlistPath: 'Playlist Path',
storagePath: 'Storage Path',
sharedStorage: 'Общее хранилище включено, используйте один и тот же корень хранилища для всех каналов!',

View File

@ -1,26 +1,26 @@
{
"name": "frontend",
"version": "0.12.2",
"version": "0.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "0.12.2",
"version": "0.13.0",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/color-mode": "^3.5.1",
"@pinia/nuxt": "^0.5.4",
"@pinia/nuxt": "^0.5.5",
"@vueform/multiselect": "^2.6.10",
"@vuepic/vue-datepicker": "^9.0.3",
"@vueuse/nuxt": "^11.1.0",
"bootstrap-icons": "^1.11.3",
"dayjs": "^1.11.13",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"mpegts.js": "^1.7.3",
"nuxt": "3.13.2",
"pinia": "^2.2.2",
"pinia": "^2.2.4",
"sortablejs-vue3": "^1.2.11",
"splitpanes": "^3.1.5",
"video.js": "^8.17.4"
@ -29,7 +29,7 @@
"@nuxt/eslint": "^0.5.7",
"@nuxtjs/i18n": "^8.5.5",
"@nuxtjs/tailwindcss": "^6.12.1",
"@types/lodash": "^4.17.9",
"@types/lodash-es": "^4.17.12",
"@types/video.js": "^7.3.58",
"daisyui": "^4.12.10",
"mini-svg-data-uri": "^1.4.4",
@ -1828,12 +1828,12 @@
}
},
"node_modules/@netlify/functions": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.1.tgz",
"integrity": "sha512-+6wtYdoz0yE06dSa9XkP47tw5zm6g13QMeCwM3MmHx1vn8hzwFa51JtmfraprdkL7amvb7gaNM+OOhQU1h6T8A==",
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.2.tgz",
"integrity": "sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==",
"license": "MIT",
"dependencies": {
"@netlify/serverless-functions-api": "1.19.1"
"@netlify/serverless-functions-api": "1.26.1"
},
"engines": {
"node": ">=14.0.0"
@ -1849,9 +1849,9 @@
}
},
"node_modules/@netlify/serverless-functions-api": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.19.1.tgz",
"integrity": "sha512-2KYkyluThg1AKfd0JWI7FzpS4A/fzVVGYIf6AM4ydWyNj8eI/86GQVLeRgDoH7CNOxt243R5tutWlmHpVq0/Ew==",
"version": "1.26.1",
"resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.26.1.tgz",
"integrity": "sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==",
"license": "MIT",
"dependencies": {
"@netlify/node-cookies": "^0.1.0",
@ -3012,13 +3012,13 @@
}
},
"node_modules/@pinia/nuxt": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.5.4.tgz",
"integrity": "sha512-nNEs2pq6+Ji5qIyRwmeD9LUdctL8aJ8QMVLTYxUc16cXEOcIIN+MSA8Xudsd0lVETYgEAROT5HiBHnOYRDY3yQ==",
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.5.5.tgz",
"integrity": "sha512-wjxS7YqIesh4OLK+qE3ZjhdOJ5pYZQ+VlEmZNtTwzQn1Kavei/khovx7mzXVXNA/mvSPXVhb9xBzhyS3XMURtw==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.9.0",
"pinia": "^2.2.2"
"pinia": "^2.2.3"
},
"funding": {
"url": "https://github.com/sponsors/posva"
@ -3268,9 +3268,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz",
"integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.23.0.tgz",
"integrity": "sha512-8OR+Ok3SGEMsAZispLx8jruuXw0HVF16k+ub2eNXKHDmdxL4cf9NlNpAzhlOhNyXzKDEJuFeq0nZm+XlNb1IFw==",
"cpu": [
"arm"
],
@ -3281,9 +3281,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz",
"integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.23.0.tgz",
"integrity": "sha512-rEFtX1nP8gqmLmPZsXRMoLVNB5JBwOzIAk/XAcEPuKrPa2nPJ+DuGGpfQUR0XjRm8KjHfTZLpWbKXkA5BoFL3w==",
"cpu": [
"arm64"
],
@ -3294,9 +3294,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz",
"integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.23.0.tgz",
"integrity": "sha512-ZbqlMkJRMMPeapfaU4drYHns7Q5MIxjM/QeOO62qQZGPh9XWziap+NF9fsqPHT0KzEL6HaPspC7sOwpgyA3J9g==",
"cpu": [
"arm64"
],
@ -3307,9 +3307,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz",
"integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.23.0.tgz",
"integrity": "sha512-PfmgQp78xx5rBCgn2oYPQ1rQTtOaQCna0kRaBlc5w7RlA3TDGGo7m3XaptgitUZ54US9915i7KeVPHoy3/W8tA==",
"cpu": [
"x64"
],
@ -3320,9 +3320,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz",
"integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.23.0.tgz",
"integrity": "sha512-WAeZfAAPus56eQgBioezXRRzArAjWJGjNo/M+BHZygUcs9EePIuGI1Wfc6U/Ki+tMW17FFGvhCfYnfcKPh18SA==",
"cpu": [
"arm"
],
@ -3333,9 +3333,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz",
"integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.23.0.tgz",
"integrity": "sha512-v7PGcp1O5XKZxKX8phTXtmJDVpE20Ub1eF6w9iMmI3qrrPak6yR9/5eeq7ziLMrMTjppkkskXyxnmm00HdtXjA==",
"cpu": [
"arm"
],
@ -3346,9 +3346,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz",
"integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.23.0.tgz",
"integrity": "sha512-nAbWsDZ9UkU6xQiXEyXBNHAKbzSAi95H3gTStJq9UGiS1v+YVXwRHcQOQEF/3CHuhX5BVhShKoeOf6Q/1M+Zhg==",
"cpu": [
"arm64"
],
@ -3359,9 +3359,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz",
"integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.23.0.tgz",
"integrity": "sha512-5QT/Di5FbGNPaVw8hHO1wETunwkPuZBIu6W+5GNArlKHD9fkMHy7vS8zGHJk38oObXfWdsuLMogD4sBySLJ54g==",
"cpu": [
"arm64"
],
@ -3372,9 +3372,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz",
"integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.23.0.tgz",
"integrity": "sha512-Sefl6vPyn5axzCsO13r1sHLcmPuiSOrKIImnq34CBurntcJ+lkQgAaTt/9JkgGmaZJ+OkaHmAJl4Bfd0DmdtOQ==",
"cpu": [
"ppc64"
],
@ -3385,9 +3385,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz",
"integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.23.0.tgz",
"integrity": "sha512-o4QI2KU/QbP7ZExMse6ULotdV3oJUYMrdx3rBZCgUF3ur3gJPfe8Fuasn6tia16c5kZBBw0aTmaUygad6VB/hQ==",
"cpu": [
"riscv64"
],
@ -3398,9 +3398,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz",
"integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.23.0.tgz",
"integrity": "sha512-+bxqx+V/D4FGrpXzPGKp/SEZIZ8cIW3K7wOtcJAoCrmXvzRtmdUhYNbgd+RztLzfDEfA2WtKj5F4tcbNPuqgeg==",
"cpu": [
"s390x"
],
@ -3411,9 +3411,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz",
"integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.23.0.tgz",
"integrity": "sha512-I/eXsdVoCKtSgK9OwyQKPAfricWKUMNCwJKtatRYMmDo5N859tbO3UsBw5kT3dU1n6ZcM1JDzPRSGhAUkxfLxw==",
"cpu": [
"x64"
],
@ -3424,9 +3424,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz",
"integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.23.0.tgz",
"integrity": "sha512-4ZoDZy5ShLbbe1KPSafbFh1vbl0asTVfkABC7eWqIs01+66ncM82YJxV2VtV3YVJTqq2P8HMx3DCoRSWB/N3rw==",
"cpu": [
"x64"
],
@ -3437,9 +3437,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz",
"integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.23.0.tgz",
"integrity": "sha512-+5Ky8dhft4STaOEbZu3/NU4QIyYssKO+r1cD3FzuusA0vO5gso15on7qGzKdNXnc1gOrsgCqZjRw1w+zL4y4hQ==",
"cpu": [
"arm64"
],
@ -3450,9 +3450,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz",
"integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.23.0.tgz",
"integrity": "sha512-0SPJk4cPZQhq9qA1UhIRumSE3+JJIBBjtlGl5PNC///BoaByckNZd53rOYD0glpTkYFBQSt7AkMeLVPfx65+BQ==",
"cpu": [
"ia32"
],
@ -3463,9 +3463,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz",
"integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz",
"integrity": "sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==",
"cpu": [
"x64"
],
@ -3558,6 +3558,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "22.7.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
@ -3594,17 +3604,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
"integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"@typescript-eslint/scope-manager": "8.8.0",
"@typescript-eslint/type-utils": "8.8.0",
"@typescript-eslint/utils": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -3628,16 +3638,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz",
"integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"@typescript-eslint/scope-manager": "8.8.0",
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/typescript-estree": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0",
"debug": "^4.3.4"
},
"engines": {
@ -3657,14 +3667,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz",
"integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0"
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3675,14 +3685,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz",
"integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/typescript-estree": "8.8.0",
"@typescript-eslint/utils": "8.8.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -3700,9 +3710,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz",
"integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==",
"dev": true,
"license": "MIT",
"engines": {
@ -3714,14 +3724,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz",
"integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -3743,16 +3753,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz",
"integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0"
"@typescript-eslint/scope-manager": "8.8.0",
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/typescript-estree": "8.8.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3766,13 +3776,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz",
"integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/types": "8.8.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -5228,9 +5238,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001664",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz",
"integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==",
"version": "1.0.30001666",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz",
"integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==",
"funding": [
{
"type": "opencollective",
@ -6346,9 +6356,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.29",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz",
"integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==",
"version": "1.5.31",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz",
"integrity": "sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@ -6644,9 +6654,9 @@
}
},
"node_modules/eslint-plugin-import-x": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.3.0.tgz",
"integrity": "sha512-PxGzP7gAjF2DLeRnQtbYkkgZDg1intFyYr/XS1LgTYXUDrSXMHGkXx8++6i2eDv2jMs0jfeO6G6ykyeWxiFX7w==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.3.1.tgz",
"integrity": "sha512-5TriWkXulDl486XnYYRgsL+VQoS/7mhN/2ci02iLCuL7gdhbiWxnsuL/NTcaKY9fpMgsMFjWZBtIGW7pb+RX0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -6669,9 +6679,9 @@
}
},
"node_modules/eslint-plugin-jsdoc": {
"version": "50.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.0.tgz",
"integrity": "sha512-P7qDB/RckdKETpBM4CtjHRQ5qXByPmFhRi86sN3E+J+tySchq+RSOGGhI2hDIefmmKFuTi/1ACjqsnDJDDDfzg==",
"version": "50.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.1.tgz",
"integrity": "sha512-SY9oUuTMr6aWoJggUS40LtMjsRzJPB5ZT7F432xZIHK3EfHF+8i48GbUBpwanrtlL9l1gILNTHK9o8gEhYLcKA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@ -7690,9 +7700,9 @@
}
},
"node_modules/globals": {
"version": "15.9.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
"integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
"version": "15.10.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.10.0.tgz",
"integrity": "sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -8932,9 +8942,9 @@
}
},
"node_modules/listhen/node_modules/jiti": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.0.0.tgz",
"integrity": "sha512-CJ7e7Abb779OTRv3lomfp7Mns/Sy1+U4pcAx5VbjxCZD5ZM/VJaXPpPjNKjtSvWQy/H86E49REXR34dl1JEz9w==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.1.0.tgz",
"integrity": "sha512-Nftp80J8poC3u+93ZxpjstsgfQ5d0o5qyD6yStv32sgnWr74xRxBppEwsUoA/GIdrJpgGRkC1930YkLcAsFdSw==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@ -8988,6 +8998,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@ -11156,9 +11172,9 @@
}
},
"node_modules/pinia": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.2.tgz",
"integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.4.tgz",
"integrity": "sha512-K7ZhpMY9iJ9ShTC0cR2+PnxdQRuwVIsXDO/WIEV/RnMC/vmSoKDTKW/exNQYPI+4ij10UjXqdNiEHwn47McANQ==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
@ -12564,9 +12580,9 @@
}
},
"node_modules/rollup": {
"version": "4.22.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz",
"integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.23.0.tgz",
"integrity": "sha512-vXB4IT9/KLDrS2WRXmY22sVB2wTsTwkpxjB8Q3mnakTENcYw3FRmfdYDy/acNmls+lHmDazgrRjK/yQ6hQAtwA==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
@ -12579,22 +12595,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.22.5",
"@rollup/rollup-android-arm64": "4.22.5",
"@rollup/rollup-darwin-arm64": "4.22.5",
"@rollup/rollup-darwin-x64": "4.22.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.22.5",
"@rollup/rollup-linux-arm-musleabihf": "4.22.5",
"@rollup/rollup-linux-arm64-gnu": "4.22.5",
"@rollup/rollup-linux-arm64-musl": "4.22.5",
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.5",
"@rollup/rollup-linux-riscv64-gnu": "4.22.5",
"@rollup/rollup-linux-s390x-gnu": "4.22.5",
"@rollup/rollup-linux-x64-gnu": "4.22.5",
"@rollup/rollup-linux-x64-musl": "4.22.5",
"@rollup/rollup-win32-arm64-msvc": "4.22.5",
"@rollup/rollup-win32-ia32-msvc": "4.22.5",
"@rollup/rollup-win32-x64-msvc": "4.22.5",
"@rollup/rollup-android-arm-eabi": "4.23.0",
"@rollup/rollup-android-arm64": "4.23.0",
"@rollup/rollup-darwin-arm64": "4.23.0",
"@rollup/rollup-darwin-x64": "4.23.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.23.0",
"@rollup/rollup-linux-arm-musleabihf": "4.23.0",
"@rollup/rollup-linux-arm64-gnu": "4.23.0",
"@rollup/rollup-linux-arm64-musl": "4.23.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.23.0",
"@rollup/rollup-linux-riscv64-gnu": "4.23.0",
"@rollup/rollup-linux-s390x-gnu": "4.23.0",
"@rollup/rollup-linux-x64-gnu": "4.23.0",
"@rollup/rollup-linux-x64-musl": "4.23.0",
"@rollup/rollup-win32-arm64-msvc": "4.23.0",
"@rollup/rollup-win32-ia32-msvc": "4.23.0",
"@rollup/rollup-win32-x64-msvc": "4.23.0",
"fsevents": "~2.3.2"
}
},
@ -13816,9 +13832,9 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
"integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz",
"integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==",
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
@ -14293,9 +14309,9 @@
}
},
"node_modules/untyped/node_modules/jiti": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.0.0.tgz",
"integrity": "sha512-CJ7e7Abb779OTRv3lomfp7Mns/Sy1+U4pcAx5VbjxCZD5ZM/VJaXPpPjNKjtSvWQy/H86E49REXR34dl1JEz9w==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.1.0.tgz",
"integrity": "sha512-Nftp80J8poC3u+93ZxpjstsgfQ5d0o5qyD6yStv32sgnWr74xRxBppEwsUoA/GIdrJpgGRkC1930YkLcAsFdSw==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"

View File

@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "0.12.2",
"version": "0.13.0",
"description": "Web GUI for ffplayout",
"author": "Jonathan Baecker",
"private": true,
@ -14,17 +14,17 @@
},
"dependencies": {
"@nuxtjs/color-mode": "^3.5.1",
"@pinia/nuxt": "^0.5.4",
"@pinia/nuxt": "^0.5.5",
"@vueform/multiselect": "^2.6.10",
"@vuepic/vue-datepicker": "^9.0.3",
"@vueuse/nuxt": "^11.1.0",
"bootstrap-icons": "^1.11.3",
"dayjs": "^1.11.13",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"mpegts.js": "^1.7.3",
"nuxt": "3.13.2",
"pinia": "^2.2.2",
"pinia": "^2.2.4",
"sortablejs-vue3": "^1.2.11",
"splitpanes": "^3.1.5",
"video.js": "^8.17.4"
@ -33,7 +33,7 @@
"@nuxt/eslint": "^0.5.7",
"@nuxtjs/i18n": "^8.5.5",
"@nuxtjs/tailwindcss": "^6.12.1",
"@types/lodash": "^4.17.9",
"@types/lodash-es": "^4.17.12",
"@types/video.js": "^7.3.58",
"daisyui": "^4.12.10",
"mini-svg-data-uri": "^1.4.4",

View File

@ -51,7 +51,7 @@ useHead({
title: `${t('button.logging')} | ffplayout`,
})
const { id } = storeToRefs(useConfig())
const { i } = storeToRefs(useConfig())
const { $dayjs } = useNuxtApp()
const authStore = useAuth()
@ -80,7 +80,7 @@ onMounted(async () => {
await getLog()
})
watch([listDate, id], () => {
watch([listDate, i], () => {
getLog()
})
@ -111,7 +111,7 @@ async function getLog() {
date = ''
}
await fetch(`/api/log/${configStore.channels[configStore.id].id}?date=${date}`, {
await fetch(`/api/log/${configStore.channels[configStore.i].id}?date=${date}`, {
method: 'GET',
headers: authStore.authHeader,
})

View File

@ -230,10 +230,22 @@
<GenericModal
:show="showDeleteModal"
:title="t('media.deleteTitle')"
:title="`${t('media.delete')} ${
extensionsArr.some((suffix) => deleteName.endsWith(suffix)) ? t('media.file') : t('media.folder')
}`"
:text="`${t('media.deleteQuestion')}:<br /><strong>${deleteName}</strong>`"
:modal-action="deleteFileOrFolder"
/>
>
<div>
<input class="input input-sm w-full" type="text" :value="dir_file(deleteName).file" disabled />
<div v-if="!extensionsArr.some((suffix) => deleteName.endsWith(suffix))" class="form-control mt-3">
<label class="label cursor-pointer w-1/4">
<input v-model="recursive" type="checkbox" class="checkbox checkbox-sm checkbox-warning" />
<span class="label-text">{{ t('media.recursive') }}</span>
</label>
</div>
</div>
</GenericModal>
<GenericModal
:show="showPreviewModal"
@ -311,8 +323,8 @@ const authStore = useAuth()
const configStore = useConfig()
const indexStore = useIndex()
const mediaStore = useMedia()
const { toMin, mediaType, filename, parent } = stringFormatter()
const { id } = storeToRefs(useConfig())
const { toMin, mediaType, filename, parent, dir_file } = stringFormatter()
const { i } = storeToRefs(useConfig())
useHead({
title: `${t('button.media')} | ffplayout`,
@ -328,7 +340,9 @@ watch([width], () => {
const horizontal = ref(false)
const deleteName = ref('')
const recursive = ref(false)
const renameOldName = ref('')
const renameOldPath = ref('')
const renameNewName = ref('')
const previewName = ref('')
const previewUrl = ref('')
@ -340,6 +354,7 @@ const showRenameModal = ref(false)
const showCreateModal = ref(false)
const showUploadModal = ref(false)
const extensions = ref('')
const extensionsArr = ref([] as string[])
const folderName = ref({} as Folder)
const inputFiles = ref([] as File[])
const fileInputName = ref()
@ -352,7 +367,7 @@ const xhr = ref(new XMLHttpRequest())
onMounted(async () => {
let config_extensions = configStore.playout.storage.extensions
let extra_extensions = configStore.channels[configStore.id].extra_extensions
let extra_extensions = configStore.channels[configStore.i].extra_extensions
if (typeof config_extensions === 'string') {
config_extensions = config_extensions.split(',')
@ -362,18 +377,18 @@ onMounted(async () => {
extra_extensions = extra_extensions.split(',')
}
const exts = [...config_extensions, ...extra_extensions].map((ext) => {
extensionsArr.value = [...config_extensions, ...extra_extensions].map((ext) => {
return `.${ext}`
})
extensions.value = exts.join(', ')
extensions.value = extensionsArr.value.join(', ')
if (!mediaStore.folderTree.parent || !mediaStore.currentPath) {
await mediaStore.getTree('')
}
})
watch([id], () => {
watch([i], () => {
mediaStore.getTree('')
})
@ -425,7 +440,7 @@ async function handleDrop(event: any, targetFolder: any, isParent: boolean | nul
}
if (source !== target) {
await fetch(`/api/file/${configStore.channels[configStore.id].id}/rename/`, {
await fetch(`/api/file/${configStore.channels[configStore.i].id}/rename/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify({ source, target }),
@ -453,7 +468,7 @@ function setPreviewData(path: string) {
}
previewName.value = fullPath.split('/').slice(-1)[0]
previewUrl.value = encodeURIComponent(`/file/${configStore.channels[configStore.id].id}${fullPath}`).replace(
previewUrl.value = encodeURIComponent(`/file/${configStore.channels[configStore.i].id}${fullPath}`).replace(
/%2F/g,
'/'
)
@ -493,10 +508,10 @@ async function deleteFileOrFolder(del: boolean) {
showDeleteModal.value = false
if (del) {
await fetch(`/api/file/${configStore.channels[configStore.id].id}/remove/`, {
await fetch(`/api/file/${configStore.channels[configStore.i].id}/remove/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify({ source: deleteName.value }),
body: JSON.stringify({ source: deleteName.value, recursive: recursive.value }),
})
.then(async (response) => {
if (response.status !== 200) {
@ -507,14 +522,19 @@ async function deleteFileOrFolder(del: boolean) {
.catch((e) => {
indexStore.msgAlert('error', `${t('media.deleteError')}: ${e}`, 5)
})
recursive.value = false
}
deleteName.value = ''
}
function setRenameValues(path: string) {
renameNewName.value = path
renameOldName.value = path
const filepath = dir_file(path)
renameOldName.value = filepath.file
renameOldPath.value = filepath.dir
renameNewName.value = filepath.file
}
async function renameFile(ren: boolean) {
@ -524,10 +544,13 @@ async function renameFile(ren: boolean) {
showRenameModal.value = false
if (ren && renameOldName.value !== renameNewName.value) {
await fetch(`/api/file/${configStore.channels[configStore.id].id}/rename/`, {
await fetch(`/api/file/${configStore.channels[configStore.i].id}/rename/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify({ source: renameOldName.value, target: renameNewName.value }),
body: JSON.stringify({
source: `${renameOldPath.value}${renameOldName.value}`,
target: `${renameOldPath.value}${renameNewName.value}`,
}),
})
.then(async (res) => {
if (res.status >= 400) {
@ -542,6 +565,7 @@ async function renameFile(ren: boolean) {
}
renameOldName.value = ''
renameOldPath.value = ''
renameNewName.value = ''
}
@ -563,7 +587,7 @@ async function createFolder(create: boolean) {
return
}
await $fetch(`/api/file/${configStore.channels[configStore.id].id}/create-folder/`, {
await $fetch(`/api/file/${configStore.channels[configStore.i].id}/create-folder/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify({ source: path }),
@ -600,7 +624,7 @@ async function upload(file: any): Promise<null | undefined> {
return new Promise((resolve) => {
xhr.value.open(
'PUT',
`/api/file/${configStore.channels[configStore.id].id}/upload/?path=${encodeURIComponent(
`/api/file/${configStore.channels[configStore.i].id}/upload/?path=${encodeURIComponent(
mediaStore.crumbs[mediaStore.crumbs.length - 1].path
)}`
)

View File

@ -227,7 +227,7 @@ const { t } = useI18n()
const authStore = useAuth()
const configStore = useConfig()
const indexStore = useIndex()
const { id } = storeToRefs(useConfig())
const { i } = storeToRefs(useConfig())
const { numberToHex, hexToNumber } = stringFormatter()
useHead({
@ -266,14 +266,14 @@ onMounted(() => {
getPreset(-1)
})
watch([id], () => {
watch([i], () => {
nextTick(() => {
getPreset(-1)
})
})
async function getPreset(index: number) {
fetch(`/api/presets/${configStore.channels[configStore.id].id}`, {
fetch(`/api/presets/${configStore.channels[configStore.i].id}`, {
method: 'GET',
headers: authStore.authHeader,
})
@ -354,10 +354,10 @@ async function savePreset() {
: form.value.boxColor + '@' + numberToHex(form.value.boxAlpha),
boxborderw: form.value.border,
alpha: form.value.overallAlpha,
channel_id: configStore.channels[configStore.id].id,
channel_id: configStore.channels[configStore.i].id,
}
const response = await fetch(`/api/presets/${configStore.channels[configStore.id].id}/${form.value.id}`, {
const response = await fetch(`/api/presets/${configStore.channels[configStore.i].id}/${form.value.id}`, {
method: 'PUT',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify(preset),
@ -393,10 +393,10 @@ async function createNewPreset(create: boolean) {
: form.value.boxColor + '@' + numberToHex(form.value.boxAlpha),
boxborderw: form.value.border.toString(),
alpha: form.value.overallAlpha.toString(),
channel_id: configStore.channels[configStore.id].id,
channel_id: configStore.channels[configStore.i].id,
}
const response = await fetch(`/api/presets/${configStore.channels[configStore.id].id}/`, {
const response = await fetch(`/api/presets/${configStore.channels[configStore.i].id}/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify(preset),
@ -417,7 +417,7 @@ async function deletePreset(del: boolean) {
showDeleteModal.value = false
if (del && selected.value && selected.value !== '') {
await fetch(`/api/presets/${configStore.channels[configStore.id].id}/${form.value.id}`, {
await fetch(`/api/presets/${configStore.channels[configStore.i].id}/${form.value.id}`, {
method: 'DELETE',
headers: authStore.authHeader,
})
@ -440,7 +440,7 @@ async function submitMessage() {
boxborderw: form.value.border.toString(),
}
const response = await fetch(`/api/control/${configStore.channels[configStore.id].id}/text/`, {
const response = await fetch(`/api/control/${configStore.channels[configStore.i].id}/text/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify(obj),

View File

@ -174,7 +174,7 @@
v-model="newSource.source"
type="text"
class="input input-sm input-bordered w-auto"
:disabled="newSource.source.includes(configStore.channels[configStore.id].storage_path)"
:disabled="newSource.source.includes(configStore.channels[configStore.i].storage)"
/>
</label>
@ -230,9 +230,11 @@
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es'
const colorMode = useColorMode()
const { locale, t } = useI18n()
const { $_, $dayjs } = useNuxtApp()
const { $dayjs } = useNuxtApp()
const { width } = useWindowSize({ initialWidth: 800 })
const { mediaType } = stringFormatter()
const { processPlaylist, genUID } = playlistOperations()
@ -314,7 +316,7 @@ function closePlayer() {
function setPreviewData(path: string) {
let fullPath = path
const storagePath = configStore.channels[configStore.id].storage_path
const storagePath = configStore.channels[configStore.i].storage
const lastIndex = storagePath.lastIndexOf('/')
if (!path.includes('/')) {
@ -330,7 +332,7 @@ function setPreviewData(path: string) {
if (path.match(/^http/)) {
previewUrl.value = path
} else {
previewUrl.value = encodeURIComponent(`/file/${configStore.channels[configStore.id].id}${fullPath}`).replace(
previewUrl.value = encodeURIComponent(`/file/${configStore.channels[configStore.i].id}${fullPath}`).replace(
/%2F/g,
'/'
)
@ -428,7 +430,7 @@ function loopClips() {
for (const item of playlistStore.playlist) {
if (length < configStore.playlistLength) {
item.uid = genUID()
tempList.push($_.cloneDeep(item))
tempList.push(cloneDeep(item))
length += item.out - item.in
} else {
break
@ -462,7 +464,7 @@ async function importPlaylist(imp: boolean) {
playlistStore.isLoading = true
await $fetch(
`/api/file/${configStore.channels[configStore.id].id}/import/?file=${textFile.value[0].name}&date=${
`/api/file/${configStore.channels[configStore.i].id}/import/?file=${textFile.value[0].name}&date=${
listDate.value
}`,
{
@ -493,13 +495,13 @@ async function savePlaylist(save: boolean) {
return
}
const saveList = processPlaylist(listDate.value, $_.cloneDeep(playlistStore.playlist), true)
const saveList = processPlaylist(listDate.value, cloneDeep(playlistStore.playlist), true)
await $fetch(`/api/playlist/${configStore.channels[configStore.id].id}/`, {
await $fetch(`/api/playlist/${configStore.channels[configStore.i].id}/`, {
method: 'POST',
headers: { ...configStore.contentType, ...authStore.authHeader },
body: JSON.stringify({
channel: configStore.channels[configStore.id].name,
channel: configStore.channels[configStore.i].name,
date: targetDate.value,
program: saveList,
}),
@ -522,7 +524,7 @@ async function deletePlaylist(del: boolean) {
showDeleteModal.value = false
if (del) {
await $fetch(`/api/playlist/${configStore.channels[configStore.id].id}/${listDate.value}`, {
await $fetch(`/api/playlist/${configStore.channels[configStore.i].id}/${listDate.value}`, {
method: 'DELETE',
headers: { ...configStore.contentType, ...authStore.authHeader },
}).then(() => {

View File

@ -1,17 +0,0 @@
import lodash from 'lodash'
import type { LoDashStatic } from 'lodash'
declare module '#app' {
interface NuxtApp {
$_: LoDashStatic
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$_: LoDashStatic
}
}
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.provide('_', lodash)
})

View File

@ -40,10 +40,10 @@ export const useAuth = defineStore('auth', {
password,
}
await $fetch<LoginObj>('/auth/login/', {
await $fetch('/auth/login/', {
method: 'POST',
body: JSON.stringify(payload),
async onResponse({ response }) {
async onResponse(response: LoginObj) {
code = response.status
},
})
@ -60,7 +60,7 @@ export const useAuth = defineStore('auth', {
},
async obtainUuid() {
await $fetch<DataAuth>('/api/generate-uuid', {
await $fetch('/api/generate-uuid', {
method: 'POST',
headers: this.authHeader,
})

View File

@ -1,9 +1,9 @@
import _ from 'lodash'
import { cloneDeep } from 'lodash-es'
import { defineStore } from 'pinia'
export const useConfig = defineStore('config', {
state: () => ({
id: 0,
i: 0,
configCount: 0,
contentType: { 'content-type': 'application/json;charset=UTF-8' },
channels: [] as Channel[],
@ -70,7 +70,7 @@ export const useConfig = defineStore('config', {
this.utcOffset = objs[0].utc_offset
this.channels = objs
this.channelsRaw = _.cloneDeep(objs)
this.channelsRaw = cloneDeep(objs)
this.configCount = objs.length
})
.catch((e) => {
@ -84,9 +84,9 @@ export const useConfig = defineStore('config', {
extra_extensions: '',
name: 'Channel 1',
preview_url: '',
hls_path: '',
playlist_path: '',
storage_path: '',
public: '',
playlists: '',
storage: '',
uts_offset: 0,
},
]
@ -95,51 +95,12 @@ export const useConfig = defineStore('config', {
})
},
async setChannelConfig(obj: Channel): Promise<any> {
const authStore = useAuth()
const stringObj = _.cloneDeep(obj)
let response
if (this.channelsRaw.some((e) => e.id === stringObj.id)) {
response = await fetch(`/api/channel/${obj.id}`, {
method: 'PATCH',
headers: { ...this.contentType, ...authStore.authHeader },
body: JSON.stringify(stringObj),
})
} else {
response = await fetch('/api/channel/', {
method: 'POST',
headers: { ...this.contentType, ...authStore.authHeader },
body: JSON.stringify(stringObj),
})
const json = await response.json()
const guiConfigs = []
for (const obj of this.channels) {
if (obj.name === stringObj.name) {
guiConfigs.push(json)
} else {
guiConfigs.push(obj)
}
}
this.channels = guiConfigs
this.channelsRaw = _.cloneDeep(guiConfigs)
this.configCount = guiConfigs.length
}
await this.getPlayoutConfig()
return response
},
async getPlayoutConfig() {
const { $i18n } = useNuxtApp()
const { timeToSeconds } = stringFormatter()
const authStore = useAuth()
const indexStore = useIndex()
const channel = this.channels[this.id].id
const channel = this.channels[this.i].id
await fetch(`/api/playout/config/${channel}`, {
method: 'GET',
@ -165,7 +126,7 @@ export const useConfig = defineStore('config', {
const { $i18n } = useNuxtApp()
const authStore = useAuth()
const indexStore = useIndex()
const channel = this.channels[this.id].id
const channel = this.channels[this.i].id
await $fetch(`/api/playout/advanced/${channel}`, {
method: 'GET',
@ -182,7 +143,7 @@ export const useConfig = defineStore('config', {
async setPlayoutConfig(obj: any) {
const { timeToSeconds } = stringFormatter()
const authStore = useAuth()
const channel = this.channels[this.id].id
const channel = this.channels[this.i].id
this.playlistLength = timeToSeconds(obj.playlist.length)
this.playout.playlist.startInSec = timeToSeconds(obj.playlist.day_start)
@ -203,7 +164,7 @@ export const useConfig = defineStore('config', {
async setAdvancedConfig() {
const authStore = useAuth()
const channel = this.channels[this.id].id
const channel = this.channels[this.i].id
const update = await fetch(`/api/playout/advanced/${channel}`, {
method: 'PUT',

View File

@ -23,7 +23,7 @@ export const useMedia = defineStore('media', {
const authStore = useAuth()
const configStore = useConfig()
const indexStore = useIndex()
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
const crumbs: Crumb[] = []
let root = '/'

View File

@ -1,4 +1,5 @@
import dayjs from 'dayjs'
import { differenceWith, isEqual, omit } from 'lodash-es'
import utc from 'dayjs/plugin/utc.js'
import timezone from 'dayjs/plugin/timezone.js'
@ -28,13 +29,13 @@ export const usePlaylist = defineStore('playlist', {
getters: {},
actions: {
async getPlaylist(date: string) {
const { $_, $i18n } = useNuxtApp()
const { $i18n } = useNuxtApp()
const authStore = useAuth()
const configStore = useConfig()
const indexStore = useIndex()
const channel = configStore.channels[configStore.id].id
const channel = configStore.channels[configStore.i].id
await $fetch<Playlist>(`/api/playlist/${channel}?date=${date}`, {
await $fetch(`/api/playlist/${channel}?date=${date}`, {
method: 'GET',
headers: authStore.authHeader,
})
@ -47,8 +48,8 @@ export const usePlaylist = defineStore('playlist', {
this.playlist.length > 0 &&
programData.length > 0 &&
(this.playlist[0].date === date || configStore.playout.playlist.infinit) &&
$_.differenceWith(this.playlist, programData, (a, b) => {
return $_.isEqual($_.omit(a, ['uid']), $_.omit(b, ['uid']))
differenceWith(this.playlist, programData, (a, b) => {
return isEqual(omit(a, ['uid']), omit(b, ['uid']))
}).length > 0
) {
indexStore.msgAlert('warning', $i18n.t('player.unsavedProgram'), 3)

View File

@ -11,6 +11,7 @@ declare global {
interface LoginObj {
message: string
status: number
user?: {
id: number
mail: string
@ -28,9 +29,9 @@ declare global {
extra_extensions: string | string[]
name: string
preview_url: string
hls_path: string
playlist_path: string
storage_path: string
public: string
playlists: string
storage: string
uts_offset?: number
}

View File

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

View File

@ -39,8 +39,8 @@ name = "api_routes"
path = "src/api_routes.rs"
[[test]]
name = "lib_utils"
path = "src/lib_utils.rs"
name = "utils"
path = "src/utils.rs"
[[test]]
name = "engine_playlist"

View File

@ -22,8 +22,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager, Pool<Sqlite>) {
sqlx::query(
r#"
UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE global SET public = "assets/hls", logs = "assets/log", playlists = "assets/playlists", storage = "assets/storage";
UPDATE channels SET public = "assets/hls", playlists = "assets/playlists", storage = "assets/storage";
"#,
)
.execute(&pool)

View File

@ -22,8 +22,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query(
r#"
UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE global SET public = "assets/hls", logs = "assets/log", playlists = "assets/playlists", storage = "assets/storage";
UPDATE channels SET public = "assets/hls", playlists = "assets/playlists", storage = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#,
)

View File

@ -25,8 +25,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query(
r#"
UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE global SET public = "assets/hls", logs = "assets/log", playlists = "assets/playlists", storages = "assets/storage";
UPDATE channels SET public = "assets/hls", playlists = "assets/playlists", storage = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#,
)
@ -104,7 +104,7 @@ fn test_generate_playlist_from_folder() {
config.processing.mode = Playlist;
config.storage.filler = "assets/".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
let playlist = generate_playlist(manager);
@ -149,7 +149,7 @@ fn test_generate_playlist_from_template() {
config.processing.mode = Playlist;
config.storage.filler = "assets/".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
let playlist = generate_playlist(manager);

View File

@ -24,8 +24,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query(
r#"
UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE global SET public = "assets/hls", logs = "assets/log", playlists = "assets/playlists", storage = "assets/storage";
UPDATE channels SET public = "assets/hls", playlists = "assets/playlists", storage = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#,
)
@ -67,7 +67,7 @@ fn test_gen_source() {
config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
let mut valid_source_with_probe = Media::new(0, "assets/media_mix/av_sync.mp4", true);
@ -114,7 +114,7 @@ fn playlist_missing() {
config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null;
config.output.output_count = 1;
@ -148,7 +148,7 @@ fn playlist_next_missing() {
config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null;
config.output.output_count = 1;
@ -182,7 +182,7 @@ fn playlist_to_short() {
config.playlist.start_sec = Some(21600.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null;
config.output.output_count = 1;
@ -216,7 +216,7 @@ fn playlist_init_after_list_end() {
config.playlist.start_sec = Some(21600.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null;
config.output.output_count = 1;
@ -250,7 +250,7 @@ fn playlist_change_at_midnight() {
config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null;
config.output.output_count = 1;
@ -284,7 +284,7 @@ fn playlist_change_before_midnight() {
config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null;
config.output.output_count = 1;
@ -318,7 +318,7 @@ fn playlist_change_at_six() {
config.playlist.start_sec = Some(21600.0);
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.channel.playlist_path = "assets/playlists".into();
config.channel.playlists = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null;
config.output.output_count = 1;

View File

@ -18,8 +18,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query(
r#"
UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE global SET public = "assets/hls", logs = "assets/log", playlists = "assets/playlists", storage = "assets/storage";
UPDATE channels SET public = "assets/hls", playlists = "assets/playlists", storage = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#,
)