Merge branch 'cleanup' into dev
This commit is contained in:
commit
792564fe4f
12
README.md
12
README.md
@ -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:`.
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
15
ffplayout/config/README.md
Normal file
15
ffplayout/config/README.md
Normal 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!**
|
3
ffplayout/config/argparse_volume.yml
Normal file
3
ffplayout/config/argparse_volume.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
short: -v
|
||||||
|
long: --volume
|
||||||
|
help: set audio volume
|
@ -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.
|
||||||
|
10
ffplayout/filters/a_volume.py
Normal file
10
ffplayout/filters/a_volume.py
Normal 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}'
|
@ -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
|
||||||
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
colorama
|
colorama
|
||||||
pyyaml
|
pyyaml
|
||||||
|
requests
|
||||||
supervisor
|
supervisor
|
||||||
watchdog
|
watchdog
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user