merge with master

This commit is contained in:
Jonathan Baecker 2019-08-12 14:30:47 +02:00
parent 1d750d4204
commit c52e17df83
2 changed files with 205 additions and 164 deletions

View File

@ -15,8 +15,6 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# set playlist_mode to False if you want to play clips from the [FOLDER] section
# sometimes it can happen, that a file is corrupt but still playable, # sometimes it can happen, that a file is corrupt but still playable,
# this can produce an streaming error over all following files # this can produce an streaming error over all following files
# the only way in this case is, to stop ffplayout and start it again # the only way in this case is, to stop ffplayout and start it again
@ -24,7 +22,6 @@
# best way is a systemd serivce on linux # best way is a systemd serivce on linux
# stop_threshold: stop ffplayout, if it is async in time above this value # stop_threshold: stop ffplayout, if it is async in time above this value
[GENERAL] [GENERAL]
playlist_mode = True
stop_on_error = True stop_on_error = True
stop_threshold = 11 stop_threshold = 11
@ -46,9 +43,11 @@ mail_level = ERROR
# Logging to file # Logging to file
# if log_to_file = False > log to stderr (console)
# path to /var/log/ only if you run this program as deamon # path to /var/log/ only if you run this program as deamon
# log_level can be: DEBUG, INFO, WARNING, ERROR # log_level can be: DEBUG, INFO, WARNING, ERROR
[LOGGING] [LOGGING]
log_to_file = True
log_file = /var/log/ffplayout/ffplayout.log log_file = /var/log/ffplayout/ffplayout.log
log_level = INFO log_level = INFO
@ -56,60 +55,48 @@ log_level = INFO
# output settings for the pre-compression # output settings for the pre-compression
# all clips get prepared in that way, # all clips get prepared in that way,
# so the input for the final compression is unique # so the input for the final compression is unique
# it produce a uncompressed avi stream # copy_mode means that there is no recomression
# aspect mus be a float number
# logo is only used if the path exist # logo is only used if the path exist
# with logo_filter = overlay=W-w-12:12 you can modify the logo position # with logo_filter = overlay=W-w-12:12 you can modify the logo position
# live_protocols is for checking input, is input a live source ffplayout handles it a bit different # live_protocols is for checking input, is input a live source ffplayout handles it a bit different
[PRE_COMPRESS] [PRE_COMPRESS]
copy_mode = False
width = 1024 width = 1024
height = 576 height = 576
aspect = 1.778
fps = 25 fps = 25
v_bitrate = 50000
logo = logo.png logo = logo.png
logo_filter = overlay=W-w-12:12 logo_filter = overlay=W-w-12:12
live_protocols = ["http", "https", "ftp", "rtmp", "rtmpe", "rtmps", "rtp", "rtsp", "srt", "tcp", "udp"] live_protocols = ["http", "https", "ftp", "rtmp", "rtmpe", "rtmps", "rtp", "rtsp", "srt", "tcp", "udp"]
copy_mode = False
ffmpeg_copy_settings = ["-c", "copy", "-bsf:v", "h264_mp4toannexb", "-f", "mpegts", "-"]
# playlist settings # 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" # put only the root path here, for example: "/playlists"
# subfolders are readed by the script # subfolders are readed by the script
# subfolders needs this structur: # subfolders needs this structur:
# "/playlists/2018/01" (/playlists/year/month) # "/playlists/2018/01" (/playlists/year/month)
# strings in playlist must have ampersan (&) as: & # day_start means at which time the playlist should start
# leave day_start blank when playlist should always start at the begin
[PLAYLIST]
playlist_mode = True
path = /playlists
day_start = 05:59:25.000
# day_start means at witch hour starts the day, as integer
# play ordered or ramdomly files from path
# extensions: search only files with this extension, can be a list
# set shuffle to True to pick files randomly
# filler_path are for the GUI only at the moment # filler_path are for the GUI only at the moment
# filler_clip get handle different, when a new length needs to calculate # filler_clip is for fill the end to reach 24 hours, it will loop when is necessary
# blackclip is for stream copy mode, # blackclip is for stream copy mode,
# best for this is a ~4 hours clip with black color and soft noise sound # best for this is a ~4 hours clip with black color and soft noise sound
[STORAGE]
# time_shift adds or subtract seconds to the clip start, path = /media
# this is usefull for example for hls, because it have a big delay
# the value will be added to the clip begin
# put 0 if you don't need it
# map_extension is only for special purpose,
# when your playlist have a different extension, then the originial clip
# example: map_extension = [".mp4", ".mkv"]
# life empty for no change
[PLAYLIST]
playlist_path = /playlists
clips_root = /media
filler_path = /media/filler/filler-clips filler_path = /media/filler/filler-clips
filler_clip = /media/filler/seperator.clock.mp4 filler_clip = /media/filler/filler.mp4
blackclip = /opt/dummy.mkv blackclip = /media/dummy.mp4
day_start = 06:00:00.000
time_shift = 35
map_extension = []
# play ordered or ramdomly files from clips_root
# extensions can be a list
# set shuffle to True to pick files randomly
[FOLDER]
storage = /media
extensions = ["*.mp4"] extensions = ["*.mp4"]
shuffle = False shuffle = False
@ -121,7 +108,7 @@ shuffle = False
# on windows fontfile path need to be like this: C\:/WINDOWS/fonts/DejaVuSans.ttf # on windows fontfile path need to be like this: C\:/WINDOWS/fonts/DejaVuSans.ttf
# textfile has the same pattern # textfile has the same pattern
[TEXT] [TEXT]
textfile = /opt/live.txt textfile = /media/live.txt
fontsize = 24 fontsize = 24
fontcolor = white fontcolor = white
fontfile = /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf fontfile = /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
@ -140,11 +127,11 @@ preview = False
service_name = Live Stream service_name = Live Stream
service_provider = example.org service_provider = example.org
post_comp_video = [ post_comp_video = [
"-c:v", "libx264", "-crf", "23", "-x264-params", "keyint=50:min-keyint=50:no-scenecut", "-c:v", "libx264", "-crf", "23", "-x264-params", "keyint=50:min-keyint=25:scenecut=-1",
"-maxrate", "1300k", "-bufsize", "2600k", "-preset", "medium", "-profile:v", "Main", "-maxrate", "1300k", "-bufsize", "2600k", "-preset", "medium", "-profile:v", "Main",
"-level", "3.1", "-refs", "3" "-level", "3.1"
] ]
post_comp_audio = ["-c:a", "libfdk_aac", "-ar", "44100", "-b:a", "128k"] post_comp_audio = ["-c:a", "aac", "-ar", "44100", "-b:a", "128k"]
post_comp_extra = ["-flags", "+global_header", "-f", "flv"] post_comp_extra = ["-flags", "+global_header", "-f", "flv"]
post_comp_copy = ["-bsf:a", "aac_adtstoasc"] post_comp_copy = ["-bsf:a", "aac_adtstoasc"]
out_addr = rtmp://127.0.0.1/live/stream out_addr = rtmp://127.0.0.1/live/stream

View File

@ -23,10 +23,12 @@ import configparser
import glob import glob
import json import json
import logging import logging
import math
import os import os
import random import random
import smtplib
import signal import signal
import ssl
import smtplib
import socket import socket
import sys import sys
import time import time
@ -40,6 +42,7 @@ from shutil import copyfileobj
from subprocess import PIPE, CalledProcessError, Popen, check_output from subprocess import PIPE, CalledProcessError, Popen, check_output
from threading import Thread from threading import Thread
from types import SimpleNamespace from types import SimpleNamespace
from urllib import request
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# read variables from config file # read variables from config file
@ -54,8 +57,7 @@ else:
_general = SimpleNamespace( _general = SimpleNamespace(
stop=cfg.getboolean('GENERAL', 'stop_on_error'), stop=cfg.getboolean('GENERAL', 'stop_on_error'),
threshold=cfg.getfloat('GENERAL', 'stop_threshold'), threshold=cfg.getfloat('GENERAL', 'stop_threshold')
playlist_mode=cfg.getboolean('GENERAL', 'playlist_mode')
) )
_mail = SimpleNamespace( _mail = SimpleNamespace(
@ -69,42 +71,43 @@ _mail = SimpleNamespace(
) )
_log = SimpleNamespace( _log = SimpleNamespace(
to_file=cfg.getboolean('LOGGING', 'log_to_file'),
path=cfg.get('LOGGING', 'log_file'), path=cfg.get('LOGGING', 'log_file'),
level=cfg.get('LOGGING', 'log_level') level=cfg.get('LOGGING', 'log_level')
) )
_pre_comp = SimpleNamespace( _pre_comp = SimpleNamespace(
copy=cfg.getboolean('PRE_COMPRESS', 'copy_mode'),
w=cfg.getint('PRE_COMPRESS', 'width'), w=cfg.getint('PRE_COMPRESS', 'width'),
h=cfg.getint('PRE_COMPRESS', 'height'), h=cfg.getint('PRE_COMPRESS', 'height'),
aspect=cfg.getfloat( aspect=cfg.getfloat('PRE_COMPRESS', 'aspect'),
'PRE_COMPRESS', 'width') / cfg.getfloat('PRE_COMPRESS', 'height'),
fps=cfg.getint('PRE_COMPRESS', 'fps'), fps=cfg.getint('PRE_COMPRESS', 'fps'),
v_bitrate=cfg.getint('PRE_COMPRESS', 'v_bitrate'), v_bitrate=cfg.getint('PRE_COMPRESS', 'width') * 50,
v_bufsize=cfg.getint('PRE_COMPRESS', 'v_bitrate') / 2, v_bufsize=cfg.getint('PRE_COMPRESS', 'width') * 50 / 2,
logo=cfg.get('PRE_COMPRESS', 'logo'), logo=cfg.get('PRE_COMPRESS', 'logo'),
logo_filter=cfg.get('PRE_COMPRESS', 'logo_filter'), logo_filter=cfg.get('PRE_COMPRESS', 'logo_filter'),
protocols=cfg.get('PRE_COMPRESS', 'live_protocols'), protocols=cfg.get('PRE_COMPRESS', 'live_protocols')
copy=cfg.getboolean('PRE_COMPRESS', 'copy_mode'),
copy_settings=json.loads(cfg.get('PRE_COMPRESS', 'ffmpeg_copy_settings'))
) )
stime = cfg.get('PLAYLIST', 'day_start').split(':')
if stime[0] and stime[1] and stime[2]:
start_t = float(stime[0]) * 3600 + float(stime[1]) * 60 + float(stime[2])
else:
start_t = None
_playlist = SimpleNamespace( _playlist = SimpleNamespace(
path=cfg.get('PLAYLIST', 'playlist_path'), mode=cfg.getboolean('PLAYLIST', 'playlist_mode'),
t=cfg.get('PLAYLIST', 'day_start').split(':'), path=cfg.get('PLAYLIST', 'path'),
start=0, start=start_t
filler=cfg.get('PLAYLIST', 'filler_clip'),
blackclip=cfg.get('PLAYLIST', 'blackclip'),
shift=cfg.getfloat('PLAYLIST', 'time_shift'),
map_ext=json.loads(cfg.get('PLAYLIST', 'map_extension'))
) )
_playlist.start = float(_playlist.t[0]) * 3600 + float(_playlist.t[1]) * 60 \ _storage = SimpleNamespace(
+ float(_playlist.t[2]) path=cfg.get('STORAGE', 'path'),
filler=cfg.get('STORAGE', 'filler_clip'),
_folder = SimpleNamespace( blackclip=cfg.get('STORAGE', 'blackclip'),
storage=cfg.get('FOLDER', 'storage'), extensions=json.loads(cfg.get('STORAGE', 'extensions')),
extensions=json.loads(cfg.get('FOLDER', 'extensions')), shuffle=cfg.getboolean('STORAGE', 'shuffle')
shuffle=cfg.getboolean('FOLDER', 'shuffle')
) )
_text = SimpleNamespace( _text = SimpleNamespace(
@ -144,7 +147,11 @@ stdin_parser.add_argument(
) )
stdin_parser.add_argument( stdin_parser.add_argument(
'-f', '--file', help='playlist file' '-p', '--playlist', help='path from playlist'
)
stdin_parser.add_argument(
'-f', '--folder', help='play folder content'
) )
# If the log file is specified on the command line then override the default # If the log file is specified on the command line then override the default
@ -157,7 +164,11 @@ logger.setLevel(_log.level)
handler = TimedRotatingFileHandler(_log.path, when='midnight', backupCount=5) handler = TimedRotatingFileHandler(_log.path, when='midnight', backupCount=5)
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler)
if _log.to_file:
logger.addHandler(handler)
else:
logger.addHandler(logging.StreamHandler())
# capture stdout and sterr in the log # capture stdout and sterr in the log
@ -262,7 +273,7 @@ def terminate_processes(decoder, encoder, watcher):
def get_time(time_format): def get_time(time_format):
t = datetime.today() + timedelta(seconds=_playlist.shift) t = datetime.today()
if time_format == 'hour': if time_format == 'hour':
return t.hour return t.hour
elif time_format == 'full_sec': elif time_format == 'full_sec':
@ -275,8 +286,8 @@ def get_time(time_format):
def get_date(seek_day): def get_date(seek_day):
d = date.today() + timedelta(seconds=_playlist.shift) d = date.today()
if seek_day and get_time('full_sec') < _playlist.start: if _playlist.start and seek_day and get_time('full_sec') < _playlist.start:
yesterday = d - timedelta(1) yesterday = d - timedelta(1)
return yesterday.strftime('%Y-%m-%d') return yesterday.strftime('%Y-%m-%d')
else: else:
@ -307,7 +318,8 @@ def check_sync(begin, encoder):
time_now = get_time('full_sec') time_now = get_time('full_sec')
time_distance = begin - time_now time_distance = begin - time_now
if 0 <= time_now < _playlist.start and not begin == _playlist.start: if _playlist.start and 0 <= time_now < _playlist.start and \
not begin == _playlist.start:
time_distance -= 86400.0 time_distance -= 86400.0
# check that we are in tolerance time # check that we are in tolerance time
@ -320,26 +332,13 @@ def check_sync(begin, encoder):
sys.exit(1) sys.exit(1)
# check begin and length # check if playlist is long enough
def check_start_and_length(json_nodes, counter): def check_length(json_nodes, total_play_time):
# check start time and set begin
if 'begin' in json_nodes:
h, m, s = json_nodes["begin"].split(':')
if is_float(h) and is_float(m) and is_float(s):
begin = float(h) * 3600 + float(m) * 60 + float(s)
else:
begin = -100.0
else:
begin = -100.0
# check if playlist is long enough
if 'length' in json_nodes: if 'length' in json_nodes:
l_h, l_m, l_s = json_nodes["length"].split(':') l_h, l_m, l_s = json_nodes["length"].split(':')
if is_float(l_h) and is_float(l_m) and is_float(l_s): if is_float(l_h) and is_float(l_m) and is_float(l_s):
length = float(l_h) * 3600 + float(l_m) * 60 + float(l_s) length = float(l_h) * 3600 + float(l_m) * 60 + float(l_s)
total_play_time = begin + counter - _playlist.start
if 'date' in json_nodes: if 'date' in json_nodes:
date = json_nodes["date"] date = json_nodes["date"]
else: else:
@ -356,13 +355,8 @@ def check_start_and_length(json_nodes, counter):
timedelta(seconds=total_play_time))) timedelta(seconds=total_play_time)))
# validate json values in new Thread # validate json values in new thread
# and test if file path exist # and test if source paths exist
# TODO: we need better and unique validation,
# now it is messy - the file get readed twice
# and values get multiple time evaluate
# IDEA: open one time the playlist,
# not in a thread and build from it a new clean dictionary
def validate_thread(clip_nodes): def validate_thread(clip_nodes):
def check_json(json_nodes): def check_json(json_nodes):
error = '' error = ''
@ -370,17 +364,11 @@ def validate_thread(clip_nodes):
# check if all values are valid # check if all values are valid
for node in json_nodes["program"]: for node in json_nodes["program"]:
if _playlist.map_ext: source = node["source"]
source = node["source"].replace(
_playlist.map_ext[0], _playlist.map_ext[1])
else:
source = node["source"]
prefix = source.split('://')[0] prefix = source.split('://')[0]
missing = [] missing = []
if prefix in _pre_comp.protocols: if source and prefix in _pre_comp.protocols:
cmd = [ cmd = [
'ffprobe', '-v', 'error', 'ffprobe', '-v', 'error',
'-show_entries', 'format=duration', '-show_entries', 'format=duration',
@ -415,7 +403,7 @@ def validate_thread(clip_nodes):
'values are missing:\n{}'.format(error) 'values are missing:\n{}'.format(error)
) )
check_start_and_length(json_nodes, counter) check_length(json_nodes, counter)
validate = Thread(name='check_json', target=check_json, args=(clip_nodes,)) validate = Thread(name='check_json', target=check_json, args=(clip_nodes,))
validate.daemon = True validate.daemon = True
@ -441,7 +429,7 @@ def set_length(duration, seek, out):
# generate a dummy clip, with black color and empty audiotrack # generate a dummy clip, with black color and empty audiotrack
def gen_dummy(duration): def gen_dummy(duration):
if _pre_comp.copy: if _pre_comp.copy:
return ['-i', _playlist.blackclip, '-t', str(duration)] return ['-i', _storage.blackclip, '-t', str(duration)]
else: else:
color = '#121212' color = '#121212'
# TODO: add noise could be an config option # TODO: add noise could be an config option
@ -456,6 +444,41 @@ def gen_dummy(duration):
] ]
# when playlist is not 24 hours long, we generate a loop from filler clip
def gen_filler_loop(duration):
if not _storage.filler:
# when no filler is set, generate a dummy
logger.warning('No filler is set!')
return gen_dummy(duration)
else:
# get duration from filler
cmd = [
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', _storage.filler]
try:
f_dur = float(check_output(cmd).decode('utf-8'))
except (CalledProcessError, ValueError):
f_dur = None
if f_dur:
if f_dur > duration:
# cut filler
logger.info('Generate filler with {} seconds'.format(duration))
return ['-i', _storage.filler] + set_length(
f_dur, 0, duration)
else:
# loop filles n times
loop_count = math.ceil(duration / f_dur)
logger.info('Loop filler {} times, total duration: {}'.format(
loop_count, duration))
return ['-stream_loop', str(loop_count),
'-i', _storage.filler, '-t', str(duration)]
else:
logger.error("Can't get filler length, generate dummy!")
return gen_dummy(duration)
# when source path exist, generate input with seek and out time # when source path exist, generate input with seek and out time
# when path not exist, generate dummy clip # when path not exist, generate dummy clip
def src_or_dummy(src, dur, seek, out): def src_or_dummy(src, dur, seek, out):
@ -480,11 +503,14 @@ def src_or_dummy(src, dur, seek, out):
# return clip only if we are in 24 hours time range # return clip only if we are in 24 hours time range
def gen_input(has_begin, src, begin, dur, seek, out, last): def gen_input(has_begin, src, begin, dur, seek, out, last):
day_in_sec = 86400.0 day_in_sec = 86400.0
ref_time = day_in_sec + _playlist.start ref_time = day_in_sec
time = get_time('full_sec') time = get_time('full_sec')
if 0 <= time < _playlist.start: if _playlist.start:
time += day_in_sec ref_time = day_in_sec + _playlist.start
if 0 <= time < _playlist.start:
time += day_in_sec
# calculate time difference to see if we are sync # calculate time difference to see if we are sync
time_diff = out - seek + time time_diff = out - seek + time
@ -501,7 +527,7 @@ def gen_input(has_begin, src, begin, dur, seek, out, last):
logger.info('we are under time, new_len is: {}'.format(new_len)) logger.info('we are under time, new_len is: {}'.format(new_len))
if time_diff >= ref_time: if time_diff >= ref_time:
if src == _playlist.filler: if src == _storage.filler:
# when filler is something like a clock, # when filler is something like a clock,
# is better to start the clip later and to play until end # is better to start the clip later and to play until end
src_cmd = src_or_dummy(src, dur, dur - new_len, dur) src_cmd = src_or_dummy(src, dur, dur - new_len, dur)
@ -524,7 +550,7 @@ def gen_input(has_begin, src, begin, dur, seek, out, last):
logger.info('we are over time, new_len is: {}'.format(new_len)) logger.info('we are over time, new_len is: {}'.format(new_len))
if new_len > 5.0: if new_len > 5.0:
if src == _playlist.filler: if src == _storage.filler:
src_cmd = src_or_dummy(src, dur, out - new_len, out) src_cmd = src_or_dummy(src, dur, out - new_len, out)
else: else:
src_cmd = src_or_dummy(src, dur, seek, new_len) src_cmd = src_or_dummy(src, dur, seek, new_len)
@ -537,6 +563,7 @@ def gen_input(has_begin, src, begin, dur, seek, out, last):
# blend logo and fade in / fade out # blend logo and fade in / fade out
# TODO: simple deinterlace, pad and fps conversion, if is necessary
def build_filtergraph(first, duration, seek, out, ad, ad_last, ad_next, dummy): def build_filtergraph(first, duration, seek, out, ad, ad_last, ad_next, dummy):
length = out - seek - 1.0 length = out - seek - 1.0
logo_chain = [] logo_chain = []
@ -728,7 +755,7 @@ class GetSourceIter(object):
self._encoder = encoder self._encoder = encoder
self.last_time = get_time('full_sec') self.last_time = get_time('full_sec')
if 0 <= self.last_time < _playlist.start: if _playlist.start and 0 <= self.last_time < _playlist.start:
self.last_time += 86400 self.last_time += 86400
self.last_mod_time = 0.0 self.last_mod_time = 0.0
@ -754,14 +781,33 @@ class GetSourceIter(object):
self.ad_next = False self.ad_next = False
def get_playlist(self): def get_playlist(self):
if stdin_args.file: if stdin_args.playlist:
self.json_file = stdin_args.file self.json_file = stdin_args.playlist
else: else:
year, month, day = self.list_date.split('-') year, month, day = self.list_date.split('-')
self.json_file = os.path.join( self.json_file = os.path.join(
_playlist.path, year, month, self.list_date + '.json') _playlist.path, year, month, self.list_date + '.json')
if os.path.isfile(self.json_file): if '://' in self.json_file:
self.json_file = self.json_file.replace('\\', '/')
try:
req = request.urlopen(self.json_file,
timeout=1,
context=ssl._create_unverified_context())
b_time = req.headers['last-modified']
temp_time = time.strptime(b_time, "%a, %d %b %Y %I:%M:%S %Z")
mod_time = time.mktime(temp_time)
if mod_time > self.last_mod_time:
self.clip_nodes = json.load(req)
self.last_mod_time = mod_time
logger.info('open: ' + self.json_file)
validate_thread(self.clip_nodes)
except (request.URLError, socket.timeout):
self.eof_handling('Get playlist from url failed!', False)
elif os.path.isfile(self.json_file):
# check last modification from playlist # check last modification from playlist
mod_time = os.path.getmtime(self.json_file) mod_time = os.path.getmtime(self.json_file)
if mod_time > self.last_mod_time: if mod_time > self.last_mod_time:
@ -775,14 +821,12 @@ class GetSourceIter(object):
# when we have no playlist for the current day, # when we have no playlist for the current day,
# then we generate a black clip # then we generate a black clip
# and calculate the seek in time, for when the playlist comes back # and calculate the seek in time, for when the playlist comes back
self.error_handling('Playlist not exist:') self.eof_handling('Playlist not exist:', False)
# when begin is in playlist, get start time from it # when _playlist.start is set, use start time
if self.clip_nodes and 'begin' in self.clip_nodes: if self.clip_nodes and _playlist.start:
h, m, s = self.clip_nodes["begin"].split(':') self.has_begin = True
if is_float(h) and is_float(m) and is_float(s): self.init_time = _playlist.start
self.has_begin = True
self.init_time = float(h) * 3600 + float(m) * 60 + float(s)
else: else:
self.has_begin = False self.has_begin = False
@ -802,33 +846,25 @@ class GetSourceIter(object):
else: else:
self.out = self.duration self.out = self.duration
def map_extension(self, node):
if _playlist.map_ext:
self.src = node["source"].replace(
_playlist.map_ext[0], _playlist.map_ext[1])
else:
self.src = node["source"]
def url_or_live_source(self): def url_or_live_source(self):
prefix = self.src.split('://')[0] prefix = self.src.split('://')[0]
# check if input is a live source # check if input is a live source
if prefix in _pre_comp.protocols: if self.src and prefix in _pre_comp.protocols:
cmd = [ cmd = [
'ffprobe', '-v', 'error', '-show_entries', 'format=duration', 'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', self.src] '-of', 'default=noprint_wrappers=1:nokey=1', self.src]
try: try:
output = check_output(cmd).decode('utf-8') output = check_output(cmd).decode('utf-8')
except CalledProcessError: except CalledProcessError as err:
logger.error("ffprobe error: {}".format(err))
output = None output = None
if not output: if not output:
self.duration = 20
mailer.error('Clip not exist:\n{}'.format(self.src)) mailer.error('Clip not exist:\n{}'.format(self.src))
logger.error('Clip not exist: {}'.format(self.src)) logger.error('Clip not exist: {}'.format(self.src))
self.src = None self.src = None
self.out = 20
elif is_float(output): elif is_float(output):
self.duration = float(output) self.duration = float(output)
else: else:
@ -882,18 +918,21 @@ class GetSourceIter(object):
self.first, self.duration, self.seek, self.out, self.first, self.duration, self.seek, self.out,
self.ad, self.ad_last, self.ad_next, self.is_dummy) self.ad, self.ad_last, self.ad_next, self.is_dummy)
def error_handling(self, message): def eof_handling(self, message, filler):
self.seek = 0.0 self.seek = 0.0
self.out = 20 self.out = 20
self.duration = 20 self.duration = 20
self.ad = False self.ad = False
day_in_sec = 86400.0 day_in_sec = 86400.0
ref_time = day_in_sec + _playlist.start ref_time = day_in_sec
time = get_time('full_sec') time = get_time('full_sec')
if 0 <= time < _playlist.start: if _playlist.start:
time += day_in_sec ref_time = day_in_sec + _playlist.start
if 0 <= time < _playlist.start:
time += day_in_sec
time_diff = self.out - self.seek + time time_diff = self.out - self.seek + time
new_len = self.out - self.seek - (time_diff - ref_time) new_len = self.out - self.seek - (time_diff - ref_time)
@ -910,8 +949,15 @@ class GetSourceIter(object):
self.list_date = get_date(True) self.list_date = get_date(True)
self.last_time += self.out - self.seek self.last_time += self.out - self.seek
self.src_cmd = gen_dummy(self.out - self.seek) if filler:
self.is_dummy = True self.src_cmd = gen_filler_loop(self.out - self.seek)
if _storage.filler:
self.is_dummy = False
self.duration += 1
else:
self.src_cmd = gen_dummy(self.out - self.seek)
self.is_dummy = True
self.set_filtergraph() self.set_filtergraph()
if get_time('stamp') - self.timestamp > 3600 \ if get_time('stamp') - self.timestamp > 3600 \
@ -947,7 +993,8 @@ class GetSourceIter(object):
# calculate seek time # calculate seek time
self.seek = self.last_time - self.begin + self.seek self.seek = self.last_time - self.begin + self.seek
self.map_extension(node) self.src = node["source"]
self.url_or_live_source() self.url_or_live_source()
self.get_input() self.get_input()
self.is_source_dummy() self.is_source_dummy()
@ -966,7 +1013,8 @@ class GetSourceIter(object):
if self.has_begin: if self.has_begin:
check_sync(self.begin, self._encoder) check_sync(self.begin, self._encoder)
self.map_extension(node) self.src = node["source"]
self.url_or_live_source() self.url_or_live_source()
self.get_input() self.get_input()
self.is_source_dummy() self.is_source_dummy()
@ -982,7 +1030,8 @@ class GetSourceIter(object):
self.last_time = self.begin self.last_time = self.begin
self.out = self.time_left self.out = self.time_left
self.error_handling('Playlist is not valid!') self.eof_handling(
'Playlist is not long enough!', False)
else: else:
# when there is no time left and we are in time, # when there is no time left and we are in time,
@ -995,16 +1044,17 @@ class GetSourceIter(object):
self.begin += self.out - self.seek self.begin += self.out - self.seek
else: else:
# when we reach currect end, stop script if not _playlist.start or 'length' not in self.clip_nodes:
if 'begin' not in self.clip_nodes or \ # when we reach currect end, stop script
'length' not in self.clip_nodes and \
self.begin < get_time('full_sec'):
logger.info('Playlist reach End!') logger.info('Playlist reach End!')
return return
# when playlist exist but is empty, or not long enough, elif self.begin == self.init_time:
# generate dummy and send log # no clip was played, generate dummy
self.error_handling('Playlist is not valid!') self.eof_handling('Playlist is empty!', False)
else:
# playlist is not long enough, play filler
self.eof_handling('Playlist is not long enough!', True)
if self.src_cmd is not None: if self.src_cmd is not None:
yield self.src_cmd + self.filtergraph yield self.src_cmd + self.filtergraph
@ -1012,9 +1062,10 @@ class GetSourceIter(object):
def main(): def main():
year = get_date(False).split('-')[0] year = get_date(False).split('-')[0]
overlay = []
if _pre_comp.copy: if _pre_comp.copy:
ff_pre_settings = _pre_comp.copy_settings ff_pre_settings = ["-c", "copy", "-f", "mpegts", "-"]
else: else:
ff_pre_settings = [ ff_pre_settings = [
'-pix_fmt', 'yuv420p', '-r', str(_pre_comp.fps), '-pix_fmt', 'yuv420p', '-r', str(_pre_comp.fps),
@ -1027,18 +1078,16 @@ def main():
'-ar', '48000', '-ac', '2', '-ar', '48000', '-ac', '2',
'-f', 'mpegts', '-'] '-f', 'mpegts', '-']
if os.path.isfile(_text.textfile): if os.path.isfile(_text.textfile):
logger.info('Use text file "{}" for overlay'.format(_text.textfile)) logger.info('Overlay text file: "{}"'.format(_text.textfile))
overlay = [ overlay = [
'-vf', ("drawtext=box={}:boxcolor='{}':boxborderw={}:fontsize={}" '-vf', ("drawtext=box={}:boxcolor='{}':boxborderw={}"
":fontcolor={}:fontfile='{}':textfile={}:reload=1" ":fontsize={}:fontcolor={}:fontfile='{}':textfile={}"
":x='{}':y='{}'").format( ":reload=1:x='{}':y='{}'").format(
_text.box, _text.boxcolor, _text.boxborderw, _text.box, _text.boxcolor, _text.boxborderw,
_text.fontsize, _text.fontcolor, _text.fontfile, _text.fontsize, _text.fontcolor, _text.fontfile,
_text.textfile, _text.x, _text.y) _text.textfile, _text.x, _text.y)
] ]
else:
overlay = []
try: try:
if _playout.preview: if _playout.preview:
@ -1053,7 +1102,8 @@ def main():
if _pre_comp.copy: if _pre_comp.copy:
encoder_cmd = [ encoder_cmd = [
'ffmpeg', '-v', 'info', '-hide_banner', '-nostats', 'ffmpeg', '-v', 'info', '-hide_banner', '-nostats',
'-re', '-i', 'pipe:0', '-c', 'copy' '-re', '-thread_queue_size', '256',
'-i', 'pipe:0', '-c', 'copy'
] + _playout.post_comp_copy ] + _playout.post_comp_copy
else: else:
encoder_cmd = [ encoder_cmd = [
@ -1072,16 +1122,16 @@ def main():
stdin=PIPE stdin=PIPE
) )
if _general.playlist_mode: if _playlist.mode and not stdin_args.folder:
watcher = None watcher = None
get_source = GetSourceIter(encoder) get_source = GetSourceIter(encoder)
else: else:
logger.info("start folder mode") logger.info("start folder mode")
media = MediaStore(_folder.extensions) media = MediaStore(_storage.extensions)
media.fill(_folder.storage) media.fill(_storage.path)
watcher = MediaWatcher(_folder.storage, _folder.extensions, media) watcher = MediaWatcher(_storage.path, _storage.extensions, media)
get_source = GetSource(media, _folder.shuffle) get_source = GetSource(media, _storage.shuffle)
try: try:
for src_cmd in get_source.next(): for src_cmd in get_source.next():
@ -1110,12 +1160,16 @@ def main():
logger.warning('program terminated') logger.warning('program terminated')
terminate_processes(decoder, encoder, watcher) terminate_processes(decoder, encoder, watcher)
# close encoder when nothing is to do anymore
if encoder.poll() is None:
encoder.terminate()
finally: finally:
encoder.wait() encoder.wait()
if __name__ == '__main__': if __name__ == '__main__':
if not _general.playlist_mode: if not _playlist.mode or stdin_args.folder:
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer from watchdog.observers import Observer