commit
677a3ddb0a
@ -6,7 +6,7 @@
|
||||
|
||||
The purpose with ffplayout is to provide a 24/7 broadcasting solution that plays a *json* playlist for every day, while keeping the current playlist editable.
|
||||
|
||||
**Check [ffplayout-gui](https://github.com/ffplayout/ffplayout-gui): web-based GUI for ffplayout**
|
||||
**Check [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend): web-based GUI for ffplayout**
|
||||
|
||||
**Features**
|
||||
-----
|
||||
|
27
ffplayout.py
27
ffplayout.py
@ -21,7 +21,7 @@
|
||||
import os
|
||||
from pydoc import locate
|
||||
|
||||
from ffplayout.utils import _playout, validate_ffmpeg_libs
|
||||
from ffplayout.utils import _playout, stdin_args, validate_ffmpeg_libs
|
||||
|
||||
try:
|
||||
if os.name != 'posix':
|
||||
@ -37,18 +37,25 @@ except ImportError:
|
||||
|
||||
def main():
|
||||
"""
|
||||
pipe ffmpeg pre-process to final ffmpeg post-process,
|
||||
or play with ffplay
|
||||
play out depending on output mode
|
||||
"""
|
||||
|
||||
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 stdin_args.mode:
|
||||
output = locate('ffplayout.output.{}.output'.format(stdin_args.mode))
|
||||
output()
|
||||
|
||||
output()
|
||||
else:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
output_dir = os.path.join(script_dir, 'ffplayout', 'output')
|
||||
|
||||
for output in os.listdir(output_dir):
|
||||
if os.path.isfile(os.path.join(output_dir, output)) \
|
||||
and output != '__init__.py':
|
||||
mode = os.path.splitext(output)[0]
|
||||
|
||||
if mode == _playout.mode:
|
||||
output = locate('ffplayout.output.{}.output'.format(mode))
|
||||
output()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -119,12 +119,11 @@ out:
|
||||
-b:a 128k
|
||||
stream_output: >-
|
||||
-flags +global_header
|
||||
-f flv "rtmp://localhost/live/stream"
|
||||
-f flv rtmp://localhost/live/stream
|
||||
hls_output: >-
|
||||
-flags +cgop
|
||||
-f hls
|
||||
-hls_time 6
|
||||
-hls_list_size 600
|
||||
-hls_delete_threshold 30
|
||||
-hls_flags append_list+delete_segments+omit_endlist+program_date_time
|
||||
/var/www/srs/live/stream.m3u8
|
||||
-hls_segment_filename /var/www/srs/live/stream-%09d.ts /var/www/srs/live/stream.m3u8
|
||||
|
@ -31,11 +31,14 @@ from .utils import _global, _pre, _text
|
||||
|
||||
def text_filter():
|
||||
filter_chain = []
|
||||
font = ''
|
||||
|
||||
if _text.add_text and _text.over_pre:
|
||||
if _text.fontfile and os.path.isfile(_text.fontfile):
|
||||
font = ":fontfile='{}'".format(_text.fontfile)
|
||||
filter_chain = [
|
||||
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
|
||||
_text.address.replace(':', '\\:'), _text.fontfile)]
|
||||
"null,zmq=b=tcp\\\\://'{}',drawtext=text=''{}".format(
|
||||
_text.address.replace(':', '\\:'), font)]
|
||||
|
||||
return filter_chain
|
||||
|
||||
|
@ -26,13 +26,13 @@ from watchdog.events import PatternMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from .filters import build_filtergraph
|
||||
from .utils import MediaProbe, _storage, messenger, stdin_args
|
||||
|
||||
from .utils import MediaProbe, _current, _ff, _storage, messenger, stdin_args
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# folder watcher
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MediaStore:
|
||||
"""
|
||||
fill media list for playing
|
||||
@ -84,9 +84,10 @@ class MediaWatcher:
|
||||
|
||||
def __init__(self, media):
|
||||
self._media = media
|
||||
self.extensions = ['*{}'.format(ext) for ext in _storage.extensions]
|
||||
|
||||
self.event_handler = PatternMatchingEventHandler(
|
||||
patterns=_storage.extensions)
|
||||
patterns=self.extensions)
|
||||
self.event_handler.on_created = self.on_created
|
||||
self.event_handler.on_moved = self.on_moved
|
||||
self.event_handler.on_deleted = self.on_deleted
|
||||
@ -115,12 +116,18 @@ class MediaWatcher:
|
||||
messenger.info('Move file from "{}" to "{}"'.format(event.src_path,
|
||||
event.dest_path))
|
||||
|
||||
if _current.clip == event.src_path:
|
||||
_ff.decoder.terminate()
|
||||
|
||||
def on_deleted(self, event):
|
||||
self._media.remove(event.src_path)
|
||||
|
||||
messenger.info(
|
||||
'Remove file from media list: "{}"'.format(event.src_path))
|
||||
|
||||
if _current.clip == event.src_path:
|
||||
_ff.decoder.terminate()
|
||||
|
||||
def stop(self):
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
|
@ -4,7 +4,7 @@ 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,
|
||||
from ffplayout.utils import (_current, _ff, _log, _playlist, _pre, _text,
|
||||
ffmpeg_stderr_reader, messenger, pre_audio_codec,
|
||||
stdin_args, terminate_processes)
|
||||
|
||||
@ -64,6 +64,7 @@ def output():
|
||||
else:
|
||||
current_file = src_cmd[3]
|
||||
|
||||
_current.clip = current_file
|
||||
messenger.info('Play: "{}"'.format(current_file))
|
||||
|
||||
with Popen([
|
||||
|
@ -6,7 +6,7 @@ from threading import Thread
|
||||
|
||||
from ffplayout.folder import GetSourceFromFolder, MediaStore, MediaWatcher
|
||||
from ffplayout.playlist import GetSourceFromPlaylist
|
||||
from ffplayout.utils import (_ff, _log, _playlist, _playout,
|
||||
from ffplayout.utils import (_current, _ff, _log, _playlist, _playout,
|
||||
ffmpeg_stderr_reader, get_date, messenger,
|
||||
stdin_args, terminate_processes)
|
||||
|
||||
@ -61,6 +61,7 @@ def output():
|
||||
else:
|
||||
current_file = src_cmd[3]
|
||||
|
||||
_current.clip = current_file
|
||||
messenger.info('Play: "{}"'.format(current_file))
|
||||
cmd = [
|
||||
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
|
||||
|
@ -4,8 +4,8 @@ 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,
|
||||
from ffplayout.utils import (_current, _ff, _log, _playlist, _playout, _pre,
|
||||
_text, ffmpeg_stderr_reader, get_date, messenger,
|
||||
pre_audio_codec, stdin_args, terminate_processes)
|
||||
|
||||
_WINDOWS = os.name == 'nt'
|
||||
@ -72,6 +72,7 @@ def output():
|
||||
else:
|
||||
current_file = src_cmd[3]
|
||||
|
||||
_current.clip = current_file
|
||||
messenger.info('Play: "{}"'.format(current_file))
|
||||
|
||||
with Popen([
|
||||
|
@ -33,6 +33,7 @@ from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from shutil import which
|
||||
from subprocess import STDOUT, CalledProcessError, check_output
|
||||
from threading import Thread
|
||||
from types import SimpleNamespace
|
||||
@ -51,10 +52,6 @@ stdin_parser.add_argument(
|
||||
'-c', '--config', help='file path to ffplayout.conf'
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-d', '--desktop', help='preview on desktop', action='store_true'
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-f', '--folder', help='play folder content'
|
||||
)
|
||||
@ -67,6 +64,10 @@ stdin_parser.add_argument(
|
||||
'-i', '--loop', help='loop playlist infinitely', action='store_true'
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-m', '--mode', help='set output mode: desktop, hls, stream'
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-p', '--playlist', help='path from playlist'
|
||||
)
|
||||
@ -121,6 +122,7 @@ _playout = SimpleNamespace()
|
||||
|
||||
_init = SimpleNamespace(load=True)
|
||||
_ff = SimpleNamespace(decoder=None, encoder=None)
|
||||
_current = SimpleNamespace(clip=None)
|
||||
_global = SimpleNamespace(time_delta=0)
|
||||
|
||||
|
||||
@ -216,8 +218,9 @@ def load_config():
|
||||
_pre.h = cfg['processing']['height']
|
||||
_pre.aspect = cfg['processing']['aspect']
|
||||
_pre.fps = cfg['processing']['fps']
|
||||
_pre.v_bitrate = cfg['processing']['width'] * 50
|
||||
_pre.v_bufsize = cfg['processing']['width'] * 50 / 2
|
||||
_pre.v_bitrate = cfg['processing']['width'] * \
|
||||
cfg['processing']['height'] / 10
|
||||
_pre.v_bufsize = _pre.v_bitrate / 2
|
||||
_pre.realtime = cfg['processing']['use_realtime']
|
||||
|
||||
_playout.mode = cfg['out']['mode']
|
||||
@ -448,14 +451,26 @@ messenger = Messenger()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# check ffmpeg libs
|
||||
# check binaries and ffmpeg libs
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def is_in_system(name):
|
||||
"""
|
||||
Check whether name is on PATH and marked as executable
|
||||
"""
|
||||
if which(name) is None:
|
||||
messenger.error('{} is not found on system'.format(name))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ffmpeg_libs():
|
||||
"""
|
||||
check which external libs are compiled in ffmpeg,
|
||||
for using them later
|
||||
"""
|
||||
is_in_system('ffmpeg')
|
||||
is_in_system('ffprobe')
|
||||
|
||||
cmd = ['ffmpeg', '-filters']
|
||||
libs = []
|
||||
filters = []
|
||||
@ -491,10 +506,6 @@ def validate_ffmpeg_libs():
|
||||
if 'libfdk-aac' not in FF_LIBS['libs']:
|
||||
playout_logger.warning(
|
||||
'ffmpeg contains no libfdk-aac! No high quality aac...')
|
||||
if 'libtwolame' not in FF_LIBS['libs']:
|
||||
playout_logger.warning(
|
||||
'ffmpeg contains no libtwolame!'
|
||||
' Loudness correction use mp2 audio codec...')
|
||||
if 'tpad' not in FF_LIBS['filters']:
|
||||
playout_logger.error('ffmpeg contains no tpad filter!')
|
||||
if 'zmq' not in FF_LIBS['filters']:
|
||||
@ -1000,7 +1011,6 @@ def pre_audio_codec():
|
||||
and works not well together with the loudnorm filter
|
||||
"""
|
||||
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']
|
||||
return ['-c:a', 'mp2', '-b:a', '384k', '-ar', '48000', '-ac', '2']
|
||||
else:
|
||||
return ['-c:a', 's302m', '-strict', '-2', '-ar', '48000', '-ac', '2']
|
||||
|
Loading…
Reference in New Issue
Block a user