Merge branch 'master' into title_support

This commit is contained in:
jb-alvarado 2024-04-30 14:59:44 +00:00 committed by GitHub
commit 65e541fd25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 1087 additions and 405 deletions

1
.gitignore vendored
View File

@ -25,5 +25,4 @@ ffpapi.1.gz
/dist/ /dist/
/public/ /public/
tmp/ tmp/
.vscode/
assets/playlist_template.json assets/playlist_template.json

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"statiolake.vscode-rustfmt",
"tamasfe.even-better-toml",
]
}

26
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
"rust-analyzer.cargo.target": null,
"rust-analyzer.checkOnSave": true,
"rust-analyzer.cargo.buildScripts.overrideCommand": null,
"rust-analyzer.rustfmt.overrideCommand": null,
"rust-analyzer.inlayHints.chainingHints.enable": false,
"rust-analyzer.inlayHints.parameterHints.enable": false,
"rust-analyzer.inlayHints.typeHints.enable": false,
"rust-analyzer.diagnostics.disabled": ["unresolved-proc-macro"],
"[dockercompose]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[rust]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "statiolake.vscode-rustfmt"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.words": [
"actix",
"rsplit",
"tokio",
"uuids"
]
}

154
Cargo.lock generated
View File

@ -267,6 +267,55 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "actix-web-lab"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7675c1a84eec1b179c844cdea8488e3e409d8e4984026e92fa96c87dd86f33c6"
dependencies = [
"actix-http",
"actix-router",
"actix-service",
"actix-utils",
"actix-web",
"actix-web-lab-derive",
"ahash",
"arc-swap",
"async-trait",
"bytes",
"bytestring",
"csv",
"derive_more",
"futures-core",
"futures-util",
"http 0.2.12",
"impl-more",
"itertools",
"local-channel",
"mediatype",
"mime",
"once_cell",
"pin-project-lite",
"regex",
"serde",
"serde_html_form",
"serde_json",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "actix-web-lab-derive"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa0b287c8de4a76b691f29dbb5451e8dd5b79d777eaf87350c9b0cbfdb5e968"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]] [[package]]
name = "actix-web-static-files" name = "actix-web-static-files"
version = "4.0.1" version = "4.0.1"
@ -406,6 +455,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e"
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]] [[package]]
name = "argon2" name = "argon2"
version = "0.5.3" version = "0.5.3"
@ -519,7 +574,7 @@ dependencies = [
"futures-lite 2.3.0", "futures-lite 2.3.0",
"parking", "parking",
"polling 3.6.0", "polling 3.6.0",
"rustix 0.38.32", "rustix 0.38.33",
"slab", "slab",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -981,6 +1036,27 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.8" version = "0.20.8"
@ -1217,7 +1293,7 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
[[package]] [[package]]
name = "ffplayout" name = "ffplayout"
version = "0.21.2" version = "0.22.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -1239,13 +1315,14 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-api" name = "ffplayout-api"
version = "0.21.2" version = "0.22.0"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-multipart", "actix-multipart",
"actix-web", "actix-web",
"actix-web-grants", "actix-web-grants",
"actix-web-httpauth", "actix-web-httpauth",
"actix-web-lab",
"actix-web-static-files", "actix-web-static-files",
"argon2", "argon2",
"chrono", "chrono",
@ -1259,6 +1336,7 @@ dependencies = [
"lexical-sort", "lexical-sort",
"local-ip-address", "local-ip-address",
"once_cell", "once_cell",
"parking_lot",
"path-clean", "path-clean",
"rand", "rand",
"regex", "regex",
@ -1274,11 +1352,13 @@ dependencies = [
"static-files", "static-files",
"sysinfo", "sysinfo",
"tokio", "tokio",
"tokio-stream",
"uuid",
] ]
[[package]] [[package]]
name = "ffplayout-lib" name = "ffplayout-lib"
version = "0.21.2" version = "0.22.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"crossbeam-channel", "crossbeam-channel",
@ -1821,6 +1901,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "impl-more"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.6" version = "2.2.6"
@ -2096,6 +2182,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "mediatype"
version = "0.19.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8878cd8d1b3c8c8ae4b2ba0a36652b7cf192f618a599a7fbdfa25cffd4ea72dd"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.2" version = "2.7.2"
@ -2519,7 +2611,7 @@ dependencies = [
"concurrent-queue", "concurrent-queue",
"hermit-abi", "hermit-abi",
"pin-project-lite", "pin-project-lite",
"rustix 0.38.32", "rustix 0.38.33",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -2804,9 +2896,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.32" version = "0.38.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"errno", "errno",
@ -2896,12 +2988,27 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "scc"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96560eea317a9cc4e0bb1f6a2c93c09a19b8c4fc5cb3fcc0ec1c094cd783e2"
dependencies = [
"sdd",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdd"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.22" version = "1.0.22"
@ -2928,6 +3035,19 @@ dependencies = [
"syn 2.0.60", "syn 2.0.60",
] ]
[[package]]
name = "serde_html_form"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5"
dependencies = [
"form_urlencoded",
"indexmap",
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.116" version = "1.0.116"
@ -2975,23 +3095,23 @@ dependencies = [
[[package]] [[package]]
name = "serial_test" name = "serial_test"
version = "3.0.0" version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" checksum = "adb86f9315df5df6a70eae0cc22395a44e544a0d8897586820770a35ede74449"
dependencies = [ dependencies = [
"dashmap",
"futures", "futures",
"lazy_static",
"log", "log",
"once_cell",
"parking_lot", "parking_lot",
"scc",
"serial_test_derive", "serial_test_derive",
] ]
[[package]] [[package]]
name = "serial_test_derive" name = "serial_test_derive"
version = "3.0.0" version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" checksum = "a9bb72430492e9549b0c4596725c0f82729bff861c45aa8099c0a8e67fc3b721"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3034,9 +3154,9 @@ checksum = "b2a4eed4c5ae38438470ab8e0108bb751012f786f44ff585cfd837c9a5fe426f"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -3443,7 +3563,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand 2.0.2", "fastrand 2.0.2",
"rustix 0.38.32", "rustix 0.38.33",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -3458,7 +3578,7 @@ dependencies = [
[[package]] [[package]]
name = "tests" name = "tests"
version = "0.21.2" version = "0.22.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"crossbeam-channel", "crossbeam-channel",

View File

@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.21.2" version = "0.22.0"
license = "GPL-3.0" license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout" repository = "https://github.com/ffplayout/ffplayout"
authors = ["Jonathan Baecker <jonbae77@gmail.com>"] authors = ["Jonathan Baecker <jonbae77@gmail.com>"]

41
Cross.toml Normal file
View File

@ -0,0 +1,41 @@
[target.x86_64-unknown-linux-musl]
pre-build = [
"apt-get update",
"apt-get --assume-yes install curl",
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
"apt-get --assume-yes install nodejs"
]
[target.aarch64-unknown-linux-gnu]
pre-build = [
"apt-get update",
"apt-get --assume-yes install curl",
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
"apt-get --assume-yes install nodejs"
]
[target.x86_64-pc-windows-gnu]
pre-build = [
"apt-get update",
"apt-get --assume-yes install curl",
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
"apt-get --assume-yes install nodejs"
]
[target.x86_64-apple-darwin]
image = "ghcr.io/cross-rs/x86_64-apple-darwin-cross:local"
pre-build = [
"apt-get update",
"apt-get --assume-yes install curl",
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
"apt-get --assume-yes install nodejs"
]
[target.aarch64-apple-darwin]
image = "ghcr.io/cross-rs/aarch64-apple-darwin-cross:local"
pre-build = [
"apt-get update",
"apt-get --assume-yes install curl",
"curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
"apt-get --assume-yes install nodejs"
]

View File

@ -176,19 +176,17 @@ Output from `{"media":"current"}` show:
```JSON ```JSON
{ {
"current_media": { "media": {
"category": "", "category": "",
"duration": 154.2, "duration": 154.2,
"out": 154.2, "out": 154.2,
"seek": 0.0, "in": 0.0,
"source": "/opt/tv-media/clip.mp4" "source": "/opt/tv-media/clip.mp4"
}, },
"index": 39, "index": 39,
"play_mode": "playlist", "mode": "playlist",
"played_sec": 67.80771999300123, "ingest": false,
"remaining_sec": 86.39228000699876, "played": 67.80771999300123,
"start_sec": 24713.631999999998,
"start_time": "06:51:53.631"
} }
``` ```

View File

@ -22,7 +22,7 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 36000s; proxy_read_timeout 36000s;
proxy_connect_timeout 36000s; proxy_connect_timeout 36000s;
proxy_send_timeout 36000s; proxy_send_timeout 36000s;
proxy_buffer_size 128k; proxy_buffer_size 128k;
proxy_buffers 4 256k; proxy_buffers 4 256k;
@ -31,6 +31,16 @@ server {
proxy_pass http://127.0.0.1:8787; proxy_pass http://127.0.0.1:8787;
} }
location /data {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:8787/data;
}
location /live/ { location /live/ {
alias /usr/share/ffplayout/public/live/; alias /usr/share/ffplayout/public/live/;
} }

View File

@ -2,24 +2,6 @@
For compiling use always the news Rust version, the best is to install it from [rustup](https://rustup.rs/). For compiling use always the news Rust version, the best is to install it from [rustup](https://rustup.rs/).
### Cross Compile
For cross compiling on fedora linux, you need to install some extra packages:
- mingw compiler:
```
dnf install mingw64-filesystem mingw64-binutils mingw64-gcc{,-c++} mingw64-crt mingw64-headers mingw64-pkg-config mingw64-hamlib mingw64-libpng mingw64-libusbx mingw64-portaudio mingw64-fltk mingw64-libgnurx mingw64-gettext mingw64-winpthreads-static intltool
```
- rust tools:
```
rustup target add x86_64-pc-windows-gnu
```
[Cross](https://github.com/cross-rs/cross#dependencies) could be an option to.
To build, run: `cargo build --release --target=x86_64-pc-windows-gnu`
### Static Linking ### Static Linking
Running `cargo build` ends up in a binary which depend on **libc.so**. But you can compile also the binary totally static: Running `cargo build` ends up in a binary which depend on **libc.so**. But you can compile also the binary totally static:
@ -33,109 +15,55 @@ Compile with: `cargo build --release --target=x86_64-unknown-linux-musl`.
This release should run on any Linux distro. This release should run on any Linux distro.
**Note: You can also create a static version with Cross Toolchain. For this, follow the next steps.**
### Cross Compile
For cross compiling install docker or podman and latest [cross-rs](https://github.com/cross-rs/cross):
```
cargo install cross --git https://github.com/cross-rs/cross
```
To build for windows, run: `cross build --release --target x86_64-pc-windows-gnu`\
To build for linux aarch64: `cross build --release --target aarch64-unknown-linux-gnu`
Etc.
### Compile from Linux for macOS ### Compile from Linux for macOS
Add toolchain: Follow [cross-toolchains](https://github.com/cross-rs/cross-toolchains) instruction to add macOS support to cross.
```Bash I created my image with:
# for arm64
rustup target add aarch64-apple-darwin
# for x86_64 ```
rustup target add x86_64-apple-darwin cargo build-docker-image x86_64-apple-darwin-cross \
--build-arg 'MACOS_SDK_URL=https://github.com/joseluisq/macosx-sdks/releases/download/12.3/MacOSX12.3.sdk.tar.xz'
``` ```
Add linker and ar settings to `~/.cargo/config`: Build then with:
```Bash ```
[target.x86_64-apple-darwin] cross build --release --target aarch64-apple-darwin
linker = "x86_64-apple-darwin20.4-clang"
ar = "x86_64-apple-darwin20.4-ar"
[target.aarch64-apple-darwin]
linker = "aarch64-apple-darwin20.4-clang"
ar = "aarch64-apple-darwin20.4-ar"
``` ```
Follow this guide: [rust-cross-compile-linux-to-macos](https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html)
Or setup [osxcross](https://github.com/tpoechtrager/osxcross) correctly.
Add **osxcross/target/bin** to your **PATH** and run cargo with:
```Bash
# for arm64
CC="aarch64-apple-darwin20.4-clang -arch arm64e" cargo build --release --target=aarch64-apple-darwin
# for x86_64
CC="o64-clang" cargo build --release --target=x86_64-apple-darwin
``` ```
### Compile for armv7 Linux
Add toolchain:
```Bash
rustup target add armv7-unknown-linux-gnueabihf
```
Add cross compiler:
```Bash
dnf copr enable lantw44/arm-linux-gnueabihf-toolchain
dnf install arm-linux-gnueabihf-{binutils,gcc,glibc}
```
Add target to `~/.cargo/config`:
```Bash
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
rustflags = [ "-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc" ]
```
### Compile for aarch64 Linux
Add toolchain:
```Bash
rustup target add aarch64-unknown-linux-gnu
```
Add cross compiler:
```Bash
dnf copr enable lantw44/aarch64-linux-gnu-toolchain
dnf install aarch64-linux-gnu-{binutils,gcc,glibc}
```
Add target to `~/.cargo/config`:
```Bash
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = [ "-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc" ]
```
### Create debian DEB and RHEL RPM packages ### Create debian DEB and RHEL RPM packages
install: install:
- `cargo install cargo-deb` - `cargo install cargo-deb`
- `cargo install cargo-generate-rpm` - `cargo install cargo-generate-rpm`
And run with: Compile to your target system with cargo or cross, and run:
```Bash ```Bash
# for debian based systems: # for debian based systems:
cargo deb --target=x86_64-unknown-linux-musl cargo deb --no-build --target=x86_64-unknown-linux-musl
# for armhf # for armhf
cargo deb --target=armv7-unknown-linux-gnueabihf --variant=armhf -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml cargo deb --no-build --target=armv7-unknown-linux-gnueabihf --variant=armhf -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml
# for arm64 # for arm64
cargo deb --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml cargo deb --no-build --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml
# for rhel based systems: # for rhel based systems:
cargo generate-rpm --target=x86_64-unknown-linux-musl cargo generate-rpm --target=x86_64-unknown-linux-musl

View File

@ -19,6 +19,7 @@ actix-multipart = "0.6"
actix-web = "4" actix-web = "4"
actix-web-grants = "4" actix-web-grants = "4"
actix-web-httpauth = "0.8" actix-web-httpauth = "0.8"
actix-web-lab = "0.20"
actix-web-static-files = "4.0" actix-web-static-files = "4.0"
argon2 = "0.5" argon2 = "0.5"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
@ -31,6 +32,7 @@ lazy_static = "1.4"
lexical-sort = "0.3" lexical-sort = "0.3"
local-ip-address = "0.6" local-ip-address = "0.6"
once_cell = "1.18" once_cell = "1.18"
parking_lot = "0.12"
path-clean = "1.0" path-clean = "1.0"
rand = "0.8" rand = "0.8"
regex = "1" regex = "1"
@ -46,6 +48,8 @@ static-files = "0.2"
sysinfo ={ version = "0.30", features = ["linux-netdevs"] } sysinfo ={ version = "0.30", features = ["linux-netdevs"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1.29", features = ["full"] } tokio = { version = "1.29", features = ["full"] }
tokio-stream = "0.1"
uuid = "1.8"
[build-dependencies] [build-dependencies]
static-files = "0.2" static-files = "0.2"

View File

@ -0,0 +1,80 @@
/// Example for a simple auth mechanism in SSE.
///
/// get new UUID: curl -X GET http://127.0.0.1:8080/generate
/// use UUID: curl --header "UUID: f2f8c29b-712a-48c5-8919-b535d3a05a3a" -X GET http://127.0.0.1:8080/check
///
use std::{collections::HashSet, sync::Mutex, time::Duration, time::SystemTime};
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpResponse, HttpServer};
use simplelog::*;
use uuid::Uuid;
use ffplayout_lib::utils::{init_logging, PlayoutConfig};
#[derive(Debug, Eq, Hash, PartialEq)]
struct UuidData {
uuid: Uuid,
expiration_time: SystemTime,
}
struct AppState {
uuids: Mutex<HashSet<UuidData>>,
}
fn prune_uuids(uuids: &mut HashSet<UuidData>) {
uuids.retain(|entry| entry.expiration_time > SystemTime::now());
}
async fn generate_uuid(data: web::Data<AppState>) -> HttpResponse {
let uuid = Uuid::new_v4();
let expiration_time = SystemTime::now() + Duration::from_secs(30); // 24 * 3600 -> for 24 hours
let mut uuids = data.uuids.lock().unwrap();
prune_uuids(&mut uuids);
uuids.insert(UuidData {
uuid,
expiration_time,
});
HttpResponse::Ok().body(uuid.to_string())
}
async fn check_uuid(data: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let uuid = req.headers().get("uuid").unwrap().to_str().unwrap();
let uuid_from_client = Uuid::parse_str(uuid).unwrap();
let mut uuids = data.uuids.lock().unwrap();
prune_uuids(&mut uuids);
match uuids.iter().find(|entry| entry.uuid == uuid_from_client) {
Some(_) => HttpResponse::Ok().body("UUID is valid"),
None => HttpResponse::Unauthorized().body("Invalid or expired UUID"),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let mut config = PlayoutConfig::new(None, None);
config.mail.recipient = String::new();
config.logging.log_to_file = false;
config.logging.timestamp = false;
let logging = init_logging(&config, None, None);
CombinedLogger::init(logging).unwrap();
let state = web::Data::new(AppState {
uuids: Mutex::new(HashSet::new()),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.wrap(Logger::default())
.route("/generate", web::get().to(generate_uuid))
.route("/check", web::get().to(check_uuid))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

View File

@ -247,7 +247,7 @@ async fn get_user(
/// ``` /// ```
#[get("/user/{name}")] #[get("/user/{name}")]
#[protect("Role::Admin", ty = "Role")] #[protect("Role::Admin", ty = "Role")]
async fn get_user_by_name( async fn get_by_name(
pool: web::Data<Pool<Sqlite>>, pool: web::Data<Pool<Sqlite>>,
name: web::Path<String>, name: web::Path<String>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
@ -327,7 +327,7 @@ async fn update_user(
return Err(ServiceError::InternalServerError); return Err(ServiceError::InternalServerError);
} }
Err(ServiceError::Unauthorized) Err(ServiceError::Unauthorized("No Permission".to_string()))
} }
/// **Add User** /// **Add User**
@ -652,7 +652,9 @@ pub async fn send_text_message(
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<HashMap<String, String>>, data: web::Json<HashMap<String, String>>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match send_message(&pool.into_inner(), *id, data.into_inner()).await { let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
match send_message(&config, data.into_inner()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -675,7 +677,9 @@ pub async fn control_playout(
id: web::Path<i32>, id: web::Path<i32>,
control: web::Json<ControlParams>, control: web::Json<ControlParams>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match control_state(&pool.into_inner(), *id, &control.control).await { let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
match control_state(&config, &control.control).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -691,25 +695,19 @@ pub async fn control_playout(
/// **Response:** /// **Response:**
/// ///
/// ```JSON /// ```JSON
/// { /// {
/// "jsonrpc": "2.0", /// "media": {
/// "result": {
/// "current_media": {
/// "category": "", /// "category": "",
/// "duration": 154.2, /// "duration": 154.2,
/// "out": 154.2, /// "out": 154.2,
/// "seek": 0.0, /// "in": 0.0,
/// "source": "/opt/tv-media/clip.mp4" /// "source": "/opt/tv-media/clip.mp4"
/// }, /// },
/// "index": 39, /// "index": 39,
/// "play_mode": "playlist", /// "ingest": false,
/// "played_sec": 67.80771999300123, /// "mode": "playlist",
/// "remaining_sec": 86.39228000699876, /// "played": 67.808
/// "start_sec": 24713.631999999998, /// }
/// "start_time": "06:51:53.631"
/// },
/// "id": 1
/// }
/// ``` /// ```
#[get("/control/{id}/media/current")] #[get("/control/{id}/media/current")]
#[protect(any("Role::Admin", "Role::User"), ty = "Role")] #[protect(any("Role::Admin", "Role::User"), ty = "Role")]
@ -717,7 +715,9 @@ pub async fn media_current(
pool: web::Data<Pool<Sqlite>>, pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match media_info(&pool.into_inner(), *id, "current".into()).await { let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
match media_info(&config, "current".into()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -734,7 +734,9 @@ pub async fn media_next(
pool: web::Data<Pool<Sqlite>>, pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match media_info(&pool.into_inner(), *id, "next".into()).await { let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
match media_info(&config, "next".into()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -752,7 +754,9 @@ pub async fn media_last(
pool: web::Data<Pool<Sqlite>>, pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match media_info(&pool.into_inner(), *id, "last".into()).await { let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
match media_info(&config, "last".into()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -779,7 +783,16 @@ pub async fn process_control(
proc: web::Json<Process>, proc: web::Json<Process>,
engine_process: web::Data<ProcessControl>, engine_process: web::Data<ProcessControl>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
control_service(&pool.into_inner(), *id, &proc.command, Some(engine_process)).await let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
control_service(
&pool.into_inner(),
&config,
*id,
&proc.command,
Some(engine_process),
)
.await
} }
/// #### ffplayout Playlist Operations /// #### ffplayout Playlist Operations

View File

@ -42,6 +42,7 @@ impl LoginUser {
Self { id, username } Self { id, username }
} }
} }
#[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)] #[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)]
pub struct TextPreset { pub struct TextPreset {
#[sqlx(default)] #[sqlx(default)]

21
ffplayout-api/src/lib.rs Normal file
View File

@ -0,0 +1,21 @@
use std::sync::{Arc, Mutex};
use clap::Parser;
use lazy_static::lazy_static;
use sysinfo::{Disks, Networks, System};
pub mod api;
pub mod db;
pub mod sse;
pub mod utils;
use utils::args_parse::Args;
lazy_static! {
pub static ref ARGS: Args = Args::parse();
pub static ref DISKS: Arc<Mutex<Disks>> =
Arc::new(Mutex::new(Disks::new_with_refreshed_list()));
pub static ref NETWORKS: Arc<Mutex<Networks>> =
Arc::new(Mutex::new(Networks::new_with_refreshed_list()));
pub static ref SYS: Arc<Mutex<System>> = Arc::new(Mutex::new(System::new_all()));
}

View File

@ -1,8 +1,4 @@
use std::{ use std::{collections::HashSet, env, process::exit, sync::Arc};
env,
process::exit,
sync::{Arc, Mutex},
};
use actix_files::Files; use actix_files::Files;
use actix_web::{ use actix_web::{
@ -14,37 +10,26 @@ use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthent
#[cfg(all(not(debug_assertions), feature = "embed_frontend"))] #[cfg(all(not(debug_assertions), feature = "embed_frontend"))]
use actix_web_static_files::ResourceFiles; use actix_web_static_files::ResourceFiles;
use clap::Parser;
use lazy_static::lazy_static;
use path_clean::PathClean; use path_clean::PathClean;
use simplelog::*; use simplelog::*;
use sysinfo::{Disks, Networks, System}; use tokio::sync::Mutex;
pub mod api; use ffplayout_api::{
pub mod db; api::{auth, routes::*},
pub mod utils; db::{db_pool, models::LoginUser},
sse::{broadcast::Broadcaster, routes::*, AuthState},
use api::{auth, routes::*}; utils::{control::ProcessControl, db_path, init_config, run_args},
use db::{db_pool, models::LoginUser}; ARGS,
use utils::{args_parse::Args, control::ProcessControl, db_path, init_config, run_args}; };
#[cfg(any(debug_assertions, not(feature = "embed_frontend")))] #[cfg(any(debug_assertions, not(feature = "embed_frontend")))]
use utils::public_path; use ffplayout_api::utils::public_path;
use ffplayout_lib::utils::{init_logging, PlayoutConfig}; use ffplayout_lib::utils::{init_logging, PlayoutConfig};
#[cfg(all(not(debug_assertions), feature = "embed_frontend"))] #[cfg(all(not(debug_assertions), feature = "embed_frontend"))]
include!(concat!(env!("OUT_DIR"), "/generated.rs")); include!(concat!(env!("OUT_DIR"), "/generated.rs"));
lazy_static! {
pub static ref ARGS: Args = Args::parse();
pub static ref DISKS: Arc<Mutex<Disks>> =
Arc::new(Mutex::new(Disks::new_with_refreshed_list()));
pub static ref NETWORKS: Arc<Mutex<Networks>> =
Arc::new(Mutex::new(Networks::new_with_refreshed_list()));
pub static ref SYS: Arc<Mutex<System>> = Arc::new(Mutex::new(System::new_all()));
}
async fn validator( async fn validator(
req: ServiceRequest, req: ServiceRequest,
credentials: BearerAuth, credentials: BearerAuth,
@ -65,7 +50,7 @@ async fn validator(
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.mail.recipient = String::new(); config.mail.recipient = String::new();
config.logging.log_to_file = false; config.logging.log_to_file = false;
config.logging.timestamp = false; config.logging.timestamp = false;
@ -95,6 +80,10 @@ async fn main() -> std::io::Result<()> {
let addr = ip_port[0]; let addr = ip_port[0];
let port = ip_port[1].parse::<u16>().unwrap(); let port = ip_port[1].parse::<u16>().unwrap();
let engine_process = web::Data::new(ProcessControl::new()); let engine_process = web::Data::new(ProcessControl::new());
let auth_state = web::Data::new(AuthState {
uuids: Mutex::new(HashSet::new()),
});
let broadcast_data = Broadcaster::create();
info!("running ffplayout API, listen on http://{conn}"); info!("running ffplayout API, listen on http://{conn}");
@ -109,14 +98,16 @@ async fn main() -> std::io::Result<()> {
let mut web_app = App::new() let mut web_app = App::new()
.app_data(db_pool) .app_data(db_pool)
.app_data(engine_process.clone()) .app_data(engine_process.clone())
.app_data(auth_state.clone())
.app_data(web::Data::from(Arc::clone(&broadcast_data)))
.wrap(logger) .wrap(logger)
.service(login) .service(login)
.service( .service(
web::scope("/api") web::scope("/api")
.wrap(auth) .wrap(auth.clone())
.service(add_user) .service(add_user)
.service(get_user) .service(get_user)
.service(get_user_by_name) .service(get_by_name)
.service(get_users) .service(get_users)
.service(remove_user) .service(remove_user)
.service(get_playout_config) .service(get_playout_config)
@ -149,7 +140,13 @@ async fn main() -> std::io::Result<()> {
.service(save_file) .service(save_file)
.service(import_playlist) .service(import_playlist)
.service(get_program) .service(get_program)
.service(get_system_stat), .service(get_system_stat)
.service(generate_uuid),
)
.service(
web::scope("/data")
.service(validate_uuid)
.service(event_stream),
) )
.service(get_file); .service(get_file);

View File

@ -0,0 +1,160 @@
use std::{sync::Arc, time::Duration};
use actix_web::{rt::time::interval, web};
use actix_web_lab::{
sse::{self, Sse},
util::InfallibleStream,
};
use ffplayout_lib::utils::PlayoutConfig;
use parking_lot::Mutex;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use crate::utils::{control::media_info, system};
#[derive(Debug, Clone)]
struct Client {
_channel: i32,
config: PlayoutConfig,
endpoint: String,
sender: mpsc::Sender<sse::Event>,
}
impl Client {
fn new(
_channel: i32,
config: PlayoutConfig,
endpoint: String,
sender: mpsc::Sender<sse::Event>,
) -> Self {
Self {
_channel,
config,
endpoint,
sender,
}
}
}
pub struct Broadcaster {
inner: Mutex<BroadcasterInner>,
}
#[derive(Debug, Clone, Default)]
struct BroadcasterInner {
clients: Vec<Client>,
}
impl Broadcaster {
/// Constructs new broadcaster and spawns ping loop.
pub fn create() -> Arc<Self> {
let this = Arc::new(Broadcaster {
inner: Mutex::new(BroadcasterInner::default()),
});
Broadcaster::spawn_ping(Arc::clone(&this));
this
}
/// Pings clients every 10 seconds to see if they are alive and remove them from the broadcast
/// list if not.
fn spawn_ping(this: Arc<Self>) {
actix_web::rt::spawn(async move {
let mut interval = interval(Duration::from_secs(1));
let mut counter = 0;
loop {
interval.tick().await;
if counter % 10 == 0 {
this.remove_stale_clients().await;
}
this.broadcast_playout().await;
this.broadcast_system().await;
counter = (counter + 1) % 61;
}
});
}
/// Removes all non-responsive clients from broadcast list.
async fn remove_stale_clients(&self) {
let clients = self.inner.lock().clients.clone();
let mut ok_clients = Vec::new();
for client in clients {
if client
.sender
.send(sse::Event::Comment("ping".into()))
.await
.is_ok()
{
ok_clients.push(client.clone());
}
}
self.inner.lock().clients = ok_clients;
}
/// Registers client with broadcaster, returning an SSE response body.
pub async fn new_client(
&self,
channel: i32,
config: PlayoutConfig,
endpoint: String,
) -> Sse<InfallibleStream<ReceiverStream<sse::Event>>> {
let (tx, rx) = mpsc::channel(10);
tx.send(sse::Data::new("connected").into()).await.unwrap();
self.inner
.lock()
.clients
.push(Client::new(channel, config, endpoint, tx));
Sse::from_infallible_receiver(rx)
}
/// Broadcasts playout status to clients.
pub async fn broadcast_playout(&self) {
let clients = self.inner.lock().clients.clone();
for client in clients.iter().filter(|client| client.endpoint == "playout") {
match media_info(&client.config, "current".into()).await {
Ok(res) => {
let _ = client
.sender
.send(
sse::Data::new(res.text().await.unwrap_or_else(|_| "Success".into()))
.into(),
)
.await;
}
Err(_) => {
let _ = client
.sender
.send(sse::Data::new("not running").into())
.await;
}
};
}
}
/// Broadcasts system status to clients.
pub async fn broadcast_system(&self) {
let clients = self.inner.lock().clients.clone();
for client in clients {
if &client.endpoint == "system" {
if let Ok(stat) = web::block(move || system::stat(client.config.clone())).await {
let stat_string = stat.to_string();
let _ = client.sender.send(sse::Data::new(stat_string).into()).await;
};
}
}
}
}

View File

@ -0,0 +1,55 @@
use std::{
collections::HashSet,
time::{Duration, SystemTime},
};
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::utils::errors::ServiceError;
pub mod broadcast;
pub mod routes;
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy)]
pub struct UuidData {
pub uuid: Uuid,
pub expiration: SystemTime,
}
impl UuidData {
pub fn new() -> Self {
Self {
uuid: Uuid::new_v4(),
expiration: SystemTime::now() + Duration::from_secs(2 * 3600), // 2 hours
}
}
}
impl Default for UuidData {
fn default() -> Self {
Self::new()
}
}
pub struct AuthState {
pub uuids: Mutex<HashSet<UuidData>>,
}
/// Remove all UUIDs from HashSet which are older the expiration time.
pub fn prune_uuids(uuids: &mut HashSet<UuidData>) {
uuids.retain(|entry| entry.expiration > SystemTime::now());
}
pub fn check_uuid(uuids: &mut HashSet<UuidData>, uuid: &str) -> Result<&'static str, ServiceError> {
let client_uuid = Uuid::parse_str(uuid)?;
prune_uuids(uuids);
match uuids.iter().find(|entry| entry.uuid == client_uuid) {
Some(_) => Ok("UUID is valid"),
None => Err(ServiceError::Unauthorized(
"Invalid or expired UUID".to_string(),
)),
}
}

View File

@ -0,0 +1,82 @@
use actix_web::{get, post, web, Responder};
use actix_web_grants::proc_macro::protect;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite};
use super::{check_uuid, prune_uuids, AuthState, UuidData};
use crate::sse::broadcast::Broadcaster;
use crate::utils::{errors::ServiceError, playout_config, Role};
#[derive(Deserialize, Serialize)]
struct User {
#[serde(default, skip_serializing)]
endpoint: String,
uuid: String,
}
impl User {
fn new(endpoint: String, uuid: String) -> Self {
Self { endpoint, uuid }
}
}
/// **Get generated UUID**
///
/// ```BASH
/// curl -X GET 'http://127.0.0.1:8787/api/generate-uuid' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/generate-uuid")]
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
async fn generate_uuid(data: web::Data<AuthState>) -> Result<impl Responder, ServiceError> {
let mut uuids = data.uuids.lock().await;
let new_uuid = UuidData::new();
let user_auth = User::new(String::new(), new_uuid.uuid.to_string());
prune_uuids(&mut uuids);
uuids.insert(new_uuid);
Ok(web::Json(user_auth))
}
/// **Validate UUID**
///
/// ```BASH
/// curl -X GET 'http://127.0.0.1:8787/data/validate?uuid=f2f8c29b-712a-48c5-8919-b535d3a05a3a'
/// ```
#[get("/validate")]
async fn validate_uuid(
data: web::Data<AuthState>,
user: web::Query<User>,
) -> Result<impl Responder, ServiceError> {
let mut uuids = data.uuids.lock().await;
match check_uuid(&mut uuids, user.uuid.as_str()) {
Ok(s) => Ok(web::Json(s)),
Err(e) => Err(e),
}
}
/// **Connect to event handler**
///
/// ```BASH
/// curl -X GET 'http://127.0.0.1:8787/data/event/1?endpoint=system&uuid=f2f8c29b-712a-48c5-8919-b535d3a05a3a'
/// ```
#[get("/event/{channel}")]
async fn event_stream(
pool: web::Data<Pool<Sqlite>>,
broadcaster: web::Data<Broadcaster>,
data: web::Data<AuthState>,
id: web::Path<i32>,
user: web::Query<User>,
) -> Result<impl Responder, ServiceError> {
let mut uuids = data.uuids.lock().await;
check_uuid(&mut uuids, user.uuid.as_str())?;
let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
Ok(broadcaster
.new_client(*id, config, user.endpoint.clone())
.await)
}

View File

@ -12,6 +12,7 @@ use crate::utils::{
use ffplayout_lib::utils::PlayoutConfig; use ffplayout_lib::utils::PlayoutConfig;
use crate::db::{handles, models::Channel}; use crate::db::{handles, models::Channel};
use crate::utils::playout_config;
pub async fn create_channel( pub async fn create_channel(
conn: &Pool<Sqlite>, conn: &Pool<Sqlite>,
@ -31,9 +32,10 @@ pub async fn create_channel(
Err(_) => rand::thread_rng().gen_range(71..99), Err(_) => rand::thread_rng().gen_range(71..99),
}; };
let mut config = PlayoutConfig::new(Some(PathBuf::from( let mut config = PlayoutConfig::new(
"/usr/share/ffplayout/ffplayout.yml.orig", Some(PathBuf::from("/usr/share/ffplayout/ffplayout.yml.orig")),
))); None,
);
config.general.stat_file = format!(".ffp_{channel_name}",); config.general.stat_file = format!(".ffp_{channel_name}",);
config.logging.path = config.logging.path.join(&channel_name); config.logging.path = config.logging.path.join(&channel_name);
@ -50,15 +52,17 @@ pub async fn create_channel(
serde_yaml::to_writer(file, &config).unwrap(); serde_yaml::to_writer(file, &config).unwrap();
let new_channel = handles::insert_channel(conn, target_channel).await?; let new_channel = handles::insert_channel(conn, target_channel).await?;
control_service(conn, new_channel.id, &ServiceCmd::Enable, None).await?; control_service(conn, &config, new_channel.id, &ServiceCmd::Enable, None).await?;
Ok(new_channel) Ok(new_channel)
} }
pub async fn delete_channel(conn: &Pool<Sqlite>, id: i32) -> Result<(), ServiceError> { pub async fn delete_channel(conn: &Pool<Sqlite>, id: i32) -> Result<(), ServiceError> {
let channel = handles::select_channel(conn, &id).await?; let channel = handles::select_channel(conn, &id).await?;
control_service(conn, channel.id, &ServiceCmd::Stop, None).await?; let (config, _) = playout_config(conn, &id).await?;
control_service(conn, channel.id, &ServiceCmd::Disable, None).await?;
control_service(conn, &config, channel.id, &ServiceCmd::Stop, None).await?;
control_service(conn, &config, channel.id, &ServiceCmd::Disable, None).await?;
if let Err(e) = fs::remove_file(channel.config_path) { if let Err(e) = fs::remove_file(channel.config_path) {
error!("{e}"); error!("{e}");

View File

@ -15,8 +15,8 @@ use tokio::{
}; };
use crate::db::handles::select_channel; use crate::db::handles::select_channel;
use crate::utils::{errors::ServiceError, playout_config}; use crate::utils::errors::ServiceError;
use ffplayout_lib::vec_strings; use ffplayout_lib::{utils::PlayoutConfig, vec_strings};
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
struct TextParams { struct TextParams {
@ -241,11 +241,10 @@ impl SystemD {
} }
} }
async fn post_request<T>(conn: &Pool<Sqlite>, id: i32, obj: T) -> Result<Response, ServiceError> async fn post_request<T>(config: &PlayoutConfig, obj: T) -> Result<Response, ServiceError>
where where
T: Serialize, T: Serialize,
{ {
let (config, _) = playout_config(conn, &id).await?;
let url = format!("http://{}", config.rpc_server.address); let url = format!("http://{}", config.rpc_server.address);
let client = Client::new(); let client = Client::new();
@ -262,8 +261,7 @@ where
} }
pub async fn send_message( pub async fn send_message(
conn: &Pool<Sqlite>, config: &PlayoutConfig,
id: i32,
message: HashMap<String, String>, message: HashMap<String, String>,
) -> Result<Response, ServiceError> { ) -> Result<Response, ServiceError> {
let json_obj = TextParams { let json_obj = TextParams {
@ -271,33 +269,29 @@ pub async fn send_message(
message, message,
}; };
post_request(conn, id, json_obj).await post_request(config, json_obj).await
} }
pub async fn control_state( pub async fn control_state(
conn: &Pool<Sqlite>, config: &PlayoutConfig,
id: i32,
command: &str, command: &str,
) -> Result<Response, ServiceError> { ) -> Result<Response, ServiceError> {
let json_obj = ControlParams { let json_obj = ControlParams {
control: command.to_owned(), control: command.to_owned(),
}; };
post_request(conn, id, json_obj).await post_request(config, json_obj).await
} }
pub async fn media_info( pub async fn media_info(config: &PlayoutConfig, command: String) -> Result<Response, ServiceError> {
conn: &Pool<Sqlite>,
id: i32,
command: String,
) -> Result<Response, ServiceError> {
let json_obj = MediaParams { media: command }; let json_obj = MediaParams { media: command };
post_request(conn, id, json_obj).await post_request(config, json_obj).await
} }
pub async fn control_service( pub async fn control_service(
conn: &Pool<Sqlite>, conn: &Pool<Sqlite>,
config: &PlayoutConfig,
id: i32, id: i32,
command: &ServiceCmd, command: &ServiceCmd,
engine: Option<web::Data<ProcessControl>>, engine: Option<web::Data<ProcessControl>>,
@ -307,14 +301,14 @@ pub async fn control_service(
match command { match command {
ServiceCmd::Start => en.start().await, ServiceCmd::Start => en.start().await,
ServiceCmd::Stop => { ServiceCmd::Stop => {
if control_state(conn, id, "stop_all").await.is_ok() { if control_state(config, "stop_all").await.is_ok() {
en.stop().await en.stop().await
} else { } else {
Err(ServiceError::NoContent("Nothing to stop".to_string())) Err(ServiceError::NoContent("Nothing to stop".to_string()))
} }
} }
ServiceCmd::Restart => { ServiceCmd::Restart => {
if control_state(conn, id, "stop_all").await.is_ok() { if control_state(config, "stop_all").await.is_ok() {
en.restart().await en.restart().await
} else { } else {
Err(ServiceError::NoContent("Nothing to restart".to_string())) Err(ServiceError::NoContent("Nothing to restart".to_string()))

View File

@ -12,8 +12,8 @@ pub enum ServiceError {
#[display(fmt = "Conflict: {_0}")] #[display(fmt = "Conflict: {_0}")]
Conflict(String), Conflict(String),
#[display(fmt = "Unauthorized")] #[display(fmt = "Unauthorized: {_0}")]
Unauthorized, Unauthorized(String),
#[display(fmt = "NoContent: {_0}")] #[display(fmt = "NoContent: {_0}")]
NoContent(String), NoContent(String),
@ -31,7 +31,7 @@ impl ResponseError for ServiceError {
} }
ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message), ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message),
ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"), ServiceError::Unauthorized(ref message) => HttpResponse::Unauthorized().json(message),
ServiceError::NoContent(ref message) => HttpResponse::NoContent().json(message), ServiceError::NoContent(ref message) => HttpResponse::NoContent().json(message),
ServiceError::ServiceUnavailable(ref message) => { ServiceError::ServiceUnavailable(ref message) => {
HttpResponse::ServiceUnavailable().json(message) HttpResponse::ServiceUnavailable().json(message)
@ -87,3 +87,9 @@ impl From<tokio::task::JoinError> for ServiceError {
ServiceError::BadRequest(err.to_string()) ServiceError::BadRequest(err.to_string())
} }
} }
impl From<uuid::Error> for ServiceError {
fn from(err: uuid::Error) -> ServiceError {
ServiceError::BadRequest(err.to_string())
}
}

View File

@ -1,4 +1,4 @@
// use std::cmp; use std::fmt;
use local_ip_address::list_afinet_netifas; use local_ip_address::list_afinet_netifas;
use serde::Serialize; use serde::Serialize;
@ -71,6 +71,12 @@ pub struct SystemStat {
pub system: MySystem, pub system: MySystem,
} }
impl fmt::Display for SystemStat {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap())
}
}
pub fn stat(config: PlayoutConfig) -> SystemStat { pub fn stat(config: PlayoutConfig) -> SystemStat {
let mut disks = DISKS.lock().unwrap(); let mut disks = DISKS.lock().unwrap();
let mut networks = NETWORKS.lock().unwrap(); let mut networks = NETWORKS.lock().unwrap();

View File

@ -14,7 +14,7 @@ use ffplayout_lib::{
controller::ProcessUnit::*, test_tcp_port, Media, PlayoutConfig, ProcessControl, controller::ProcessUnit::*, test_tcp_port, Media, PlayoutConfig, ProcessControl,
FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS,
}, },
vec_strings, ADVANCED_CONFIG, vec_strings,
}; };
fn server_monitor( fn server_monitor(
@ -64,7 +64,11 @@ pub fn ingest_server(
dummy_media.unit = Ingest; dummy_media.unit = Ingest;
dummy_media.add_filter(&config, &None); dummy_media.add_filter(&config, &None);
if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.ingest.input_cmd { if let Some(ingest_input_cmd) = config
.advanced
.as_ref()
.and_then(|a| a.ingest.input_cmd.clone())
{
server_cmd.append(&mut ingest_input_cmd.clone()); server_cmd.append(&mut ingest_input_cmd.clone());
} }

View File

@ -119,21 +119,22 @@ impl CurrentProgram {
self.current_node.out self.current_node.out
}; };
trace!( let node_index = self.current_node.index.unwrap_or_default();
"delta: {delta}, total_delta: {total_delta}, current index: {}",
self.current_node.index.unwrap_or_default()
);
let mut next_start = let mut next_start =
self.current_node.begin.unwrap_or_default() - self.start_sec + duration + delta; self.current_node.begin.unwrap_or_default() - self.start_sec + duration + delta;
if self.player_control.current_index.load(Ordering::SeqCst) if node_index > 0
== self.player_control.current_list.lock().unwrap().len() - 1 && node_index == self.player_control.current_list.lock().unwrap().len() - 1
{ {
next_start += self.config.general.stop_threshold; next_start += self.config.general.stop_threshold;
} }
trace!("next_start: {next_start}, end_sec: {}", self.end_sec); trace!(
"delta: {delta} | total_delta: {total_delta}, index: {node_index} \nnext_start: {next_start} | end_sec: {} | source {}",
self.end_sec,
self.current_node.source
);
// Check if we over the target length or we are close to it, if so we load the next playlist. // Check if we over the target length or we are close to it, if so we load the next playlist.
if !self.config.playlist.infinit if !self.config.playlist.infinit
@ -788,7 +789,7 @@ fn handle_list_end(
player_control: &PlayerControl, player_control: &PlayerControl,
last_index: usize, last_index: usize,
) -> Media { ) -> Media {
debug!("Playlist end"); debug!("Last clip from day");
let mut out = if node.seek > 0.0 { let mut out = if node.seek > 0.0 {
node.seek + total_delta node.seek + total_delta

View File

@ -2,7 +2,7 @@ use std::process::{self, Command, Stdio};
use simplelog::*; use simplelog::*;
use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings, ADVANCED_CONFIG}; use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings};
/// Desktop Output /// Desktop Output
/// ///
@ -12,7 +12,11 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format]; let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format];
if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd { if let Some(encoder_input_cmd) = config
.advanced
.as_ref()
.and_then(|a| a.encoder.input_cmd.clone())
{
enc_cmd.append(&mut encoder_input_cmd.clone()); enc_cmd.append(&mut encoder_input_cmd.clone());
} }

View File

@ -34,7 +34,7 @@ use ffplayout_lib::{
controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media, controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media,
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl, PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
}, },
vec_strings, ADVANCED_CONFIG, vec_strings,
}; };
/// Ingest Server for HLS /// Ingest Server for HLS
@ -50,7 +50,11 @@ fn ingest_to_hls_server(
let mut dummy_media = Media::new(0, "Live Stream", false); let mut dummy_media = Media::new(0, "Live Stream", false);
dummy_media.unit = Ingest; dummy_media.unit = Ingest;
if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.ingest.input_cmd { if let Some(ingest_input_cmd) = config
.advanced
.as_ref()
.and_then(|a| a.ingest.input_cmd.clone())
{
server_prefix.append(&mut ingest_input_cmd.clone()); server_prefix.append(&mut ingest_input_cmd.clone());
} }
@ -203,7 +207,11 @@ pub fn write_hls(
let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format]; let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd { if let Some(encoder_input_cmd) = config
.advanced
.as_ref()
.and_then(|a| a.encoder.input_cmd.clone())
{
enc_prefix.append(&mut encoder_input_cmd.clone()); enc_prefix.append(&mut encoder_input_cmd.clone());
} }

View File

@ -19,14 +19,11 @@ pub use hls::write_hls;
use crate::input::{ingest_server, source_generator}; use crate::input::{ingest_server, source_generator};
use crate::utils::task_runner; use crate::utils::task_runner;
use ffplayout_lib::vec_strings; use ffplayout_lib::utils::{
use ffplayout_lib::{ sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
utils::{ ProcessControl, ProcessUnit::*,
sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
ProcessControl, ProcessUnit::*,
},
ADVANCED_CONFIG,
}; };
use ffplayout_lib::vec_strings;
/// Player /// Player
/// ///
@ -147,7 +144,11 @@ pub fn player(
let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format]; let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
if let Some(decoder_input_cmd) = &ADVANCED_CONFIG.decoder.input_cmd { if let Some(decoder_input_cmd) = config
.advanced
.as_ref()
.and_then(|a| a.decoder.input_cmd.clone())
{
dec_cmd.append(&mut decoder_input_cmd.clone()); dec_cmd.append(&mut decoder_input_cmd.clone());
} }

View File

@ -16,15 +16,17 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
media.unit = Encoder; media.unit = Encoder;
media.add_filter(config, &None); media.add_filter(config, &None);
let enc_prefix = vec_strings![ let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", log_format];
"-hide_banner",
"-nostats", if let Some(input_cmd) = config
"-v", .advanced
log_format, .as_ref()
"-re", .and_then(|a| a.encoder.input_cmd.clone())
"-i", {
"pipe:0" enc_prefix.append(&mut input_cmd.clone());
]; }
enc_prefix.append(&mut vec_strings!["-re", "-i", "pipe:0"]);
let enc_cmd = prepare_output_cmd(config, enc_prefix, &media.filter); let enc_cmd = prepare_output_cmd(config, enc_prefix, &media.filter);

View File

@ -5,7 +5,7 @@ use simplelog::*;
use crate::utils::prepare_output_cmd; use crate::utils::prepare_output_cmd;
use ffplayout_lib::{ use ffplayout_lib::{
utils::{Media, PlayoutConfig, ProcessUnit::*}, utils::{Media, PlayoutConfig, ProcessUnit::*},
vec_strings, ADVANCED_CONFIG, vec_strings,
}; };
/// Streaming Output /// Streaming Output
@ -18,7 +18,11 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", log_format]; let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", log_format];
if let Some(input_cmd) = &ADVANCED_CONFIG.encoder.input_cmd { if let Some(input_cmd) = config
.advanced
.as_ref()
.and_then(|a| a.encoder.input_cmd.clone())
{
enc_prefix.append(&mut input_cmd.clone()); enc_prefix.append(&mut input_cmd.clone());
} }

View File

@ -11,6 +11,9 @@ use ffplayout_lib::utils::{OutputMode, ProcessMode};
\n ffplayout (ARGS) [OPTIONS]\n\n Pass channel name only in multi channel environment!", \n ffplayout (ARGS) [OPTIONS]\n\n Pass channel name only in multi channel environment!",
long_about = None)] long_about = None)]
pub struct Args { pub struct Args {
#[clap(long, help = "File path to advanced.yml")]
pub advanced_config: Option<PathBuf>,
#[clap(index = 1, value_parser, help = "Channel name")] #[clap(index = 1, value_parser, help = "Channel name")]
pub channel: Option<String>, pub channel: Option<String>,

View File

@ -1,4 +1,5 @@
use std::{ use std::{
env,
fs::File, fs::File,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -14,8 +15,8 @@ pub use arg_parse::Args;
use ffplayout_lib::{ use ffplayout_lib::{
filter::Filters, filter::Filters,
utils::{ utils::{
config::Template, errors::ProcError, parse_log_level_filter, sec_to_time, time_in_seconds, config::Template, errors::ProcError, parse_log_level_filter, time_in_seconds, time_to_sec,
time_to_sec, Media, OutputMode::*, PlayoutConfig, PlayoutStatus, ProcessMode::*, Media, OutputMode::*, PlayoutConfig, PlayoutStatus, ProcessMode::*,
}, },
vec_strings, vec_strings,
}; };
@ -37,7 +38,19 @@ pub fn get_config(args: Args) -> Result<PlayoutConfig, ProcError> {
None => args.config, None => args.config,
}; };
let mut config = PlayoutConfig::new(cfg_path); let mut adv_config_path = PathBuf::from("/etc/ffplayout/advanced.yml");
if let Some(adv_path) = args.advanced_config {
adv_config_path = adv_path;
} else if !adv_config_path.is_file() {
if Path::new("./assets/advanced.yml").is_file() {
adv_config_path = PathBuf::from("./assets/advanced.yml")
} else if let Some(p) = env::current_exe().ok().as_ref().and_then(|op| op.parent()) {
adv_config_path = p.join("advanced.yml")
};
}
let mut config = PlayoutConfig::new(cfg_path, Some(adv_config_path));
if let Some(gen) = args.generate { if let Some(gen) = args.generate {
config.general.generate = Some(gen); config.general.generate = Some(gen);
@ -240,7 +253,7 @@ pub fn prepare_output_cmd(
pub fn get_media_map(media: Media) -> Value { pub fn get_media_map(media: Media) -> Value {
json!({ json!({
"title": media.title, "title": media.title,
"seek": media.seek, "in": media.seek,
"out": media.out, "out": media.out,
"duration": media.duration, "duration": media.duration,
"category": media.category, "category": media.category,
@ -259,22 +272,20 @@ pub fn get_data_map(
let current_time = time_in_seconds(); let current_time = time_in_seconds();
let shift = *playout_stat.time_shift.lock().unwrap(); let shift = *playout_stat.time_shift.lock().unwrap();
let begin = media.begin.unwrap_or(0.0) - shift; let begin = media.begin.unwrap_or(0.0) - shift;
let played_time = current_time - begin;
data_map.insert("play_mode".to_string(), json!(config.processing.mode));
data_map.insert("ingest_runs".to_string(), json!(server_is_running));
data_map.insert("index".to_string(), json!(media.index)); data_map.insert("index".to_string(), json!(media.index));
data_map.insert("start_sec".to_string(), json!(begin)); data_map.insert("ingest".to_string(), json!(server_is_running));
data_map.insert("mode".to_string(), json!(config.processing.mode));
if begin > 0.0 { data_map.insert(
let played_time = current_time - begin; "shift".to_string(),
let remaining_time = media.out - played_time; json!((shift * 1000.0).round() / 1000.0),
);
data_map.insert("start_time".to_string(), json!(sec_to_time(begin))); data_map.insert(
data_map.insert("played_sec".to_string(), json!(played_time)); "elapsed".to_string(),
data_map.insert("remaining_sec".to_string(), json!(remaining_time)); json!((played_time * 1000.0).round() / 1000.0),
} );
data_map.insert("media".to_string(), get_media_map(media));
data_map.insert("current_media".to_string(), get_media_map(media));
data_map data_map
} }

@ -1 +1 @@
Subproject commit 56db578d8cf69aee7fc573828016fa728315bf79 Subproject commit 6111c2686d14b3bf33a4c0b29c85672f7e4f4399

View File

@ -14,7 +14,6 @@ use crate::utils::{
controller::ProcessUnit::*, custom_format, fps_calc, is_close, Media, OutputMode::*, controller::ProcessUnit::*, custom_format, fps_calc, is_close, Media, OutputMode::*,
PlayoutConfig, PlayoutConfig,
}; };
use crate::ADVANCED_CONFIG;
use super::vec_strings; use super::vec_strings;
@ -179,15 +178,19 @@ impl Filters {
impl Default for Filters { impl Default for Filters {
fn default() -> Self { fn default() -> Self {
Self::new(PlayoutConfig::new(None), 0) Self::new(PlayoutConfig::new(None, None), 0)
} }
} }
fn deinterlace(field_order: &Option<String>, chain: &mut Filters) { fn deinterlace(field_order: &Option<String>, chain: &mut Filters, config: &PlayoutConfig) {
if let Some(order) = field_order { if let Some(order) = field_order {
if order != "progressive" { if order != "progressive" {
let deinterlace = match &ADVANCED_CONFIG.decoder.filters.deinterlace { let deinterlace = match config
Some(deinterlace) => deinterlace.clone(), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.deinterlace.clone())
{
Some(deinterlace) => deinterlace,
None => "yadif=0:-1:0".to_string(), None => "yadif=0:-1:0".to_string(),
}; };
@ -202,14 +205,22 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl
if let (Some(w), Some(h)) = (v_stream.width, v_stream.height) { if let (Some(w), Some(h)) = (v_stream.width, v_stream.height) {
if w > config.processing.width && aspect > config.processing.aspect { if w > config.processing.width && aspect > config.processing.aspect {
scale = match &ADVANCED_CONFIG.decoder.filters.pad_scale_w { scale = match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.pad_scale_w.clone())
{
Some(pad_scale_w) => { Some(pad_scale_w) => {
custom_format(&format!("{pad_scale_w},"), &[&config.processing.width]) custom_format(&format!("{pad_scale_w},"), &[&config.processing.width])
} }
None => format!("scale={}:-1,", config.processing.width), None => format!("scale={}:-1,", config.processing.width),
}; };
} else if h > config.processing.height && aspect < config.processing.aspect { } else if h > config.processing.height && aspect < config.processing.aspect {
scale = match &ADVANCED_CONFIG.decoder.filters.pad_scale_h { scale = match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.pad_scale_h.clone())
{
Some(pad_scale_h) => { Some(pad_scale_h) => {
custom_format(&format!("{pad_scale_h},"), &[&config.processing.width]) custom_format(&format!("{pad_scale_h},"), &[&config.processing.width])
} }
@ -218,7 +229,11 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl
} }
} }
let pad = match &ADVANCED_CONFIG.decoder.filters.pad_video { let pad = match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.pad_video.clone())
{
Some(pad_video) => custom_format( Some(pad_video) => custom_format(
&format!("{scale}{pad_video}"), &format!("{scale}{pad_video}"),
&[ &[
@ -238,8 +253,12 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl
fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) { fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) {
if fps != config.processing.fps { if fps != config.processing.fps {
let fps_filter = match &ADVANCED_CONFIG.decoder.filters.fps { let fps_filter = match config
Some(fps) => custom_format(fps, &[&config.processing.fps]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.fps.clone())
{
Some(fps) => custom_format(&fps, &[&config.processing.fps]),
None => format!("fps={}", config.processing.fps), None => format!("fps={}", config.processing.fps),
}; };
@ -257,9 +276,13 @@ fn scale(
// width: i64, height: i64 // width: i64, height: i64
if let (Some(w), Some(h)) = (width, height) { if let (Some(w), Some(h)) = (width, height) {
if w != config.processing.width || h != config.processing.height { if w != config.processing.width || h != config.processing.height {
let scale = match &ADVANCED_CONFIG.decoder.filters.scale { let scale = match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.scale.clone())
{
Some(scale) => custom_format( Some(scale) => custom_format(
scale, &scale,
&[&config.processing.width, &config.processing.height], &[&config.processing.width, &config.processing.height],
), ),
None => format!( None => format!(
@ -274,17 +297,25 @@ fn scale(
} }
if !is_close(aspect, config.processing.aspect, 0.03) { if !is_close(aspect, config.processing.aspect, 0.03) {
let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar { let dar = match config
Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.set_dar.clone())
{
Some(set_dar) => custom_format(&set_dar, &[&config.processing.aspect]),
None => format!("setdar=dar={}", config.processing.aspect), None => format!("setdar=dar={}", config.processing.aspect),
}; };
chain.add_filter(&dar, 0, Video); chain.add_filter(&dar, 0, Video);
} }
} else { } else {
let scale = match &ADVANCED_CONFIG.decoder.filters.scale { let scale = match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.scale.clone())
{
Some(scale) => custom_format( Some(scale) => custom_format(
scale, &scale,
&[&config.processing.width, &config.processing.height], &[&config.processing.width, &config.processing.height],
), ),
None => format!( None => format!(
@ -294,8 +325,12 @@ fn scale(
}; };
chain.add_filter(&scale, 0, Video); chain.add_filter(&scale, 0, Video);
let dar = match &ADVANCED_CONFIG.decoder.filters.set_dar { let dar = match config
Some(set_dar) => custom_format(set_dar, &[&config.processing.aspect]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.set_dar.clone())
{
Some(set_dar) => custom_format(&set_dar, &[&config.processing.aspect]),
None => format!("setdar=dar={}", config.processing.aspect), None => format!("setdar=dar={}", config.processing.aspect),
}; };
@ -303,7 +338,13 @@ fn scale(
} }
} }
fn fade(node: &mut Media, chain: &mut Filters, nr: i32, filter_type: FilterType) { fn fade(
node: &mut Media,
chain: &mut Filters,
nr: i32,
filter_type: FilterType,
config: &PlayoutConfig,
) {
let mut t = ""; let mut t = "";
let mut fade_audio = false; let mut fade_audio = false;
@ -319,11 +360,19 @@ fn fade(node: &mut Media, chain: &mut Filters, nr: i32, filter_type: FilterType)
let mut fade_in = format!("{t}fade=in:st=0:d=0.5"); let mut fade_in = format!("{t}fade=in:st=0:d=0.5");
if t == "a" { if t == "a" {
if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.afade_in { if let Some(fade) = config
fade_in = custom_format(fade, &[t]); .advanced
.as_ref()
.and_then(|a| a.decoder.filters.afade_in.clone())
{
fade_in = custom_format(&fade, &[t]);
} }
} else if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.fade_in { } else if let Some(fade) = config
fade_in = custom_format(fade, &[t]); .advanced
.as_ref()
.and_then(|a| a.decoder.filters.fade_in.clone())
{
fade_in = custom_format(&fade, &[t]);
}; };
chain.add_filter(&fade_in, nr, filter_type); chain.add_filter(&fade_in, nr, filter_type);
@ -333,11 +382,20 @@ fn fade(node: &mut Media, chain: &mut Filters, nr: i32, filter_type: FilterType)
let mut fade_out = format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0)); let mut fade_out = format!("{t}fade=out:st={}:d=1.0", (node.out - node.seek - 1.0));
if t == "a" { if t == "a" {
if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.afade_out { if let Some(fade) = config
fade_out = custom_format(fade, &[t]); .advanced
.as_ref()
.and_then(|a| a.decoder.filters.afade_out.clone())
{
fade_out = custom_format(&fade, &[node.out - node.seek - 1.0]);
} }
} else if let Some(fade) = &ADVANCED_CONFIG.decoder.filters.fade_out { } else if let Some(fade) = config
fade_out = custom_format(fade, &[t]); .advanced
.as_ref()
.and_then(|a| a.decoder.filters.fade_out.clone())
.clone()
{
fade_out = custom_format(&fade, &[node.out - node.seek - 1.0]);
}; };
chain.add_filter(&fade_out, nr, filter_type); chain.add_filter(&fade_out, nr, filter_type);
@ -360,7 +418,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
); );
if node.last_ad { if node.last_ad {
match &ADVANCED_CONFIG.decoder.filters.overlay_logo_fade_in { match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.overlay_logo_fade_in.clone())
{
Some(fade_in) => logo_chain.push_str(&format!(",{fade_in}")), Some(fade_in) => logo_chain.push_str(&format!(",{fade_in}")),
None => logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1"), None => logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1"),
}; };
@ -369,7 +431,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
if node.next_ad { if node.next_ad {
let length = node.out - node.seek - 1.0; let length = node.out - node.seek - 1.0;
match &ADVANCED_CONFIG.decoder.filters.overlay_logo_fade_out { match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.overlay_logo_fade_out.clone())
{
Some(fade_out) => { Some(fade_out) => {
logo_chain.push_str(&custom_format(&format!(",{fade_out}"), &[length])) logo_chain.push_str(&custom_format(&format!(",{fade_out}"), &[length]))
} }
@ -378,7 +444,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
} }
if !config.processing.logo_scale.is_empty() { if !config.processing.logo_scale.is_empty() {
match &ADVANCED_CONFIG.decoder.filters.overlay_logo_scale { match &config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.overlay_logo_scale.clone())
{
Some(logo_scale) => logo_chain.push_str(&custom_format( Some(logo_scale) => logo_chain.push_str(&custom_format(
&format!(",{logo_scale}"), &format!(",{logo_scale}"),
&[&config.processing.logo_scale], &[&config.processing.logo_scale],
@ -387,13 +457,20 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
} }
} }
match &ADVANCED_CONFIG.decoder.filters.overlay_logo { match config
.advanced
.as_ref()
.and_then(|a| a.decoder.filters.overlay_logo.clone())
{
Some(overlay) => { Some(overlay) => {
if !overlay.starts_with(',') { if !overlay.starts_with(',') {
logo_chain.push(','); logo_chain.push(',');
} }
logo_chain.push_str(&custom_format(overlay, &[&config.processing.logo_position])) logo_chain.push_str(&custom_format(
&overlay,
&[&config.processing.logo_position],
))
} }
None => logo_chain.push_str(&format!( None => logo_chain.push_str(&format!(
"[l];[v][l]overlay={}:shortest=1", "[l];[v][l]overlay={}:shortest=1",
@ -405,7 +482,7 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
} }
} }
fn extend_video(node: &mut Media, chain: &mut Filters) { fn extend_video(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
if let Some(video_duration) = node if let Some(video_duration) = node
.probe .probe
.as_ref() .as_ref()
@ -416,8 +493,12 @@ fn extend_video(node: &mut Media, chain: &mut Filters) {
if node.out - node.seek > video_duration - node.seek + 0.1 && node.duration >= node.out { if node.out - node.seek > video_duration - node.seek + 0.1 && node.duration >= node.out {
let duration = (node.out - node.seek) - (video_duration - node.seek); let duration = (node.out - node.seek) - (video_duration - node.seek);
let tpad = match &ADVANCED_CONFIG.decoder.filters.tpad { let tpad = match config
Some(pad) => custom_format(pad, &[duration]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.tpad.clone())
{
Some(pad) => custom_format(&pad, &[duration]),
None => format!("tpad=stop_mode=add:stop_duration={duration}"), None => format!("tpad=stop_mode=add:stop_duration={duration}"),
}; };
@ -442,9 +523,13 @@ fn add_text(
} }
} }
fn add_audio(node: &Media, chain: &mut Filters, nr: i32) { fn add_audio(node: &Media, chain: &mut Filters, nr: i32, config: &PlayoutConfig) {
let audio = match &ADVANCED_CONFIG.decoder.filters.aevalsrc { let audio = match config
Some(aevalsrc) => custom_format(aevalsrc, &[node.out - node.seek]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.aevalsrc.clone())
{
Some(aevalsrc) => custom_format(&aevalsrc, &[node.out - node.seek]),
None => format!( None => format!(
"aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000", "aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000",
node.out - node.seek node.out - node.seek
@ -454,7 +539,7 @@ fn add_audio(node: &Media, chain: &mut Filters, nr: i32) {
chain.add_filter(&audio, nr, Audio); chain.add_filter(&audio, nr, Audio);
} }
fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) { fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32, config: &PlayoutConfig) {
if !Path::new(&node.audio).is_file() { if !Path::new(&node.audio).is_file() {
if let Some(audio_duration) = node if let Some(audio_duration) = node
.probe .probe
@ -465,8 +550,12 @@ fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) {
{ {
if node.out - node.seek > audio_duration - node.seek + 0.1 && node.duration >= node.out if node.out - node.seek > audio_duration - node.seek + 0.1 && node.duration >= node.out
{ {
let apad = match &ADVANCED_CONFIG.decoder.filters.apad { let apad = match config
Some(apad) => custom_format(apad, &[node.out - node.seek]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.apad.clone())
{
Some(apad) => custom_format(&apad, &[node.out - node.seek]),
None => format!("apad=whole_dur={}", node.out - node.seek), None => format!("apad=whole_dur={}", node.out - node.seek),
}; };
@ -478,8 +567,12 @@ fn extend_audio(node: &mut Media, chain: &mut Filters, nr: i32) {
fn audio_volume(chain: &mut Filters, config: &PlayoutConfig, nr: i32) { fn audio_volume(chain: &mut Filters, config: &PlayoutConfig, nr: i32) {
if config.processing.volume != 1.0 { if config.processing.volume != 1.0 {
let volume = match &ADVANCED_CONFIG.decoder.filters.volume { let volume = match config
Some(volume) => custom_format(volume, &[config.processing.volume]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.volume.clone())
{
Some(volume) => custom_format(&volume, &[config.processing.volume]),
None => format!("volume={}", config.processing.volume), None => format!("volume={}", config.processing.volume),
}; };
@ -500,7 +593,13 @@ fn aspect_calc(aspect_string: &Option<String>, config: &PlayoutConfig) -> f64 {
source_aspect source_aspect
} }
pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: FilterType) { pub fn split_filter(
chain: &mut Filters,
count: usize,
nr: i32,
filter_type: FilterType,
config: &PlayoutConfig,
) {
if count > 1 { if count > 1 {
let out_link = match filter_type { let out_link = match filter_type {
Audio => &mut chain.audio_out_link, Audio => &mut chain.audio_out_link,
@ -514,8 +613,12 @@ pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: Fil
} }
} }
let split = match &ADVANCED_CONFIG.decoder.filters.split { let split = match config
Some(split) => custom_format(split, &[count.to_string(), out_link.join("")]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.split.clone())
{
Some(split) => custom_format(&split, &[count.to_string(), out_link.join("")]),
None => format!("split={count}{}", out_link.join("")), None => format!("split={count}{}", out_link.join("")),
}; };
@ -582,7 +685,7 @@ pub fn filter_chains(
if let Some(f) = config.out.output_filter.clone() { if let Some(f) = config.out.output_filter.clone() {
process_output_filters(config, &mut filters, &f) process_output_filters(config, &mut filters, &f)
} else if config.out.output_count > 1 && !config.processing.audio_only { } else if config.out.output_count > 1 && !config.processing.audio_only {
split_filter(&mut filters, config.out.output_count, 0, Video); split_filter(&mut filters, config.out.output_count, 0, Video, config);
} }
return filters; return filters;
@ -598,7 +701,7 @@ pub fn filter_chains(
let aspect = aspect_calc(&v_stream.display_aspect_ratio, config); let aspect = aspect_calc(&v_stream.display_aspect_ratio, config);
let frame_per_sec = fps_calc(&v_stream.r_frame_rate, 1.0); let frame_per_sec = fps_calc(&v_stream.r_frame_rate, 1.0);
deinterlace(&v_stream.field_order, &mut filters); deinterlace(&v_stream.field_order, &mut filters, config);
pad(aspect, &mut filters, v_stream, config); pad(aspect, &mut filters, v_stream, config);
fps(frame_per_sec, &mut filters, config); fps(frame_per_sec, &mut filters, config);
scale( scale(
@ -610,14 +713,14 @@ pub fn filter_chains(
); );
} }
extend_video(node, &mut filters); extend_video(node, &mut filters, config);
} else { } else {
fps(0.0, &mut filters, config); fps(0.0, &mut filters, config);
scale(None, None, 1.0, &mut filters, config); scale(None, None, 1.0, &mut filters, config);
} }
add_text(node, &mut filters, config, filter_chain); add_text(node, &mut filters, config, filter_chain);
fade(node, &mut filters, 0, Video); fade(node, &mut filters, 0, Video, config);
overlay(node, &mut filters, config); overlay(node, &mut filters, config);
} }
@ -653,7 +756,7 @@ pub fn filter_chains(
.is_some() .is_some()
|| Path::new(&node.audio).is_file() || Path::new(&node.audio).is_file()
{ {
extend_audio(node, &mut filters, i); extend_audio(node, &mut filters, i, config);
} else if node.unit == Decoder { } else if node.unit == Decoder {
if !node.source.contains("color=c=") { if !node.source.contains("color=c=") {
warn!( warn!(
@ -662,14 +765,14 @@ pub fn filter_chains(
); );
} }
add_audio(node, &mut filters, i); add_audio(node, &mut filters, i, config);
} }
// add at least anull filter, for correct filter construction, // add at least anull filter, for correct filter construction,
// is important for split filter in HLS mode // is important for split filter in HLS mode
filters.add_filter("anull", i, Audio); filters.add_filter("anull", i, Audio);
fade(node, &mut filters, i, Audio); fade(node, &mut filters, i, Audio, config);
audio_volume(&mut filters, config, i); audio_volume(&mut filters, config, i);
custom(&proc_af, &mut filters, i, Audio); custom(&proc_af, &mut filters, i, Audio);

View File

@ -7,7 +7,6 @@ use std::{
use regex::Regex; use regex::Regex;
use crate::utils::{controller::ProcessUnit::*, custom_format, Media, PlayoutConfig}; use crate::utils::{controller::ProcessUnit::*, custom_format, Media, PlayoutConfig};
use crate::ADVANCED_CONFIG;
pub fn filter_node( pub fn filter_node(
config: &PlayoutConfig, config: &PlayoutConfig,
@ -45,7 +44,11 @@ pub fn filter_node(
.replace('%', "\\\\\\%") .replace('%', "\\\\\\%")
.replace(':', "\\:"); .replace(':', "\\:");
filter = match &ADVANCED_CONFIG.decoder.filters.drawtext_from_file { filter = match &config
.advanced
.clone()
.and_then(|a| a.decoder.filters.drawtext_from_file)
{
Some(drawtext) => custom_format(drawtext, &[&escaped_text, &config.text.style, &font]), Some(drawtext) => custom_format(drawtext, &[&escaped_text, &config.text.style, &font]),
None => format!("drawtext=text='{escaped_text}':{}{font}", config.text.style), None => format!("drawtext=text='{escaped_text}':{}{font}", config.text.style),
}; };
@ -58,8 +61,12 @@ pub fn filter_node(
} }
} }
filter = match &ADVANCED_CONFIG.decoder.filters.drawtext_from_zmq { filter = match config
Some(drawtext) => custom_format(drawtext, &[&socket.replace(':', "\\:"), &filter_cmd]), .advanced
.as_ref()
.and_then(|a| a.decoder.filters.drawtext_from_zmq.clone())
{
Some(drawtext) => custom_format(&drawtext, &[&socket.replace(':', "\\:"), &filter_cmd]),
None => format!( None => format!(
"zmq=b=tcp\\\\://'{}',drawtext@dyntext={filter_cmd}", "zmq=b=tcp\\\\://'{}',drawtext@dyntext={filter_cmd}",
socket.replace(':', "\\:") socket.replace(':', "\\:")

View File

@ -1,16 +1,8 @@
use std::sync::Arc;
extern crate log; extern crate log;
extern crate simplelog; extern crate simplelog;
use lazy_static::lazy_static;
pub mod filter; pub mod filter;
pub mod macros; pub mod macros;
pub mod utils; pub mod utils;
use utils::advanced_config::AdvancedConfig; use utils::advanced_config::AdvancedConfig;
lazy_static! {
pub static ref ADVANCED_CONFIG: Arc<AdvancedConfig> = Arc::new(AdvancedConfig::new());
}

View File

@ -1,8 +1,4 @@
use std::{ use std::{fs::File, path::PathBuf};
env,
fs::File,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use shlex::split; use shlex::split;
@ -67,19 +63,10 @@ pub struct Filters {
} }
impl AdvancedConfig { impl AdvancedConfig {
pub fn new() -> Self { pub fn new(cfg_path: PathBuf) -> Self {
let mut config: AdvancedConfig = Default::default(); let mut config: AdvancedConfig = Default::default();
let mut config_path = PathBuf::from("/etc/ffplayout/advanced.yml");
if !config_path.is_file() { if let Ok(f) = File::open(cfg_path) {
if Path::new("./assets/advanced.yml").is_file() {
config_path = PathBuf::from("./assets/advanced.yml")
} else if let Some(p) = env::current_exe().ok().as_ref().and_then(|op| op.parent()) {
config_path = p.join("advanced.yml")
};
}
if let Ok(f) = File::open(&config_path) {
config = match serde_yaml::from_reader(f) { config = match serde_yaml::from_reader(f) {
Ok(yaml) => yaml, Ok(yaml) => yaml,
Err(_) => AdvancedConfig::default(), Err(_) => AdvancedConfig::default(),

View File

@ -11,7 +11,7 @@ use log::LevelFilter;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use shlex::split; use shlex::split;
use crate::ADVANCED_CONFIG; use crate::AdvancedConfig;
use super::vec_strings; use super::vec_strings;
use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*}; use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*};
@ -145,6 +145,8 @@ pub struct Source {
/// This we init ones, when ffplayout is starting and use them globally in the hole program. /// This we init ones, when ffplayout is starting and use them globally in the hole program.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PlayoutConfig { pub struct PlayoutConfig {
#[serde(default, skip_serializing, skip_deserializing)]
pub advanced: Option<AdvancedConfig>,
pub general: General, pub general: General,
pub rpc_server: RpcServer, pub rpc_server: RpcServer,
pub mail: Mail, pub mail: Mail,
@ -362,7 +364,7 @@ fn default_channels() -> u8 {
impl PlayoutConfig { impl PlayoutConfig {
/// Read config from YAML file, and set some extra config values. /// Read config from YAML file, and set some extra config values.
pub fn new(cfg_path: Option<PathBuf>) -> Self { pub fn new(cfg_path: Option<PathBuf>, advanced_path: Option<PathBuf>) -> Self {
let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml");
if let Some(cfg) = cfg_path { if let Some(cfg) = cfg_path {
@ -389,6 +391,11 @@ impl PlayoutConfig {
let mut config: PlayoutConfig = let mut config: PlayoutConfig =
serde_yaml::from_reader(f).expect("Could not read config file."); serde_yaml::from_reader(f).expect("Could not read config file.");
if let Some(adv_path) = advanced_path {
config.advanced = Some(AdvancedConfig::new(adv_path))
}
config.general.generate = None; config.general.generate = None;
config.general.config_path = config_path.to_string_lossy().to_string(); config.general.config_path = config_path.to_string_lossy().to_string();
@ -430,12 +437,16 @@ impl PlayoutConfig {
} }
let mut process_cmd = vec_strings![]; let mut process_cmd = vec_strings![];
let advanced_output_cmd = config
.advanced
.as_ref()
.and_then(|a| a.decoder.output_cmd.clone());
if config.processing.audio_only { if config.processing.audio_only {
process_cmd.append(&mut vec_strings!["-vn"]); process_cmd.append(&mut vec_strings!["-vn"]);
} else if config.processing.copy_video { } else if config.processing.copy_video {
process_cmd.append(&mut vec_strings!["-c:v", "copy"]); process_cmd.append(&mut vec_strings!["-c:v", "copy"]);
} else if let Some(decoder_cmd) = &ADVANCED_CONFIG.decoder.output_cmd { } else if let Some(decoder_cmd) = &advanced_output_cmd {
process_cmd.append(&mut decoder_cmd.clone()); process_cmd.append(&mut decoder_cmd.clone());
} else { } else {
let bitrate = format!( let bitrate = format!(
@ -470,7 +481,7 @@ impl PlayoutConfig {
if config.processing.copy_audio { if config.processing.copy_audio {
process_cmd.append(&mut vec_strings!["-c:a", "copy"]); process_cmd.append(&mut vec_strings!["-c:a", "copy"]);
} else if ADVANCED_CONFIG.decoder.output_cmd.is_none() { } else if advanced_output_cmd.is_none() {
process_cmd.append(&mut pre_audio_codec( process_cmd.append(&mut pre_audio_codec(
&config.processing.custom_filter, &config.processing.custom_filter,
&config.ingest.custom_filter, &config.ingest.custom_filter,
@ -530,7 +541,7 @@ impl PlayoutConfig {
impl Default for PlayoutConfig { impl Default for PlayoutConfig {
fn default() -> Self { fn default() -> Self {
Self::new(None) Self::new(None, None)
} }
} }

View File

@ -127,8 +127,14 @@ pub fn read_json(
let headers = resp.headers().clone(); let headers = resp.headers().clone();
if let Ok(body) = resp.text() { if let Ok(body) = resp.text() {
let mut playlist: JsonPlaylist = let mut playlist: JsonPlaylist = match serde_json::from_str(&body) {
serde_json::from_str(&body).expect("Could't read remote json playlist."); Ok(p) => p,
Err(e) => {
error!("Could't read remote json playlist. {e:?}");
JsonPlaylist::new(date.clone(), start_sec)
}
};
playlist.path = Some(current_file); playlist.path = Some(current_file);
playlist.start_sec = Some(start_sec); playlist.start_sec = Some(start_sec);

View File

@ -17,7 +17,6 @@ use crate::utils::{
JsonPlaylist, Media, OutputMode::Null, PlayerControl, PlayoutConfig, FFMPEG_IGNORE_ERRORS, JsonPlaylist, Media, OutputMode::Null, PlayerControl, PlayoutConfig, FFMPEG_IGNORE_ERRORS,
IMAGE_FORMAT, IMAGE_FORMAT,
}; };
use crate::ADVANCED_CONFIG;
/// Validate a single media file. /// Validate a single media file.
/// ///
@ -38,7 +37,11 @@ fn check_media(
let mut process_length = 0.1; let mut process_length = 0.1;
if let Some(decoder_input_cmd) = &ADVANCED_CONFIG.decoder.input_cmd { if let Some(decoder_input_cmd) = config
.advanced
.as_ref()
.and_then(|a| a.decoder.input_cmd.clone())
{
dec_cmd.append(&mut decoder_input_cmd.clone()); dec_cmd.append(&mut decoder_input_cmd.clone());
} }

View File

@ -191,6 +191,7 @@ pub fn init_logging(
.add_filter_ignore_str("reqwest") .add_filter_ignore_str("reqwest")
.add_filter_ignore_str("rpc") .add_filter_ignore_str("rpc")
.add_filter_ignore_str("rustls") .add_filter_ignore_str("rustls")
.add_filter_ignore_str("serial_test")
.add_filter_ignore_str("sqlx") .add_filter_ignore_str("sqlx")
.add_filter_ignore_str("tiny_http") .add_filter_ignore_str("tiny_http")
.set_level_padding(LevelPadding::Left) .set_level_padding(LevelPadding::Left)

View File

@ -32,36 +32,18 @@ for target in "${targets[@]}"; do
rm -f "ffplayout-v${version}_${target}.zip" rm -f "ffplayout-v${version}_${target}.zip"
fi fi
cargo build --release --target=$target cross build --release --target=$target
cp ./target/${target}/release/ffpapi.exe . cp ./target/${target}/release/ffpapi.exe .
cp ./target/${target}/release/ffplayout.exe . cp ./target/${target}/release/ffplayout.exe .
zip -r "ffplayout-v${version}_${target}.zip" assets docker docs LICENSE README.md CHANGELOG.md ffplayout.exe ffpapi.exe -x *.db -x *.db-shm -x *.db-wal -x '11-ffplayout' -x *.service zip -r "ffplayout-v${version}_${target}.zip" assets docker docs LICENSE README.md CHANGELOG.md ffplayout.exe ffpapi.exe -x *.db -x *.db-shm -x *.db-wal -x '11-ffplayout' -x *.service
rm -f ffplayout.exe ffpapi.exe rm -f ffplayout.exe ffpapi.exe
elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then
if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then
rm -f "ffplayout-v${version}_${target}.tar.gz"
fi
c_cc="x86_64-apple-darwin20.4-clang"
c_cxx="x86_64-apple-darwin20.4-clang++"
if [[ $target == "aarch64-apple-darwin" ]]; then
c_cc="aarch64-apple-darwin20.4-clang"
c_cxx="aarch64-apple-darwin20.4-clang++"
fi
CC="$c_cc" CXX="$c_cxx" cargo build --release --target=$target
cp ./target/${target}/release/ffpapi .
cp ./target/${target}/release/ffplayout .
tar -czvf "ffplayout-v${version}_${target}.tar.gz" --exclude='*.db' --exclude='*.db-shm' --exclude='*.db-wal' --exclude='11-ffplayout' --exclude='*.service' assets docker docs LICENSE README.md CHANGELOG.md ffplayout ffpapi
rm -f ffplayout ffpapi
else else
if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then
rm -f "ffplayout-v${version}_${target}.tar.gz" rm -f "ffplayout-v${version}_${target}.tar.gz"
fi fi
cargo build --release --target=$target cross build --release --target=$target
cp ./target/${target}/release/ffpapi . cp ./target/${target}/release/ffpapi .
cp ./target/${target}/release/ffplayout . cp ./target/${target}/release/ffplayout .
@ -73,10 +55,10 @@ for target in "${targets[@]}"; do
done done
if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "x86_64-unknown-linux-musl" ]]; then if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "x86_64-unknown-linux-musl" ]]; then
cargo deb --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_amd64.deb cargo deb --no-build --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_amd64.deb
cargo generate-rpm --payload-compress none --target=x86_64-unknown-linux-musl -p ffplayout-engine -o ffplayout-${version}-1.x86_64.rpm cargo generate-rpm --payload-compress none --target=x86_64-unknown-linux-musl -p ffplayout-engine -o ffplayout-${version}-1.x86_64.rpm
fi fi
if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "aarch64-unknown-linux-gnu" ]]; then if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "aarch64-unknown-linux-gnu" ]]; then
cargo deb --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_arm64.deb cargo deb --no-build --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}-1_arm64.deb
fi fi

View File

@ -8,7 +8,7 @@ use ffplayout_lib::{
#[test] #[test]
fn video_audio_input() { fn video_audio_input() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = Stream; config.out.mode = Stream;
@ -37,7 +37,7 @@ fn video_audio_input() {
#[test] #[test]
fn video_audio_custom_filter1_input() { fn video_audio_custom_filter1_input() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = Stream; config.out.mode = Stream;
@ -64,7 +64,7 @@ fn video_audio_custom_filter1_input() {
#[test] #[test]
fn video_audio_custom_filter2_input() { fn video_audio_custom_filter2_input() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = Stream; config.out.mode = Stream;
@ -93,7 +93,7 @@ fn video_audio_custom_filter2_input() {
#[test] #[test]
fn video_audio_custom_filter3_input() { fn video_audio_custom_filter3_input() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = Stream; config.out.mode = Stream;
@ -121,7 +121,7 @@ fn video_audio_custom_filter3_input() {
#[test] #[test]
fn dual_audio_aevalsrc_input() { fn dual_audio_aevalsrc_input() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = Stream; config.out.mode = Stream;
@ -149,7 +149,7 @@ fn dual_audio_aevalsrc_input() {
#[test] #[test]
fn dual_audio_input() { fn dual_audio_input() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = Stream; config.out.mode = Stream;
@ -176,7 +176,7 @@ fn dual_audio_input() {
#[test] #[test]
fn video_separate_audio_input() { fn video_separate_audio_input() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = Stream; config.out.mode = Stream;
@ -213,7 +213,7 @@ fn video_separate_audio_input() {
#[test] #[test]
fn video_audio_stream() { fn video_audio_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.out.output_cmd = Some(vec_strings![ config.out.output_cmd = Some(vec_strings![
@ -272,7 +272,7 @@ fn video_audio_stream() {
#[test] #[test]
fn video_audio_filter1_stream() { fn video_audio_filter1_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.text.add_text = false; config.text.add_text = false;
@ -347,7 +347,7 @@ fn video_audio_filter1_stream() {
#[test] #[test]
fn video_audio_filter2_stream() { fn video_audio_filter2_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.text.add_text = true; config.text.add_text = true;
@ -430,7 +430,7 @@ fn video_audio_filter2_stream() {
#[test] #[test]
fn video_audio_filter3_stream() { fn video_audio_filter3_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.text.add_text = true; config.text.add_text = true;
@ -516,7 +516,7 @@ fn video_audio_filter3_stream() {
#[test] #[test]
fn video_audio_filter4_stream() { fn video_audio_filter4_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.text.add_text = true; config.text.add_text = true;
@ -602,7 +602,7 @@ fn video_audio_filter4_stream() {
#[test] #[test]
fn video_dual_audio_stream() { fn video_dual_audio_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.processing.audio_tracks = 2; config.processing.audio_tracks = 2;
@ -673,7 +673,7 @@ fn video_dual_audio_stream() {
#[test] #[test]
fn video_dual_audio_filter_stream() { fn video_dual_audio_filter_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.processing.audio_tracks = 2; config.processing.audio_tracks = 2;
@ -753,7 +753,7 @@ fn video_dual_audio_filter_stream() {
#[test] #[test]
fn video_audio_multi_stream() { fn video_audio_multi_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.out.output_cmd = Some(vec_strings![ config.out.output_cmd = Some(vec_strings![
@ -842,7 +842,7 @@ fn video_audio_multi_stream() {
#[test] #[test]
fn video_dual_audio_multi_stream() { fn video_dual_audio_multi_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.processing.audio_tracks = 2; config.processing.audio_tracks = 2;
@ -956,7 +956,7 @@ fn video_dual_audio_multi_stream() {
#[test] #[test]
fn video_audio_text_multi_stream() { fn video_audio_text_multi_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.text.add_text = true; config.text.add_text = true;
@ -1069,7 +1069,7 @@ fn video_audio_text_multi_stream() {
#[test] #[test]
fn video_dual_audio_multi_filter_stream() { fn video_dual_audio_multi_filter_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.processing.audio_tracks = 2; config.processing.audio_tracks = 2;
@ -1198,7 +1198,7 @@ fn video_dual_audio_multi_filter_stream() {
#[test] #[test]
fn video_audio_text_filter_stream() { fn video_audio_text_filter_stream() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.out.mode = Stream; config.out.mode = Stream;
config.processing.add_logo = false; config.processing.add_logo = false;
config.processing.audio_tracks = 1; config.processing.audio_tracks = 1;
@ -1320,7 +1320,7 @@ fn video_audio_text_filter_stream() {
#[test] #[test]
fn video_audio_hls() { fn video_audio_hls() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = HLS; config.out.mode = HLS;
@ -1407,7 +1407,7 @@ fn video_audio_hls() {
#[test] #[test]
fn video_audio_sub_meta_hls() { fn video_audio_sub_meta_hls() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = HLS; config.out.mode = HLS;
@ -1502,7 +1502,7 @@ fn video_audio_sub_meta_hls() {
#[test] #[test]
fn video_multi_audio_hls() { fn video_multi_audio_hls() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = HLS; config.out.mode = HLS;
@ -1592,7 +1592,7 @@ fn video_multi_audio_hls() {
#[test] #[test]
fn multi_video_audio_hls() { fn multi_video_audio_hls() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = HLS; config.out.mode = HLS;
@ -1707,7 +1707,7 @@ fn multi_video_audio_hls() {
#[test] #[test]
fn multi_video_multi_audio_hls() { fn multi_video_multi_audio_hls() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
let player_control = PlayerControl::new(); let player_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new(); let playout_stat = PlayoutStatus::new();
config.out.mode = HLS; config.out.mode = HLS;

View File

@ -53,7 +53,7 @@ fn test_ordered_list() {
#[test] #[test]
#[ignore] #[ignore]
fn test_filler_list() { fn test_filler_list() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.storage.filler = "assets/".into(); config.storage.filler = "assets/".into();
let f_list = filler_list(&config, 2440.0); let f_list = filler_list(&config, 2440.0);
@ -64,7 +64,7 @@ fn test_filler_list() {
#[test] #[test]
#[ignore] #[ignore]
fn test_generate_playlist_from_folder() { fn test_generate_playlist_from_folder() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.generate = Some(vec!["2023-09-11".to_string()]); config.general.generate = Some(vec!["2023-09-11".to_string()]);
config.processing.mode = Playlist; config.processing.mode = Playlist;
config.logging.log_to_file = false; config.logging.log_to_file = false;
@ -98,7 +98,7 @@ fn test_generate_playlist_from_folder() {
#[test] #[test]
#[ignore] #[ignore]
fn test_generate_playlist_from_template() { fn test_generate_playlist_from_template() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.generate = Some(vec!["2023-09-12".to_string()]); config.general.generate = Some(vec!["2023-09-12".to_string()]);
config.general.template = Some(Template { config.general.template = Some(Template {
sources: vec![ sources: vec![

View File

@ -20,7 +20,7 @@ fn timed_stop(sec: u64, proc_ctl: ProcessControl) {
#[test] #[test]
#[ignore] #[ignore]
fn test_gen_source() { fn test_gen_source() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
@ -76,7 +76,7 @@ fn test_gen_source() {
100, 100,
); );
assert_eq!(valid_media.out, 1.9); assert_eq!(valid_media.out, 1.2);
let mut no_valid_source_with_probe = Media::new(0, "assets/media_mix/av_snc.mp4", true); let mut no_valid_source_with_probe = Media::new(0, "assets/media_mix/av_snc.mp4", true);
no_valid_source_with_probe.duration = 30.0; no_valid_source_with_probe.duration = 30.0;
@ -97,7 +97,7 @@ fn test_gen_source() {
#[serial] #[serial]
#[ignore] #[ignore]
fn playlist_missing() { fn playlist_missing() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
@ -140,7 +140,7 @@ fn playlist_missing() {
#[serial] #[serial]
#[ignore] #[ignore]
fn playlist_next_missing() { fn playlist_next_missing() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
@ -183,7 +183,7 @@ fn playlist_next_missing() {
#[serial] #[serial]
#[ignore] #[ignore]
fn playlist_to_short() { fn playlist_to_short() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
@ -226,7 +226,7 @@ fn playlist_to_short() {
#[serial] #[serial]
#[ignore] #[ignore]
fn playlist_init_after_list_end() { fn playlist_init_after_list_end() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
@ -269,7 +269,7 @@ fn playlist_init_after_list_end() {
#[serial] #[serial]
#[ignore] #[ignore]
fn playlist_change_at_midnight() { fn playlist_change_at_midnight() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
@ -312,7 +312,7 @@ fn playlist_change_at_midnight() {
#[serial] #[serial]
#[ignore] #[ignore]
fn playlist_change_before_midnight() { fn playlist_change_before_midnight() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
@ -355,7 +355,7 @@ fn playlist_change_before_midnight() {
#[serial] #[serial]
#[ignore] #[ignore]
fn playlist_change_at_six() { fn playlist_change_at_six() {
let mut config = PlayoutConfig::new(None); let mut config = PlayoutConfig::new(None, None);
config.general.skip_validation = true; config.general.skip_validation = true;
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;

View File

@ -40,7 +40,7 @@ fn get_date_tomorrow() {
#[test] #[test]
fn test_delta() { fn test_delta() {
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")), None);
config.mail.recipient = "".into(); config.mail.recipient = "".into();
config.processing.mode = Playlist; config.processing.mode = Playlist;
config.playlist.day_start = "00:00:00".into(); config.playlist.day_start = "00:00:00".into();