Merge branch 'cleanup' into dev

This commit is contained in:
jb-alvarado 2021-02-04 17:12:55 +01:00
commit 792564fe4f
15 changed files with 324 additions and 293 deletions

View File

@ -37,6 +37,8 @@ The purpose with ffplayout is to provide a 24/7 broadcasting solution that plays
- **aevalsrc** (if video have no audio) - **aevalsrc** (if video have no audio)
- **apad** (add silence if audio duration is to short) - **apad** (add silence if audio duration is to short)
- **tpad** (add black frames if video duration is to short) - **tpad** (add black frames if video duration is to short)
- add custom [filters](https://github.com/ffplayout/ffplayout-engine/tree/master/ffplayout/filters)
- add custom [arguments](https://github.com/ffplayout/ffplayout-engine/tree/master/ffplayout/config)
- different types of [output](https://github.com/ffplayout/ffplayout-engine/wiki/Outputs): - different types of [output](https://github.com/ffplayout/ffplayout-engine/wiki/Outputs):
- **stream** - **stream**
- **desktop** - **desktop**
@ -49,6 +51,7 @@ Requirements
- python version 3.6+ - python version 3.6+
- python module **watchdog** (only when `playlist_mode: False`) - python module **watchdog** (only when `playlist_mode: False`)
- python module **colorama** if you are on windows - python module **colorama** if you are on windows
- python modules **PyYAML**, **requests**, **supervisor**
- **ffmpeg v4.2+** and **ffprobe** (**ffplay** if you want to play on desktop) - **ffmpeg v4.2+** and **ffprobe** (**ffplay** if you want to play on desktop)
- if you want to overlay text, ffmpeg needs to have **libzmq** - if you want to overlay text, ffmpeg needs to have **libzmq**
- RAM and CPU depends on video resolution, minimum 4 threads and 3GB RAM for 720p are recommend - RAM and CPU depends on video resolution, minimum 4 threads and 3GB RAM for 720p are recommend
@ -122,10 +125,10 @@ Start with Arguments
ffplayout also allows the passing of parameters: ffplayout also allows the passing of parameters:
- `-c, --config` use given config file - `-c, --config` use given config file
- `-d, --desktop` preview on desktop
- `-f, --folder` use folder for playing - `-f, --folder` use folder for playing
- `-l, --log` for user-defined log path, *none* for console output - `-l, --log` for user-defined log path, *none* for console output
- `-i, --loop` loop playlist infinitely - `-i, --loop` loop playlist infinitely
- `-m, --mode` set output mode: desktop, hls, stream, ...
- `-p, --playlist` for playlist file - `-p, --playlist` for playlist file
- `-s, --start` set start time in *hh:mm:ss*, *now* for start with first' - `-s, --start` set start time in *hh:mm:ss*, *now* for start with first'
- `-t, --length` set length in *hh:mm:ss*, *none* for no length check - `-t, --length` set length in *hh:mm:ss*, *none* for no length check
@ -133,10 +136,5 @@ ffplayout also allows the passing of parameters:
You can run the command like: You can run the command like:
```SHELL ```SHELL
./ffplayout.py -l none -p ~/playlist.json -d -s now -t none ./ffplayout.py -l none -p ~/playlist.json -d -s now -t none -m desktop
``` ```
Play on Desktop
-----
For playing on desktop use `-d` argument or set `mode: 'desktop'` in config under `out:`.

View File

@ -41,7 +41,7 @@ def main():
""" """
if stdin_args.mode: if stdin_args.mode:
output = locate('ffplayout.output.{}.output'.format(stdin_args.mode)) output = locate(f'ffplayout.output.{stdin_args.mode}.output')
output() output()
else: else:
@ -54,7 +54,7 @@ def main():
mode = os.path.splitext(output)[0] mode = os.path.splitext(output)[0]
if mode == _playout.mode: if mode == _playout.mode:
output = locate('ffplayout.output.{}.output'.format(mode)) output = locate(f'ffplayout.output.{mode}.output')
output() output()

View File

@ -0,0 +1,15 @@
# Custom Configuration
Extend your arguments for using them in your custom extensions.
The file name must have the **argparse_** prefix. The content should look like:
```YAML
short: -v
long: --volume
help: set audio volume
```
At least **short** or **long** have to exist, all other parameters are optional. You can also extend the config, with keys which are exist in **ArgumentParser.add_argument()**.
**Every argument must have its own yaml file!**

View File

@ -0,0 +1,3 @@
short: -v
long: --volume
help: set audio volume

View File

@ -7,3 +7,18 @@ Add your one filters here. They must have the correct file naming:
The file itself should contain only one filter in a function named `def filter(prope):` The file itself should contain only one filter in a function named `def filter(prope):`
Check **v_addtext.py** for example. Check **v_addtext.py** for example.
In your filter you can also read custom properties from the current program node. That you can use for any usecase you wish, like reading a subtitle file, or a different logo for every clip and so on.
The normal program node looks like:
```JSON
{
"in": 0,
"out": 3600.162,
"duration": 3600.162,
"source": "/dir/input.mp4"
}
```
This you can extend to your needs, and apply this values to your filters.

View File

@ -0,0 +1,10 @@
from ffplayout.utils import get_float, stdin_args
def filter(probe, node=None):
"""
set audio volume
"""
if stdin_args.volume and get_float(stdin_args.volume, False):
return f'volume={stdin_args.volume}'

View File

@ -23,7 +23,8 @@ import re
from glob import glob from glob import glob
from pydoc import locate from pydoc import locate
from ffplayout.utils import _global, _pre, _text from ffplayout.utils import (_global, _pre, _text, get_float, is_advertisement,
messenger)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# building filters, # building filters,
@ -37,7 +38,7 @@ def text_filter():
if _text.add_text and _text.over_pre: if _text.add_text and _text.over_pre:
if _text.fontfile and os.path.isfile(_text.fontfile): if _text.fontfile and os.path.isfile(_text.fontfile):
font = ":fontfile='{}'".format(_text.fontfile) font = f":fontfile='{_text.fontfile}'"
filter_chain = [ filter_chain = [
"null,zmq=b=tcp\\\\://'{}',drawtext=text=''{}".format( "null,zmq=b=tcp\\\\://'{}',drawtext=text=''{}".format(
_text.address.replace(':', '\\:'), font)] _text.address.replace(':', '\\:'), font)]
@ -70,12 +71,10 @@ def pad_filter(probe):
_pre.aspect, abs_tol=0.03): _pre.aspect, abs_tol=0.03):
if probe.video[0]['aspect'] < _pre.aspect: if probe.video[0]['aspect'] < _pre.aspect:
filter_chain.append( filter_chain.append(
'pad=ih*{}/{}/sar:ih:(ow-iw)/2:(oh-ih)/2'.format(_pre.w, f'pad=ih*{_pre.w}/{_pre.h}/sar:ih:(ow-iw)/2:(oh-ih)/2')
_pre.h))
elif probe.video[0]['aspect'] > _pre.aspect: elif probe.video[0]['aspect'] > _pre.aspect:
filter_chain.append( filter_chain.append(
'pad=iw:iw*{}/{}/sar:(ow-iw)/2:(oh-ih)/2'.format(_pre.h, f'pad=iw:iw*{_pre.h}/{_pre.w}/sar:(ow-iw)/2:(oh-ih)/2')
_pre.w))
return filter_chain return filter_chain
@ -87,7 +86,7 @@ def fps_filter(probe):
filter_chain = [] filter_chain = []
if probe.video[0]['fps'] != _pre.fps: if probe.video[0]['fps'] != _pre.fps:
filter_chain.append('fps={}'.format(_pre.fps)) filter_chain.append(f'fps={_pre.fps}')
return filter_chain return filter_chain
@ -101,11 +100,11 @@ def scale_filter(probe):
if int(probe.video[0]['width']) != _pre.w or \ if int(probe.video[0]['width']) != _pre.w or \
int(probe.video[0]['height']) != _pre.h: int(probe.video[0]['height']) != _pre.h:
filter_chain.append('scale={}:{}'.format(_pre.w, _pre.h)) filter_chain.append(f'scale={_pre.w}:{_pre.h}')
if not math.isclose(probe.video[0]['aspect'], if not math.isclose(probe.video[0]['aspect'],
_pre.aspect, abs_tol=0.03): _pre.aspect, abs_tol=0.03):
filter_chain.append('setdar=dar={}'.format(_pre.aspect)) filter_chain.append(f'setdar=dar={_pre.aspect}')
return filter_chain return filter_chain
@ -117,11 +116,10 @@ def fade_filter(duration, seek, out, track=''):
filter_chain = [] filter_chain = []
if seek > 0.0: if seek > 0.0:
filter_chain.append('{}fade=in:st=0:d=0.5'.format(track)) filter_chain.append(f'{track}fade=in:st=0:d=0.5')
if out != duration: if out != duration:
filter_chain.append('{}fade=out:st={}:d=1.0'.format(track, filter_chain.append(f'{track}fade=out:st={out - seek - 1.0}:d=1.0')
out - seek - 1.0))
return filter_chain return filter_chain
@ -139,35 +137,32 @@ def overlay_filter(duration, ad, ad_last, ad_next):
logo_chain = [] logo_chain = []
if _pre.logo_scale and \ if _pre.logo_scale and \
re.match(r'\d+:-?\d+', _pre.logo_scale): re.match(r'\d+:-?\d+', _pre.logo_scale):
scale_filter = 'scale={},'.format(_pre.logo_scale) scale_filter = f'scale={_pre.logo_scale},'
logo_extras = 'format=rgba,{}colorchannelmixer=aa={}'.format( logo_extras = (f'format=rgba,{scale_filter}'
scale_filter, _pre.logo_opacity) f'colorchannelmixer=aa={_pre.logo_opacity}')
loop = 'loop=loop=-1:size=1:start=0' loop = 'loop=loop=-1:size=1:start=0'
logo_chain.append( logo_chain.append(f'movie={_pre.logo},{loop},{logo_extras}')
'movie={},{},{}'.format(_pre.logo, loop, logo_extras))
if ad_last: if ad_last:
logo_chain.append('fade=in:st=0:d=1.0:alpha=1') logo_chain.append('fade=in:st=0:d=1.0:alpha=1')
if ad_next: if ad_next:
logo_chain.append('fade=out:st={}:d=1.0:alpha=1'.format( logo_chain.append(f'fade=out:st={duration - 1}:d=1.0:alpha=1')
duration - 1))
logo_filter = '{}[l];[v][l]{}:shortest=1'.format( logo_filter = (f'{",".join(logo_chain)}[l];[v][l]'
','.join(logo_chain), _pre.logo_filter) f'{_pre.logo_filter}:shortest=1')
return logo_filter return logo_filter
def add_audio(probe, duration, msg): def add_audio(probe, duration):
""" """
when clip has no audio we generate an audio line when clip has no audio we generate an audio line
""" """
line = [] line = []
if not probe.audio: if not probe.audio:
msg.warning('Clip "{}" has no audio!'.format(probe.src)) messenger.warning(f'Clip "{probe.src}" has no audio!')
line = [ line = [(f'aevalsrc=0:channel_layout=2:duration={duration}:'
'aevalsrc=0:channel_layout=2:duration={}:sample_rate={}'.format( f'sample_rate={48000}')]
duration, 48000)]
return line return line
@ -179,8 +174,8 @@ def add_loudnorm(probe):
loud_filter = [] loud_filter = []
if probe.audio and _pre.add_loudnorm: if probe.audio and _pre.add_loudnorm:
loud_filter = [('loudnorm=I={}:TP={}:LRA={}').format( loud_filter = [
_pre.loud_i, _pre.loud_tp, _pre.loud_lra)] f'loudnorm=I={_pre.loud_i}:TP={_pre.loud_tp}:LRA={_pre.loud_lra}']
return loud_filter return loud_filter
@ -193,7 +188,7 @@ def extend_audio(probe, duration):
if probe.audio and 'duration' in probe.audio[0] and \ if probe.audio and 'duration' in probe.audio[0] and \
duration > float(probe.audio[0]['duration']) + 0.1: duration > float(probe.audio[0]['duration']) + 0.1:
pad_filter.append('apad=whole_dur={}'.format(duration)) pad_filter.append(f'apad=whole_dur={duration}')
return pad_filter return pad_filter
@ -203,12 +198,11 @@ def extend_video(probe, duration, target_duration):
check video duration, is it shorter then clip duration - pad it check video duration, is it shorter then clip duration - pad it
""" """
pad_filter = [] pad_filter = []
vid_dur = probe.video[0].get('duration')
if 'duration' in probe.video[0] and \ if vid_dur and target_duration < duration > float(vid_dur) + 0.1:
target_duration < duration > float( pad_filter.append(
probe.video[0]['duration']) + 0.1: f'tpad=stop_mode=add:stop_duration={duration - float(vid_dur)}')
pad_filter.append('tpad=stop_mode=add:stop_duration={}'.format(
duration - float(probe.video[0]['duration'])))
return pad_filter return pad_filter
@ -217,46 +211,45 @@ def realtime_filter(duration, track=''):
speed_filter = '' speed_filter = ''
if _pre.realtime: if _pre.realtime:
speed_filter = ',{}realtime=speed=1'.format(track) speed_filter = f',{track}realtime=speed=1'
if _global.time_delta < 0: if _global.time_delta < 0:
speed = duration / (duration + _global.time_delta) speed = duration / (duration + _global.time_delta)
if speed < 1.1: if speed < 1.1:
speed_filter = ',{}realtime=speed={}'.format(track, speed) speed_filter = f',{track}realtime=speed={speed}'
return speed_filter return speed_filter
def split_filter(filter_type): def split_filter(filter_type):
map_node = [] map_node = []
filter_prefix = '' prefix = ''
_filter = '' _filter = ''
if filter_type == 'a': if filter_type == 'a':
filter_prefix = 'a' prefix = 'a'
if _pre.output_count > 1: if _pre.output_count > 1:
for num in range(_pre.output_count): for num in range(_pre.output_count):
map_node.append('[{}out{}]'.format(filter_type, num + 1)) map_node.append(f'[{filter_type}out{num + 1}]')
_filter = ',{}split={}{}'.format(filter_prefix, _pre.output_count, _filter = f',{prefix}split={_pre.output_count}{"".join(map_node)}'
''.join(map_node))
else: else:
_filter = '[{}out1]'.format(filter_type) _filter = f'[{filter_type}out1]'
return _filter return _filter
def custom_filter(probe, type): def custom_filter(probe, type, node):
filter_dir = os.path.dirname(os.path.abspath(__file__)) filter_dir = os.path.dirname(os.path.abspath(__file__))
filters = [] filters = []
for filter in glob(os.path.join(filter_dir, f'{type}_*')): for filter in glob(os.path.join(filter_dir, f'{type}_*')):
filter = os.path.splitext(os.path.basename(filter))[0] filter = os.path.splitext(os.path.basename(filter))[0]
filter_func = locate(f'ffplayout.filters.{filter}.filter') filter_func = locate(f'ffplayout.filters.{filter}.filter')
link = filter_func(probe) link = filter_func(probe, node)
if link is not None: if link is not None:
filters.append(link) filters.append(link)
@ -264,10 +257,17 @@ def custom_filter(probe, type):
return filters return filters
def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg): def build_filtergraph(node, node_last, node_next, seek, probe):
""" """
build final filter graph, with video and audio chain build final filter graph, with video and audio chain
""" """
duration = get_float(node.get('duration'), 20)
out = get_float(node.get('out'), duration)
ad = is_advertisement(node)
ad_last = is_advertisement(node_last)
ad_next = is_advertisement(node_next)
video_chain = [] video_chain = []
audio_chain = [] audio_chain = []
@ -275,7 +275,7 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
seek = 0 seek = 0
if probe.video[0]: if probe.video[0]:
custom_v_filter = custom_filter(probe, 'v') custom_v_filter = custom_filter(probe, 'v', node)
video_chain += text_filter() video_chain += text_filter()
video_chain += deinterlace_filter(probe) video_chain += deinterlace_filter(probe)
video_chain += pad_filter(probe) video_chain += pad_filter(probe)
@ -286,10 +286,10 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
video_chain += custom_v_filter video_chain += custom_v_filter
video_chain += fade_filter(duration, seek, out) video_chain += fade_filter(duration, seek, out)
audio_chain += add_audio(probe, out - seek, msg) audio_chain += add_audio(probe, out - seek)
if not audio_chain: if not audio_chain:
custom_a_filter = custom_filter(probe, 'a') custom_a_filter = custom_filter(probe, 'a', node)
audio_chain.append('[0:a]anull') audio_chain.append('[0:a]anull')
audio_chain += add_loudnorm(probe) audio_chain += add_loudnorm(probe)
@ -299,7 +299,7 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
audio_chain += fade_filter(duration, seek, out, 'a') audio_chain += fade_filter(duration, seek, out, 'a')
if video_chain: if video_chain:
video_filter = '{}[v]'.format(','.join(video_chain)) video_filter = f'{",".join(video_chain)}[v]'
else: else:
video_filter = 'null[v]' video_filter = 'null[v]'
@ -308,15 +308,14 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
v_split = split_filter('v') v_split = split_filter('v')
video_map = ['-map', '[vout1]'] video_map = ['-map', '[vout1]']
video_filter = [ video_filter = [
'-filter_complex', '[0:v]{};{}{}{}'.format( '-filter_complex',
video_filter, logo_filter, v_speed, v_split)] f'[0:v]{video_filter};{logo_filter}{v_speed}{v_split}']
a_speed = realtime_filter(out - seek, 'a') a_speed = realtime_filter(out - seek, 'a')
a_split = split_filter('a') a_split = split_filter('a')
audio_map = ['-map', '[aout1]'] audio_map = ['-map', '[aout1]']
audio_filter = [ audio_filter = [
'-filter_complex', '{}{}{}'.format(','.join(audio_chain), '-filter_complex', f'{",".join(audio_chain)}{a_speed}{a_split}']
a_speed, a_split)]
if probe.video[0]: if probe.video[0]:
return video_filter + audio_filter + video_map + audio_map return video_filter + audio_filter + video_map + audio_map

View File

@ -4,7 +4,7 @@ import re
from ffplayout.utils import _text from ffplayout.utils import _text
def filter(probe): def filter(probe, node=None):
""" """
extract title from file name and overlay it extract title from file name and overlay it
""" """

View File

@ -52,7 +52,7 @@ class MediaStore:
def fill(self): def fill(self):
for ext in _storage.extensions: for ext in _storage.extensions:
self.store.extend( self.store.extend(
glob.glob(os.path.join(self.folder, '**', '*{}'.format(ext)), glob.glob(os.path.join(self.folder, '**', f'*{ext}'),
recursive=True)) recursive=True))
if _storage.shuffle: if _storage.shuffle:
@ -84,7 +84,7 @@ class MediaWatcher:
def __init__(self, media): def __init__(self, media):
self._media = media self._media = media
self.extensions = ['*{}'.format(ext) for ext in _storage.extensions] self.extensions = [f'*{ext}' for ext in _storage.extensions]
self.event_handler = PatternMatchingEventHandler( self.event_handler = PatternMatchingEventHandler(
patterns=self.extensions) patterns=self.extensions)
@ -107,14 +107,14 @@ class MediaWatcher:
self._media.add(event.src_path) self._media.add(event.src_path)
messenger.info('Add file to media list: "{}"'.format(event.src_path)) messenger.info(f'Add file to media list: "{event.src_path}"')
def on_moved(self, event): def on_moved(self, event):
self._media.remove(event.src_path) self._media.remove(event.src_path)
self._media.add(event.dest_path) self._media.add(event.dest_path)
messenger.info('Move file from "{}" to "{}"'.format(event.src_path, messenger.info(
event.dest_path)) f'Move file from "{event.src_path}" to "{event.dest_path}"')
if _current.clip == event.src_path: if _current.clip == event.src_path:
_ff.decoder.terminate() _ff.decoder.terminate()
@ -122,8 +122,7 @@ class MediaWatcher:
def on_deleted(self, event): def on_deleted(self, event):
self._media.remove(event.src_path) self._media.remove(event.src_path)
messenger.info( messenger.info(f'Remove file from media list: "{event.src_path}"')
'Remove file from media list: "{}"'.format(event.src_path))
if _current.clip == event.src_path: if _current.clip == event.src_path:
_ff.decoder.terminate() _ff.decoder.terminate()

View File

@ -21,16 +21,15 @@ def output():
ff_pre_settings = [ ff_pre_settings = [
'-pix_fmt', 'yuv420p', '-r', str(_pre.fps), '-pix_fmt', 'yuv420p', '-r', str(_pre.fps),
'-c:v', 'mpeg2video', '-intra', '-c:v', 'mpeg2video', '-intra',
'-b:v', '{}k'.format(_pre.v_bitrate), '-b:v', f'{_pre.v_bitrate}k',
'-minrate', '{}k'.format(_pre.v_bitrate), '-minrate', f'{_pre.v_bitrate}k',
'-maxrate', '{}k'.format(_pre.v_bitrate), '-maxrate', f'{_pre.v_bitrate}k',
'-bufsize', '{}k'.format(_pre.v_bufsize) '-bufsize', f'{_pre.v_bufsize}k'
] + pre_audio_codec() + ['-f', 'mpegts', '-'] ] + pre_audio_codec() + ['-f', 'mpegts', '-']
if _text.add_text and not _text.over_pre: if _text.add_text and not _text.over_pre:
messenger.info('Using drawtext node, listening on address: {}'.format( messenger.info(
_text.address f'Using drawtext node, listening on address: {_text.address}')
))
overlay = [ overlay = [
'-vf', '-vf',
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format( "null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
@ -38,9 +37,13 @@ def output():
] ]
try: try:
_ff.encoder = Popen([ enc_cmd = [
'ffplay', '-hide_banner', '-nostats', '-i', 'pipe:0' 'ffplay', '-hide_banner', '-nostats', '-i', 'pipe:0'
] + overlay, stderr=PIPE, stdin=PIPE, stdout=None) ] + overlay
messenger.debug(f'Encoder CMD: "{" ".join(enc_cmd)}"')
_ff.encoder = Popen(enc_cmd, stderr=PIPE, stdin=PIPE, stdout=None)
enc_err_thread = Thread(target=ffmpeg_stderr_reader, enc_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.encoder.stderr, False)) args=(_ff.encoder.stderr, False))
@ -58,20 +61,21 @@ def output():
try: try:
for src_cmd in get_source.next(): for src_cmd in get_source.next():
messenger.debug('src_cmd: "{}"'.format(src_cmd))
if src_cmd[0] == '-i': if src_cmd[0] == '-i':
current_file = src_cmd[1] current_file = src_cmd[1]
else: else:
current_file = src_cmd[3] current_file = src_cmd[3]
_current.clip = current_file _current.clip = current_file
messenger.info('Play: "{}"'.format(current_file)) messenger.info(f'Play: {current_file}')
with Popen([ dec_cmd = ['ffmpeg', '-v', _log.ff_level.lower(),
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner', '-hide_banner', '-nostats'
'-nostats'] + src_cmd + ff_pre_settings, ] + src_cmd + ff_pre_settings
stdout=PIPE, stderr=PIPE) as _ff.decoder:
messenger.debug(f'Decoder CMD: "{" ".join(dec_cmd)}"')
with Popen(dec_cmd, stdout=PIPE, stderr=PIPE) as _ff.decoder:
dec_err_thread = Thread(target=ffmpeg_stderr_reader, dec_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.decoder.stderr, True)) args=(_ff.decoder.stderr, True))
dec_err_thread.daemon = True dec_err_thread.daemon = True

View File

@ -23,16 +23,15 @@ def output():
ff_pre_settings = [ ff_pre_settings = [
'-pix_fmt', 'yuv420p', '-r', str(_pre.fps), '-pix_fmt', 'yuv420p', '-r', str(_pre.fps),
'-c:v', 'mpeg2video', '-intra', '-c:v', 'mpeg2video', '-intra',
'-b:v', '{}k'.format(_pre.v_bitrate), '-b:v', f'{_pre.v_bitrate}k',
'-minrate', '{}k'.format(_pre.v_bitrate), '-minrate', f'{_pre.v_bitrate}k',
'-maxrate', '{}k'.format(_pre.v_bitrate), '-maxrate', f'{_pre.v_bitrate}k',
'-bufsize', '{}k'.format(_pre.v_bufsize) '-bufsize', f'{_pre.v_bufsize}k'
] + pre_audio_codec() + ['-f', 'mpegts', '-'] ] + pre_audio_codec() + ['-f', 'mpegts', '-']
if _text.add_text and not _text.over_pre: if _text.add_text and not _text.over_pre:
messenger.info('Using drawtext node, listening on address: {}'.format( messenger.info(
_text.address f'Using drawtext node, listening on address: {_text.address}')
))
overlay = [ overlay = [
'-vf', '-vf',
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format( "null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
@ -40,15 +39,18 @@ def output():
] ]
try: try:
_ff.encoder = Popen([ enc_cmd = [
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner', 'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
'-nostats', '-re', '-thread_queue_size', '256', '-i', 'pipe:0' '-nostats', '-re', '-thread_queue_size', '256', '-i', 'pipe:0'
] + overlay + [ ] + overlay + [
'-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', f'year={year}'
] + _playout.ffmpeg_param + _playout.stream_output, ] + _playout.ffmpeg_param + _playout.stream_output
stdin=PIPE, stderr=PIPE)
messenger.debug(f'Encoder CMD: "{" ".join(enc_cmd)}"')
_ff.encoder = Popen(enc_cmd, stdin=PIPE, stderr=PIPE)
enc_err_thread = Thread(target=ffmpeg_stderr_reader, enc_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.encoder.stderr, False)) args=(_ff.encoder.stderr, False))
@ -66,20 +68,21 @@ def output():
try: try:
for src_cmd in get_source.next(): for src_cmd in get_source.next():
messenger.debug('src_cmd: "{}"'.format(src_cmd))
if src_cmd[0] == '-i': if src_cmd[0] == '-i':
current_file = src_cmd[1] current_file = src_cmd[1]
else: else:
current_file = src_cmd[3] current_file = src_cmd[3]
_current.clip = current_file _current.clip = current_file
messenger.info('Play: "{}"'.format(current_file)) messenger.info(f'Play: {current_file}')
with Popen([ dec_cmd = ['ffmpeg', '-v', _log.ff_level.lower(),
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner', '-hide_banner', '-nostats'
'-nostats'] + src_cmd + ff_pre_settings, ] + src_cmd + ff_pre_settings
stdout=PIPE, stderr=PIPE) as _ff.decoder:
messenger.debug(f'Decoder CMD: "{" ".join(dec_cmd)}"')
with Popen(dec_cmd, stdout=PIPE, stderr=PIPE) as _ff.decoder:
dec_err_thread = Thread(target=ffmpeg_stderr_reader, dec_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.decoder.stderr, True)) args=(_ff.decoder.stderr, True))
dec_err_thread.daemon = True dec_err_thread.daemon = True

View File

@ -17,16 +17,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os
import socket
import ssl
import time
from urllib import request
from .filters.default import build_filtergraph from .filters.default import build_filtergraph
from .utils import (MediaProbe, _playlist, gen_filler, get_date, get_delta, from .utils import (MediaProbe, _playlist, gen_filler, get_date, get_delta,
get_time, is_float, messenger, stdin_args, timed_source, get_float, get_time, messenger, read_playlist, stdin_args,
valid_json, validate_thread) timed_source)
class GetSourceFromPlaylist: class GetSourceFromPlaylist:
@ -37,6 +31,7 @@ class GetSourceFromPlaylist:
""" """
def __init__(self): def __init__(self):
self.list_date = get_date(True)
self.init_time = _playlist.start self.init_time = _playlist.start
self.last_time = get_time('full_sec') self.last_time = get_time('full_sec')
@ -49,79 +44,25 @@ class GetSourceFromPlaylist:
self.last_time += self.total_playtime self.last_time += self.total_playtime
self.last_mod_time = 0.0 self.last_mod_time = 0.0
self.json_file = None
self.clip_nodes = None self.clip_nodes = None
self.src_cmd = None self.src_cmd = None
self.probe = MediaProbe() self.probe = MediaProbe()
self.filtergraph = [] self.filtergraph = []
self.first = True self.first = True
self.last = False self.last = False
self.list_date = get_date(True) self.node = None
self.node_last = None
self.node_next = None
self.src = None self.src = None
self.begin = 0 self.begin = 0
self.seek = 0 self.seek = 0
self.out = 20 self.out = 20
self.duration = 20 self.duration = 20
self.ad = False
self.ad_last = False
self.ad_next = False
def get_playlist(self): def get_playlist(self):
if stdin_args.playlist: self.clip_nodes, self.last_mod_time = read_playlist(self.list_date,
self.json_file = stdin_args.playlist self.last_mod_time,
else: self.clip_nodes)
year, month, day = self.list_date.split('-')
self.json_file = os.path.join(
_playlist.path, year, month, self.list_date + '.json')
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 %H:%M:%S %Z")
mod_time = time.mktime(temp_time)
if mod_time > self.last_mod_time:
self.clip_nodes = valid_json(req)
self.last_mod_time = mod_time
messenger.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
mod_time = os.path.getmtime(self.json_file)
if mod_time > self.last_mod_time:
with open(self.json_file, 'r', encoding='utf-8') as f:
self.clip_nodes = valid_json(f)
self.last_mod_time = mod_time
messenger.info('Open: ' + self.json_file)
validate_thread(self.clip_nodes)
else:
self.clip_nodes = None
def get_clip_in_out(self, node):
if is_float(node["in"]):
self.seek = node["in"]
else:
self.seek = 0
if is_float(node["duration"]):
self.duration = node["duration"]
else:
self.duration = 20
if is_float(node["out"]):
self.out = node["out"]
else:
self.out = self.duration
def get_input(self): def get_input(self):
self.src_cmd, self.seek, self.out, self.next_playlist = timed_source( self.src_cmd, self.seek, self.out, self.next_playlist = timed_source(
@ -129,39 +70,20 @@ class GetSourceFromPlaylist:
self.seek, self.out, self.first, self.last self.seek, self.out, self.first, self.last
) )
def get_category(self, index, node): def last_and_next_node(self, index):
if 'category' in node: if index - 1 >= 0:
if index - 1 >= 0: self.node_last = self.clip_nodes['program'][index - 1]
last_category = self.clip_nodes[ else:
"program"][index - 1]["category"] self.node_last = None
else:
last_category = 'noad'
if index + 2 <= len(self.clip_nodes["program"]): if index + 2 <= len(self.clip_nodes['program']):
next_category = self.clip_nodes[ self.node_next = self.clip_nodes['program'][index + 1]
"program"][index + 1]["category"] else:
else: self.node_next = None
next_category = 'noad'
if node["category"] == 'advertisement':
self.ad = True
else:
self.ad = False
if last_category == 'advertisement':
self.ad_last = True
else:
self.ad_last = False
if next_category == 'advertisement':
self.ad_next = True
else:
self.ad_next = False
def set_filtergraph(self): def set_filtergraph(self):
self.filtergraph = build_filtergraph( self.filtergraph = build_filtergraph(
self.duration, self.seek, self.out, self.ad, self.ad_last, self.node, self.node_last, self.node_next, self.seek, self.probe)
self.ad_next, self.probe, messenger)
def check_for_next_playlist(self): def check_for_next_playlist(self):
if not self.next_playlist: if not self.next_playlist:
@ -177,11 +99,8 @@ class GetSourceFromPlaylist:
self.last_mod_time = 0.0 self.last_mod_time = 0.0
self.last_time = _playlist.start - 1 self.last_time = _playlist.start - 1
def eof_handling(self, message, fill, duration=None): def eof_handling(self, fill, duration=None):
self.seek = 0.0 self.seek = 0.0
self.ad = False
messenger.error(message)
if duration: if duration:
self.out = duration self.out = duration
@ -207,13 +126,13 @@ class GetSourceFromPlaylist:
self.last = False self.last = False
def peperation_task(self, index, node): def peperation_task(self, index):
# call functions in order to prepare source and filter # call functions in order to prepare source and filter
self.src = node["source"] self.probe.load(self.node.get('source'))
self.probe.load(self.src) self.src = self.probe.src
self.get_input() self.get_input()
self.get_category(index, node) self.last_and_next_node(index)
self.set_filtergraph() self.set_filtergraph()
self.check_for_next_playlist() self.check_for_next_playlist()
@ -222,31 +141,34 @@ class GetSourceFromPlaylist:
self.get_playlist() self.get_playlist()
if self.clip_nodes is None: if self.clip_nodes is None:
self.eof_handling( self.node = {'in': 0, 'out': 30, 'duration': 30}
'No valid playlist:\n{}'.format(self.json_file), True, 30) messenger.error('clip_nodes are empty')
self.eof_handling(True, 30)
yield self.src_cmd + self.filtergraph yield self.src_cmd + self.filtergraph
continue continue
self.begin = self.init_time self.begin = self.init_time
# loop through all clips in playlist and get correct clip in time # loop through all clips in playlist and get correct clip in time
for index, node in enumerate(self.clip_nodes["program"]): for index, self.node in enumerate(self.clip_nodes['program']):
self.get_clip_in_out(node) self.seek = get_float(self.node['in'], 0)
self.duration = get_float(self.node['duration'], 20)
self.out = get_float(self.node['out'], self.duration)
# first time we end up here # first time we end up here
if self.first and \ if self.first and \
self.last_time < self.begin + self.out - self.seek: self.last_time < self.begin + self.out - self.seek:
self.peperation_task(index, node) self.peperation_task(index)
self.first = False self.first = False
break break
elif self.last_time < self.begin: elif self.last_time < self.begin:
if index + 1 == len(self.clip_nodes["program"]): if index + 1 == len(self.clip_nodes['program']):
self.last = True self.last = True
else: else:
self.last = False self.last = False
self.peperation_task(index, node) self.peperation_task(index)
break break
self.begin += self.out - self.seek self.begin += self.out - self.seek
@ -261,10 +183,12 @@ class GetSourceFromPlaylist:
return None return None
elif self.begin == self.init_time: elif self.begin == self.init_time:
# no clip was played, generate dummy # no clip was played, generate dummy
self.eof_handling('Playlist is empty!', False) messenger.error('Playlist is empty!')
self.eof_handling(False)
else: else:
# playlist is not long enough, play filler # playlist is not long enough, play filler
self.eof_handling('Playlist is not long enough!', True) messenger.error('Playlist is not long enough!')
self.eof_handling(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

View File

@ -27,26 +27,32 @@ import smtplib
import socket import socket
import sys import sys
import tempfile import tempfile
import time
import urllib
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
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import formatdate from email.utils import formatdate
from glob import glob
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from shutil import which from shutil import which
from subprocess import STDOUT, CalledProcessError, check_output from subprocess import STDOUT, CalledProcessError, check_output
from threading import Thread from threading import Thread
from types import SimpleNamespace from types import SimpleNamespace
import requests
import yaml import yaml
# path to user define configs
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'config')
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# argument parsing # argument parsing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
stdin_parser = ArgumentParser( stdin_parser = ArgumentParser(description='python and ffmpeg based playout')
description='python and ffmpeg based playout',
epilog="don't use parameters if you want to use this settings from config")
stdin_parser.add_argument( stdin_parser.add_argument(
'-c', '--config', help='file path to ffplayout.conf' '-c', '--config', help='file path to ffplayout.conf'
@ -82,6 +88,19 @@ stdin_parser.add_argument(
help='set length in "hh:mm:ss", "none" for no length check' help='set length in "hh:mm:ss", "none" for no length check'
) )
# read dynamical new arguments
for arg_file in glob(os.path.join(CONFIG_PATH, 'argparse_*')):
with open(arg_file, 'r') as _file:
config = yaml.safe_load(_file)
short = config.pop('short') if config.get('short') else None
long = config.pop('long') if config.get('long') else None
stdin_parser.add_argument(
*filter(None, [short, long]),
**config
)
stdin_args = stdin_parser.parse_args() stdin_args = stdin_parser.parse_args()
@ -270,7 +289,7 @@ class CustomFormatter(logging.Formatter):
} }
def format_message(self, msg): def format_message(self, msg):
if '"' in msg and '[' in msg: if '"' in msg:
msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg) msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg)
elif '[decoder]' in msg: elif '[decoder]' in msg:
msg = re.sub(r'(\[decoder\])', self.reset + r'\1', msg) msg = re.sub(r'(\[decoder\])', self.reset + r'\1', msg)
@ -377,7 +396,7 @@ class Mailer:
message['To'] = _mail.recip message['To'] = _mail.recip
message['Subject'] = _mail.subject message['Subject'] = _mail.subject
message['Date'] = formatdate(localtime=True) message['Date'] = formatdate(localtime=True)
message.attach(MIMEText('{} {}'.format(self.time, msg), 'plain')) message.attach(MIMEText(f'{self.time} {msg}', 'plain'))
text = message.as_string() text = message.as_string()
try: try:
@ -463,7 +482,7 @@ def is_in_system(name):
Check whether name is on PATH and marked as executable Check whether name is on PATH and marked as executable
""" """
if which(name) is None: if which(name) is None:
messenger.error('{} is not found on system'.format(name)) messenger.error(f'{name} is not found on system')
sys.exit(1) sys.exit(1)
@ -483,7 +502,7 @@ def ffmpeg_libs():
info = check_output(cmd, stderr=STDOUT).decode('UTF-8') info = check_output(cmd, stderr=STDOUT).decode('UTF-8')
except CalledProcessError as err: except CalledProcessError as err:
messenger.error('ffmpeg - libs could not be readed!\n' messenger.error('ffmpeg - libs could not be readed!\n'
'Processing is not possible. Error:\n{}'.format(err)) f'Processing is not possible. Error:\n{err}')
sys.exit(1) sys.exit(1)
for line in info.split('\n'): for line in info.split('\n'):
@ -534,6 +553,8 @@ class MediaProbe:
self.video = [] self.video = []
if self.src and self.src.split('://')[0] in self.remote_source: if self.src and self.src.split('://')[0] in self.remote_source:
url = self.src.split('://')
self.src = f'{url[0]}://{urllib.parse.quote(url[1])}'
self.is_remote = True self.is_remote = True
else: else:
self.is_remote = False self.is_remote = False
@ -550,8 +571,7 @@ class MediaProbe:
try: try:
info = json.loads(check_output(cmd).decode('UTF-8')) info = json.loads(check_output(cmd).decode('UTF-8'))
except CalledProcessError as err: except CalledProcessError as err:
messenger.error('MediaProbe error in: "{}"\n {}'.format(self.src, messenger.error(f'MediaProbe error in: "{self.src}"\n{err}')
err))
self.audio.append(None) self.audio.append(None)
self.video.append(None) self.video.append(None)
@ -564,12 +584,12 @@ class MediaProbe:
self.audio.append(stream) self.audio.append(stream)
if stream['codec_type'] == 'video': if stream['codec_type'] == 'video':
if 'display_aspect_ratio' not in stream: if stream.get('display_aspect_ratio'):
stream['aspect'] = float(
stream['width']) / float(stream['height'])
else:
w, h = stream['display_aspect_ratio'].split(':') w, h = stream['display_aspect_ratio'].split(':')
stream['aspect'] = float(w) / float(h) stream['aspect'] = float(w) / float(h)
else:
stream['aspect'] = float(
stream['width']) / float(stream['height'])
a, b = stream['r_frame_rate'].split('/') a, b = stream['r_frame_rate'].split('/')
stream['fps'] = float(a) / float(b) stream['fps'] = float(a) / float(b)
@ -628,14 +648,11 @@ def ffmpeg_stderr_reader(std_errors, decoder):
try: try:
for line in std_errors: for line in std_errors:
if _log.ff_level == 'INFO': if _log.ff_level == 'INFO':
logger.info('{}{}'.format( logger.info(f'{prefix}{line.decode("utf-8").rstrip()}')
prefix, line.decode("utf-8").rstrip()))
elif _log.ff_level == 'WARNING': elif _log.ff_level == 'WARNING':
logger.warning('{}{}'.format( logger.warning(f'{prefix}{line.decode("utf-8").rstrip()}')
prefix, line.decode("utf-8").rstrip()))
else: else:
logger.error('{}{}'.format( logger.error(f'{prefix}{line.decode("utf-8").rstrip()}')
prefix, line.decode("utf-8").rstrip()))
except ValueError: except ValueError:
pass pass
@ -648,32 +665,28 @@ def get_date(seek_day):
""" """
d = date.today() d = date.today()
if seek_day and get_time('full_sec') < _playlist.start: if seek_day and get_time('full_sec') < _playlist.start:
yesterday = d - timedelta(1) return (d - timedelta(1)).strftime('%Y-%m-%d')
return yesterday.strftime('%Y-%m-%d')
else: else:
if _playlist.start == 0 and \
4 > 86400.0 - get_time('full_sec') > 0:
return (d + timedelta(1)).strftime('%Y-%m-%d')
return d.strftime('%Y-%m-%d') return d.strftime('%Y-%m-%d')
def is_float(value): def get_float(value, default=False):
""" """
test if value is float test if value is float
""" """
try: try:
float(value) return float(value)
return True
except (ValueError, TypeError): except (ValueError, TypeError):
return False return default
def is_int(value): def is_advertisement(node):
""" if node and node.get('category') == 'advertisement':
test if value is int
"""
try:
int(value)
return True return True
except ValueError:
return False
def valid_json(file): def valid_json(file):
@ -684,7 +697,7 @@ def valid_json(file):
json_object = json.load(file) json_object = json.load(file)
return json_object return json_object
except ValueError: except ValueError:
messenger.error("Playlist {} is not JSON conform".format(file)) messenger.error(f'Playlist {file} is not JSON conform')
return None return None
@ -699,8 +712,8 @@ def check_sync(delta):
if _general.stop and abs(delta) > _general.threshold: if _general.stop and abs(delta) > _general.threshold:
messenger.error( messenger.error(
'Sync tolerance value exceeded with {0:.2f} seconds,\n' f'Sync tolerance value exceeded with {delta:.2f} seconds,\n'
'program terminated!'.format(delta)) 'program terminated!')
terminate_processes() terminate_processes()
sys.exit(1) sys.exit(1)
@ -712,11 +725,9 @@ def check_length(total_play_time):
if _playlist.length and total_play_time < _playlist.length - 5 \ if _playlist.length and total_play_time < _playlist.length - 5 \
and not stdin_args.loop: and not stdin_args.loop:
messenger.error( messenger.error(
'Playlist ({}) is not long enough!\n' f'Playlist ({get_date(True)}) is not long enough!\n'
'Total play time is: {}, target length is: {}'.format( f'Total play time is: {timedelta(seconds=total_play_time)}, '
get_date(True), f'target length is: {timedelta(seconds=_playlist.length)}'
timedelta(seconds=total_play_time),
timedelta(seconds=_playlist.length))
) )
@ -735,29 +746,35 @@ def validate_thread(clip_nodes):
source = node["source"] source = node["source"]
probe.load(source) probe.load(source)
missing = [] missing = []
_in = get_float(node.get('in'), 0)
_out = get_float(node.get('out'), 0)
duration = get_float(node.get('duration'), 0)
if probe.is_remote: if probe.is_remote:
if not probe.video[0]: if not probe.video[0]:
missing.append('Stream not exist: "{}"'.format(source)) missing.append(f'Remote file not exist: "{source}"')
elif not os.path.isfile(source): elif not os.path.isfile(source):
missing.append('File not exist: "{}"'.format(source)) missing.append(f'File not exist: "{source}"')
if is_float(node["in"]) and is_float(node["out"]): if not node.get('in') == 0 and not _in:
counter += node["out"] - node["in"] missing.append(f'No in Value in: "{node}"')
else:
missing.append('Missing Value in: "{}"'.format(node))
if not is_float(node["duration"]): if not node.get('out') and not _out:
missing.append('No duration Value!') missing.append(f'No out Value in: "{node}"')
if not node.get('duration') and not duration:
missing.append(f'No duration Value in: "{node}"')
counter += _out - _in
line = '\n'.join(missing) line = '\n'.join(missing)
if line: if line:
error += line + '\nIn line: {}\n\n'.format(node) error += line + f'\nIn line: {node}\n\n'
if error: if error:
messenger.error( messenger.error(
'Validation error, check JSON playlist, ' 'Validation error, check JSON playlist, '
'values are missing:\n{}'.format(error) f'values are missing:\n{error}'
) )
check_length(counter) check_length(counter)
@ -790,9 +807,8 @@ def set_length(duration, seek, out):
def loop_input(source, src_duration, target_duration): def loop_input(source, src_duration, target_duration):
# loop filles n times # loop filles n times
loop_count = math.ceil(target_duration / src_duration) loop_count = math.ceil(target_duration / src_duration)
messenger.info( messenger.info(f'Loop "{source}" {loop_count} times, '
'Loop "{0}" {1} times, total duration: {2:.2f}'.format( f'total duration: {target_duration:.2f}')
source, loop_count, target_duration))
return ['-stream_loop', str(loop_count), return ['-stream_loop', str(loop_count),
'-i', source, '-t', str(target_duration)] '-i', source, '-t', str(target_duration)]
@ -806,11 +822,9 @@ def gen_dummy(duration):
# noise = 'noise=alls=50:allf=t+u,hue=s=0' # noise = 'noise=alls=50:allf=t+u,hue=s=0'
return [ return [
'-f', 'lavfi', '-i', '-f', 'lavfi', '-i',
'color=c={}:s={}x{}:d={}:r={},format=pix_fmts=yuv420p'.format( f'color=c={color}:s={_pre.w}x{_pre.h}:d={duration}:r={_pre.fps},'
color, _pre.w, _pre.h, duration, _pre.fps 'format=pix_fmts=yuv420p',
), '-f', 'lavfi', '-i', f'anoisesrc=d={duration}:c=pink:r=48000:a=0.05'
'-f', 'lavfi', '-i', 'anoisesrc=d={}:c=pink:r=48000:a=0.05'.format(
duration)
] ]
@ -822,12 +836,12 @@ def gen_filler(duration):
probe.load(_storage.filler) probe.load(_storage.filler)
if probe.format: if probe.format:
if 'duration' in probe.format: if probe.format.get('duration'):
filler_duration = float(probe.format['duration']) filler_duration = float(probe.format['duration'])
if filler_duration > duration: if filler_duration > duration:
# cut filler # cut filler
messenger.info( messenger.info(
'Generate filler with {0:.2f} seconds'.format(duration)) f'Generate filler with {duration:.2f} seconds')
return probe, ['-i', _storage.filler] + set_length( return probe, ['-i', _storage.filler] + set_length(
filler_duration, 0, duration) filler_duration, 0, duration)
else: else:
@ -854,13 +868,13 @@ def src_or_dummy(probe, src, dur, seek, out):
if probe.is_remote and probe.video[0]: if probe.is_remote and probe.video[0]:
if seek > 0.0: if seek > 0.0:
messenger.warning( messenger.warning(
'Seek in live source "{}" not supported!'.format(src)) f'Seek in remote source "{src}" not supported!')
return ['-i', src] + set_length(86400.0, seek, out) return ['-i', src] + set_length(86400.0, seek, out)
elif src and os.path.isfile(src): elif src and os.path.isfile(src):
if out > dur: if out > dur:
if seek > 0.0: if seek > 0.0:
messenger.warning( messenger.warning(
'Seek in looped source "{}" not supported!'.format(src)) f'Seek in looped source "{src}" not supported!')
return ['-i', src] + set_length(dur, seek, out - seek) return ['-i', src] + set_length(dur, seek, out - seek)
else: else:
# FIXME: when list starts with looped clip, # FIXME: when list starts with looped clip,
@ -869,7 +883,7 @@ def src_or_dummy(probe, src, dur, seek, out):
else: else:
return seek_in(seek) + ['-i', src] + set_length(dur, seek, out) return seek_in(seek) + ['-i', src] + set_length(dur, seek, out)
else: else:
messenger.error('Clip/URL not exist:\n{}'.format(src)) messenger.error(f'Clip/URL not exist:\n{src}')
return gen_dummy(out - seek) return gen_dummy(out - seek)
@ -938,34 +952,76 @@ def handle_list_end(probe, new_length, src, begin, dur, seek, out):
if new_out > dur: if new_out > dur:
new_out = dur new_out = dur
else: else:
messenger.info( messenger.info(f'We are over time, new length is: {new_length:.2f}')
'We are over time, new length is: {0:.2f}'.format(new_length))
missing_secs = abs(new_length - (dur - seek)) missing_secs = abs(new_length - (dur - seek))
if dur > new_length > 1.5 and dur - seek >= new_length: if dur > new_length > 1.5 and dur - seek >= new_length:
src_cmd = src_or_dummy(probe, src, dur, seek, new_out) src_cmd = src_or_dummy(probe, src, dur, seek, new_out)
elif dur > new_length > 0.0: elif dur > new_length > 0.0:
messenger.info( messenger.info(f'Last clip less then 1.5 second long, skip:\n{src}')
'Last clip less then 1.5 second long, skip:\n{}'.format(src))
src_cmd = None src_cmd = None
if missing_secs > 2: if missing_secs > 2:
new_playlist = False new_playlist = False
messenger.error( messenger.error(
'Reach playlist end,\n{0:.2f} seconds needed.'.format( f'Reach playlist end,\n{missing_secs:.2f} seconds needed.')
missing_secs))
else: else:
new_out = out new_out = out
new_playlist = False new_playlist = False
src_cmd = src_or_dummy(probe, src, dur, seek, out) src_cmd = src_or_dummy(probe, src, dur, seek, out)
messenger.error( messenger.error(
'Playlist is not long enough:' f'Playlist is not long enough:\n{missing_secs:.2f} seconds needed.'
'\n{0:.2f} seconds needed.'.format(missing_secs)) )
return src_cmd, seek, new_out, new_playlist return src_cmd, seek, new_out, new_playlist
def read_playlist(list_date, modification_time, clip_nodes):
"""
read playlists from remote url or local file
and give his nodes and modification time back
"""
if stdin_args.playlist:
json_file = stdin_args.playlist
else:
year, month, day = list_date.split('-')
json_file = os.path.join(_playlist.path, year, month,
f'{list_date}.json')
if '://' in json_file:
json_file = json_file.replace('\\', '/')
try:
result = requests.get(json_file, timeout=1, verify=False)
b_time = result.headers['last-modified']
temp_time = time.strptime(b_time, "%a, %d %b %Y %H:%M:%S %Z")
mod_time = time.mktime(temp_time)
if mod_time > modification_time:
if isinstance(result.json(), dict):
clip_nodes = result.json()
modification_time = mod_time
messenger.info('Open: ' + json_file)
validate_thread(clip_nodes)
except (requests.exceptions.ConnectionError, socket.timeout):
messenger.error(f'No valid playlist from url: {json_file}')
elif os.path.isfile(json_file):
# check last modification from playlist
mod_time = os.path.getmtime(json_file)
if mod_time > modification_time:
with open(json_file, 'r', encoding='utf-8') as f:
clip_nodes = valid_json(f)
modification_time = mod_time
messenger.info('Open: ' + json_file)
validate_thread(clip_nodes)
return clip_nodes, modification_time
def timed_source(probe, src, begin, dur, seek, out, first, last): def timed_source(probe, src, begin, dur, seek, out, first, last):
""" """
prepare input clip prepare input clip
@ -981,14 +1037,14 @@ def timed_source(probe, src, begin, dur, seek, out, first, last):
return src_or_dummy(probe, src, dur, _seek, _out), \ return src_or_dummy(probe, src, dur, _seek, _out), \
_seek, _out, new_list _seek, _out, new_list
else: else:
messenger.warning('Clip less then a second, skip:\n{}'.format(src)) messenger.warning(f'Clip less then a second, skip:\n{src}')
return None, 0, 0, True return None, 0, 0, True
else: else:
if not stdin_args.loop and _playlist.length: if not stdin_args.loop and _playlist.length:
check_sync(current_delta) check_sync(current_delta)
messenger.debug('current_delta: {:f}'.format(current_delta)) messenger.debug(f'current_delta: {current_delta:f}')
messenger.debug('total_delta: {:f}'.format(total_delta)) messenger.debug(f'total_delta: {total_delta:f}')
if (total_delta > out - seek and not last) \ if (total_delta > out - seek and not last) \
or stdin_args.loop or not _playlist.length: or stdin_args.loop or not _playlist.length:
@ -996,8 +1052,7 @@ def timed_source(probe, src, begin, dur, seek, out, first, last):
return src_or_dummy(probe, src, dur, seek, out), seek, out, False return src_or_dummy(probe, src, dur, seek, out), seek, out, False
elif total_delta <= 0: elif total_delta <= 0:
messenger.info( messenger.info(f'Start time is over playtime, skip clip:\n{src}')
'Start time is over playtime, skip clip:\n{}'.format(src))
return None, 0, 0, True return None, 0, 0, True
elif total_delta < out - seek or last: elif total_delta < out - seek or last:

View File

@ -1,4 +1,5 @@
colorama colorama
pyyaml pyyaml
requests
supervisor supervisor
watchdog watchdog

View File

@ -1,4 +1,9 @@
certifi==2020.12.5
chardet==4.0.0
colorama==0.4.4 colorama==0.4.4
idna==2.10
PyYAML==5.3.1 PyYAML==5.3.1
requests==2.25.1
supervisor==4.2.1 supervisor==4.2.1
urllib3==1.26.3
watchdog==1.0.2 watchdog==1.0.2