migrate to yaml config file
This commit is contained in:
parent
6c4bb61bf7
commit
472b11965e
147
ffplayout.py
147
ffplayout.py
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import configparser
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -33,6 +32,7 @@ import ssl
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import yaml
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
@ -142,36 +142,52 @@ _WINDOWS = os.name == 'nt'
|
|||||||
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024
|
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_sec(s):
|
||||||
|
if s in ['now', '', None, 'none']:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
s = s.split(':')
|
||||||
|
try:
|
||||||
|
return float(s[0]) * 3600 + float(s[1]) * 60 + float(s[2])
|
||||||
|
except ValueError:
|
||||||
|
print('Wrong time format!')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(path):
|
||||||
|
with open(path, 'r') as config_file:
|
||||||
|
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():
|
def load_config():
|
||||||
"""
|
"""
|
||||||
this function can reload most settings from configuration file,
|
this function can reload most settings from configuration file,
|
||||||
the change does not take effect immediately, but with the after next file,
|
the change does not take effect immediately, but with the after next file,
|
||||||
some settings cannot be changed - like resolution, aspect, or output
|
some settings cannot be changed - like resolution, aspect, or output
|
||||||
"""
|
"""
|
||||||
cfg = configparser.ConfigParser()
|
|
||||||
|
|
||||||
def str_to_sec(s):
|
|
||||||
if s in ['now', '', None, 'none']:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
s = s.split(':')
|
|
||||||
try:
|
|
||||||
return float(s[0]) * 3600 + float(s[1]) * 60 + float(s[2])
|
|
||||||
except ValueError:
|
|
||||||
print('Wrong time format!')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if stdin_args.config:
|
if stdin_args.config:
|
||||||
cfg.read(stdin_args.config)
|
cfg = read_config(stdin_args.config)
|
||||||
elif os.path.isfile('/etc/ffplayout/ffplayout.conf'):
|
elif os.path.isfile('/etc/ffplayout/ffplayout.yml'):
|
||||||
cfg.read('/etc/ffplayout/ffplayout.conf')
|
cfg = read_config('/etc/ffplayout/ffplayout.yml')
|
||||||
else:
|
else:
|
||||||
cfg.read('ffplayout.conf')
|
cfg = read_config('ffplayout.yml')
|
||||||
|
|
||||||
if stdin_args.start:
|
if stdin_args.start:
|
||||||
p_start = str_to_sec(stdin_args.start)
|
p_start = str_to_sec(stdin_args.start)
|
||||||
else:
|
else:
|
||||||
p_start = str_to_sec(cfg.get('PLAYLIST', 'day_start'))
|
p_start = str_to_sec(cfg['playlist']['day_start'])
|
||||||
|
|
||||||
if not p_start:
|
if not p_start:
|
||||||
p_start = get_time('full_sec')
|
p_start = get_time('full_sec')
|
||||||
@ -179,65 +195,61 @@ def load_config():
|
|||||||
if stdin_args.length:
|
if stdin_args.length:
|
||||||
p_length = str_to_sec(stdin_args.length)
|
p_length = str_to_sec(stdin_args.length)
|
||||||
else:
|
else:
|
||||||
p_length = str_to_sec(cfg.get('PLAYLIST', 'length'))
|
p_length = str_to_sec(cfg['playlist']['length'])
|
||||||
|
|
||||||
_general.stop = cfg.getboolean('GENERAL', 'stop_on_error')
|
_general.stop = cfg['general']['stop_on_error']
|
||||||
_general.threshold = cfg.getfloat('GENERAL', 'stop_threshold')
|
_general.threshold = cfg['general']['stop_threshold']
|
||||||
|
|
||||||
_mail.subject = cfg.get('MAIL', 'subject')
|
_mail.subject = cfg['mail']['subject']
|
||||||
_mail.server = cfg.get('MAIL', 'smpt_server')
|
_mail.server = cfg['mail']['smpt_server']
|
||||||
_mail.port = cfg.getint('MAIL', 'smpt_port')
|
_mail.port = cfg['mail']['smpt_port']
|
||||||
_mail.s_addr = cfg.get('MAIL', 'sender_addr')
|
_mail.s_addr = cfg['mail']['sender_addr']
|
||||||
_mail.s_pass = cfg.get('MAIL', 'sender_pass')
|
_mail.s_pass = cfg['mail']['sender_pass']
|
||||||
_mail.recip = cfg.get('MAIL', 'recipient')
|
_mail.recip = cfg['mail']['recipient']
|
||||||
_mail.level = cfg.get('MAIL', 'mail_level')
|
_mail.level = cfg['mail']['mail_level']
|
||||||
|
|
||||||
_pre_comp.add_logo = cfg.getboolean('PRE_COMPRESS', 'add_logo')
|
_pre_comp.add_logo = cfg['pre_compress']['add_logo']
|
||||||
_pre_comp.logo = cfg.get('PRE_COMPRESS', 'logo')
|
_pre_comp.logo = cfg['pre_compress']['logo']
|
||||||
_pre_comp.opacity = cfg.get('PRE_COMPRESS', 'logo_opacity')
|
_pre_comp.opacity = cfg['pre_compress']['logo_opacity']
|
||||||
_pre_comp.logo_filter = cfg.get('PRE_COMPRESS', 'logo_filter')
|
_pre_comp.logo_filter = cfg['pre_compress']['logo_filter']
|
||||||
_pre_comp.add_loudnorm = cfg.getboolean('PRE_COMPRESS', 'add_loudnorm')
|
_pre_comp.add_loudnorm = cfg['pre_compress']['add_loudnorm']
|
||||||
_pre_comp.loud_i = cfg.getfloat('PRE_COMPRESS', 'loud_I')
|
_pre_comp.loud_i = cfg['pre_compress']['loud_I']
|
||||||
_pre_comp.loud_tp = cfg.getfloat('PRE_COMPRESS', 'loud_TP')
|
_pre_comp.loud_tp = cfg['pre_compress']['loud_TP']
|
||||||
_pre_comp.loud_lra = cfg.getfloat('PRE_COMPRESS', 'loud_LRA')
|
_pre_comp.loud_lra = cfg['pre_compress']['loud_LRA']
|
||||||
|
|
||||||
_playlist.mode = cfg.getboolean('PLAYLIST', 'playlist_mode')
|
_playlist.mode = cfg['playlist']['playlist_mode']
|
||||||
_playlist.path = cfg.get('PLAYLIST', 'path')
|
_playlist.path = cfg['playlist']['path']
|
||||||
_playlist.start = p_start
|
_playlist.start = p_start
|
||||||
_playlist.length = p_length
|
_playlist.length = p_length
|
||||||
|
|
||||||
_storage.path = cfg.get('STORAGE', 'path')
|
_storage.path = cfg['storage']['path']
|
||||||
_storage.filler = cfg.get('STORAGE', 'filler_clip')
|
_storage.filler = cfg['storage']['filler_clip']
|
||||||
_storage.extensions = json.loads(cfg.get('STORAGE', 'extensions'))
|
_storage.extensions = cfg['storage']['extensions']
|
||||||
_storage.shuffle = cfg.getboolean('STORAGE', 'shuffle')
|
_storage.shuffle = cfg['storage']['shuffle']
|
||||||
|
|
||||||
_text.add_text = cfg.getboolean('TEXT', 'add_text')
|
_text.add_text = cfg['text']['add_text']
|
||||||
_text.address = cfg.get('TEXT', 'bind_address')
|
_text.address = cfg['text']['bind_address']
|
||||||
_text.fontfile = cfg.get('TEXT', 'fontfile')
|
_text.fontfile = cfg['text']['fontfile']
|
||||||
|
|
||||||
if _init.load:
|
if _init.load:
|
||||||
_log.to_file = cfg.getboolean('LOGGING', 'log_to_file')
|
_log.to_file = cfg['logging']['log_to_file']
|
||||||
_log.path = cfg.get('LOGGING', 'log_path')
|
_log.path = cfg['logging']['log_path']
|
||||||
_log.level = cfg.get('LOGGING', 'log_level')
|
_log.level = cfg['logging']['log_level']
|
||||||
_log.ff_level = cfg.get('LOGGING', 'ffmpeg_level')
|
_log.ff_level = cfg['logging']['ffmpeg_level']
|
||||||
|
|
||||||
_pre_comp.w = cfg.getint('PRE_COMPRESS', 'width')
|
_pre_comp.w = cfg['pre_compress']['width']
|
||||||
_pre_comp.h = cfg.getint('PRE_COMPRESS', 'height')
|
_pre_comp.h = cfg['pre_compress']['height']
|
||||||
_pre_comp.aspect = cfg.getfloat('PRE_COMPRESS', 'aspect')
|
_pre_comp.aspect = cfg['pre_compress']['aspect']
|
||||||
_pre_comp.fps = cfg.getint('PRE_COMPRESS', 'fps')
|
_pre_comp.fps = cfg['pre_compress']['fps']
|
||||||
_pre_comp.v_bitrate = cfg.getint('PRE_COMPRESS', 'width') * 50
|
_pre_comp.v_bitrate = cfg['pre_compress']['width'] * 50
|
||||||
_pre_comp.v_bufsize = cfg.getint('PRE_COMPRESS', 'width') * 50 / 2
|
_pre_comp.v_bufsize = cfg['pre_compress']['width'] * 50 / 2
|
||||||
|
|
||||||
_playout.preview = cfg.getboolean('OUT', 'preview')
|
_playout.preview = cfg['out']['preview']
|
||||||
_playout.name = cfg.get('OUT', 'service_name')
|
_playout.name = cfg['out']['service_name']
|
||||||
_playout.provider = cfg.get('OUT', 'service_provider')
|
_playout.provider = cfg['out']['service_provider']
|
||||||
_playout.out_addr = cfg.get('OUT', 'out_addr')
|
_playout.post_comp_param = dict_to_list(
|
||||||
_playout.post_comp_video = json.loads(
|
cfg['out']['post_ffmpeg_param'])
|
||||||
cfg.get('OUT', 'post_comp_video'))
|
_playout.out_addr = cfg['out']['out_addr']
|
||||||
_playout.post_comp_audio = json.loads(
|
|
||||||
cfg.get('OUT', 'post_comp_audio'))
|
|
||||||
_playout.post_comp_extra = json.loads(
|
|
||||||
cfg.get('OUT', 'post_comp_extra'))
|
|
||||||
|
|
||||||
_init.load = False
|
_init.load = False
|
||||||
|
|
||||||
@ -1606,12 +1618,11 @@ def main():
|
|||||||
_ff.encoder = Popen([
|
_ff.encoder = Popen([
|
||||||
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
|
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
|
||||||
'-nostats', '-re', '-thread_queue_size', '256',
|
'-nostats', '-re', '-thread_queue_size', '256',
|
||||||
'-i', 'pipe:0'] + overlay + _playout.post_comp_video
|
'-i', 'pipe:0'] + overlay + [
|
||||||
+ _playout.post_comp_audio + [
|
|
||||||
'-metadata', 'service_name=' + _playout.name,
|
'-metadata', 'service_name=' + _playout.name,
|
||||||
'-metadata', 'service_provider=' + _playout.provider,
|
'-metadata', 'service_provider=' + _playout.provider,
|
||||||
'-metadata', 'year={}'.format(year)
|
'-metadata', 'year={}'.format(year)
|
||||||
] + _playout.post_comp_extra + [_playout.out_addr],
|
] + _playout.post_comp_param + [_playout.out_addr],
|
||||||
stdin=PIPE, stderr=PIPE)
|
stdin=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
|
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
|
144
ffplayout.yml
Normal file
144
ffplayout.yml
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# 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:
|
||||||
|
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:
|
||||||
|
subject: "Playout Error"
|
||||||
|
smpt_server: "mail.example.org"
|
||||||
|
smpt_port: 587
|
||||||
|
sender_addr: "ffplayout@example.org"
|
||||||
|
sender_pass: "12345"
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
width: 1024
|
||||||
|
height: 576
|
||||||
|
aspect: 1.778
|
||||||
|
fps: 25
|
||||||
|
add_logo: True
|
||||||
|
logo: "docs/logo.png"
|
||||||
|
logo_opacity: 0.7
|
||||||
|
logo_filter: "overlay=W-w-12:12"
|
||||||
|
add_loudnorm: False
|
||||||
|
loud_I: -18
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
path: "/mediaStorage"
|
||||||
|
filler_path: "/mediaStorage/filler/filler-clips"
|
||||||
|
filler_clip: "/mediaStorage/filler/filler.mp4"
|
||||||
|
extensions:
|
||||||
|
- "*.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"
|
||||||
|
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:
|
||||||
|
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"
|
||||||
|
out_addr: "rtmp://localhost/live/stream"
|
Loading…
x
Reference in New Issue
Block a user