Merge pull request #768 from jb-alvarado/master
experimental closed captions
This commit is contained in:
commit
5a6a6292f2
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -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
106
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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>"]
|
||||
|
@ -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
BIN
assets/DejaVuSans.ttf
Normal file
Binary file not shown.
187
assets/FONT_LICENSE.txt
Normal file
187
assets/FONT_LICENSE.txt
Normal 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
1
assets/dummy.vtt
Normal file
@ -0,0 +1 @@
|
||||
WEBVTT
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
23
docs/closed_captions.md
Normal 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. Here’s 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
|
||||
```
|
@ -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.
|
||||
|
@ -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" },
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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("://")) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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?;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user