Merge pull request #768 from jb-alvarado/master

experimental closed captions
This commit is contained in:
jb-alvarado 2024-09-27 16:27:07 +02:00 committed by GitHub
commit 5a6a6292f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 717 additions and 305 deletions

View File

@ -48,19 +48,23 @@
},
"cSpell.words": [
"actix",
"canonicalize",
"ffpengine",
"flexi",
"httpauth",
"lettre",
"libc",
"nuxt",
"neli",
"nuxt",
"paris",
"Referer",
"reqwest",
"rsplit",
"rustls",
"sqlx",
"starttls",
"tokio",
"unistd",
"uuids"
]
}

106
Cargo.lock generated
View File

@ -549,9 +549,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.82"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
@ -582,9 +582,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "awc"
@ -793,9 +793,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.17"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [
"clap_builder",
"clap_derive",
@ -803,9 +803,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.17"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [
"anstream",
"anstyle",
@ -815,9 +815,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.13"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
@ -1215,7 +1215,7 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "ffplayout"
version = "0.24.0-rc1"
version = "0.24.0-beta5"
dependencies = [
"actix-files",
"actix-multipart",
@ -1314,9 +1314,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.0.33"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -1697,9 +1697,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba"
checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
dependencies = [
"bytes",
"futures-channel",
@ -1710,7 +1710,6 @@ dependencies = [
"pin-project-lite",
"socket2",
"tokio",
"tower",
"tower-service",
"tracing",
]
@ -2068,9 +2067,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.158"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libm"
@ -2520,26 +2519,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "pin-project-lite"
version = "0.2.14"
@ -2575,9 +2554,9 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "powerfmt"
@ -2739,9 +2718,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.4"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b"
dependencies = [
"bitflags 2.6.0",
]
@ -3089,9 +3068,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.7"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
@ -3681,7 +3660,7 @@ dependencies = [
[[package]]
name = "tests"
version = "0.24.0-rc1"
version = "0.24.0-beta5"
dependencies = [
"actix-rt",
"actix-test",
@ -3711,18 +3690,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
@ -3861,9 +3840,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.21"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.5.0",
"serde",
@ -3872,27 +3851,6 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
@ -4495,9 +4453,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]

View File

@ -3,7 +3,7 @@ members = ["engine", "tests"]
resolver = "2"
[workspace.package]
version = "0.24.0-rc1"
version = "0.24.0-beta5"
license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout"
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]

View File

@ -3,7 +3,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
## **ffplayout-engine (ffplayout)**
![player](/docs/images/player.png)
[ffplayout](/ffplayout-engine/README.md) is a 24/7 broadcasting solution. It can playout a folder containing audio or video clips, or play a *JSON* playlist for each day, keeping the current playlist editable.
@ -120,6 +120,9 @@ If you are in playlist mode and move backwards or forwards in time, the time shi
(Endless) streaming over multiple days will only work if config has a **day_start** value and the **length** value is **24 hours**. If you only need a few hours for each day, use a *cron* job or something similar.
## Note
This project includes the DejaVu font, which are licensed under the [Bitstream Vera Fonts License](/assets/FONT_LICENSE.txt).
-----
## Sponsoring

BIN
assets/DejaVuSans.ttf Normal file

Binary file not shown.

187
assets/FONT_LICENSE.txt Normal file
View File

@ -0,0 +1,187 @@
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
Bitstream Vera Fonts Copyright
------------------------------
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org.
Arev Fonts Copyright
------------------------------
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and
associated documentation files (the "Font Software"), to reproduce
and distribute the modifications to the Bitstream Vera Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to
the following conditions:
The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.
The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Tavmjong Bah" or the word "Arev".
This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the
"Tavmjong Bah Arev" names.
The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the name of Tavmjong Bah shall not
be used in advertising or otherwise to promote the sale, use or other
dealings in this Font Software without prior written authorization
from Tavmjong Bah. For further information, contact: tavmjong @ free
. fr.
TeX Gyre DJV Math
-----------------
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski
(on behalf of TeX users groups) are in public domain.
Letters imported from Euler Fraktur from AMSfonts are (c) American
Mathematical Society (see below).
Bitstream Vera Fonts Copyright
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera
is a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license (“Fonts”) and associated
documentation
files (the “Font Software”), to reproduce and distribute the Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute,
and/or sell copies of the Font Software, and to permit persons to whom
the Font Software is furnished to do so, subject to the following
conditions:
The above copyright and trademark notices and this permission notice
shall be
included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional
glyphs or characters may be added to the Fonts, only if the fonts are
renamed
to names not containing either the words “Bitstream” or the word “Vera”.
This License becomes null and void to the extent applicable to Fonts or
Font Software
that has been modified and is distributed under the “Bitstream Vera”
names.
The Font Software may be sold as part of a larger software package but
no copy
of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL,
SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN
ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
INABILITY TO USE
THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the names of GNOME, the GNOME
Foundation,
and Bitstream Inc., shall not be used in advertising or otherwise to promote
the sale, use or other dealings in this Font Software without prior written
authorization from the GNOME Foundation or Bitstream Inc., respectively.
For further information, contact: fonts at gnome dot org.
AMSFonts (v. 2.2) copyright
The PostScript Type 1 implementation of the AMSFonts produced by and
previously distributed by Blue Sky Research and Y&Y, Inc. are now freely
available for general use. This has been accomplished through the
cooperation
of a consortium of scientific publishers with Blue Sky Research and Y&Y.
Members of this consortium include:
Elsevier Science IBM Corporation Society for Industrial and Applied
Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS)
In order to assure the authenticity of these fonts, copyright will be
held by
the American Mathematical Society. This is not meant to restrict in any way
the legitimate use of the fonts, such as (but not limited to) electronic
distribution of documents containing these fonts, inclusion of these fonts
into other public domain or commercial font collections or computer
applications, use of the outline data to create derivative fonts and/or
faces, etc. However, the AMS does require that the AMS copyright notice be
removed from any derivative versions of the fonts which have been altered in
any way. In addition, to ensure the fidelity of TeX documents using Computer
Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
has requested that any alterations which yield different font metrics be
given a different name.
$Id$

1
assets/dummy.vtt Normal file
View File

@ -0,0 +1 @@
WEBVTT

View File

@ -1,6 +1,6 @@
FROM alpine:latest
ARG FFPLAYOUT_VERSION=0.24.0-rc1
ARG FFPLAYOUT_VERSION=0.24.0-beta5
ARG SHARED_STORAGE=false
ENV DB=/db

View File

@ -1,6 +1,6 @@
FROM alpine:latest
ARG FFPLAYOUT_VERSION=0.24.0-rc1
ARG FFPLAYOUT_VERSION=0.24.0-beta5
ARG SHARED_STORAGE=false
ENV DB=/db

View File

@ -1,6 +1,6 @@
FROM nvidia/cuda:12.5.0-runtime-rockylinux9
ARG FFPLAYOUT_VERSION=0.24.0-rc1
ARG FFPLAYOUT_VERSION=0.24.0-beta5
ARG SHARED_STORAGE=false
ENV DB=/db

23
docs/closed_captions.md Normal file
View File

@ -0,0 +1,23 @@
## Closed Captions
#### Note:
**This is only an _experimental feature_. Please be aware that bugs and unexpected behavior may occur. To utilize this feature, a [special patched](https://github.com/jb-alvarado/compile-ffmpeg-osx-linux) version of FFmpeg is required. Importantly, there is currently no official support for this functionality.**
### Usage
**ffplayout** can handle closed captions in WebVTT format for HLS streaming.
The captions can be embedded in the file, such as in a [Matroska](https://www.matroska.org/technical/subtitles.html) file, or they can be a separate *.vtt file that shares the same filename as the video file. In either case, the processing option **vtt_enable** must be enabled, and the path to the **vtt_dummy** file must exist.
To encode the closed captions, the **hls** mode needs to be enabled, and specific output parameters must be provided. Heres an example:
```
-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 \
-maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency \
-profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop \
-muxpreload 0 -muxdelay 0 -f hls -hls_time 6 -hls_list_size 600 \
-hls_flags append_list+delete_segments+omit_endlist \
-var_stream_map v:0,a:0,s:0,sgroup:subs,name:English,language:en-US,default:YES \
-master_pl_name master.m3u8 \
-hls_segment_filename \
live/stream-%d.ts live/stream.m3u8
```

View File

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

View File

@ -78,7 +78,6 @@ static-files = "0.2"
name = "ffplayout"
path = "src/main.rs"
# DEBIAN DEB PACKAGE
[package.metadata.deb]
name = "ffplayout"
@ -99,6 +98,21 @@ assets = [
"/lib/systemd/system/",
"644",
],
[
"../assets/dummy.vtt",
"/usr/share/ffplayout/",
"644",
],
[
"../assets/DejaVuSans.ttf",
"/usr/share/ffplayout/",
"644",
],
[
"../assets/FONT_LICENSE.txt",
"/usr/share/ffplayout/",
"644",
],
[
"../assets/logo.png",
"/usr/share/ffplayout/",
@ -135,6 +149,21 @@ assets = [
"/lib/systemd/system/",
"644",
],
[
"../assets/dummy.vtt",
"/usr/share/ffplayout/",
"644",
],
[
"../assets/DejaVuSans.ttf",
"/usr/share/ffplayout/",
"644",
],
[
"../assets/FONT_LICENSE.txt",
"/usr/share/ffplayout/",
"644",
],
[
"../assets/logo.png",
"/usr/share/ffplayout/",
@ -157,7 +186,7 @@ assets = [
],
]
# REHL RPM PACKAGE
# RHEL RPM PACKAGE
[package.metadata.generate-rpm]
name = "ffplayout"
license = "GPL-3.0"
@ -167,6 +196,9 @@ assets = [
{ source = "../README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644" },
{ source = "../assets/ffplayout.1.gz", dest = "/usr/share/man/man1/ffplayout.1.gz", mode = "644", doc = true },
{ source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" },
{ source = "../assets/dummy.vtt", dest = "/usr/share/ffplayout/dummy.vtt", mode = "644" },
{ source = "../assets/DejaVuSans.ttf", dest = "/usr/share/ffplayout/DejaVuSans.ttf", mode = "644" },
{ source = "../assets/FONT_LICENSE.txt", dest = "/usr/share/ffplayout/FONT_LICENSE.txt", mode = "644" },
{ source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" },
{ source = "../assets/ffplayout.conf", dest = "/usr/share/ffplayout/ffplayout.conf.example", mode = "644" },
{ source = "../debian/postinst", dest = "/usr/share/ffplayout/postinst", mode = "755" },

View File

@ -480,7 +480,11 @@ async fn patch_channel(
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let manager = controllers
.lock()
.unwrap()
.get(*id)
.ok_or(format!("Channel {id} not found!"))?;
let mut data = data.into_inner();
if !role.has_authority(&Role::GlobalAdmin) {
@ -1328,7 +1332,10 @@ async fn get_public(
) -> Result<actix_files::NamedFile, ServiceError> {
let (id, public, file_stem) = path.into_inner();
let absolute_path = if file_stem.ends_with(".ts") || file_stem.ends_with(".m3u8") {
let absolute_path = if file_stem.ends_with(".ts")
|| file_stem.ends_with(".m3u8")
|| file_stem.ends_with(".vtt")
{
let manager = controllers.lock().unwrap().get(id).unwrap();
let config = manager.config.lock().unwrap();
config.channel.hls_path.join(public)

View File

@ -211,7 +211,7 @@ pub async fn update_configuration(
id: i32,
config: PlayoutConfig,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "UPDATE configurations SET general_stop_threshold = $2, mail_subject = $3, mail_smtp = $4, mail_addr = $5, mail_pass = $6, mail_recipient = $7, mail_starttls = $8, mail_level = $9, mail_interval = $10, logging_ffmpeg_level = $11, logging_ingest_level = $12, logging_detect_silence = $13, logging_ignore = $14, processing_mode = $15, processing_audio_only = $16, processing_copy_audio = $17, processing_copy_video = $18, processing_width = $19, processing_height = $20, processing_aspect = $21, processing_fps = $22, processing_add_logo = $23, processing_logo = $24, processing_logo_scale = $25, processing_logo_opacity = $26, processing_logo_position = $27, processing_audio_tracks = $28, processing_audio_track_index = $29, processing_audio_channels = $30, processing_volume = $31, processing_filter = $32, ingest_enable = $33, ingest_param = $34, ingest_filter = $35, playlist_day_start = $36, playlist_length = $37, playlist_infinit = $38, storage_filler = $39, storage_extensions = $40, storage_shuffle = $41, text_add = $42, text_from_filename = $43, text_font = $44, text_style = $45, text_regex = $46, task_enable = $47, task_path = $48, output_mode = $49, output_param = $50 WHERE id = $1";
let query = "UPDATE configurations SET general_stop_threshold = $2, mail_subject = $3, mail_smtp = $4, mail_addr = $5, mail_pass = $6, mail_recipient = $7, mail_starttls = $8, mail_level = $9, mail_interval = $10, logging_ffmpeg_level = $11, logging_ingest_level = $12, logging_detect_silence = $13, logging_ignore = $14, processing_mode = $15, processing_audio_only = $16, processing_copy_audio = $17, processing_copy_video = $18, processing_width = $19, processing_height = $20, processing_aspect = $21, processing_fps = $22, processing_add_logo = $23, processing_logo = $24, processing_logo_scale = $25, processing_logo_opacity = $26, processing_logo_position = $27, processing_audio_tracks = $28, processing_audio_track_index = $29, processing_audio_channels = $30, processing_volume = $31, processing_filter = $32, processing_vtt_enable = $33, processing_vtt_dummy = $34, ingest_enable = $35, ingest_param = $36, ingest_filter = $37, playlist_day_start = $38, playlist_length = $39, playlist_infinit = $40, storage_filler = $41, storage_extensions = $42, storage_shuffle = $43, text_add = $44, text_from_filename = $45, text_font = $46, text_style = $47, text_regex = $48, task_enable = $49, task_path = $50, output_mode = $51, output_param = $52 WHERE id = $1";
sqlx::query(query)
.bind(id)
@ -246,6 +246,8 @@ pub async fn update_configuration(
.bind(config.processing.audio_channels)
.bind(config.processing.volume)
.bind(config.processing.custom_filter)
.bind(config.processing.vtt_enable)
.bind(config.processing.vtt_dummy)
.bind(config.ingest.enable)
.bind(config.ingest.input_param)
.bind(config.ingest.custom_filter)

View File

@ -302,6 +302,10 @@ pub struct Configuration {
pub processing_volume: f64,
#[serde(default)]
pub processing_filter: String,
#[serde(default)]
pub processing_vtt_enable: bool,
#[serde(default)]
pub processing_vtt_dummy: Option<String>,
pub ingest_help: String,
pub ingest_enable: bool,
@ -375,6 +379,8 @@ impl Configuration {
processing_audio_channels: config.processing.audio_channels,
processing_volume: config.processing.volume,
processing_filter: config.processing.custom_filter,
processing_vtt_enable: config.processing.vtt_enable,
processing_vtt_dummy: config.processing.vtt_dummy,
ingest_help: config.ingest.help_text,
ingest_enable: config.ingest.enable,
ingest_param: config.ingest.input_param,

View File

@ -199,6 +199,8 @@ async fn main() -> std::io::Result<()> {
.workers(thread_count)
.run()
.await?;
} else if ARGS.drop_db {
db_drop().await;
} else {
let channels = ARGS.channels.clone().unwrap_or_else(|| vec![1]);
@ -267,8 +269,6 @@ async fn main() -> std::io::Result<()> {
playlist,
Arc::new(AtomicBool::new(false)),
);
} else if ARGS.drop_db {
db_drop().await;
} else if !ARGS.init {
error!("Run ffplayout with parameters! Run ffplayout -h for more information.");
}

View File

@ -72,6 +72,10 @@ pub fn ingest_server(
dummy_media.add_filter(&config, &None);
let is_terminated = channel_mgr.is_terminated.clone();
let ingest_is_running = channel_mgr.ingest_is_running.clone();
let vtt_dummy = config
.channel
.storage_path
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if let Some(ingest_input_cmd) = config.advanced.ingest.input_cmd {
server_cmd.append(&mut ingest_input_cmd.clone());
@ -79,11 +83,19 @@ pub fn ingest_server(
server_cmd.append(&mut stream_input.clone());
if config.processing.vtt_enable && vtt_dummy.is_file() {
server_cmd.append(&mut vec_strings!["-i", vtt_dummy.to_string_lossy()]);
}
if let Some(mut filter) = dummy_media.filter {
server_cmd.append(&mut filter.cmd());
server_cmd.append(&mut filter.map());
}
if config.processing.vtt_enable && vtt_dummy.is_file() {
server_cmd.append(&mut vec_strings!("-map", "1:s"));
}
if let Some(mut cmd) = config.processing.cmd {
server_cmd.append(&mut cmd);
}

View File

@ -640,14 +640,14 @@ pub fn gen_source(
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
.is_some()
{
node.cmd = Some(loop_image(&node));
node.cmd = Some(loop_image(config, &node));
} else {
if node.seek > 0.0 && node.out > node.duration {
warn!(target: Target::file_mail(), channel = config.general.channel_id; "Clip loops and has seek value: duplicate clip to separate loop and seek.");
duplicate_for_seek_and_loop(&mut node, &manager.current_list);
}
node.cmd = Some(seek_and_length(&mut node));
node.cmd = Some(seek_and_length(config, &mut node));
}
} else {
trace!("clip index: {node_index} | last index: {last_index}");
@ -694,7 +694,7 @@ pub fn gen_source(
node.seek = 0.0;
node.out = filler_media.out;
node.duration = filler_media.duration;
node.cmd = Some(loop_filler(&node));
node.cmd = Some(loop_filler(config, &node));
node.probe = filler_media.probe;
} else {
match MediaProbe::new(&config.storage.filler_path.to_string_lossy()) {
@ -715,7 +715,7 @@ pub fn gen_source(
.clone()
.to_string_lossy()
.to_string();
node.cmd = Some(loop_image(&node));
node.cmd = Some(loop_image(config, &node));
node.probe = Some(probe);
} else if let Some(filler_duration) = probe
.clone()
@ -739,7 +739,7 @@ pub fn gen_source(
node.seek = 0.0;
node.out = filler_out;
node.duration = filler_duration;
node.cmd = Some(loop_filler(&node));
node.cmd = Some(loop_filler(config, &node));
node.probe = Some(probe);
} else {
// Create colored placeholder.

View File

@ -62,6 +62,17 @@ fn ingest_to_hls_server(manager: ChannelManager) -> Result<(), ProcessError> {
server_prefix.append(&mut stream_input.clone());
if config.processing.vtt_enable {
let vtt_dummy = config
.channel
.storage_path
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if vtt_dummy.is_file() {
server_prefix.append(&mut vec_strings!["-i", vtt_dummy.to_string_lossy()]);
}
}
let mut is_running;
if let Some(url) = stream_input.iter().find(|s| s.contains("://")) {

View File

@ -150,6 +150,16 @@ pub fn player(manager: ChannelManager) -> Result<(), ProcessError> {
dec_cmd.append(&mut filter.map());
}
if config.processing.vtt_enable && dec_cmd.iter().any(|s| s.ends_with(".vtt")) {
let i = dec_cmd
.iter()
.filter(|&n| n == "-i")
.count()
.saturating_sub(1);
dec_cmd.append(&mut vec_strings!("-map", format!("{i}:s"), "-c:s", "copy"));
}
if let Some(mut cmd) = config.processing.cmd.clone() {
dec_cmd.append(&mut cmd);
}

View File

@ -65,9 +65,9 @@ fn check_media(
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
.is_some()
{
node.cmd = Some(loop_image(&node));
node.cmd = Some(loop_image(&config, &node));
} else {
node.cmd = Some(seek_and_length(&mut node));
node.cmd = Some(seek_and_length(&config, &mut node));
}
node.add_filter(&config, &None);

View File

@ -67,6 +67,10 @@ pub fn prepare_output_cmd(
let mut new_params = vec![];
let mut count = 0;
let re_v = Regex::new(r"\[?0:v(:0)?\]?").unwrap();
let vtt_dummy = config
.channel
.storage_path
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if let Some(mut filter) = filters.clone() {
for (i, param) in output_params.iter().enumerate() {
@ -119,6 +123,12 @@ pub fn prepare_output_cmd(
}
}
if config.processing.vtt_enable && vtt_dummy.is_file() {
let i = cmd.iter().filter(|&n| n == "-i").count().saturating_sub(1);
cmd.append(&mut vec_strings!("-map", format!("{i}:s?")));
}
cmd.append(&mut output_params);
cmd
@ -589,7 +599,7 @@ pub fn get_delta(config: &PlayoutConfig, begin: &f64) -> (f64, f64) {
}
/// Loop image until target duration is reached.
pub fn loop_image(node: &Media) -> Vec<String> {
pub fn loop_image(config: &PlayoutConfig, node: &Media) -> Vec<String> {
let duration = node.out - node.seek;
let mut source_cmd: Vec<String> = vec_strings!["-loop", "1", "-i", node.source.clone()];
@ -608,11 +618,34 @@ pub fn loop_image(node: &Media) -> Vec<String> {
source_cmd.append(&mut vec_strings!["-t", duration]);
if config.processing.vtt_enable {
let vtt_file = Path::new(&node.source).with_extension("vtt");
let vtt_dummy = config
.channel
.storage_path
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if node.seek > 0.5 {
source_cmd.append(&mut vec_strings!["-ss", node.seek]);
}
if vtt_file.is_file() {
source_cmd.append(&mut vec_strings![
"-i",
vtt_file.to_string_lossy(),
"-t",
node.out
]);
} else if vtt_dummy.is_file() {
source_cmd.append(&mut vec_strings!["-i", vtt_dummy.to_string_lossy()]);
}
}
source_cmd
}
/// Loop filler until target duration is reached.
pub fn loop_filler(node: &Media) -> Vec<String> {
pub fn loop_filler(config: &PlayoutConfig, node: &Media) -> Vec<String> {
let loop_count = (node.out / node.duration).ceil() as i32;
let mut source_cmd = vec![];
@ -624,11 +657,34 @@ pub fn loop_filler(node: &Media) -> Vec<String> {
source_cmd.append(&mut vec_strings!["-i", node.source, "-t", node.out]);
if config.processing.vtt_enable {
let vtt_file = Path::new(&node.source).with_extension("vtt");
let vtt_dummy = config
.channel
.storage_path
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if vtt_file.is_file() {
if loop_count > 1 {
source_cmd.append(&mut vec_strings!["-stream_loop", loop_count]);
}
source_cmd.append(&mut vec_strings![
"-i",
vtt_file.to_string_lossy(),
"-t",
node.out
]);
} else if vtt_dummy.is_file() {
source_cmd.append(&mut vec_strings!["-i", vtt_dummy.to_string_lossy()]);
}
}
source_cmd
}
/// Set clip seek in and length value.
pub fn seek_and_length(node: &mut Media) -> Vec<String> {
pub fn seek_and_length(config: &PlayoutConfig, node: &mut Media) -> Vec<String> {
let loop_count = (node.out / node.duration).ceil() as i32;
let mut source_cmd = vec![];
let mut cut_audio = false;
@ -673,6 +729,34 @@ pub fn seek_and_length(node: &mut Media) -> Vec<String> {
}
}
if config.processing.vtt_enable {
let vtt_file = Path::new(&node.source).with_extension("vtt");
let vtt_dummy = config
.channel
.storage_path
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if node.seek > 0.5 {
source_cmd.append(&mut vec_strings!["-ss", node.seek]);
}
if vtt_file.is_file() {
if loop_count > 1 {
source_cmd.append(&mut vec_strings!["-stream_loop", loop_count]);
}
source_cmd.append(&mut vec_strings!["-i", vtt_file.to_string_lossy()]);
if node.duration > node.out || remote_source || loop_count > 1 {
source_cmd.append(&mut vec_strings!["-t", node.out - node.seek]);
}
} else if vtt_dummy.is_file() {
source_cmd.append(&mut vec_strings!["-i", vtt_dummy.to_string_lossy()]);
} else {
error!("<b><magenta>{:?}</></b> not found!", vtt_dummy)
}
}
source_cmd
}
@ -683,7 +767,7 @@ pub fn gen_dummy(config: &PlayoutConfig, duration: f64) -> (String, Vec<String>)
"color=c={color}:s={}x{}:d={duration}",
config.processing.width, config.processing.height
);
let cmd: Vec<String> = vec_strings![
let mut source_cmd: Vec<String> = vec_strings![
"-f",
"lavfi",
"-i",
@ -697,7 +781,18 @@ pub fn gen_dummy(config: &PlayoutConfig, duration: f64) -> (String, Vec<String>)
format!("anoisesrc=d={duration}:c=pink:r=48000:a=0.3")
];
(source, cmd)
if config.processing.vtt_enable {
let vtt_dummy = config
.channel
.storage_path
.join(config.processing.vtt_dummy.clone().unwrap_or_default());
if vtt_dummy.is_file() {
source_cmd.append(&mut vec_strings!["-i", vtt_dummy.to_string_lossy()]);
}
}
(source, source_cmd)
}
// fn get_output_count(cmd: &[String]) -> i32 {

View File

@ -4,12 +4,15 @@ use std::{
};
#[cfg(target_family = "unix")]
use std::{fs, process::exit};
use std::os::unix::fs::MetadataExt;
use clap::Parser;
use rpassword::read_password;
use sqlx::{Pool, Sqlite};
#[cfg(target_family = "unix")]
use tokio::fs;
use crate::db::{
handles,
models::{Channel, GlobalSettings, User},
@ -17,6 +20,7 @@ use crate::db::{
use crate::utils::{
advanced_config::AdvancedConfig,
config::{OutputMode, PlayoutConfig},
copy_assets,
};
use crate::ARGS;
@ -26,145 +30,154 @@ use crate::utils::db_path;
#[derive(Parser, Debug, Clone)]
#[clap(version,
about = "ffplayout - 24/7 broadcasting solution",
long_about = None)]
long_about = Some("ffplayout - 24/7 broadcasting solution\n
Stream dynamic playlists or folder contents with the power of ffmpeg.
The target can be an HLS playlist, rtmp/srt/udp server, desktop player
or any other output supported by ffmpeg.\n
ffplayout also provides a web frontend and API to control streaming,
manage config, files, text overlay, etc. "))]
pub struct Args {
#[clap(
short,
long,
help_heading = Some("Initial Setup"),
help = "Initialize defaults: global admin, paths, settings, etc."
)]
pub init: bool,
#[clap(short, long, help = "Add a global admin user")]
#[clap(short, long, help_heading = Some("Initial Setup"), help = "Add a global admin user")]
pub add: bool,
#[clap(long, env, help = "Path to database file")]
pub db: Option<PathBuf>,
#[clap(short, long, help_heading = Some("Initial Setup"), help = "Create admin user")]
pub username: Option<String>,
#[clap(short, long, help_heading = Some("Initial Setup"), help = "Admin mail address")]
pub mail: Option<String>,
#[clap(short, long, help_heading = Some("Initial Setup"), help = "Admin password")]
pub password: Option<String>,
#[clap(long, env, help_heading = Some("Initial Setup"), help = "Storage root path")]
pub storage: Option<String>,
#[clap(
long,
env,
help_heading = Some("Initial Setup"),
help = "Share storage across channels, important for running in Containers"
)]
pub shared_storage: bool,
#[clap(long, env, help_heading = Some("Initial Setup / General"), help = "Logging path")]
pub log_path: Option<PathBuf>,
#[clap(long, env, help_heading = Some("Initial Setup / General"), help = "Path to public files, also HLS playlists")]
pub public: Option<String>,
#[clap(long, help_heading = Some("Initial Setup / Playlist"), help = "Path to playlist, or playlist root folder.")]
pub playlist: Option<String>,
#[clap(long, env, help_heading = Some("General"), help = "Path to database file")]
pub db: Option<PathBuf>,
#[clap(
long,
help_heading = Some("General"),
help = "Drop database. WARNING: this will delete all configurations!"
)]
pub drop_db: bool,
#[clap(
short,
long,
env,
help = "Channels by ids to process (for foreground, etc.)",
num_args = 1..,
)]
pub channels: Option<Vec<i32>>,
#[clap(long, env, help = "Run playout without webserver and frontend.")]
pub foreground: bool,
#[clap(
long,
help_heading = Some("General"),
help = "Dump advanced channel configuration to advanced_{channel}.toml"
)]
pub dump_advanced: bool,
#[clap(long, help = "Dump channel configuration to ffplayout_{channel}.toml")]
#[clap(long, help_heading = Some("General"), help = "Dump channel configuration to ffplayout_{channel}.toml")]
pub dump_config: bool,
#[clap(
long,
help = "import advanced channel configuration from file.",
num_args = 2
help_heading = Some("General"),
help = "import advanced channel configuration from file."
)]
pub import_advanced: Option<PathBuf>,
#[clap(long, help = "import channel configuration from file.", num_args = 2)]
#[clap(long, help_heading = Some("General"), help = "import channel configuration from file.")]
pub import_config: Option<PathBuf>,
#[clap(long, help = "List available channel ids")]
#[clap(long, help_heading = Some("General"), help = "List available channel ids")]
pub list_channels: bool,
#[clap(short, env, long, help = "Listen on IP:PORT, like: 127.0.0.1:8787")]
#[clap(short, env, long, help_heading = Some("General"), help = "Listen on IP:PORT, like: 127.0.0.1:8787")]
pub listen: Option<String>,
#[clap(short, long, help = "Play folder content")]
pub folder: Option<PathBuf>,
#[clap(
long,
env,
help_heading = Some("General"),
help = "Override logging level: trace, debug, println, warn, eprintln"
)]
pub log_level: Option<String>,
#[clap(long, env, help_heading = Some("General"), help = "Log to console")]
pub log_to_console: bool,
#[clap(
short,
long,
env,
help_heading = Some("General / Playout"),
help = "Channels by ids to process (for export config, foreground running, etc.)",
num_args = 1..,
)]
pub channels: Option<Vec<i32>>,
#[clap(
short,
long,
help_heading = Some("Playlist"),
help = "Generate playlist for dates, like: 2022-01-01 - 2022-01-10",
name = "YYYY-MM-DD",
num_args = 1..,
)]
pub generate: Option<Vec<String>>,
#[clap(long, help = "Optional path list for playlist generations", num_args = 1..)]
#[clap(long, help_heading = Some("Playlist"), help = "Optional path list for playlist generations", num_args = 1..)]
pub paths: Option<Vec<PathBuf>>,
#[clap(long, env, help = "Keep log file for given days")]
pub log_backup_count: Option<usize>,
#[clap(
long,
env,
help = "Override logging level: trace, debug, println, warn, eprintln"
)]
pub log_level: Option<String>,
#[clap(long, env, help = "Logging path")]
pub log_path: Option<PathBuf>,
#[clap(long, env, help = "Log to console")]
pub log_to_console: bool,
#[clap(long, env, help = "Path to public files, also HLS playlists")]
pub public: Option<String>,
#[clap(long, env, help = "Playlist root path")]
pub playlist_root: Option<String>,
#[clap(long, env, help = "Storage root path")]
pub storage_root: Option<String>,
#[clap(
long,
env,
help = "Share storage root across channels, important for running in Container"
)]
pub shared_storage: bool,
#[clap(short, long, help = "Create admin user")]
pub username: Option<String>,
#[clap(short, long, help = "Admin mail address")]
pub mail: Option<String>,
#[clap(short, long, help = "Admin password")]
pub password: Option<String>,
#[clap(long, help = "Path to playlist, or playlist root folder.")]
pub playlist: Option<PathBuf>,
#[clap(
short,
long,
help_heading = Some("Playlist"),
help = "Start time in 'hh:mm:ss', 'now' for start with first"
)]
pub start: Option<String>,
#[clap(short = 'T', long, help = "JSON Template file for generating playlist")]
#[clap(short = 'T', long, help_heading = Some("Playlist"), help = "JSON template file for generating playlist")]
pub template: Option<PathBuf>,
#[clap(short, long, help = "Set output mode: desktop, hls, null, stream")]
#[clap(long, help_heading = Some("Playlist"), help = "Only validate given playlist")]
pub validate: bool,
#[clap(long, env, help_heading = Some("Playout"), help = "Run playout without webserver and frontend.")]
pub foreground: bool,
#[clap(short, long, help_heading = Some("Playout"), help = "Play folder content")]
pub folder: Option<PathBuf>,
#[clap(long, env, help_heading = Some("Playout"), help = "Keep log file for given days")]
pub log_backup_count: Option<usize>,
#[clap(short, long, help_heading = Some("Playout"), help = "Set output mode: desktop, hls, null, stream")]
pub output: Option<OutputMode>,
#[clap(short, long, help = "Set audio volume")]
#[clap(short, long, help_heading = Some("Playout"), help = "Set audio volume")]
pub volume: Option<f64>,
#[clap(long, help = "Skip validation process")]
#[clap(long, help_heading = Some("Playout"), help = "Skip validation process")]
pub skip_validation: bool,
#[clap(long, help = "Only validate given playlist")]
pub validate: bool,
}
fn global_user(args: &mut Args) {
@ -212,43 +225,6 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
let mut error_code = -1;
if args.init {
#[cfg(target_family = "unix")]
let process_user = nix::unistd::User::from_name("ffpu").unwrap_or_default();
#[cfg(target_family = "unix")]
let mut fix_permission = false;
#[cfg(target_family = "unix")]
{
let uid = nix::unistd::Uid::current();
let current_user = nix::unistd::User::from_uid(uid).unwrap_or_default();
if current_user != process_user {
let user_name = current_user.unwrap().name;
let mut fix_perm = String::new();
println!(
"\nYou run the initialization as user {}.\nFix permissions after initialization?\n",
user_name
);
print!("Fix permission [Y/n]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut fix_perm)
.expect("Did not enter a yes or no?");
fix_permission = fix_perm.trim().to_lowercase().starts_with('y');
if fix_permission && user_name != "root" {
println!("\nYou do not have permission to change DB file ownership!\nRun as proper process user or root.");
exit(1);
}
}
}
let check_user = handles::select_users(pool).await;
let mut storage = String::new();
@ -353,7 +329,11 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
channel.playlist_path = global.playlist_root;
channel.storage_path = global.storage_root;
let mut storage_path = PathBuf::from(channel.storage_path.clone());
if global.shared_storage {
storage_path = storage_path.join("1");
channel.preview_url = "http://127.0.0.1:8787/1/stream.m3u8".to_string();
channel.hls_path = Path::new(&channel.hls_path)
.join("1")
@ -363,34 +343,18 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
.join("1")
.to_string_lossy()
.to_string();
channel.storage_path = Path::new(&channel.storage_path)
.join("1")
.to_string_lossy()
.to_string();
channel.storage_path = storage_path.to_string_lossy().to_string();
};
if let Err(e) = copy_assets(&storage_path).await {
eprintln!("{e}");
};
handles::update_channel(pool, 1, channel).await.unwrap();
#[cfg(target_family = "unix")]
if fix_permission {
let db_path = Path::new(db_path().unwrap()).with_extension("");
let user = process_user.unwrap();
let db = fs::canonicalize(db_path.with_extension("db")).unwrap();
let shm = fs::canonicalize(db_path.with_extension("db-shm")).unwrap();
let wal = fs::canonicalize(db_path.with_extension("db-wal")).unwrap();
nix::unistd::chown(&db, Some(user.uid), Some(user.gid)).expect("Change DB owner");
if shm.is_file() {
nix::unistd::chown(&shm, Some(user.uid), Some(user.gid))
.expect("Change DB-SHM owner");
}
if wal.is_file() {
nix::unistd::chown(&wal, Some(user.uid), Some(user.gid))
.expect("Change DB-WAL owner");
}
{
update_permissions().await;
}
println!("\nSet global settings done...");
@ -410,7 +374,7 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
let chl: Vec<i32> = channels.clone().iter().map(|c| c.id).collect();
let user = User {
let ff_user = User {
id: 0,
mail: Some(args.mail.unwrap()),
username: username.clone(),
@ -420,7 +384,7 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
token: None,
};
if let Err(e) = handles::insert_user(pool, user).await {
if let Err(e) = handles::insert_user(pool, ff_user).await {
eprintln!("{e}");
error_code = 1;
};
@ -429,8 +393,8 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
}
if !args.init
&& args.storage_root.is_some()
&& args.playlist_root.is_some()
&& args.storage.is_some()
&& args.playlist.is_some()
&& args.public.is_some()
&& args.log_path.is_some()
{
@ -440,17 +404,20 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
id: 0,
secret: None,
logging_path: args.log_path.unwrap().to_string_lossy().to_string(),
playlist_root: args.playlist_root.unwrap(),
playlist_root: args.playlist.unwrap(),
public_root: args.public.unwrap(),
storage_root: args.storage_root.unwrap(),
storage_root: args.storage.unwrap(),
shared_storage: args.shared_storage,
};
let mut channel = handles::select_channel(pool, &1)
.await
.expect("Select Channel 1");
let mut storage_path = PathBuf::from(global.storage_root.clone());
if args.shared_storage {
storage_path = storage_path.join("1");
channel.hls_path = Path::new(&global.public_root)
.join("1")
.to_string_lossy()
@ -459,16 +426,17 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
.join("1")
.to_string_lossy()
.to_string();
channel.storage_path = Path::new(&global.storage_root)
.join("1")
.to_string_lossy()
.to_string();
channel.storage_path = storage_path.to_string_lossy().to_string();
} else {
channel.hls_path = global.public_root.clone();
channel.playlist_path = global.playlist_root.clone();
channel.storage_path = global.storage_root.clone();
}
if let Err(e) = copy_assets(&storage_path).await {
eprintln!("{e}");
};
match handles::update_global(pool, global.clone()).await {
Ok(_) => println!("Update globals done..."),
Err(e) => {
@ -484,6 +452,11 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
error_code = 1;
}
};
#[cfg(target_family = "unix")]
{
update_permissions().await;
}
}
if ARGS.list_channels {
@ -589,3 +562,35 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
Ok(())
}
}
#[cfg(target_family = "unix")]
async fn update_permissions() {
let db_path = Path::new(db_path().unwrap());
let uid = nix::unistd::Uid::current();
let parent_owner = db_path.parent().unwrap().metadata().unwrap().uid();
let user = nix::unistd::User::from_uid(parent_owner.into())
.unwrap_or_default()
.unwrap();
if uid.is_root() && uid.to_string() != parent_owner.to_string() {
println!("Adjust DB permission...");
let db = fs::canonicalize(db_path).await.unwrap();
let shm = fs::canonicalize(db_path.with_extension("db-shm"))
.await
.unwrap();
let wal = fs::canonicalize(db_path.with_extension("db-wal"))
.await
.unwrap();
nix::unistd::chown(&db, Some(user.uid), Some(user.gid)).expect("Change DB owner");
if shm.is_file() {
nix::unistd::chown(&shm, Some(user.uid), Some(user.gid)).expect("Change DB-SHM owner");
}
if wal.is_file() {
nix::unistd::chown(&wal, Some(user.uid), Some(user.gid)).expect("Change DB-WAL owner");
}
}
}

View File

@ -1,7 +1,6 @@
use std::{
ffi::OsStr,
io,
path::Path,
path::PathBuf,
sync::{Arc, Mutex},
};
@ -11,7 +10,7 @@ use sqlx::{Pool, Sqlite};
use super::logging::MailQueue;
use crate::db::{handles, models::Channel};
use crate::player::controller::{ChannelController, ChannelManager};
use crate::utils::{config::get_config, errors::ServiceError};
use crate::utils::{config::get_config, copy_assets, errors::ServiceError};
async fn map_global_admins(conn: &Pool<Sqlite>) -> Result<(), ServiceError> {
let channels = handles::select_related_channels(conn, None).await?;
@ -29,58 +28,20 @@ async fn map_global_admins(conn: &Pool<Sqlite>) -> Result<(), ServiceError> {
Ok(())
}
fn preview_url(url: &str, id: i32) -> String {
let url_path = Path::new(url);
if let Some(parent) = url_path.parent() {
if let Some(filename) = url_path.file_name() {
let new_path = if parent
.file_name()
.unwrap_or_else(|| OsStr::new("0"))
.to_string_lossy()
.to_string()
.parse::<i32>()
.is_ok()
{
parent.join(filename)
} else {
parent.join(id.to_string()).join(filename)
};
if let Some(new_url) = new_path.to_str() {
return new_url.to_string();
}
}
}
url.to_string()
}
pub async fn create_channel(
conn: &Pool<Sqlite>,
controllers: Arc<Mutex<ChannelController>>,
queue: Arc<Mutex<Vec<Arc<Mutex<MailQueue>>>>>,
target_channel: Channel,
) -> Result<Channel, ServiceError> {
let global = handles::select_global(conn).await?;
let mut channel = handles::insert_channel(conn, target_channel).await?;
let channel = handles::insert_channel(conn, target_channel).await?;
let storage_path = PathBuf::from(channel.storage_path.clone());
handles::new_channel_presets(conn, channel.id).await?;
channel.preview_url = preview_url(&channel.preview_url, channel.id);
if global.shared_storage {
channel.hls_path = Path::new(&global.public_root)
.join(channel.id.to_string())
.to_string_lossy()
.to_string();
channel.playlist_path = Path::new(&global.playlist_root)
.join(channel.id.to_string())
.to_string_lossy()
.to_string();
channel.storage_path = Path::new(&global.storage_root)
.join(channel.id.to_string())
.to_string_lossy()
.to_string();
}
if let Err(e) = copy_assets(&storage_path).await {
error!("{e}");
};
handles::update_channel(conn, channel.id, channel.clone()).await?;

View File

@ -328,6 +328,10 @@ pub struct Processing {
pub audio_channels: u8,
pub volume: f64,
pub custom_filter: String,
#[serde(default)]
pub vtt_enable: bool,
#[serde(default)]
pub vtt_dummy: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub cmd: Option<Vec<String>>,
}
@ -355,6 +359,8 @@ impl Processing {
audio_channels: config.processing_audio_channels,
volume: config.processing_volume,
custom_filter: config.processing_filter.clone(),
vtt_enable: config.processing_vtt_enable,
vtt_dummy: config.processing_vtt_dummy.clone(),
cmd: None,
}
}
@ -452,6 +458,7 @@ pub struct Text {
pub zmq_stream_socket: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub zmq_server_socket: Option<String>,
#[serde(alias = "fontfile")]
pub font: String,
#[serde(skip_serializing, skip_deserializing)]
pub font_path: String,
@ -655,7 +662,9 @@ impl PlayoutConfig {
"-maxrate",
&bitrate,
"-bufsize",
&buff_size
&buff_size,
"-mpegts_flags",
"initial_discontinuity"
]);
}
@ -845,7 +854,7 @@ pub async fn get_config(
}
if let Some(playlist) = args.playlist {
config.channel.playlist_path = playlist;
config.channel.playlist_path = PathBuf::from(&playlist);
}
if let Some(folder) = args.folder {

View File

@ -4,6 +4,9 @@ use std::{
path::{Path, PathBuf},
};
#[cfg(target_family = "unix")]
use std::os::unix::fs::MetadataExt;
use chrono::{format::ParseErrorKind, prelude::*};
use faccess::PathExt;
use log::*;
@ -310,3 +313,59 @@ pub fn round_to_nearest_ten(num: i64) -> i64 {
(num / 10) * 10
}
}
pub async fn copy_assets(storage_path: &Path) -> Result<(), std::io::Error> {
if storage_path.is_dir() {
let target = storage_path.join("00-assets");
let mut dummy_source = Path::new("/usr/share/ffplayout/dummy.vtt");
let mut font_source = Path::new("/usr/share/ffplayout/DejaVuSans.ttf");
let mut logo_source = Path::new("/usr/share/ffplayout/logo.png");
if !dummy_source.is_file() {
dummy_source = Path::new("./assets/dummy.vtt")
}
if !font_source.is_file() {
font_source = Path::new("./assets/DejaVuSans.ttf")
}
if !logo_source.is_file() {
logo_source = Path::new("./assets/logo.png")
}
if !target.is_dir() {
let dummy_target = target.join("dummy.vtt");
let font_target = target.join("DejaVuSans.ttf");
let logo_target = target.join("logo.png");
fs::create_dir(&target).await?;
fs::copy(&dummy_source, &dummy_target).await?;
fs::copy(&font_source, &font_target).await?;
fs::copy(&logo_source, &logo_target).await?;
#[cfg(target_family = "unix")]
{
let uid = nix::unistd::Uid::current();
let parent_owner = storage_path.metadata().unwrap().uid();
if uid.is_root() && uid.to_string() != parent_owner.to_string() {
let user = nix::unistd::User::from_uid(parent_owner.into())
.unwrap_or_default()
.unwrap();
nix::unistd::chown(&target, Some(user.uid), Some(user.gid))?;
if dummy_target.is_file() {
nix::unistd::chown(&dummy_target, Some(user.uid), Some(user.gid))?;
}
if font_target.is_file() {
nix::unistd::chown(&font_target, Some(user.uid), Some(user.gid))?;
}
if logo_target.is_file() {
nix::unistd::chown(&logo_target, Some(user.uid), Some(user.gid))?;
}
}
}
}
}
Ok(())
}

View File

@ -120,7 +120,7 @@ function newChannel() {
newChannel.id = channels.length + 1
newChannel.name = `Channel ${newChannel.id}`
newChannel.preview_url = `${window.location.protocol}//${window.location.host}/live/${newChannel.id}/stream.m3u8`
newChannel.preview_url = `${window.location.protocol}//${window.location.host}/${newChannel.id}/live/stream.m3u8`
newChannel.hls_path = `${rmId(newChannel.hls_path)}/${newChannel.id}`
newChannel.playlist_path = `${rmId(newChannel.playlist_path)}/${newChannel.id}`
newChannel.storage_path = `${rmId(newChannel.storage_path)}/${newChannel.id}`
@ -158,6 +158,7 @@ async function deleteChannel() {
})
config.splice(configStore.id, 1)
configStore.channelsRaw.splice(configStore.id, 1)
configStore.channels = config
configStore.id = configStore.channels.length - 1

View File

@ -15,7 +15,14 @@
class="form-control w-full"
:class="[typeof prop === 'boolean' && 'flex-row', name.toString() !== 'help_text' && 'mt-2']"
>
<template v-if="name.toString() !== 'startInSec' && name.toString() !== 'lengthInSec' && !(name.toString() === 'path' && key.toString() === 'storage')" >
<template
v-if="
name.toString() !== 'startInSec' &&
name.toString() !== 'lengthInSec' &&
!(name.startsWith('vtt_') && !config.public.buildExperimental) &&
!(name.toString() === 'path' && key.toString() === 'storage')
"
>
<div v-if="name.toString() !== 'help_text'" class="label">
<span class="label-text !text-md font-bold">{{ name }}</span>
</div>
@ -87,6 +94,7 @@
</template>
<script setup lang="ts">
const config = useRuntimeConfig()
const { t } = useI18n()
const authStore = useAuth()

View File

@ -78,7 +78,7 @@
'!bg-lime-500/30':
playlistStore.playoutIsRunning && listDate === todayDate && index === currentIndex,
'!bg-amber-600/40': element.overtime,
'text-base-content/50': element.category === 'advertisement',
'text-blue-300': element.category === 'advertisement',
}"
>
<td v-if="!configStore.playout.playlist.infinit" class="ps-4 py-2 text-left">

View File

@ -1,5 +1,5 @@
<template>
<div class="grid grid-cols-1 xs:grid-cols-2 border-4 rounded-md border-primary text-left shadow min-w-[320px] max-w-[960px] mt-5 xs:mt-0">
<div class="grid grid-cols-1 xs:grid-cols-2 border-4 rounded-md border-primary text-left shadow min-w-[320px] md:min-w-[728px] max-w-[960px] mt-5 xs:mt-0">
<div class="p-4 bg-base-100">
<span class="text-3xl">{{ sysStat.system.name }} {{ sysStat.system.version }}</span>
<span v-if="sysStat.system.kernel">

View File

@ -17,6 +17,12 @@ export default defineNuxtConfig({
},
},
runtimeConfig: {
public: {
buildExperimental: process.env.NUXT_BUILD_EXPERIMENTAL,
},
},
ignore: ['**/public/tv-media**', '**/public/Videos**', '**/public/live**', '**/public/home**'],
ssr: false,

View File

@ -104,7 +104,7 @@ CREATE TABLE
processing_aspect REAL NOT NULL DEFAULT 1.778,
processing_fps REAL NOT NULL DEFAULT 25.0,
processing_add_logo INTEGER NOT NULL DEFAULT 1,
processing_logo TEXT NOT NULL DEFAULT "graphics/logo.png",
processing_logo TEXT NOT NULL DEFAULT "00-assets/logo.png",
processing_logo_scale TEXT NOT NULL DEFAULT "",
processing_logo_opacity REAL NOT NULL DEFAULT 0.7,
processing_logo_position TEXT NOT NULL DEFAULT "W-w-12:12",
@ -113,6 +113,8 @@ CREATE TABLE
processing_audio_channels INTEGER NOT NULL DEFAULT 2,
processing_volume REAL NOT NULL DEFAULT 1.0,
processing_filter TEXT NOT NULL DEFAULT "",
processing_vtt_enable INTEGER NOT NULL DEFAULT 0,
processing_vtt_dummy TEXT NULL DEFAULT "00-assets/dummy.vtt",
ingest_help "Run a server for a ingest stream. This stream will override the normal streaming until is done. There is only a very simple authentication mechanism, which check if the stream name is correct.\n'custom_filter' can be used in the same way then the one in the process section.",
ingest_enable INTEGER NOT NULL DEFAULT 0,
ingest_param TEXT NOT NULL DEFAULT "-f live_flv -listen 1 -i rtmp://127.0.0.1:1936/live/stream",
@ -128,7 +130,7 @@ CREATE TABLE
text_help TEXT NOT NULL DEFAULT "Overlay text in combination with libzmq for remote text manipulation. fontfile is a relative path to your storage folder.\n'text_from_filename' activate the extraction from text of a filename. With 'style' you can define the drawtext parameters like position, color, etc. Post Text over API will override this. With 'regex' you can format file names, to get a title from it.",
text_add INTEGER NOT NULL DEFAULT 1,
text_from_filename INTEGER NOT NULL DEFAULT 0,
text_font TEXT NOT NULL DEFAULT "fonts/DejaVuSans.ttf",
text_font TEXT NOT NULL DEFAULT "00-assets/DejaVuSans.ttf",
text_style TEXT NOT NULL DEFAULT "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4",
text_regex TEXT NOT NULL DEFAULT "^.+[/\\](.*)(.mp4|.mkv|.webm)$",
task_help TEXT NOT NULL DEFAULT "Run an external program with a given media object. The media object is in json format and contains all the information about the current clip. The external program can be a script or a binary, but should only run for a short time.",
@ -136,7 +138,7 @@ CREATE TABLE
task_path TEXT NOT NULL DEFAULT "",
output_help TEXT NOT NULL DEFAULT "The final playout compression. Set the settings to your needs. 'mode' has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust 'output_param:' settings when you want to stream to a rtmp/rtsp/srt/... server.\nIn production don't serve hls playlist with ffplayout, use nginx or another web server!",
output_mode TEXT NOT NULL DEFAULT "hls",
output_param TEXT NOT NULL DEFAULT "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename live/stream-%d.ts live/stream.m3u8",
output_param TEXT NOT NULL DEFAULT "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -muxpreload 0 -muxdelay 0 -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename live/stream-%d.ts live/stream.m3u8",
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE CASCADE ON DELETE CASCADE
);

View File

@ -47,10 +47,10 @@ for target in "${targets[@]}"; do
done
if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "x86_64-unknown-linux-musl" ]]; then
cargo deb --no-build --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=ffplayout/Cargo.toml -o ffplayout_${version}-1_amd64.deb
cargo generate-rpm --payload-compress none --target=x86_64-unknown-linux-musl -p ffplayout -o ffplayout-${version}-1.x86_64.rpm
cargo deb --no-build --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=engine/Cargo.toml -o ffplayout_${version}-1_amd64.deb
cargo generate-rpm --payload-compress none --target=x86_64-unknown-linux-musl -p engine -o ffplayout-${version}-1.x86_64.rpm
fi
if [[ "${#targets[@]}" == "5" ]] || [[ $targets == "aarch64-unknown-linux-gnu" ]]; then
cargo deb --no-build --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=ffplayout/Cargo.toml -o ffplayout_${version}-1_arm64.deb
cargo deb --no-build --target=aarch64-unknown-linux-gnu --variant=arm64 -p ffplayout --manifest-path=engine/Cargo.toml -o ffplayout_${version}-1_arm64.deb
fi