diff --git a/ffplayout.py b/ffplayout.py index 7976c738..fb03b124 100755 --- a/ffplayout.py +++ b/ffplayout.py @@ -41,7 +41,7 @@ def main(): """ if stdin_args.mode: - output = locate('ffplayout.output.{}.output'.format(stdin_args.mode)) + output = locate(f'ffplayout.output.{stdin_args.mode}.output') output() else: @@ -54,7 +54,7 @@ def main(): mode = os.path.splitext(output)[0] if mode == _playout.mode: - output = locate('ffplayout.output.{}.output'.format(mode)) + output = locate(f'ffplayout.output.{mode}.output') output() diff --git a/ffplayout/config/README.md b/ffplayout/config/README.md new file mode 100644 index 00000000..a78f5971 --- /dev/null +++ b/ffplayout/config/README.md @@ -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!** diff --git a/ffplayout/config/argparse_volume.yml b/ffplayout/config/argparse_volume.yml new file mode 100644 index 00000000..a015200e --- /dev/null +++ b/ffplayout/config/argparse_volume.yml @@ -0,0 +1,3 @@ +short: -v +long: --volume +help: set audio volume diff --git a/ffplayout/filters/README.md b/ffplayout/filters/README.md index dd7ae8fb..b83c410e 100644 --- a/ffplayout/filters/README.md +++ b/ffplayout/filters/README.md @@ -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):` 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. diff --git a/ffplayout/filters/a_volume.py b/ffplayout/filters/a_volume.py new file mode 100644 index 00000000..ca495dfb --- /dev/null +++ b/ffplayout/filters/a_volume.py @@ -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}' diff --git a/ffplayout/filters/default.py b/ffplayout/filters/default.py index 3dc97ad6..a7d088a5 100644 --- a/ffplayout/filters/default.py +++ b/ffplayout/filters/default.py @@ -23,7 +23,8 @@ import re from glob import glob 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, @@ -37,7 +38,7 @@ def text_filter(): if _text.add_text and _text.over_pre: if _text.fontfile and os.path.isfile(_text.fontfile): - font = ":fontfile='{}'".format(_text.fontfile) + font = f":fontfile='{_text.fontfile}'" filter_chain = [ "null,zmq=b=tcp\\\\://'{}',drawtext=text=''{}".format( _text.address.replace(':', '\\:'), font)] @@ -70,12 +71,10 @@ def pad_filter(probe): _pre.aspect, abs_tol=0.03): if probe.video[0]['aspect'] < _pre.aspect: filter_chain.append( - 'pad=ih*{}/{}/sar:ih:(ow-iw)/2:(oh-ih)/2'.format(_pre.w, - _pre.h)) + f'pad=ih*{_pre.w}/{_pre.h}/sar:ih:(ow-iw)/2:(oh-ih)/2') elif probe.video[0]['aspect'] > _pre.aspect: filter_chain.append( - 'pad=iw:iw*{}/{}/sar:(ow-iw)/2:(oh-ih)/2'.format(_pre.h, - _pre.w)) + f'pad=iw:iw*{_pre.h}/{_pre.w}/sar:(ow-iw)/2:(oh-ih)/2') return filter_chain @@ -87,7 +86,7 @@ def fps_filter(probe): filter_chain = [] if probe.video[0]['fps'] != _pre.fps: - filter_chain.append('fps={}'.format(_pre.fps)) + filter_chain.append(f'fps={_pre.fps}') return filter_chain @@ -101,11 +100,11 @@ def scale_filter(probe): if int(probe.video[0]['width']) != _pre.w or \ 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'], _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 @@ -117,11 +116,10 @@ def fade_filter(duration, seek, out, track=''): filter_chain = [] 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: - filter_chain.append('{}fade=out:st={}:d=1.0'.format(track, - out - seek - 1.0)) + filter_chain.append(f'{track}fade=out:st={out - seek - 1.0}:d=1.0') return filter_chain @@ -139,35 +137,32 @@ def overlay_filter(duration, ad, ad_last, ad_next): logo_chain = [] if _pre.logo_scale and \ re.match(r'\d+:-?\d+', _pre.logo_scale): - scale_filter = 'scale={},'.format(_pre.logo_scale) - logo_extras = 'format=rgba,{}colorchannelmixer=aa={}'.format( - scale_filter, _pre.logo_opacity) + scale_filter = f'scale={_pre.logo_scale},' + logo_extras = (f'format=rgba,{scale_filter}' + f'colorchannelmixer=aa={_pre.logo_opacity}') loop = 'loop=loop=-1:size=1:start=0' - logo_chain.append( - 'movie={},{},{}'.format(_pre.logo, loop, logo_extras)) + logo_chain.append(f'movie={_pre.logo},{loop},{logo_extras}') if ad_last: logo_chain.append('fade=in:st=0:d=1.0:alpha=1') if ad_next: - logo_chain.append('fade=out:st={}:d=1.0:alpha=1'.format( - duration - 1)) + logo_chain.append(f'fade=out:st={duration - 1}:d=1.0:alpha=1') - logo_filter = '{}[l];[v][l]{}:shortest=1'.format( - ','.join(logo_chain), _pre.logo_filter) + logo_filter = (f'{",".join(logo_chain)}[l];[v][l]' + f'{_pre.logo_filter}:shortest=1') return logo_filter -def add_audio(probe, duration, msg): +def add_audio(probe, duration): """ when clip has no audio we generate an audio line """ line = [] if not probe.audio: - msg.warning('Clip "{}" has no audio!'.format(probe.src)) - line = [ - 'aevalsrc=0:channel_layout=2:duration={}:sample_rate={}'.format( - duration, 48000)] + messenger.warning(f'Clip "{probe.src}" has no audio!') + line = [(f'aevalsrc=0:channel_layout=2:duration={duration}:' + f'sample_rate={48000}')] return line @@ -179,8 +174,8 @@ def add_loudnorm(probe): loud_filter = [] if probe.audio and _pre.add_loudnorm: - loud_filter = [('loudnorm=I={}:TP={}:LRA={}').format( - _pre.loud_i, _pre.loud_tp, _pre.loud_lra)] + loud_filter = [ + f'loudnorm=I={_pre.loud_i}:TP={_pre.loud_tp}:LRA={_pre.loud_lra}'] return loud_filter @@ -193,7 +188,7 @@ def extend_audio(probe, duration): if probe.audio and 'duration' in probe.audio[0] and \ 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 @@ -203,12 +198,11 @@ def extend_video(probe, duration, target_duration): check video duration, is it shorter then clip duration - pad it """ pad_filter = [] + vid_dur = probe.video[0].get('duration') - if 'duration' in probe.video[0] and \ - target_duration < duration > float( - probe.video[0]['duration']) + 0.1: - pad_filter.append('tpad=stop_mode=add:stop_duration={}'.format( - duration - float(probe.video[0]['duration']))) + if vid_dur and target_duration < duration > float(vid_dur) + 0.1: + pad_filter.append( + f'tpad=stop_mode=add:stop_duration={duration - float(vid_dur)}') return pad_filter @@ -217,46 +211,45 @@ def realtime_filter(duration, track=''): speed_filter = '' if _pre.realtime: - speed_filter = ',{}realtime=speed=1'.format(track) + speed_filter = f',{track}realtime=speed=1' if _global.time_delta < 0: speed = duration / (duration + _global.time_delta) if speed < 1.1: - speed_filter = ',{}realtime=speed={}'.format(track, speed) + speed_filter = f',{track}realtime=speed={speed}' return speed_filter def split_filter(filter_type): map_node = [] - filter_prefix = '' + prefix = '' _filter = '' if filter_type == 'a': - filter_prefix = 'a' + prefix = 'a' if _pre.output_count > 1: 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, - ''.join(map_node)) + _filter = f',{prefix}split={_pre.output_count}{"".join(map_node)}' else: - _filter = '[{}out1]'.format(filter_type) + _filter = f'[{filter_type}out1]' return _filter -def custom_filter(probe, type): +def custom_filter(probe, type, node): filter_dir = os.path.dirname(os.path.abspath(__file__)) filters = [] for filter in glob(os.path.join(filter_dir, f'{type}_*')): filter = os.path.splitext(os.path.basename(filter))[0] filter_func = locate(f'ffplayout.filters.{filter}.filter') - link = filter_func(probe) + link = filter_func(probe, node) if link is not None: filters.append(link) @@ -264,10 +257,17 @@ def custom_filter(probe, type): 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 """ + + duration = get_float(node['duration'], 20) + out = get_float(node['out'], duration) + ad = is_advertisement(node) + ad_last = is_advertisement(node_last) + ad_next = is_advertisement(node_next) + video_chain = [] audio_chain = [] @@ -275,7 +275,7 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg): seek = 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 += deinterlace_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 += fade_filter(duration, seek, out) - audio_chain += add_audio(probe, out - seek, msg) + audio_chain += add_audio(probe, out - seek) 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 += 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') if video_chain: - video_filter = '{}[v]'.format(','.join(video_chain)) + video_filter = f'{",".join(video_chain)}[v]' else: 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') video_map = ['-map', '[vout1]'] video_filter = [ - '-filter_complex', '[0:v]{};{}{}{}'.format( - video_filter, logo_filter, v_speed, v_split)] + '-filter_complex', + f'[0:v]{video_filter};{logo_filter}{v_speed}{v_split}'] a_speed = realtime_filter(out - seek, 'a') a_split = split_filter('a') audio_map = ['-map', '[aout1]'] audio_filter = [ - '-filter_complex', '{}{}{}'.format(','.join(audio_chain), - a_speed, a_split)] + '-filter_complex', f'{",".join(audio_chain)}{a_speed}{a_split}'] if probe.video[0]: return video_filter + audio_filter + video_map + audio_map diff --git a/ffplayout/filters/v_drawtext.py b/ffplayout/filters/v_drawtext.py index 76dcb222..4ea0d791 100644 --- a/ffplayout/filters/v_drawtext.py +++ b/ffplayout/filters/v_drawtext.py @@ -4,7 +4,7 @@ import re from ffplayout.utils import _text -def filter(probe): +def filter(probe, node=None): """ extract title from file name and overlay it """ diff --git a/ffplayout/folder.py b/ffplayout/folder.py index 82560182..05cdfcfa 100644 --- a/ffplayout/folder.py +++ b/ffplayout/folder.py @@ -52,7 +52,7 @@ class MediaStore: def fill(self): for ext in _storage.extensions: self.store.extend( - glob.glob(os.path.join(self.folder, '**', '*{}'.format(ext)), + glob.glob(os.path.join(self.folder, '**', f'*{ext}'), recursive=True)) if _storage.shuffle: @@ -84,7 +84,7 @@ class MediaWatcher: def __init__(self, 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( patterns=self.extensions) @@ -107,14 +107,14 @@ class MediaWatcher: 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): self._media.remove(event.src_path) self._media.add(event.dest_path) - messenger.info('Move file from "{}" to "{}"'.format(event.src_path, - event.dest_path)) + messenger.info( + f'Move file from "{event.src_path}" to "{event.dest_path}"') if _current.clip == event.src_path: _ff.decoder.terminate() @@ -122,8 +122,7 @@ class MediaWatcher: def on_deleted(self, event): self._media.remove(event.src_path) - messenger.info( - 'Remove file from media list: "{}"'.format(event.src_path)) + messenger.info(f'Remove file from media list: "{event.src_path}"') if _current.clip == event.src_path: _ff.decoder.terminate() diff --git a/ffplayout/output/desktop.py b/ffplayout/output/desktop.py index 0a2a393a..d43100a8 100644 --- a/ffplayout/output/desktop.py +++ b/ffplayout/output/desktop.py @@ -21,16 +21,15 @@ def output(): ff_pre_settings = [ '-pix_fmt', 'yuv420p', '-r', str(_pre.fps), '-c:v', 'mpeg2video', '-intra', - '-b:v', '{}k'.format(_pre.v_bitrate), - '-minrate', '{}k'.format(_pre.v_bitrate), - '-maxrate', '{}k'.format(_pre.v_bitrate), - '-bufsize', '{}k'.format(_pre.v_bufsize) + '-b:v', f'{_pre.v_bitrate}k', + '-minrate', f'{_pre.v_bitrate}k', + '-maxrate', f'{_pre.v_bitrate}k', + '-bufsize', f'{_pre.v_bufsize}k' ] + pre_audio_codec() + ['-f', 'mpegts', '-'] if _text.add_text and not _text.over_pre: - messenger.info('Using drawtext node, listening on address: {}'.format( - _text.address - )) + messenger.info( + f'Using drawtext node, listening on address: {_text.address}') overlay = [ '-vf', "null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format( @@ -38,9 +37,13 @@ def output(): ] try: - _ff.encoder = Popen([ + enc_cmd = [ '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, args=(_ff.encoder.stderr, False)) @@ -58,20 +61,21 @@ def output(): try: for src_cmd in get_source.next(): - messenger.debug('src_cmd: "{}"'.format(src_cmd)) if src_cmd[0] == '-i': current_file = src_cmd[1] else: current_file = src_cmd[3] _current.clip = current_file - messenger.info('Play: "{}"'.format(current_file)) + messenger.info(f'Play: {current_file}') - with Popen([ - 'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner', - '-nostats'] + src_cmd + ff_pre_settings, - stdout=PIPE, stderr=PIPE) as _ff.decoder: + dec_cmd = ['ffmpeg', '-v', _log.ff_level.lower(), + '-hide_banner', '-nostats' + ] + src_cmd + ff_pre_settings + 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, args=(_ff.decoder.stderr, True)) dec_err_thread.daemon = True diff --git a/ffplayout/output/stream.py b/ffplayout/output/stream.py index 49bf9de3..d1d964ac 100644 --- a/ffplayout/output/stream.py +++ b/ffplayout/output/stream.py @@ -23,16 +23,15 @@ def output(): ff_pre_settings = [ '-pix_fmt', 'yuv420p', '-r', str(_pre.fps), '-c:v', 'mpeg2video', '-intra', - '-b:v', '{}k'.format(_pre.v_bitrate), - '-minrate', '{}k'.format(_pre.v_bitrate), - '-maxrate', '{}k'.format(_pre.v_bitrate), - '-bufsize', '{}k'.format(_pre.v_bufsize) + '-b:v', f'{_pre.v_bitrate}k', + '-minrate', f'{_pre.v_bitrate}k', + '-maxrate', f'{_pre.v_bitrate}k', + '-bufsize', f'{_pre.v_bufsize}k' ] + pre_audio_codec() + ['-f', 'mpegts', '-'] if _text.add_text and not _text.over_pre: - messenger.info('Using drawtext node, listening on address: {}'.format( - _text.address - )) + messenger.info( + f'Using drawtext node, listening on address: {_text.address}') overlay = [ '-vf', "null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format( @@ -40,15 +39,18 @@ def output(): ] try: - _ff.encoder = Popen([ + enc_cmd = [ 'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner', '-nostats', '-re', '-thread_queue_size', '256', '-i', 'pipe:0' ] + overlay + [ '-metadata', 'service_name=' + _playout.name, '-metadata', 'service_provider=' + _playout.provider, - '-metadata', 'year={}'.format(year) - ] + _playout.ffmpeg_param + _playout.stream_output, - stdin=PIPE, stderr=PIPE) + '-metadata', f'year={year}' + ] + _playout.ffmpeg_param + _playout.stream_output + + 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, args=(_ff.encoder.stderr, False)) @@ -66,20 +68,21 @@ def output(): try: for src_cmd in get_source.next(): - messenger.debug('src_cmd: "{}"'.format(src_cmd)) if src_cmd[0] == '-i': current_file = src_cmd[1] else: current_file = src_cmd[3] _current.clip = current_file - messenger.info('Play: "{}"'.format(current_file)) + messenger.info(f'Play: {current_file}') - with Popen([ - 'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner', - '-nostats'] + src_cmd + ff_pre_settings, - stdout=PIPE, stderr=PIPE) as _ff.decoder: + dec_cmd = ['ffmpeg', '-v', _log.ff_level.lower(), + '-hide_banner', '-nostats' + ] + src_cmd + ff_pre_settings + 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, args=(_ff.decoder.stderr, True)) dec_err_thread.daemon = True diff --git a/ffplayout/playlist.py b/ffplayout/playlist.py index ccd03ca9..9982363a 100644 --- a/ffplayout/playlist.py +++ b/ffplayout/playlist.py @@ -25,7 +25,7 @@ from urllib import request from .filters.default import build_filtergraph 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, stdin_args, timed_source, valid_json, validate_thread) @@ -58,6 +58,9 @@ class GetSourceFromPlaylist: self.last = False self.list_date = get_date(True) + self.node = None + self.node_last = None + self.node_next = None self.src = None self.begin = 0 self.seek = 0 @@ -107,61 +110,26 @@ class GetSourceFromPlaylist: 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): self.src_cmd, self.seek, self.out, self.next_playlist = timed_source( self.probe, self.src, self.begin, self.duration, self.seek, self.out, self.first, self.last ) - def get_category(self, index, node): - if 'category' in node: - if index - 1 >= 0: - last_category = self.clip_nodes[ - "program"][index - 1]["category"] - else: - last_category = 'noad' + def last_and_next_node(self, index): + if index - 1 >= 0: + self.node_last = self.clip_nodes['program'][index - 1] + else: + self.node_last = None - if index + 2 <= len(self.clip_nodes["program"]): - next_category = self.clip_nodes[ - "program"][index + 1]["category"] - else: - 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 + if index + 2 <= len(self.clip_nodes['program']): + self.node_next = self.clip_nodes['program'][index + 1] + else: + self.node_next = None def set_filtergraph(self): self.filtergraph = build_filtergraph( - self.duration, self.seek, self.out, self.ad, self.ad_last, - self.ad_next, self.probe, messenger) + self.node, self.node_last, self.node_next, self.seek, self.probe) def check_for_next_playlist(self): if not self.next_playlist: @@ -207,13 +175,13 @@ class GetSourceFromPlaylist: self.last = False - def peperation_task(self, index, node): + def peperation_task(self, index): # call functions in order to prepare source and filter - self.src = node["source"] + self.src = self.node['source'] self.probe.load(self.src) self.get_input() - self.get_category(index, node) + self.last_and_next_node(index) self.set_filtergraph() self.check_for_next_playlist() @@ -223,30 +191,32 @@ class GetSourceFromPlaylist: if self.clip_nodes is None: self.eof_handling( - 'No valid playlist:\n{}'.format(self.json_file), True, 30) + f'No valid playlist:\n{self.json_file}', True, 30) yield self.src_cmd + self.filtergraph continue self.begin = self.init_time # loop through all clips in playlist and get correct clip in time - for index, node in enumerate(self.clip_nodes["program"]): - self.get_clip_in_out(node) + for index, self.node in enumerate(self.clip_nodes['program']): + 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 if self.first and \ self.last_time < self.begin + self.out - self.seek: - self.peperation_task(index, node) + self.peperation_task(index) self.first = False break 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 else: self.last = False - self.peperation_task(index, node) + self.peperation_task(index) break self.begin += self.out - self.seek diff --git a/ffplayout/utils.py b/ffplayout/utils.py index ee44f23e..aeba47a5 100644 --- a/ffplayout/utils.py +++ b/ffplayout/utils.py @@ -32,6 +32,7 @@ from datetime import date, datetime, timedelta from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formatdate +from glob import glob from logging.handlers import TimedRotatingFileHandler from shutil import which from subprocess import STDOUT, CalledProcessError, check_output @@ -40,13 +41,15 @@ from types import SimpleNamespace import yaml +# path to user define configs +CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'config') + # ------------------------------------------------------------------------------ # argument parsing # ------------------------------------------------------------------------------ -stdin_parser = ArgumentParser( - description='python and ffmpeg based playout', - epilog="don't use parameters if you want to use this settings from config") +stdin_parser = ArgumentParser(description='python and ffmpeg based playout') stdin_parser.add_argument( '-c', '--config', help='file path to ffplayout.conf' @@ -82,6 +85,19 @@ stdin_parser.add_argument( 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() @@ -270,7 +286,7 @@ class CustomFormatter(logging.Formatter): } def format_message(self, msg): - if '"' in msg and '[' in msg: + if '"' in msg: msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg) elif '[decoder]' in msg: msg = re.sub(r'(\[decoder\])', self.reset + r'\1', msg) @@ -377,7 +393,7 @@ class Mailer: message['To'] = _mail.recip message['Subject'] = _mail.subject 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() try: @@ -463,7 +479,7 @@ def is_in_system(name): Check whether name is on PATH and marked as executable """ 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) @@ -483,7 +499,7 @@ def ffmpeg_libs(): info = check_output(cmd, stderr=STDOUT).decode('UTF-8') except CalledProcessError as err: messenger.error('ffmpeg - libs could not be readed!\n' - 'Processing is not possible. Error:\n{}'.format(err)) + f'Processing is not possible. Error:\n{err}') sys.exit(1) for line in info.split('\n'): @@ -550,8 +566,7 @@ class MediaProbe: try: info = json.loads(check_output(cmd).decode('UTF-8')) except CalledProcessError as err: - messenger.error('MediaProbe error in: "{}"\n {}'.format(self.src, - err)) + messenger.error(f'MediaProbe error in: "{self.src}"\n{err}') self.audio.append(None) self.video.append(None) @@ -564,12 +579,12 @@ class MediaProbe: self.audio.append(stream) if stream['codec_type'] == 'video': - if 'display_aspect_ratio' not in stream: - stream['aspect'] = float( - stream['width']) / float(stream['height']) - else: + if stream.get('display_aspect_ratio'): w, h = stream['display_aspect_ratio'].split(':') stream['aspect'] = float(w) / float(h) + else: + stream['aspect'] = float( + stream['width']) / float(stream['height']) a, b = stream['r_frame_rate'].split('/') stream['fps'] = float(a) / float(b) @@ -628,14 +643,11 @@ def ffmpeg_stderr_reader(std_errors, decoder): try: for line in std_errors: if _log.ff_level == 'INFO': - logger.info('{}{}'.format( - prefix, line.decode("utf-8").rstrip())) + logger.info(f'{prefix}{line.decode("utf-8").rstrip()}') elif _log.ff_level == 'WARNING': - logger.warning('{}{}'.format( - prefix, line.decode("utf-8").rstrip())) + logger.warning(f'{prefix}{line.decode("utf-8").rstrip()}') else: - logger.error('{}{}'.format( - prefix, line.decode("utf-8").rstrip())) + logger.error(f'{prefix}{line.decode("utf-8").rstrip()}') except ValueError: pass @@ -648,32 +660,28 @@ def get_date(seek_day): """ d = date.today() if seek_day and get_time('full_sec') < _playlist.start: - yesterday = d - timedelta(1) - return yesterday.strftime('%Y-%m-%d') + return (d - timedelta(1)).strftime('%Y-%m-%d') 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') -def is_float(value): +def get_float(value, default=False): """ test if value is float """ try: - float(value) - return True + return float(value) except (ValueError, TypeError): - return False + return default -def is_int(value): - """ - test if value is int - """ - try: - int(value) +def is_advertisement(node): + if node and node.get('category') == 'advertisement': return True - except ValueError: - return False def valid_json(file): @@ -684,7 +692,7 @@ def valid_json(file): json_object = json.load(file) return json_object except ValueError: - messenger.error("Playlist {} is not JSON conform".format(file)) + messenger.error(f'Playlist {file} is not JSON conform') return None @@ -699,8 +707,8 @@ def check_sync(delta): if _general.stop and abs(delta) > _general.threshold: messenger.error( - 'Sync tolerance value exceeded with {0:.2f} seconds,\n' - 'program terminated!'.format(delta)) + f'Sync tolerance value exceeded with {delta:.2f} seconds,\n' + 'program terminated!') terminate_processes() sys.exit(1) @@ -712,11 +720,9 @@ def check_length(total_play_time): if _playlist.length and total_play_time < _playlist.length - 5 \ and not stdin_args.loop: messenger.error( - 'Playlist ({}) is not long enough!\n' - 'Total play time is: {}, target length is: {}'.format( - get_date(True), - timedelta(seconds=total_play_time), - timedelta(seconds=_playlist.length)) + f'Playlist ({get_date(True)}) is not long enough!\n' + f'Total play time is: {timedelta(seconds=total_play_time)}, ' + f'target length is: {timedelta(seconds=_playlist.length)}' ) @@ -735,29 +741,35 @@ def validate_thread(clip_nodes): source = node["source"] probe.load(source) 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 not probe.video[0]: - missing.append('Stream not exist: "{}"'.format(source)) + missing.append(f'Stream not exist: "{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"]): - counter += node["out"] - node["in"] - else: - missing.append('Missing Value in: "{}"'.format(node)) + if not node.get('in') == 0 and not _in: + missing.append(f'No in Value in: "{node}"') - if not is_float(node["duration"]): - missing.append('No duration Value!') + if not node.get('out') and not _out: + 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) if line: - error += line + '\nIn line: {}\n\n'.format(node) + error += line + f'\nIn line: {node}\n\n' if error: messenger.error( 'Validation error, check JSON playlist, ' - 'values are missing:\n{}'.format(error) + f'values are missing:\n{error}' ) check_length(counter) @@ -790,9 +802,8 @@ def set_length(duration, seek, out): def loop_input(source, src_duration, target_duration): # loop filles n times loop_count = math.ceil(target_duration / src_duration) - messenger.info( - 'Loop "{0}" {1} times, total duration: {2:.2f}'.format( - source, loop_count, target_duration)) + messenger.info(f'Loop "{source}" {loop_count} times, ' + f'total duration: {target_duration:.2f}') return ['-stream_loop', str(loop_count), '-i', source, '-t', str(target_duration)] @@ -806,11 +817,9 @@ def gen_dummy(duration): # noise = 'noise=alls=50:allf=t+u,hue=s=0' return [ '-f', 'lavfi', '-i', - 'color=c={}:s={}x{}:d={}:r={},format=pix_fmts=yuv420p'.format( - color, _pre.w, _pre.h, duration, _pre.fps - ), - '-f', 'lavfi', '-i', 'anoisesrc=d={}:c=pink:r=48000:a=0.05'.format( - duration) + f'color=c={color}:s={_pre.w}x{_pre.h}:d={duration}:r={_pre.fps},' + 'format=pix_fmts=yuv420p', + '-f', 'lavfi', '-i', f'anoisesrc=d={duration}:c=pink:r=48000:a=0.05' ] @@ -822,12 +831,12 @@ def gen_filler(duration): probe.load(_storage.filler) if probe.format: - if 'duration' in probe.format: + if probe.format.get('duration'): filler_duration = float(probe.format['duration']) if filler_duration > duration: # cut filler messenger.info( - 'Generate filler with {0:.2f} seconds'.format(duration)) + f'Generate filler with {duration:.2f} seconds') return probe, ['-i', _storage.filler] + set_length( filler_duration, 0, duration) else: @@ -854,13 +863,13 @@ def src_or_dummy(probe, src, dur, seek, out): if probe.is_remote and probe.video[0]: if seek > 0.0: messenger.warning( - 'Seek in live source "{}" not supported!'.format(src)) + f'Seek in live source "{src}" not supported!') return ['-i', src] + set_length(86400.0, seek, out) elif src and os.path.isfile(src): if out > dur: if seek > 0.0: 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) else: # FIXME: when list starts with looped clip, @@ -869,7 +878,7 @@ def src_or_dummy(probe, src, dur, seek, out): else: return seek_in(seek) + ['-i', src] + set_length(dur, seek, out) else: - messenger.error('Clip/URL not exist:\n{}'.format(src)) + messenger.error(f'Clip/URL not exist:\n{src}') return gen_dummy(out - seek) @@ -938,30 +947,27 @@ def handle_list_end(probe, new_length, src, begin, dur, seek, out): if new_out > dur: new_out = dur else: - messenger.info( - 'We are over time, new length is: {0:.2f}'.format(new_length)) + messenger.info(f'We are over time, new length is: {new_length:.2f}') missing_secs = abs(new_length - (dur - seek)) if dur > new_length > 1.5 and dur - seek >= new_length: src_cmd = src_or_dummy(probe, src, dur, seek, new_out) elif dur > new_length > 0.0: - messenger.info( - 'Last clip less then 1.5 second long, skip:\n{}'.format(src)) + messenger.info(f'Last clip less then 1.5 second long, skip:\n{src}') src_cmd = None if missing_secs > 2: new_playlist = False messenger.error( - 'Reach playlist end,\n{0:.2f} seconds needed.'.format( - missing_secs)) + f'Reach playlist end,\n{missing_secs:.2f} seconds needed.') else: new_out = out new_playlist = False src_cmd = src_or_dummy(probe, src, dur, seek, out) messenger.error( - 'Playlist is not long enough:' - '\n{0:.2f} seconds needed.'.format(missing_secs)) + f'Playlist is not long enough:\n{missing_secs:.2f} seconds needed.' + ) return src_cmd, seek, new_out, new_playlist @@ -981,14 +987,14 @@ def timed_source(probe, src, begin, dur, seek, out, first, last): return src_or_dummy(probe, src, dur, _seek, _out), \ _seek, _out, new_list 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 else: if not stdin_args.loop and _playlist.length: check_sync(current_delta) - messenger.debug('current_delta: {:f}'.format(current_delta)) - messenger.debug('total_delta: {:f}'.format(total_delta)) + messenger.debug(f'current_delta: {current_delta:f}') + messenger.debug(f'total_delta: {total_delta:f}') if (total_delta > out - seek and not last) \ or stdin_args.loop or not _playlist.length: @@ -996,8 +1002,7 @@ def timed_source(probe, src, begin, dur, seek, out, first, last): return src_or_dummy(probe, src, dur, seek, out), seek, out, False elif total_delta <= 0: - messenger.info( - 'Start time is over playtime, skip clip:\n{}'.format(src)) + messenger.info(f'Start time is over playtime, skip clip:\n{src}') return None, 0, 0, True elif total_delta < out - seek or last: