commit
2739da8917
8
Makefile
8
Makefile
@ -33,9 +33,9 @@ install:
|
||||
if [ ! -f "/etc/ffplayout/ffplayout.yml" ]; then \
|
||||
install -m 644 -o $(USER) -g $(USER) ffplayout.yml /etc/ffplayout/; \
|
||||
fi
|
||||
if [ -d "/etc/systemd/system" ] && [ ! -f "/etc/systemd/system/ffplayout.service" ]; then \
|
||||
install -m 644 docs/ffplayout.service /etc/systemd/system/; \
|
||||
sed -i "s/root/$(USER)/g" "/etc/systemd/system/ffplayout.service"; \
|
||||
if [ -d "/etc/systemd/system" ] && [ ! -f "/etc/systemd/system/ffplayout-engine.service" ]; then \
|
||||
install -m 644 docs/ffplayout-engine.service /etc/systemd/system/; \
|
||||
sed -i "s/root/$(USER)/g" "/etc/systemd/system/ffplayout-engine.service"; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "-------------------------------------------------------------------"
|
||||
@ -49,7 +49,7 @@ clean:
|
||||
uninstall:
|
||||
rm -rf "/etc/ffplayout"
|
||||
rm -rf "/var/log/ffplayout"
|
||||
rm -rf "/etc/systemd/system/ffplayout.service"
|
||||
rm -rf "/etc/systemd/system/ffplayout-engine.service"
|
||||
if [ ! "$(CURRENT_DIR)" == "/opt/ffplayout-engine" ]; then \
|
||||
rm -rf "/opt/ffplayout-engine"; \
|
||||
fi
|
||||
|
@ -99,17 +99,15 @@ Subfolders is read by the script and needs this structur:
|
||||
```YAML
|
||||
storage:
|
||||
path: "/mediaStorage"
|
||||
filler_path: "/mediaStorage/filler/filler-clips"
|
||||
filler_clip: "/mediaStorage/filler/filler.mp4"
|
||||
extensions:
|
||||
- "*.mp4"
|
||||
- "*.mkv"
|
||||
- ".mp4"
|
||||
- ".mkv"
|
||||
shuffle: True
|
||||
```
|
||||
Play ordered or ramdomly files from path, `filler_path` are for the GUI only at the moment.
|
||||
`filler_clip` is for fill the end to reach 24 hours, it will loop when is necessary.
|
||||
`extensions:` search only files with this extension, add as many as you want.
|
||||
Set `shuffle` to **True** to pick files randomly.
|
||||
Play ordered or ramdomly files from path, `filler_clip` is for fill the end
|
||||
to reach 24 hours, it will loop when is necessary. `extensions:` search only files
|
||||
with this extension, add as many as you want. Set `shuffle` to **True** to pick files randomly.
|
||||
|
||||
---
|
||||
|
||||
@ -130,20 +128,20 @@ out:
|
||||
preview: False
|
||||
service_name: "Live Stream"
|
||||
service_provider: "example.org"
|
||||
post_ffmpeg_param:
|
||||
c:v: "libx264"
|
||||
crf: "23"
|
||||
x264-params: "keyint=50:min-keyint=25:scenecut=-1"
|
||||
maxrate: "1300k"
|
||||
bufsize: "2600k"
|
||||
preset: "medium"
|
||||
profile:v: "Main"
|
||||
level: "3.1"
|
||||
c:a: "aac"
|
||||
ar: "44100"
|
||||
b:a: "128k"
|
||||
flags: +global_header
|
||||
f: "flv"
|
||||
post_ffmpeg_param: >-
|
||||
-c:v libx264
|
||||
-crf 23
|
||||
-x264-params keyint=50:min-keyint=25:scenecut=-1
|
||||
-maxrate 1300k
|
||||
-bufsize 2600k
|
||||
-preset medium
|
||||
-profile:v Main
|
||||
-level 3.1
|
||||
-c:a aac
|
||||
-ar 44100
|
||||
-b:a 128k
|
||||
-flags +global_header
|
||||
-f flv
|
||||
out_addr: "rtmp://localhost/live/stream"
|
||||
```
|
||||
|
||||
|
@ -19,7 +19,7 @@ Installation
|
||||
- create playlists folder, in that format: **/playlists/year/month**
|
||||
- set variables in config file to your needs
|
||||
- use `docs/gen_playlist_from_subfolders.sh /path/to/mp4s/` as a starting point for your playlists (path in script needs to change)
|
||||
- activate service and start it: `sudo systemctl enable ffplayout && sudo systemctl start ffplayout`
|
||||
- activate service and start it: `sudo systemctl enable ffplayout-engine && sudo systemctl start ffplayout-engine`
|
||||
|
||||
Cleanup
|
||||
-----
|
||||
@ -35,7 +35,7 @@ The routine with `make` build a virtual environment with all dependencies, and i
|
||||
|
||||
Just copy the project where you want to have it, run inside `pip3 install -r requirements.txt`. For logging you have to create the folder **ffplayout** under **/var/log/**, or adjust the settings in config. **ffplayout.yml** have to go to **/etc/ffplayout/**, or should stay in same folder.
|
||||
|
||||
If you want to use the systemd service, edit the service file in **docs/ffplayout.service**, copy it to **/etc/systemd/system/** and activate it with: `sudo systemctl enable ffplayout`.
|
||||
If you want to use the systemd service, edit the service file in **docs/ffplayout-engine.service**, copy it to **/etc/systemd/system/** and activate it with: `sudo systemctl enable ffplayout-engine`.
|
||||
|
||||
Using it Without Installation
|
||||
-----
|
||||
|
@ -26,7 +26,8 @@ from ffplayout.folder import GetSourceFromFolder, MediaStore, MediaWatcher
|
||||
from ffplayout.playlist import GetSourceFromPlaylist
|
||||
from ffplayout.utils import (_ff, _log, _playlist, _playout, _pre_comp, _text,
|
||||
ffmpeg_stderr_reader, get_date, messenger,
|
||||
pre_audio_codec, stdin_args, terminate_processes)
|
||||
pre_audio_codec, stdin_args, terminate_processes,
|
||||
validate_ffmpeg_libs)
|
||||
|
||||
try:
|
||||
if os.name != 'posix':
|
||||
@ -65,7 +66,7 @@ def main():
|
||||
_text.address
|
||||
))
|
||||
overlay = [
|
||||
'-vf', "null,zmq=b='{}',drawtext=text='':fontfile='{}'".format(
|
||||
'-vf', "null,zmq=b=tcp\\\://'{}',drawtext=text='':fontfile='{}'".format(
|
||||
_text.address.replace(':', '\\:'), _text.fontfile)
|
||||
]
|
||||
|
||||
@ -149,4 +150,6 @@ def main():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# check if ffmpeg contains all codecs and filters
|
||||
validate_ffmpeg_libs()
|
||||
main()
|
||||
|
147
ffplayout.yml
147
ffplayout.yml
@ -1,71 +1,42 @@
|
||||
# This file is part of ffplayout.
|
||||
#
|
||||
# ffplayout is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# ffplayout is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with ffplayout. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# sometimes it can happen, that a file is corrupt but still playable,
|
||||
# this can produce an streaming error over all following files
|
||||
# the only way in this case is, to stop ffplayout and start it again
|
||||
# here we only say it can stop, the starting process is in your hand
|
||||
# best way is a systemd serivce on linux
|
||||
# stop_threshold: stop ffplayout, if it is async in time above this value
|
||||
general:
|
||||
helptext: Sometimes it can happen, that a file is corrupt but still playable,
|
||||
this can produce an streaming error over all following files. The only way
|
||||
in this case is, to stop ffplayout and start it again here we only say it
|
||||
can stop, the starting process is in your hand. Best way is a systemd serivce
|
||||
on linux. 'stop_threshold' stop ffplayout, if it is async in time above this
|
||||
value.
|
||||
stop_on_error: True
|
||||
stop_threshold: 11
|
||||
|
||||
|
||||
# send error messages to email address, like:
|
||||
# missing playlist
|
||||
# unvalid json format
|
||||
# missing clip path
|
||||
# leave recipient blank, if you don't need this
|
||||
# mail_level can be: WARNING, ERROR
|
||||
mail:
|
||||
helptext: Send error messages to email address, like missing playlist; unvalid
|
||||
json format; missing clip path. Leave recipient blank, if you don't need this.
|
||||
'mail_level' can be WARNING or ERROR.
|
||||
subject: "Playout Error"
|
||||
smpt_server: "mail.example.org"
|
||||
smpt_port: 587
|
||||
sender_addr: "ffplayout@example.org"
|
||||
sender_pass: "12345"
|
||||
sender_pass: "abc123"
|
||||
recipient:
|
||||
mail_level: "ERROR"
|
||||
|
||||
|
||||
# Logging to file
|
||||
# if log_to_file: False > log to console
|
||||
# path to /var/log/ only if you run this program as deamon
|
||||
# log_level can be: DEBUG, INFO, WARNING, ERROR
|
||||
# ffmpeg_level can be: INFO, WARNING, ERROR
|
||||
logging:
|
||||
helptext: Logging to file, if 'log_to_file' False log to console. Path to /var/log/
|
||||
only if you run this program as deamon. 'log_level' can be DEBUG, INFO, WARNING,
|
||||
ERROR. 'ffmpeg_level' can be INFO, WARNING, ERROR.
|
||||
log_to_file: True
|
||||
log_path: "/var/log/ffplayout/"
|
||||
log_level: "DEBUG"
|
||||
ffmpeg_level: "ERROR"
|
||||
|
||||
|
||||
# output settings for the pre-compression
|
||||
# all clips get prepared in that way,
|
||||
# so the input for the final compression is unique
|
||||
# aspect mus be a float number
|
||||
# logo is only used if the path exist
|
||||
# with logo_opacity logo can make transparent
|
||||
# with logo_filter: overlay=W-w-12:12 you can modify the logo position
|
||||
# with use_loudnorm you can activate single pass EBU R128 loudness normalization
|
||||
# loud_* can adjust the loudnorm filter
|
||||
# INFO: output is progressive!
|
||||
pre_compress:
|
||||
helptext: Settings for the pre-compression. All clips get prepared in that way,
|
||||
so the input for the final compression is unique. 'aspect' must be a float
|
||||
number. 'logo' is only used if the path exist. With 'logo_opacity' logo can
|
||||
become transparent. With 'logo_filter' 'overlay=W-w-12:12' you can modify
|
||||
the logo position. With 'use_loudnorm' you can activate single pass EBU R128
|
||||
loudness normalization. 'loud_*' can adjust the loudnorm filter. [Output is
|
||||
always progressive!]
|
||||
width: 1024
|
||||
height: 576
|
||||
aspect: 1.778
|
||||
@ -79,66 +50,58 @@ pre_compress:
|
||||
loud_TP: -1.5
|
||||
loud_LRA: 11
|
||||
|
||||
|
||||
# playlist settings
|
||||
# set playlist_mode to False if you want to play clips from the [STORAGE] section
|
||||
# put only the root path here, for example: "/playlists"
|
||||
# subfolders are readed by the script
|
||||
# subfolders needs this structur:
|
||||
# "/playlists/2018/01" (/playlists/year/month)
|
||||
# day_start means at which time the playlist should start
|
||||
# leave day_start blank when playlist should always start at the begin
|
||||
# length represent the target length from playlist, when is blank real length will not consider
|
||||
playlist:
|
||||
helptext: Set 'playlist_mode' to 'False' if you want to play clips from the [STORAGE]
|
||||
section. Put only the root path here, for example '/playlists' subfolders
|
||||
are readed by the script. Subfolders needs this structur '/playlists/2018/01'
|
||||
(/playlists/year/month). 'day_start' means at which time the playlist should
|
||||
start, leave day_start blank when playlist should always start at the begin.
|
||||
'length' represent the target length from playlist, when is blank real length
|
||||
will not consider.
|
||||
playlist_mode: True
|
||||
path: "/playlists"
|
||||
day_start: "5:59:25"
|
||||
length: "24:00:00"
|
||||
|
||||
|
||||
# play ordered or ramdomly files from path
|
||||
# filler_path are for the GUI only at the moment
|
||||
# filler_clip is for fill the end to reach 24 hours, it will loop when is necessary
|
||||
# extensions: search only files with this extension, can be a list
|
||||
# set shuffle to True to pick files randomly
|
||||
storage:
|
||||
helptext: Play ordered or ramdomly files from path. 'filler_clip' is for fill
|
||||
the end to reach 24 hours, it will loop when is necessary extensions search
|
||||
only files with this extension, can be a list. Set 'shuffle' to 'True' to
|
||||
pick files randomly.
|
||||
path: "/mediaStorage"
|
||||
filler_path: "/mediaStorage/filler/filler-clips"
|
||||
filler_clip: "/mediaStorage/filler/filler.mp4"
|
||||
extensions:
|
||||
- "*.mp4"
|
||||
- "*.mkv"
|
||||
- ".mp4"
|
||||
- ".mkv"
|
||||
shuffle: True
|
||||
|
||||
|
||||
# overlay text in combination with messenger: https://github.com/ffplayout/messenger
|
||||
# on windows fontfile path need to be like this: C\:/WINDOWS/fonts/DejaVuSans.ttf
|
||||
# in a standard environment the filter drawtext node is: Parsed_drawtext_2
|
||||
text:
|
||||
add_text: True
|
||||
bind_address: "tcp://127.0.0.1:5555"
|
||||
helptext: Overlay text in combination with messenger 'https://github.com/ffplayout/messenger'.
|
||||
On windows fontfile path need to be like this 'C\:/WINDOWS/fonts/DejaVuSans.ttf'.
|
||||
In a standard environment the filter drawtext node is Parsed_drawtext_2.
|
||||
add_text: False
|
||||
bind_address: "127.0.0.1:5555"
|
||||
fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||
|
||||
|
||||
# the final playout post compression
|
||||
# set the settings to your needs
|
||||
# preview works only on a desktop system with ffplay!! Set it to True, if you need it
|
||||
out:
|
||||
helptext: The final playout post compression. Set the settings to your needs.
|
||||
'preview' works only on a desktop system with ffplay!! Set it to 'True', if
|
||||
you need it.
|
||||
preview: False
|
||||
service_name: "Live Stream"
|
||||
service_provider: "example.org"
|
||||
post_ffmpeg_param:
|
||||
c:v: "libx264"
|
||||
crf: "23"
|
||||
x264-params: "keyint=50:min-keyint=25:scenecut=-1"
|
||||
maxrate: "1300k"
|
||||
bufsize: "2600k"
|
||||
preset: "medium"
|
||||
profile:v: "Main"
|
||||
level: "3.1"
|
||||
c:a: "aac"
|
||||
ar: "44100"
|
||||
b:a: "128k"
|
||||
flags: +global_header
|
||||
f: "flv"
|
||||
post_ffmpeg_param: >-
|
||||
-c:v libx264
|
||||
-crf 23
|
||||
-x264-params keyint=50:min-keyint=25:scenecut=-1
|
||||
-maxrate 1300k
|
||||
-bufsize 2600k
|
||||
-preset medium
|
||||
-profile:v Main
|
||||
-level 3.1
|
||||
-c:a aac
|
||||
-ar 44100
|
||||
-b:a 128k
|
||||
-flags +global_header
|
||||
-f flv
|
||||
out_addr: "rtmp://localhost/live/stream"
|
||||
|
@ -52,7 +52,7 @@ class MediaStore:
|
||||
def fill(self):
|
||||
for ext in _storage.extensions:
|
||||
self.store.extend(
|
||||
glob.glob(os.path.join(self.folder, '**', ext),
|
||||
glob.glob(os.path.join(self.folder, '**', '*{}'.format(ext)),
|
||||
recursive=True))
|
||||
|
||||
if _storage.shuffle:
|
||||
|
@ -27,17 +27,17 @@ import smtplib
|
||||
import socket
|
||||
import sys
|
||||
import tempfile
|
||||
import yaml
|
||||
from argparse import ArgumentParser
|
||||
from datetime import date, datetime, timedelta
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from subprocess import CalledProcessError, check_output
|
||||
from subprocess import STDOUT, CalledProcessError, check_output
|
||||
from threading import Thread
|
||||
from types import SimpleNamespace
|
||||
|
||||
import yaml
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# argument parsing
|
||||
@ -140,17 +140,6 @@ def read_config(path):
|
||||
return yaml.safe_load(config_file)
|
||||
|
||||
|
||||
def dict_to_list(d):
|
||||
li = []
|
||||
|
||||
for key, value in d.items():
|
||||
if value:
|
||||
li += ['-{}'.format(key), str(value)]
|
||||
else:
|
||||
li += ['-{}'.format(key)]
|
||||
return li
|
||||
|
||||
|
||||
def load_config():
|
||||
"""
|
||||
this function can reload most settings from configuration file,
|
||||
@ -228,8 +217,7 @@ def load_config():
|
||||
_playout.preview = cfg['out']['preview']
|
||||
_playout.name = cfg['out']['service_name']
|
||||
_playout.provider = cfg['out']['service_provider']
|
||||
_playout.post_comp_param = dict_to_list(
|
||||
cfg['out']['post_ffmpeg_param'])
|
||||
_playout.post_comp_param = cfg['out']['post_ffmpeg_param'].split(' ')
|
||||
_playout.out_addr = cfg['out']['out_addr']
|
||||
|
||||
_init.load = False
|
||||
@ -312,8 +300,8 @@ if _log.to_file and _log.path != 'none':
|
||||
log_dir = os.path.join(base_dir, 'log')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
playout_log = os.path.join(log_dir, 'ffplayout.log')
|
||||
decoder_log = os.path.join(log_dir, 'ffdecoder.log')
|
||||
encoder_log = os.path.join(log_dir, 'ffencoder.log')
|
||||
decoder_log = os.path.join(log_dir, 'decoder.log')
|
||||
encoder_log = os.path.join(log_dir, 'encoder.log')
|
||||
|
||||
p_format = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
|
||||
f_format = logging.Formatter('[%(asctime)s] %(message)s')
|
||||
@ -461,11 +449,12 @@ def ffmpeg_libs():
|
||||
check which external libs are compiled in ffmpeg,
|
||||
for using them later
|
||||
"""
|
||||
cmd = ['ffmpeg', '-version']
|
||||
cmd = ['ffmpeg', '-filters']
|
||||
libs = []
|
||||
filters = []
|
||||
|
||||
try:
|
||||
info = check_output(cmd).decode('UTF-8')
|
||||
info = check_output(cmd, stderr=STDOUT).decode('UTF-8')
|
||||
except CalledProcessError as err:
|
||||
messenger.error('ffmpeg - libs could not be readed!\n'
|
||||
'Processing is not possible. Error:\n{}'.format(err))
|
||||
@ -478,14 +467,34 @@ def ffmpeg_libs():
|
||||
for cfg in configs:
|
||||
if '--enable-lib' in cfg:
|
||||
libs.append(cfg.replace('--enable-', ''))
|
||||
break
|
||||
elif re.match(r'^(?!.*=) [TSC.]+', line):
|
||||
filter_list = line.split()
|
||||
if len(filter_list) > 3:
|
||||
filters.append(filter_list[1])
|
||||
|
||||
return libs
|
||||
return {'libs': libs, 'filters': filters}
|
||||
|
||||
|
||||
FF_LIBS = ffmpeg_libs()
|
||||
|
||||
|
||||
def validate_ffmpeg_libs():
|
||||
if 'libx264' not in FF_LIBS['libs']:
|
||||
playout_logger.error('ffmpeg contains no libx264!')
|
||||
if 'libfdk-aac' not in FF_LIBS['libs']:
|
||||
playout_logger.warning(
|
||||
'ffmpeg contains no libfdk-aac! No high quality aac...')
|
||||
if 'libtwolame' not in FF_LIBS['libs']:
|
||||
playout_logger.warning(
|
||||
'ffmpeg contains no libtwolame!'
|
||||
' Loudness correction use mp2 audio codec...')
|
||||
if 'tpad' not in FF_LIBS['filters']:
|
||||
playout_logger.error('ffmpeg contains no tpad filter!')
|
||||
if 'zmq' not in FF_LIBS['filters']:
|
||||
playout_logger.error(
|
||||
'ffmpeg contains no zmq filter! Text messages will not work...')
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# probe media infos
|
||||
# ------------------------------------------------------------------------------
|
||||
@ -979,7 +988,7 @@ def pre_audio_codec():
|
||||
and works not well together with the loudnorm filter
|
||||
"""
|
||||
if _pre_comp.add_loudnorm:
|
||||
acodec = 'libtwolame' if 'libtwolame' in FF_LIBS else 'mp2'
|
||||
acodec = 'libtwolame' if 'libtwolame' in FF_LIBS['libs'] else 'mp2'
|
||||
audio = ['-c:a', acodec, '-b:a', '384k', '-ar', '48000', '-ac', '2']
|
||||
else:
|
||||
audio = ['-c:a', 's302m', '-strict', '-2', '-ar', '48000', '-ac', '2']
|
||||
|
Loading…
Reference in New Issue
Block a user