modularize output
This commit is contained in:
parent
9af5a67530
commit
fbfb9a7712
117
ffplayout.py
117
ffplayout.py
@ -19,15 +19,9 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
from subprocess import PIPE, Popen
|
||||
from threading import Thread
|
||||
from pydoc import locate
|
||||
|
||||
from ffplayout.folder import GetSourceFromFolder, MediaStore, MediaWatcher
|
||||
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)
|
||||
from ffplayout.utils import _playout, validate_ffmpeg_libs
|
||||
|
||||
try:
|
||||
if os.name != 'posix':
|
||||
@ -36,9 +30,6 @@ try:
|
||||
except ImportError:
|
||||
print('colorama import failed, no colored console output on windows...')
|
||||
|
||||
_WINDOWS = os.name == 'nt'
|
||||
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 65424
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# main functions
|
||||
@ -49,105 +40,15 @@ def main():
|
||||
pipe ffmpeg pre-process to final ffmpeg post-process,
|
||||
or play with ffplay
|
||||
"""
|
||||
year = get_date(False).split('-')[0]
|
||||
overlay = []
|
||||
|
||||
ff_pre_settings = [
|
||||
'-pix_fmt', 'yuv420p', '-r', str(_pre_comp.fps),
|
||||
'-c:v', 'mpeg2video', '-intra',
|
||||
'-b:v', '{}k'.format(_pre_comp.v_bitrate),
|
||||
'-minrate', '{}k'.format(_pre_comp.v_bitrate),
|
||||
'-maxrate', '{}k'.format(_pre_comp.v_bitrate),
|
||||
'-bufsize', '{}k'.format(_pre_comp.v_bufsize)
|
||||
] + pre_audio_codec() + ['-f', 'mpegts', '-']
|
||||
for output in os.listdir('ffplayout/output'):
|
||||
if os.path.isfile(os.path.join('ffplayout/output', output)) \
|
||||
and output != '__init__.py':
|
||||
mode = os.path.splitext(output)[0]
|
||||
if mode == _playout.mode:
|
||||
output = locate('ffplayout.output.{}.output'.format(mode))
|
||||
|
||||
if _text.add_text:
|
||||
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()
|
||||
output()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -31,16 +31,18 @@ logging:
|
||||
log_level: "DEBUG"
|
||||
ffmpeg_level: "ERROR"
|
||||
|
||||
pre_compress:
|
||||
helptext: Settings for the pre-compression. All clips get prepared in that way,
|
||||
pre_process:
|
||||
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
|
||||
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',
|
||||
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
|
||||
the logo position. With 'use_loudnorm' you can activate single pass EBU R128
|
||||
loudness normalization. 'loud_*' can adjust the loudnorm filter. [Output is
|
||||
always progressive!]
|
||||
loudness normalization. 'loud_*' can adjust the loudnorm filter. 'output_count'
|
||||
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
|
||||
height: 576
|
||||
aspect: 1.778
|
||||
@ -54,6 +56,7 @@ pre_compress:
|
||||
loud_I: -18
|
||||
loud_TP: -1.5
|
||||
loud_LRA: 11
|
||||
output_count: 1
|
||||
|
||||
playlist:
|
||||
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.
|
||||
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.
|
||||
'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
|
||||
over_pre: False
|
||||
bind_address: "127.0.0.1:5555"
|
||||
fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||
|
||||
out:
|
||||
helptext: The final playout post compression. Set the settings to your needs.
|
||||
'preview' works only on a desktop system with ffplay!! Set it to 'True', if
|
||||
you need it.
|
||||
preview: False
|
||||
helptext: The final playout compression. Set the settings to your needs.
|
||||
'mode' has the standard options 'desktop', 'hls', 'stream'. Self made outputs
|
||||
can be define, by adding script in output folder with an 'output' function inside.
|
||||
mode: 'stream'
|
||||
service_name: "Live Stream"
|
||||
service_provider: "example.org"
|
||||
post_ffmpeg_param: >-
|
||||
ffmpeg_param: >-
|
||||
-c:v libx264
|
||||
-crf 23
|
||||
-x264-params keyint=50:min-keyint=25:scenecut=-1
|
||||
@ -108,5 +114,4 @@ out:
|
||||
-ar 44100
|
||||
-b:a 128k
|
||||
-flags +global_header
|
||||
-f flv
|
||||
out_addr: "rtmp://localhost/live/stream"
|
||||
-f flv "rtmp://localhost/live/stream"
|
||||
|
@ -21,13 +21,25 @@ import math
|
||||
import os
|
||||
import re
|
||||
|
||||
from .utils import _pre_comp
|
||||
from .utils import _pre, _text
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# building filters,
|
||||
# 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):
|
||||
"""
|
||||
when material is interlaced,
|
||||
@ -50,15 +62,15 @@ def pad_filter(probe):
|
||||
filter_chain = []
|
||||
|
||||
if not math.isclose(probe.video[0]['aspect'],
|
||||
_pre_comp.aspect, abs_tol=0.03):
|
||||
if probe.video[0]['aspect'] < _pre_comp.aspect:
|
||||
_pre.aspect, abs_tol=0.03):
|
||||
if probe.video[0]['aspect'] < _pre.aspect:
|
||||
filter_chain.append(
|
||||
'pad=ih*{}/{}/sar:ih:(ow-iw)/2:(oh-ih)/2'.format(_pre_comp.w,
|
||||
_pre_comp.h))
|
||||
elif probe.video[0]['aspect'] > _pre_comp.aspect:
|
||||
'pad=ih*{}/{}/sar:ih:(ow-iw)/2:(oh-ih)/2'.format(_pre.w,
|
||||
_pre.h))
|
||||
elif probe.video[0]['aspect'] > _pre.aspect:
|
||||
filter_chain.append(
|
||||
'pad=iw:iw*{}/{}/sar:(ow-iw)/2:(oh-ih)/2'.format(_pre_comp.h,
|
||||
_pre_comp.w))
|
||||
'pad=iw:iw*{}/{}/sar:(ow-iw)/2:(oh-ih)/2'.format(_pre.h,
|
||||
_pre.w))
|
||||
|
||||
return filter_chain
|
||||
|
||||
@ -69,8 +81,8 @@ def fps_filter(probe):
|
||||
"""
|
||||
filter_chain = []
|
||||
|
||||
if probe.video[0]['fps'] != _pre_comp.fps:
|
||||
filter_chain.append('fps={}'.format(_pre_comp.fps))
|
||||
if probe.video[0]['fps'] != _pre.fps:
|
||||
filter_chain.append('fps={}'.format(_pre.fps))
|
||||
|
||||
return filter_chain
|
||||
|
||||
@ -82,13 +94,13 @@ def scale_filter(probe):
|
||||
"""
|
||||
filter_chain = []
|
||||
|
||||
if int(probe.video[0]['width']) != _pre_comp.w or \
|
||||
int(probe.video[0]['height']) != _pre_comp.h:
|
||||
filter_chain.append('scale={}:{}'.format(_pre_comp.w, _pre_comp.h))
|
||||
if int(probe.video[0]['width']) != _pre.w or \
|
||||
int(probe.video[0]['height']) != _pre.h:
|
||||
filter_chain.append('scale={}:{}'.format(_pre.w, _pre.h))
|
||||
|
||||
if not math.isclose(probe.video[0]['aspect'],
|
||||
_pre_comp.aspect, abs_tol=0.03):
|
||||
filter_chain.append('setdar=dar={}'.format(_pre_comp.aspect))
|
||||
_pre.aspect, abs_tol=0.03):
|
||||
filter_chain.append('setdar=dar={}'.format(_pre.aspect))
|
||||
|
||||
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 clip before was an ad fade logo in
|
||||
"""
|
||||
logo_filter = '[v]null[logo]'
|
||||
logo_filter = '[v]null'
|
||||
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 = []
|
||||
if _pre_comp.logo_scale and \
|
||||
re.match(r'\d+:-?\d+', _pre_comp.logo_scale):
|
||||
scale_filter = 'scale={},'.format(_pre_comp.logo_scale)
|
||||
if _pre.logo_scale and \
|
||||
re.match(r'\d+:-?\d+', _pre.logo_scale):
|
||||
scale_filter = 'scale={},'.format(_pre.logo_scale)
|
||||
logo_extras = 'format=rgba,{}colorchannelmixer=aa={}'.format(
|
||||
scale_filter, _pre_comp.logo_opacity)
|
||||
scale_filter, _pre.logo_opacity)
|
||||
loop = 'loop=loop=-1:size=1:start=0'
|
||||
logo_chain.append(
|
||||
'movie={},{},{}'.format(_pre_comp.logo, loop, logo_extras))
|
||||
'movie={},{},{}'.format(_pre.logo, loop, logo_extras))
|
||||
if ad_last:
|
||||
logo_chain.append('fade=in:st=0:d=1.0:alpha=1')
|
||||
if ad_next:
|
||||
logo_chain.append('fade=out:st={}:d=1.0:alpha=1'.format(
|
||||
duration - 1))
|
||||
|
||||
logo_filter = '{}[l];[v][l]{}:shortest=1[logo]'.format(
|
||||
','.join(logo_chain), _pre_comp.logo_filter)
|
||||
logo_filter = '{}[l];[v][l]{}:shortest=1'.format(
|
||||
','.join(logo_chain), _pre.logo_filter)
|
||||
|
||||
return logo_filter
|
||||
|
||||
@ -161,9 +173,9 @@ def add_loudnorm(probe):
|
||||
"""
|
||||
loud_filter = []
|
||||
|
||||
if probe.audio and _pre_comp.add_loudnorm:
|
||||
if probe.audio and _pre.add_loudnorm:
|
||||
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
|
||||
|
||||
@ -175,7 +187,7 @@ def extend_audio(probe, duration):
|
||||
pad_filter = []
|
||||
|
||||
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))
|
||||
|
||||
return pad_filter
|
||||
@ -189,25 +201,45 @@ def extend_video(probe, duration, target_duration):
|
||||
|
||||
if 'duration' in probe.video[0] and \
|
||||
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(
|
||||
duration - float(probe.video[0]['duration'])))
|
||||
|
||||
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):
|
||||
"""
|
||||
build final filter graph, with video and audio chain
|
||||
"""
|
||||
video_chain = []
|
||||
audio_chain = []
|
||||
video_map = ['-map', '[logo]']
|
||||
|
||||
if out > duration:
|
||||
seek = 0
|
||||
|
||||
if probe.video[0]:
|
||||
video_chain += text_filter()
|
||||
video_chain += deinterlace_filter(probe)
|
||||
video_chain += pad_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]'
|
||||
|
||||
logo_filter = overlay_filter(out - seek, ad, ad_last, ad_next)
|
||||
v_split = split_filter('v')
|
||||
video_map = ['-map', '[vout1]']
|
||||
video_filter = [
|
||||
'-filter_complex', '[0:v]{};{}'.format(
|
||||
video_filter, logo_filter)]
|
||||
'-filter_complex', '[0:v]{};{}{}'.format(
|
||||
video_filter, logo_filter, v_split)]
|
||||
|
||||
if audio_chain:
|
||||
audio_filter = [
|
||||
'-filter_complex', '{}[a]'.format(','.join(audio_chain))]
|
||||
audio_map = ['-map', '[a]']
|
||||
else:
|
||||
audio_filter = []
|
||||
audio_map = ['-map', '0:a']
|
||||
a_split = split_filter('a')
|
||||
audio_map = ['-map', '[aout1]']
|
||||
audio_filter = [
|
||||
'-filter_complex', '{}{}'.format(','.join(audio_chain), a_split)]
|
||||
|
||||
if probe.video[0]:
|
||||
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:
|
||||
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
|
||||
continue
|
||||
|
||||
|
@ -113,7 +113,7 @@ def get_time(time_format):
|
||||
_general = SimpleNamespace()
|
||||
_mail = SimpleNamespace()
|
||||
_log = SimpleNamespace()
|
||||
_pre_comp = SimpleNamespace()
|
||||
_pre = SimpleNamespace()
|
||||
_playlist = SimpleNamespace()
|
||||
_storage = SimpleNamespace()
|
||||
_text = SimpleNamespace()
|
||||
@ -178,15 +178,16 @@ def load_config():
|
||||
_mail.recip = cfg['mail']['recipient']
|
||||
_mail.level = cfg['mail']['mail_level']
|
||||
|
||||
_pre_comp.add_logo = cfg['pre_compress']['add_logo']
|
||||
_pre_comp.logo = cfg['pre_compress']['logo']
|
||||
_pre_comp.logo_scale = cfg['pre_compress']['logo_scale']
|
||||
_pre_comp.logo_filter = cfg['pre_compress']['logo_filter']
|
||||
_pre_comp.logo_opacity = cfg['pre_compress']['logo_opacity']
|
||||
_pre_comp.add_loudnorm = cfg['pre_compress']['add_loudnorm']
|
||||
_pre_comp.loud_i = cfg['pre_compress']['loud_I']
|
||||
_pre_comp.loud_tp = cfg['pre_compress']['loud_TP']
|
||||
_pre_comp.loud_lra = cfg['pre_compress']['loud_LRA']
|
||||
_pre.add_logo = cfg['pre_process']['add_logo']
|
||||
_pre.logo = cfg['pre_process']['logo']
|
||||
_pre.logo_scale = cfg['pre_process']['logo_scale']
|
||||
_pre.logo_filter = cfg['pre_process']['logo_filter']
|
||||
_pre.logo_opacity = cfg['pre_process']['logo_opacity']
|
||||
_pre.add_loudnorm = cfg['pre_process']['add_loudnorm']
|
||||
_pre.loud_i = cfg['pre_process']['loud_I']
|
||||
_pre.loud_tp = cfg['pre_process']['loud_TP']
|
||||
_pre.loud_lra = cfg['pre_process']['loud_LRA']
|
||||
_pre.output_count = cfg['pre_process']['output_count']
|
||||
|
||||
_playlist.mode = cfg['playlist']['playlist_mode']
|
||||
_playlist.path = cfg['playlist']['path']
|
||||
@ -199,6 +200,7 @@ def load_config():
|
||||
_storage.shuffle = cfg['storage']['shuffle']
|
||||
|
||||
_text.add_text = cfg['text']['add_text']
|
||||
_text.over_pre = cfg['text']['over_pre']
|
||||
_text.address = cfg['text']['bind_address']
|
||||
_text.fontfile = cfg['text']['fontfile']
|
||||
|
||||
@ -209,18 +211,17 @@ def load_config():
|
||||
_log.level = cfg['logging']['log_level']
|
||||
_log.ff_level = cfg['logging']['ffmpeg_level']
|
||||
|
||||
_pre_comp.w = cfg['pre_compress']['width']
|
||||
_pre_comp.h = cfg['pre_compress']['height']
|
||||
_pre_comp.aspect = cfg['pre_compress']['aspect']
|
||||
_pre_comp.fps = cfg['pre_compress']['fps']
|
||||
_pre_comp.v_bitrate = cfg['pre_compress']['width'] * 50
|
||||
_pre_comp.v_bufsize = cfg['pre_compress']['width'] * 50 / 2
|
||||
_pre.w = cfg['pre_process']['width']
|
||||
_pre.h = cfg['pre_process']['height']
|
||||
_pre.aspect = cfg['pre_process']['aspect']
|
||||
_pre.fps = cfg['pre_process']['fps']
|
||||
_pre.v_bitrate = cfg['pre_process']['width'] * 50
|
||||
_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.provider = cfg['out']['service_provider']
|
||||
_playout.post_comp_param = cfg['out']['post_ffmpeg_param'].split(' ')
|
||||
_playout.out_addr = cfg['out']['out_addr']
|
||||
_playout.ffmpeg_param = cfg['out']['ffmpeg_param'].split(' ')
|
||||
|
||||
_init.load = False
|
||||
|
||||
@ -782,7 +783,7 @@ def gen_dummy(duration):
|
||||
return [
|
||||
'-f', 'lavfi', '-i',
|
||||
'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(
|
||||
duration)
|
||||
@ -989,7 +990,7 @@ def pre_audio_codec():
|
||||
s302m has higher quality, but is experimental
|
||||
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'
|
||||
return ['-c:a', acodec, '-b:a', '384k', '-ar', '48000', '-ac', '2']
|
||||
else:
|
||||
|
Loading…
x
Reference in New Issue
Block a user