modularize output
This commit is contained in:
parent
9af5a67530
commit
fbfb9a7712
117
ffplayout.py
117
ffplayout.py
@ -19,15 +19,9 @@
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from subprocess import PIPE, Popen
|
from pydoc import locate
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from ffplayout.folder import GetSourceFromFolder, MediaStore, MediaWatcher
|
from ffplayout.utils import _playout, validate_ffmpeg_libs
|
||||||
from ffplayout.playlist import GetSourceFromPlaylist
|
|
||||||
from ffplayout.utils import (_ff, _log, _playlist, _playout, _pre_comp, _text,
|
|
||||||
ffmpeg_stderr_reader, get_date, messenger,
|
|
||||||
pre_audio_codec, stdin_args, terminate_processes,
|
|
||||||
validate_ffmpeg_libs)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if os.name != 'posix':
|
if os.name != 'posix':
|
||||||
@ -36,9 +30,6 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
print('colorama import failed, no colored console output on windows...')
|
print('colorama import failed, no colored console output on windows...')
|
||||||
|
|
||||||
_WINDOWS = os.name == 'nt'
|
|
||||||
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 65424
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# main functions
|
# main functions
|
||||||
@ -49,105 +40,15 @@ def main():
|
|||||||
pipe ffmpeg pre-process to final ffmpeg post-process,
|
pipe ffmpeg pre-process to final ffmpeg post-process,
|
||||||
or play with ffplay
|
or play with ffplay
|
||||||
"""
|
"""
|
||||||
year = get_date(False).split('-')[0]
|
|
||||||
overlay = []
|
|
||||||
|
|
||||||
ff_pre_settings = [
|
for output in os.listdir('ffplayout/output'):
|
||||||
'-pix_fmt', 'yuv420p', '-r', str(_pre_comp.fps),
|
if os.path.isfile(os.path.join('ffplayout/output', output)) \
|
||||||
'-c:v', 'mpeg2video', '-intra',
|
and output != '__init__.py':
|
||||||
'-b:v', '{}k'.format(_pre_comp.v_bitrate),
|
mode = os.path.splitext(output)[0]
|
||||||
'-minrate', '{}k'.format(_pre_comp.v_bitrate),
|
if mode == _playout.mode:
|
||||||
'-maxrate', '{}k'.format(_pre_comp.v_bitrate),
|
output = locate('ffplayout.output.{}.output'.format(mode))
|
||||||
'-bufsize', '{}k'.format(_pre_comp.v_bufsize)
|
|
||||||
] + pre_audio_codec() + ['-f', 'mpegts', '-']
|
|
||||||
|
|
||||||
if _text.add_text:
|
output()
|
||||||
messenger.info('Using drawtext node, listening on address: {}'.format(
|
|
||||||
_text.address
|
|
||||||
))
|
|
||||||
overlay = [
|
|
||||||
'-vf',
|
|
||||||
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
|
|
||||||
_text.address.replace(':', '\\:'), _text.fontfile)
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if _playout.preview or stdin_args.desktop:
|
|
||||||
# preview playout to player
|
|
||||||
_ff.encoder = Popen([
|
|
||||||
'ffplay', '-hide_banner', '-nostats', '-i', 'pipe:0'
|
|
||||||
] + overlay, stderr=PIPE, stdin=PIPE, stdout=None)
|
|
||||||
else:
|
|
||||||
_ff.encoder = Popen([
|
|
||||||
'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.post_comp_param + [_playout.out_addr],
|
|
||||||
stdin=PIPE, stderr=PIPE)
|
|
||||||
|
|
||||||
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
|
|
||||||
args=(_ff.encoder.stderr, False))
|
|
||||||
enc_err_thread.daemon = True
|
|
||||||
enc_err_thread.start()
|
|
||||||
|
|
||||||
if _playlist.mode and not stdin_args.folder:
|
|
||||||
watcher = None
|
|
||||||
get_source = GetSourceFromPlaylist()
|
|
||||||
else:
|
|
||||||
messenger.info('Start folder mode')
|
|
||||||
media = MediaStore()
|
|
||||||
watcher = MediaWatcher(media)
|
|
||||||
get_source = GetSourceFromFolder(media)
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
messenger.info('Play: "{}"'.format(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_err_thread = Thread(target=ffmpeg_stderr_reader,
|
|
||||||
args=(_ff.decoder.stderr, True))
|
|
||||||
dec_err_thread.daemon = True
|
|
||||||
dec_err_thread.start()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
buf = _ff.decoder.stdout.read(COPY_BUFSIZE)
|
|
||||||
if not buf:
|
|
||||||
break
|
|
||||||
_ff.encoder.stdin.write(buf)
|
|
||||||
|
|
||||||
except BrokenPipeError:
|
|
||||||
messenger.error('Broken Pipe!')
|
|
||||||
terminate_processes(watcher)
|
|
||||||
|
|
||||||
except SystemExit:
|
|
||||||
messenger.info('Got close command')
|
|
||||||
terminate_processes(watcher)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
messenger.warning('Program terminated')
|
|
||||||
terminate_processes(watcher)
|
|
||||||
|
|
||||||
# close encoder when nothing is to do anymore
|
|
||||||
if _ff.encoder.poll() is None:
|
|
||||||
_ff.encoder.terminate()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if _ff.encoder.poll() is None:
|
|
||||||
_ff.encoder.terminate()
|
|
||||||
_ff.encoder.wait()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -31,16 +31,18 @@ logging:
|
|||||||
log_level: "DEBUG"
|
log_level: "DEBUG"
|
||||||
ffmpeg_level: "ERROR"
|
ffmpeg_level: "ERROR"
|
||||||
|
|
||||||
pre_compress:
|
pre_process:
|
||||||
helptext: Settings for the pre-compression. All clips get prepared in that way,
|
helptext: Settings for the pre_process. All clips get prepared in that way,
|
||||||
so the input for the final compression is unique. 'aspect' must be a float
|
so the input for the final compression is unique. 'aspect' must be a float
|
||||||
number. 'logo' is only used if the path exist. 'logo_scale' scale the logo to
|
number. 'logo' is only used if the path exist. 'logo_scale' scale the logo to
|
||||||
target size, leave it blank when no scaling is needed, format is 'number:number',
|
target size, leave it blank when no scaling is needed, format is 'number:number',
|
||||||
for example '100:-1' for proportional scaling. With 'logo_opacity' logo can
|
for example '100:-1' for proportional scaling. With 'logo_opacity' logo can
|
||||||
become transparent. With 'logo_filter' 'overlay=W-w-12:12' you can modify
|
become transparent. With 'logo_filter' 'overlay=W-w-12:12' you can modify
|
||||||
the logo position. With 'use_loudnorm' you can activate single pass EBU R128
|
the logo position. With 'use_loudnorm' you can activate single pass EBU R128
|
||||||
loudness normalization. 'loud_*' can adjust the loudnorm filter. [Output is
|
loudness normalization. 'loud_*' can adjust the loudnorm filter. 'output_count'
|
||||||
always progressive!]
|
sets the outputs for the filtering, > 1 gives the option to use the same filters
|
||||||
|
for multiple outputs. This outputs can be taken in 'ffmpeg_param', names will be
|
||||||
|
vout2, vout3; aout2, aout2 etc.
|
||||||
width: 1024
|
width: 1024
|
||||||
height: 576
|
height: 576
|
||||||
aspect: 1.778
|
aspect: 1.778
|
||||||
@ -54,6 +56,7 @@ pre_compress:
|
|||||||
loud_I: -18
|
loud_I: -18
|
||||||
loud_TP: -1.5
|
loud_TP: -1.5
|
||||||
loud_LRA: 11
|
loud_LRA: 11
|
||||||
|
output_count: 1
|
||||||
|
|
||||||
playlist:
|
playlist:
|
||||||
helptext: Set 'playlist_mode' to 'False' if you want to play clips from the [STORAGE]
|
helptext: Set 'playlist_mode' to 'False' if you want to play clips from the [STORAGE]
|
||||||
@ -84,18 +87,21 @@ text:
|
|||||||
helptext: Overlay text in combination with libzmq for remote text manipulation.
|
helptext: Overlay text in combination with libzmq for remote text manipulation.
|
||||||
On windows fontfile path need to be like this 'C\:/WINDOWS/fonts/DejaVuSans.ttf'.
|
On windows fontfile path need to be like this 'C\:/WINDOWS/fonts/DejaVuSans.ttf'.
|
||||||
In a standard environment the filter drawtext node is Parsed_drawtext_2.
|
In a standard environment the filter drawtext node is Parsed_drawtext_2.
|
||||||
|
'over_pre' if True text will be overlay in pre processing. Continue same text
|
||||||
|
over multiple files is in that mode not possible.
|
||||||
add_text: False
|
add_text: False
|
||||||
|
over_pre: False
|
||||||
bind_address: "127.0.0.1:5555"
|
bind_address: "127.0.0.1:5555"
|
||||||
fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||||
|
|
||||||
out:
|
out:
|
||||||
helptext: The final playout post compression. Set the settings to your needs.
|
helptext: The final playout compression. Set the settings to your needs.
|
||||||
'preview' works only on a desktop system with ffplay!! Set it to 'True', if
|
'mode' has the standard options 'desktop', 'hls', 'stream'. Self made outputs
|
||||||
you need it.
|
can be define, by adding script in output folder with an 'output' function inside.
|
||||||
preview: False
|
mode: 'stream'
|
||||||
service_name: "Live Stream"
|
service_name: "Live Stream"
|
||||||
service_provider: "example.org"
|
service_provider: "example.org"
|
||||||
post_ffmpeg_param: >-
|
ffmpeg_param: >-
|
||||||
-c:v libx264
|
-c:v libx264
|
||||||
-crf 23
|
-crf 23
|
||||||
-x264-params keyint=50:min-keyint=25:scenecut=-1
|
-x264-params keyint=50:min-keyint=25:scenecut=-1
|
||||||
@ -108,5 +114,4 @@ out:
|
|||||||
-ar 44100
|
-ar 44100
|
||||||
-b:a 128k
|
-b:a 128k
|
||||||
-flags +global_header
|
-flags +global_header
|
||||||
-f flv
|
-f flv "rtmp://localhost/live/stream"
|
||||||
out_addr: "rtmp://localhost/live/stream"
|
|
||||||
|
@ -21,13 +21,25 @@ import math
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .utils import _pre_comp
|
from .utils import _pre, _text
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# building filters,
|
# building filters,
|
||||||
# when is needed add individuell filters to match output format
|
# when is needed add individuell filters to match output format
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def text_filter():
|
||||||
|
filter_chain = []
|
||||||
|
|
||||||
|
if _text.add_text and _text.over_pre:
|
||||||
|
filter_chain = [
|
||||||
|
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
|
||||||
|
_text.address.replace(':', '\\:'), _text.fontfile)]
|
||||||
|
|
||||||
|
return filter_chain
|
||||||
|
|
||||||
|
|
||||||
def deinterlace_filter(probe):
|
def deinterlace_filter(probe):
|
||||||
"""
|
"""
|
||||||
when material is interlaced,
|
when material is interlaced,
|
||||||
@ -50,15 +62,15 @@ def pad_filter(probe):
|
|||||||
filter_chain = []
|
filter_chain = []
|
||||||
|
|
||||||
if not math.isclose(probe.video[0]['aspect'],
|
if not math.isclose(probe.video[0]['aspect'],
|
||||||
_pre_comp.aspect, abs_tol=0.03):
|
_pre.aspect, abs_tol=0.03):
|
||||||
if probe.video[0]['aspect'] < _pre_comp.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_comp.w,
|
'pad=ih*{}/{}/sar:ih:(ow-iw)/2:(oh-ih)/2'.format(_pre.w,
|
||||||
_pre_comp.h))
|
_pre.h))
|
||||||
elif probe.video[0]['aspect'] > _pre_comp.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_comp.h,
|
'pad=iw:iw*{}/{}/sar:(ow-iw)/2:(oh-ih)/2'.format(_pre.h,
|
||||||
_pre_comp.w))
|
_pre.w))
|
||||||
|
|
||||||
return filter_chain
|
return filter_chain
|
||||||
|
|
||||||
@ -69,8 +81,8 @@ def fps_filter(probe):
|
|||||||
"""
|
"""
|
||||||
filter_chain = []
|
filter_chain = []
|
||||||
|
|
||||||
if probe.video[0]['fps'] != _pre_comp.fps:
|
if probe.video[0]['fps'] != _pre.fps:
|
||||||
filter_chain.append('fps={}'.format(_pre_comp.fps))
|
filter_chain.append('fps={}'.format(_pre.fps))
|
||||||
|
|
||||||
return filter_chain
|
return filter_chain
|
||||||
|
|
||||||
@ -82,13 +94,13 @@ def scale_filter(probe):
|
|||||||
"""
|
"""
|
||||||
filter_chain = []
|
filter_chain = []
|
||||||
|
|
||||||
if int(probe.video[0]['width']) != _pre_comp.w or \
|
if int(probe.video[0]['width']) != _pre.w or \
|
||||||
int(probe.video[0]['height']) != _pre_comp.h:
|
int(probe.video[0]['height']) != _pre.h:
|
||||||
filter_chain.append('scale={}:{}'.format(_pre_comp.w, _pre_comp.h))
|
filter_chain.append('scale={}:{}'.format(_pre.w, _pre.h))
|
||||||
|
|
||||||
if not math.isclose(probe.video[0]['aspect'],
|
if not math.isclose(probe.video[0]['aspect'],
|
||||||
_pre_comp.aspect, abs_tol=0.03):
|
_pre.aspect, abs_tol=0.03):
|
||||||
filter_chain.append('setdar=dar={}'.format(_pre_comp.aspect))
|
filter_chain.append('setdar=dar={}'.format(_pre.aspect))
|
||||||
|
|
||||||
return filter_chain
|
return filter_chain
|
||||||
|
|
||||||
@ -115,27 +127,27 @@ def overlay_filter(duration, ad, ad_last, ad_next):
|
|||||||
when ad is comming next fade logo out,
|
when ad is comming next fade logo out,
|
||||||
when clip before was an ad fade logo in
|
when clip before was an ad fade logo in
|
||||||
"""
|
"""
|
||||||
logo_filter = '[v]null[logo]'
|
logo_filter = '[v]null'
|
||||||
scale_filter = ''
|
scale_filter = ''
|
||||||
|
|
||||||
if _pre_comp.add_logo and os.path.isfile(_pre_comp.logo) and not ad:
|
if _pre.add_logo and os.path.isfile(_pre.logo) and not ad:
|
||||||
logo_chain = []
|
logo_chain = []
|
||||||
if _pre_comp.logo_scale and \
|
if _pre.logo_scale and \
|
||||||
re.match(r'\d+:-?\d+', _pre_comp.logo_scale):
|
re.match(r'\d+:-?\d+', _pre.logo_scale):
|
||||||
scale_filter = 'scale={},'.format(_pre_comp.logo_scale)
|
scale_filter = 'scale={},'.format(_pre.logo_scale)
|
||||||
logo_extras = 'format=rgba,{}colorchannelmixer=aa={}'.format(
|
logo_extras = 'format=rgba,{}colorchannelmixer=aa={}'.format(
|
||||||
scale_filter, _pre_comp.logo_opacity)
|
scale_filter, _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(
|
||||||
'movie={},{},{}'.format(_pre_comp.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('fade=out:st={}:d=1.0:alpha=1'.format(
|
||||||
duration - 1))
|
duration - 1))
|
||||||
|
|
||||||
logo_filter = '{}[l];[v][l]{}:shortest=1[logo]'.format(
|
logo_filter = '{}[l];[v][l]{}:shortest=1'.format(
|
||||||
','.join(logo_chain), _pre_comp.logo_filter)
|
','.join(logo_chain), _pre.logo_filter)
|
||||||
|
|
||||||
return logo_filter
|
return logo_filter
|
||||||
|
|
||||||
@ -161,9 +173,9 @@ def add_loudnorm(probe):
|
|||||||
"""
|
"""
|
||||||
loud_filter = []
|
loud_filter = []
|
||||||
|
|
||||||
if probe.audio and _pre_comp.add_loudnorm:
|
if probe.audio and _pre.add_loudnorm:
|
||||||
loud_filter = [('loudnorm=I={}:TP={}:LRA={}').format(
|
loud_filter = [('loudnorm=I={}:TP={}:LRA={}').format(
|
||||||
_pre_comp.loud_i, _pre_comp.loud_tp, _pre_comp.loud_lra)]
|
_pre.loud_i, _pre.loud_tp, _pre.loud_lra)]
|
||||||
|
|
||||||
return loud_filter
|
return loud_filter
|
||||||
|
|
||||||
@ -175,7 +187,7 @@ def extend_audio(probe, duration):
|
|||||||
pad_filter = []
|
pad_filter = []
|
||||||
|
|
||||||
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.3:
|
duration > float(probe.audio[0]['duration']) + 0.1:
|
||||||
pad_filter.append('apad=whole_dur={}'.format(duration))
|
pad_filter.append('apad=whole_dur={}'.format(duration))
|
||||||
|
|
||||||
return pad_filter
|
return pad_filter
|
||||||
@ -189,25 +201,45 @@ def extend_video(probe, duration, target_duration):
|
|||||||
|
|
||||||
if 'duration' in probe.video[0] and \
|
if 'duration' in probe.video[0] and \
|
||||||
target_duration < duration > float(
|
target_duration < duration > float(
|
||||||
probe.video[0]['duration']) + 0.3:
|
probe.video[0]['duration']) + 0.1:
|
||||||
pad_filter.append('tpad=stop_mode=add:stop_duration={}'.format(
|
pad_filter.append('tpad=stop_mode=add:stop_duration={}'.format(
|
||||||
duration - float(probe.video[0]['duration'])))
|
duration - float(probe.video[0]['duration'])))
|
||||||
|
|
||||||
return pad_filter
|
return pad_filter
|
||||||
|
|
||||||
|
|
||||||
|
def split_filter(filter_type):
|
||||||
|
map_node = []
|
||||||
|
filter_prefix = ''
|
||||||
|
|
||||||
|
if filter_type == 'a':
|
||||||
|
filter_prefix = 'a'
|
||||||
|
|
||||||
|
if _pre.output_count > 1:
|
||||||
|
for num in range(_pre.output_count):
|
||||||
|
map_node.append('[{}out{}]'.format(filter_type, num + 1))
|
||||||
|
|
||||||
|
filter = ',{}split={}{}'.format(filter_prefix, _pre.output_count,
|
||||||
|
''.join(map_node))
|
||||||
|
|
||||||
|
else:
|
||||||
|
filter = '[{}out1]'.format(filter_type)
|
||||||
|
|
||||||
|
return filter
|
||||||
|
|
||||||
|
|
||||||
def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
|
def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
|
||||||
"""
|
"""
|
||||||
build final filter graph, with video and audio chain
|
build final filter graph, with video and audio chain
|
||||||
"""
|
"""
|
||||||
video_chain = []
|
video_chain = []
|
||||||
audio_chain = []
|
audio_chain = []
|
||||||
video_map = ['-map', '[logo]']
|
|
||||||
|
|
||||||
if out > duration:
|
if out > duration:
|
||||||
seek = 0
|
seek = 0
|
||||||
|
|
||||||
if probe.video[0]:
|
if probe.video[0]:
|
||||||
|
video_chain += text_filter()
|
||||||
video_chain += deinterlace_filter(probe)
|
video_chain += deinterlace_filter(probe)
|
||||||
video_chain += pad_filter(probe)
|
video_chain += pad_filter(probe)
|
||||||
video_chain += fps_filter(probe)
|
video_chain += fps_filter(probe)
|
||||||
@ -229,17 +261,16 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
|
|||||||
video_filter = 'null[v]'
|
video_filter = 'null[v]'
|
||||||
|
|
||||||
logo_filter = overlay_filter(out - seek, ad, ad_last, ad_next)
|
logo_filter = overlay_filter(out - seek, ad, ad_last, ad_next)
|
||||||
|
v_split = split_filter('v')
|
||||||
|
video_map = ['-map', '[vout1]']
|
||||||
video_filter = [
|
video_filter = [
|
||||||
'-filter_complex', '[0:v]{};{}'.format(
|
'-filter_complex', '[0:v]{};{}{}'.format(
|
||||||
video_filter, logo_filter)]
|
video_filter, logo_filter, v_split)]
|
||||||
|
|
||||||
if audio_chain:
|
a_split = split_filter('a')
|
||||||
audio_filter = [
|
audio_map = ['-map', '[aout1]']
|
||||||
'-filter_complex', '{}[a]'.format(','.join(audio_chain))]
|
audio_filter = [
|
||||||
audio_map = ['-map', '[a]']
|
'-filter_complex', '{}{}'.format(','.join(audio_chain), a_split)]
|
||||||
else:
|
|
||||||
audio_filter = []
|
|
||||||
audio_map = ['-map', '0:a']
|
|
||||||
|
|
||||||
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
|
||||||
|
0
ffplayout/output/__init__.py
Normal file
0
ffplayout/output/__init__.py
Normal file
104
ffplayout/output/desktop.py
Normal file
104
ffplayout/output/desktop.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import os
|
||||||
|
from subprocess import PIPE, Popen
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from ffplayout.folder import GetSourceFromFolder, MediaStore, MediaWatcher
|
||||||
|
from ffplayout.playlist import GetSourceFromPlaylist
|
||||||
|
from ffplayout.utils import (_ff, _log, _playlist, _pre, _text,
|
||||||
|
ffmpeg_stderr_reader, messenger, pre_audio_codec,
|
||||||
|
stdin_args, terminate_processes)
|
||||||
|
|
||||||
|
_WINDOWS = os.name == 'nt'
|
||||||
|
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 65424
|
||||||
|
|
||||||
|
|
||||||
|
def output():
|
||||||
|
"""
|
||||||
|
this output is for playing on desktop with ffplay
|
||||||
|
"""
|
||||||
|
overlay = []
|
||||||
|
|
||||||
|
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)
|
||||||
|
] + 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
|
||||||
|
))
|
||||||
|
overlay = [
|
||||||
|
'-vf',
|
||||||
|
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
|
||||||
|
_text.address.replace(':', '\\:'), _text.fontfile)
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
_ff.encoder = Popen([
|
||||||
|
'ffplay', '-hide_banner', '-nostats', '-i', 'pipe:0'
|
||||||
|
] + overlay, stderr=PIPE, stdin=PIPE, stdout=None)
|
||||||
|
|
||||||
|
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
|
args=(_ff.encoder.stderr, False))
|
||||||
|
enc_err_thread.daemon = True
|
||||||
|
enc_err_thread.start()
|
||||||
|
|
||||||
|
if _playlist.mode and not stdin_args.folder:
|
||||||
|
watcher = None
|
||||||
|
get_source = GetSourceFromPlaylist()
|
||||||
|
else:
|
||||||
|
messenger.info('Start folder mode')
|
||||||
|
media = MediaStore()
|
||||||
|
watcher = MediaWatcher(media)
|
||||||
|
get_source = GetSourceFromFolder(media)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
messenger.info('Play: "{}"'.format(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_err_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
|
args=(_ff.decoder.stderr, True))
|
||||||
|
dec_err_thread.daemon = True
|
||||||
|
dec_err_thread.start()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
buf = _ff.decoder.stdout.read(COPY_BUFSIZE)
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
_ff.encoder.stdin.write(buf)
|
||||||
|
|
||||||
|
except BrokenPipeError:
|
||||||
|
messenger.error('Broken Pipe!')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
except SystemExit:
|
||||||
|
messenger.info('Got close command')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
messenger.warning('Program terminated')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
# close encoder when nothing is to do anymore
|
||||||
|
if _ff.encoder.poll() is None:
|
||||||
|
_ff.encoder.terminate()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if _ff.encoder.poll() is None:
|
||||||
|
_ff.encoder.terminate()
|
||||||
|
_ff.encoder.wait()
|
72
ffplayout/output/hls.py
Normal file
72
ffplayout/output/hls.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
from subprocess import PIPE, Popen
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from ffplayout.folder import GetSourceFromFolder, MediaStore, MediaWatcher
|
||||||
|
from ffplayout.playlist import GetSourceFromPlaylist
|
||||||
|
from ffplayout.utils import (_ff, _log, _playlist, _playout,
|
||||||
|
ffmpeg_stderr_reader, get_date, messenger,
|
||||||
|
stdin_args, terminate_processes)
|
||||||
|
|
||||||
|
|
||||||
|
def output():
|
||||||
|
"""
|
||||||
|
this output is hls output, no preprocess is needed.
|
||||||
|
"""
|
||||||
|
year = get_date(False).split('-')[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if _playlist.mode and not stdin_args.folder:
|
||||||
|
watcher = None
|
||||||
|
get_source = GetSourceFromPlaylist()
|
||||||
|
else:
|
||||||
|
messenger.info('Start folder mode')
|
||||||
|
media = MediaStore()
|
||||||
|
watcher = MediaWatcher(media)
|
||||||
|
get_source = GetSourceFromFolder(media)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
messenger.info('Play: "{}"'.format(current_file))
|
||||||
|
cmd = [
|
||||||
|
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
|
||||||
|
'-nostats', '-re', '-thread_queue_size', '256'
|
||||||
|
] + src_cmd + [
|
||||||
|
'-metadata', 'service_name=' + _playout.name,
|
||||||
|
'-metadata', 'service_provider=' + _playout.provider,
|
||||||
|
'-metadata', 'year={}'.format(year)
|
||||||
|
] + _playout.ffmpeg_param
|
||||||
|
|
||||||
|
_ff.encoder = Popen(cmd, stdin=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
enc_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
|
args=(_ff.encoder.stderr, True))
|
||||||
|
enc_thread.daemon = True
|
||||||
|
enc_thread.start()
|
||||||
|
enc_thread.join()
|
||||||
|
|
||||||
|
except BrokenPipeError:
|
||||||
|
messenger.error('Broken Pipe!')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
except SystemExit:
|
||||||
|
messenger.info('Got close command')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
messenger.warning('Program terminated')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
# close encoder when nothing is to do anymore
|
||||||
|
if _ff.encoder.poll() is None:
|
||||||
|
_ff.encoder.terminate()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if _ff.encoder.poll() is None:
|
||||||
|
_ff.encoder.terminate()
|
||||||
|
_ff.encoder.wait()
|
111
ffplayout/output/stream.py
Normal file
111
ffplayout/output/stream.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import os
|
||||||
|
from subprocess import PIPE, Popen
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from ffplayout.folder import GetSourceFromFolder, MediaStore, MediaWatcher
|
||||||
|
from ffplayout.playlist import GetSourceFromPlaylist
|
||||||
|
from ffplayout.utils import (_ff, _log, _playlist, _playout, _pre, _text,
|
||||||
|
ffmpeg_stderr_reader, get_date, messenger,
|
||||||
|
pre_audio_codec, stdin_args, terminate_processes)
|
||||||
|
|
||||||
|
_WINDOWS = os.name == 'nt'
|
||||||
|
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 65424
|
||||||
|
|
||||||
|
|
||||||
|
def output():
|
||||||
|
"""
|
||||||
|
this output is for streaming to a target address,
|
||||||
|
like rtmp, rtp, svt, etc.
|
||||||
|
"""
|
||||||
|
year = get_date(False).split('-')[0]
|
||||||
|
overlay = []
|
||||||
|
|
||||||
|
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)
|
||||||
|
] + 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
|
||||||
|
))
|
||||||
|
overlay = [
|
||||||
|
'-vf',
|
||||||
|
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
|
||||||
|
_text.address.replace(':', '\\:'), _text.fontfile)
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
_ff.encoder = Popen([
|
||||||
|
'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, stdin=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
|
args=(_ff.encoder.stderr, False))
|
||||||
|
enc_err_thread.daemon = True
|
||||||
|
enc_err_thread.start()
|
||||||
|
|
||||||
|
if _playlist.mode and not stdin_args.folder:
|
||||||
|
watcher = None
|
||||||
|
get_source = GetSourceFromPlaylist()
|
||||||
|
else:
|
||||||
|
messenger.info('Start folder mode')
|
||||||
|
media = MediaStore()
|
||||||
|
watcher = MediaWatcher(media)
|
||||||
|
get_source = GetSourceFromFolder(media)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
messenger.info('Play: "{}"'.format(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_err_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
|
args=(_ff.decoder.stderr, True))
|
||||||
|
dec_err_thread.daemon = True
|
||||||
|
dec_err_thread.start()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
buf = _ff.decoder.stdout.read(COPY_BUFSIZE)
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
_ff.encoder.stdin.write(buf)
|
||||||
|
|
||||||
|
except BrokenPipeError:
|
||||||
|
messenger.error('Broken Pipe!')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
except SystemExit:
|
||||||
|
messenger.info('Got close command')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
messenger.warning('Program terminated')
|
||||||
|
terminate_processes(watcher)
|
||||||
|
|
||||||
|
# close encoder when nothing is to do anymore
|
||||||
|
if _ff.encoder.poll() is None:
|
||||||
|
_ff.encoder.terminate()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if _ff.encoder.poll() is None:
|
||||||
|
_ff.encoder.terminate()
|
||||||
|
_ff.encoder.wait()
|
@ -224,7 +224,7 @@ class GetSourceFromPlaylist:
|
|||||||
|
|
||||||
if self.clip_nodes is None:
|
if self.clip_nodes is None:
|
||||||
self.eof_handling(
|
self.eof_handling(
|
||||||
'No valid playlist:\n{}'.format(self.json_file), True, 300)
|
'No valid playlist:\n{}'.format(self.json_file), True, 30)
|
||||||
yield self.src_cmd + self.filtergraph
|
yield self.src_cmd + self.filtergraph
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ def get_time(time_format):
|
|||||||
_general = SimpleNamespace()
|
_general = SimpleNamespace()
|
||||||
_mail = SimpleNamespace()
|
_mail = SimpleNamespace()
|
||||||
_log = SimpleNamespace()
|
_log = SimpleNamespace()
|
||||||
_pre_comp = SimpleNamespace()
|
_pre = SimpleNamespace()
|
||||||
_playlist = SimpleNamespace()
|
_playlist = SimpleNamespace()
|
||||||
_storage = SimpleNamespace()
|
_storage = SimpleNamespace()
|
||||||
_text = SimpleNamespace()
|
_text = SimpleNamespace()
|
||||||
@ -178,15 +178,16 @@ def load_config():
|
|||||||
_mail.recip = cfg['mail']['recipient']
|
_mail.recip = cfg['mail']['recipient']
|
||||||
_mail.level = cfg['mail']['mail_level']
|
_mail.level = cfg['mail']['mail_level']
|
||||||
|
|
||||||
_pre_comp.add_logo = cfg['pre_compress']['add_logo']
|
_pre.add_logo = cfg['pre_process']['add_logo']
|
||||||
_pre_comp.logo = cfg['pre_compress']['logo']
|
_pre.logo = cfg['pre_process']['logo']
|
||||||
_pre_comp.logo_scale = cfg['pre_compress']['logo_scale']
|
_pre.logo_scale = cfg['pre_process']['logo_scale']
|
||||||
_pre_comp.logo_filter = cfg['pre_compress']['logo_filter']
|
_pre.logo_filter = cfg['pre_process']['logo_filter']
|
||||||
_pre_comp.logo_opacity = cfg['pre_compress']['logo_opacity']
|
_pre.logo_opacity = cfg['pre_process']['logo_opacity']
|
||||||
_pre_comp.add_loudnorm = cfg['pre_compress']['add_loudnorm']
|
_pre.add_loudnorm = cfg['pre_process']['add_loudnorm']
|
||||||
_pre_comp.loud_i = cfg['pre_compress']['loud_I']
|
_pre.loud_i = cfg['pre_process']['loud_I']
|
||||||
_pre_comp.loud_tp = cfg['pre_compress']['loud_TP']
|
_pre.loud_tp = cfg['pre_process']['loud_TP']
|
||||||
_pre_comp.loud_lra = cfg['pre_compress']['loud_LRA']
|
_pre.loud_lra = cfg['pre_process']['loud_LRA']
|
||||||
|
_pre.output_count = cfg['pre_process']['output_count']
|
||||||
|
|
||||||
_playlist.mode = cfg['playlist']['playlist_mode']
|
_playlist.mode = cfg['playlist']['playlist_mode']
|
||||||
_playlist.path = cfg['playlist']['path']
|
_playlist.path = cfg['playlist']['path']
|
||||||
@ -199,6 +200,7 @@ def load_config():
|
|||||||
_storage.shuffle = cfg['storage']['shuffle']
|
_storage.shuffle = cfg['storage']['shuffle']
|
||||||
|
|
||||||
_text.add_text = cfg['text']['add_text']
|
_text.add_text = cfg['text']['add_text']
|
||||||
|
_text.over_pre = cfg['text']['over_pre']
|
||||||
_text.address = cfg['text']['bind_address']
|
_text.address = cfg['text']['bind_address']
|
||||||
_text.fontfile = cfg['text']['fontfile']
|
_text.fontfile = cfg['text']['fontfile']
|
||||||
|
|
||||||
@ -209,18 +211,17 @@ def load_config():
|
|||||||
_log.level = cfg['logging']['log_level']
|
_log.level = cfg['logging']['log_level']
|
||||||
_log.ff_level = cfg['logging']['ffmpeg_level']
|
_log.ff_level = cfg['logging']['ffmpeg_level']
|
||||||
|
|
||||||
_pre_comp.w = cfg['pre_compress']['width']
|
_pre.w = cfg['pre_process']['width']
|
||||||
_pre_comp.h = cfg['pre_compress']['height']
|
_pre.h = cfg['pre_process']['height']
|
||||||
_pre_comp.aspect = cfg['pre_compress']['aspect']
|
_pre.aspect = cfg['pre_process']['aspect']
|
||||||
_pre_comp.fps = cfg['pre_compress']['fps']
|
_pre.fps = cfg['pre_process']['fps']
|
||||||
_pre_comp.v_bitrate = cfg['pre_compress']['width'] * 50
|
_pre.v_bitrate = cfg['pre_process']['width'] * 50
|
||||||
_pre_comp.v_bufsize = cfg['pre_compress']['width'] * 50 / 2
|
_pre.v_bufsize = cfg['pre_process']['width'] * 50 / 2
|
||||||
|
|
||||||
_playout.preview = cfg['out']['preview']
|
_playout.mode = cfg['out']['mode']
|
||||||
_playout.name = cfg['out']['service_name']
|
_playout.name = cfg['out']['service_name']
|
||||||
_playout.provider = cfg['out']['service_provider']
|
_playout.provider = cfg['out']['service_provider']
|
||||||
_playout.post_comp_param = cfg['out']['post_ffmpeg_param'].split(' ')
|
_playout.ffmpeg_param = cfg['out']['ffmpeg_param'].split(' ')
|
||||||
_playout.out_addr = cfg['out']['out_addr']
|
|
||||||
|
|
||||||
_init.load = False
|
_init.load = False
|
||||||
|
|
||||||
@ -782,7 +783,7 @@ def gen_dummy(duration):
|
|||||||
return [
|
return [
|
||||||
'-f', 'lavfi', '-i',
|
'-f', 'lavfi', '-i',
|
||||||
'color=c={}:s={}x{}:d={}:r={},format=pix_fmts=yuv420p'.format(
|
'color=c={}:s={}x{}:d={}:r={},format=pix_fmts=yuv420p'.format(
|
||||||
color, _pre_comp.w, _pre_comp.h, duration, _pre_comp.fps
|
color, _pre.w, _pre.h, duration, _pre.fps
|
||||||
),
|
),
|
||||||
'-f', 'lavfi', '-i', 'anoisesrc=d={}:c=pink:r=48000:a=0.05'.format(
|
'-f', 'lavfi', '-i', 'anoisesrc=d={}:c=pink:r=48000:a=0.05'.format(
|
||||||
duration)
|
duration)
|
||||||
@ -989,7 +990,7 @@ def pre_audio_codec():
|
|||||||
s302m has higher quality, but is experimental
|
s302m has higher quality, but is experimental
|
||||||
and works not well together with the loudnorm filter
|
and works not well together with the loudnorm filter
|
||||||
"""
|
"""
|
||||||
if _pre_comp.add_loudnorm:
|
if _pre.add_loudnorm:
|
||||||
acodec = 'libtwolame' if 'libtwolame' in FF_LIBS['libs'] else 'mp2'
|
acodec = 'libtwolame' if 'libtwolame' in FF_LIBS['libs'] else 'mp2'
|
||||||
return ['-c:a', acodec, '-b:a', '384k', '-ar', '48000', '-ac', '2']
|
return ['-c:a', acodec, '-b:a', '384k', '-ar', '48000', '-ac', '2']
|
||||||
else:
|
else:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user