diff options
41 files changed, 3136 insertions, 2855 deletions
diff --git a/check_pep8.sh b/check_pep8.sh new file mode 100755 index 0000000..d988d09 --- /dev/null +++ b/check_pep8.sh @@ -0,0 +1,3 @@ +#!/bin/sh +pep8 --ignore=E402 . +[ $? = 0 ] && echo "Success!" || echo "There were some warnings." diff --git a/example-scripts/control-server/generate-cut-list.py b/example-scripts/control-server/generate-cut-list.py index 959ab58..394865b 100755 --- a/example-scripts/control-server/generate-cut-list.py +++ b/example-scripts/control-server/generate-cut-list.py @@ -6,16 +6,16 @@ import sys host = 'localhost' port = 9999 -conn = socket.create_connection( (host, port) ) +conn = socket.create_connection((host, port)) fd = conn.makefile('rw') for line in fd: - words = line.rstrip('\n').split(' ') + words = line.rstrip('\n').split(' ') - signal = words[0] - args = words[1:] + signal = words[0] + args = words[1:] - if signal == 'message' and args[0] == 'cut': - ts = datetime.datetime.now().strftime("%Y-%m-%d/%H_%M_%S") - print(ts) - sys.stdout.flush() + if signal == 'message' and args[0] == 'cut': + ts = datetime.datetime.now().strftime("%Y-%m-%d/%H_%M_%S") + print(ts) + sys.stdout.flush() diff --git a/example-scripts/ffmpeg/record-all-audio-streams.py b/example-scripts/ffmpeg/record-all-audio-streams.py index f5bff8f..29cfd6b 100755 --- a/example-scripts/ffmpeg/record-all-audio-streams.py +++ b/example-scripts/ffmpeg/record-all-audio-streams.py @@ -14,29 +14,28 @@ host = 'localhost' port = 9999 log.info('Connecting to %s:%u', host, port) -conn = socket.create_connection( (host, port) ) +conn = socket.create_connection((host, port)) fd = conn.makefile('rw') log.info('Fetching Config from Server') -fd.write("get_config\n"); +fd.write("get_config\n") fd.flush() for line in fd: - if line.startswith('server_config'): - words = line.split(' ') - args = words[1:] - server_config_json = " ".join(args) - log.info('Received Config from Server') - break + if line.startswith('server_config'): + [cmd, arg] = line.split(' ', 1) + server_config_json = arg + log.info('Received Config from Server') + break log.info('Parsing Server-Config') server_config = json.loads(server_config_json) + def getlist(self, section, option): - return [x.strip() for x in self.get(section, option).split(',')] + return [x.strip() for x in self.get(section, option).split(',')] SafeConfigParser.getlist = getlist - config = SafeConfigParser() config.read_dict(server_config) @@ -45,26 +44,26 @@ sources = config.getlist('mix', 'sources') inputs = [] maps = [] for idx, source in enumerate(sources): - inputs.append('-i tcp://localhost:%u' % (13000+idx)) - maps.append('-map %u:a -metadata:s:a:%u language=und' % (idx, idx)) + inputs.append('-i tcp://localhost:{:d}'.format(13000 + idx)) + maps.append('-map {0:d}:a -metadata:s:a:{0:d} language=und'.format(idx)) try: - output = sys.argv[1] + output = sys.argv[1] except: - output = 'output.ts' + output = 'output.ts' cmd = """ ffmpeg \ - -hide_banner - -y -nostdin - %s - -ac 2 -channel_layout stereo - %s - -c:a mp2 -b:a 192k -ac:a 2 -ar:a 48000 - -flags +global_header -flags +ilme+ildct - -f mpegts - %s -""" % (' '.join(inputs), ' '.join(maps), output) + -hide_banner + -y -nostdin + {} + -ac 2 -channel_layout stereo + {} + -c:a mp2 -b:a 192k -ac:a 2 -ar:a 48000 + -flags +global_header -flags +ilme+ildct + -f mpegts + {} +""".format(' '.join(inputs), ' '.join(maps), output) log.info('running command:\n%s', cmd) args = shlex.split(cmd) p = subprocess.run(args) diff --git a/example-scripts/gstreamer/ingest.py b/example-scripts/gstreamer/ingest.py index a9e06c4..efb7be2 100755 --- a/example-scripts/gstreamer/ingest.py +++ b/example-scripts/gstreamer/ingest.py @@ -9,7 +9,6 @@ Features: Mix and match audio and video sources muxed into one streem. Can display video locally, including frame count and fps. Defaults to test audio and video sent to local core. - """ import argparse @@ -27,8 +26,8 @@ GObject.threads_init() Gst.init([]) # this is to use the same code tha gui uses to get config from core -sys.path.insert(0, '../..' ) -sys.path.insert(0, '.' ) +sys.path.insert(0, '../..') +sys.path.insert(0, '.') import voctogui.lib.connection as Connection @@ -39,142 +38,143 @@ def mk_video_src(args, videocaps): video_device = "device={}".format(args.video_dev) \ if args.video_dev else "" - monitor = """tee name=t ! queue ! - videoconvert ! fpsdisplaysink sync=false - t. ! queue !""" \ - if args.monitor else "" + monitor = """ + tee name=t ! + queue ! + videoconvert ! + fpsdisplaysink sync=false + t. ! queue ! + """ if args.monitor else "" if args.video_source == 'dv': video_src = """ - dv1394src name=videosrc {video_device}! - dvdemux name=demux ! - queue ! - dvdec ! - {monitor} - deinterlace mode=1 ! - videoconvert ! - videorate ! - videoscale ! - """ - + dv1394src name=videosrc {video_device} ! + dvdemux name=demux ! + queue ! + dvdec ! + {monitor} + deinterlace mode=1 ! + videoconvert ! + videorate ! + videoscale ! + """ + elif args.video_source == 'hdv': video_src = """ hdv1394src {video_device} do-timestamp=true name=videosrc ! - tsdemux name=demux! - queue ! - decodebin ! - {monitor} - deinterlace mode=1 ! - videorate ! - videoscale ! - videoconvert ! - """ + tsdemux name=demux! + queue ! + decodebin ! + {monitor} + deinterlace mode=1 ! + videorate ! + videoscale ! + videoconvert ! + """ elif args.video_source == 'hdmi2usb': # https://hdmi2usb.tv # Note: this code works with 720p video_src = """ v4l2src {video_device} name=videosrc ! - queue ! - image/jpeg,width=1280,height=720 ! - jpegdec ! - {monitor} - videoconvert ! - videorate ! - """ + queue ! + image/jpeg,width=1280,height=720 ! + jpegdec ! + {monitor} + videoconvert ! + videorate ! + """ elif args.video_source == 'ximage': video_src = """ - ximagesrc name=videosrc - use-damage=false ! - {monitor} - videoconvert ! - videorate ! - videoscale ! - """ - # startx=0 starty=0 endx=1919 endy=1079 ! + ximagesrc name=videosrc + use-damage=false ! + {monitor} + videoconvert ! + videorate ! + videoscale ! + """ + # startx=0 starty=0 endx=1919 endy=1079 ! elif args.video_source == 'blackmagichdmi': video_src = """ decklinkvideosrc mode=17 connection=2 ! - {monitor} - videoconvert ! - videorate ! - videoscale ! - """ + {monitor} + videoconvert ! + videorate ! + videoscale ! + """ elif args.video_source == 'test': video_src = """ - videotestsrc name=videosrc - pattern=ball + videotestsrc name=videosrc + pattern=ball foreground-color=0x00ff0000 background-color=0x00440000 ! - {monitor} - """ + {monitor} + """ - video_src = video_src.format( - video_device=video_device, - monitor=monitor) + video_src = video_src.format(video_device=video_device, monitor=monitor) video_src += videocaps + "!\n" return video_src -def mk_audio_src(args, audiocaps): +def mk_audio_src(args, audiocaps): audio_device = "device={}".format(args.audio_dev) \ if args.audio_dev else "" - if args.audio_source in [ 'dv', 'hdv' ]: + if args.audio_source in ['dv', 'hdv']: # this only works if video is from DV also. # or some gst source that gets demux ed audio_src = """ demux. ! - audioconvert ! - """ + audioconvert ! + """ elif args.audio_source == 'pulse': audio_src = """ - pulsesrc {audio_device} name=audiosrc ! - """.format(audio_device=audio_device) + pulsesrc {audio_device} name=audiosrc ! + """.format(audio_device=audio_device) elif args.audio_source == 'alsa': audio_src = """ - alsasrc {audio_device} name=audiosrc ! - """.format(audio_device=audio_device) + alsasrc {audio_device} name=audiosrc ! + """.format(audio_device=audio_device) elif args.audio_source == 'blackmagichdmi': audio_src = """ decklinkaudiosrc ! - """ + """ elif args.audio_source == 'test': audio_src = """ audiotestsrc name=audiosrc freq=330 ! - """ + """ audio_src += audiocaps + "!\n" return audio_src -def mk_mux(args): +def mk_mux(args): mux = """ - mux. - matroskamux name=mux ! - """ + mux. + matroskamux name=mux ! + """ return mux + def mk_client(args): core_ip = socket.gethostbyname(args.host) - client = """ - tcpclientsink host={host} port={port} - """.format(host=core_ip, port=args.port) + client = """ + tcpclientsink host={host} port={port} + """.format(host=core_ip, port=args.port) return client def mk_pipeline(args, server_caps): - video_src = mk_video_src(args, server_caps['videocaps']) audio_src = mk_audio_src(args, server_caps['audiocaps']) mux = mk_mux(args) @@ -183,22 +183,21 @@ def mk_pipeline(args, server_caps): pipeline = video_src + "mux.\n" + audio_src + mux + client # remove blank lines to make it more human readable - pipeline = pipeline.replace("\n\n","\n") + pipeline = pipeline.replace("\n\n", "\n") return pipeline -def get_server_caps(): - +def get_server_caps(): # fetch config from server server_config = Connection.fetchServerConfig() server_caps = {'videocaps': server_config['mix']['videocaps'], - 'audiocaps': server_config['mix']['audiocaps']} + 'audiocaps': server_config['mix']['audiocaps']} return server_caps -def run_pipeline(pipeline, args): +def run_pipeline(pipeline, args): core_ip = socket.gethostbyname(args.host) clock = GstNet.NetClientClock.new('voctocore', core_ip, 9998, 0) @@ -212,17 +211,16 @@ def run_pipeline(pipeline, args): senderPipeline.use_clock(clock) src = senderPipeline.get_by_name('src') - def on_eos(self, bus, message): + def on_eos(bus, message): print('Received EOS-Signal') sys.exit(1) - def on_error(self, bus, message): + def on_error(bus, message): print('Received Error-Signal') (error, debug) = message.parse_error() print('Error-Details: #%u: %s' % (error.code, debug)) sys.exit(1) - # Binding End-of-Stream-Signal on Source-Pipeline senderPipeline.bus.add_signal_watch() senderPipeline.bus.connect("message::eos", on_eos) @@ -230,7 +228,7 @@ def run_pipeline(pipeline, args): print("playing") senderPipeline.set_state(Gst.State.PLAYING) - + mainloop = GObject.MainLoop() try: mainloop.run() @@ -243,65 +241,64 @@ def run_pipeline(pipeline, args): return -def get_args(): +def get_args(): parser = argparse.ArgumentParser( - description='''Vocto-ingest Client with Net-time support. + description='''Vocto-ingest Client with Net-time support. Gst caps are retrieved from the server. Run without parameters: send test av to localhost:10000 - ''') - + ''' + ) + parser.add_argument('-v', '--verbose', action='count', default=0, - help="Also print INFO and DEBUG messages.") + help="Also print INFO and DEBUG messages.") - parser.add_argument( '--video-source', action='store', - choices=[ - 'dv', 'hdv', 'hdmi2usb', 'blackmagichdmi', - 'ximage', - 'test', ], - default='test', - help="Where to get video from") + parser.add_argument('--video-source', action='store', + choices=['dv', 'hdv', 'hdmi2usb', + 'blackmagichdmi', 'ximage', 'test'], + default='test', + help="Where to get video from") - parser.add_argument( '--video-dev', action='store', - help="video device") + parser.add_argument('--video-dev', action='store', + help="video device") - parser.add_argument( '--audio-source', action='store', - choices=['dv', 'alsa', 'pulse', 'blackmagichdmi', 'test'], - default='test', - help="Where to get audio from") + parser.add_argument('--audio-source', action='store', + choices=['dv', 'alsa', 'pulse', + 'blackmagichdmi', 'test'], + default='test', + help="Where to get audio from") - parser.add_argument( '--audio-dev', action='store', - default='hw:CARD=CODEC', - help="for alsa/pulse, audio device") - # maybe hw:1,0 + # maybe hw:1,0 + parser.add_argument('--audio-dev', action='store', + default='hw:CARD=CODEC', + help="for alsa/pulse, audio device") - parser.add_argument( '--audio-delay', action='store', - default='10', - help="ms to delay audio") + parser.add_argument('--audio-delay', action='store', + default='10', + help="ms to delay audio") parser.add_argument('-m', '--monitor', action='store_true', - help="fps display sink") + help="fps display sink") - parser.add_argument( '--host', action='store', - default='localhost', - help="hostname of vocto core") + parser.add_argument('--host', action='store', + default='localhost', + help="hostname of vocto core") - parser.add_argument( '--port', action='store', - default='10000', - help="port of vocto core") + parser.add_argument('--port', action='store', + default='10000', + help="port of vocto core") args = parser.parse_args() return args - + def main(): - args = get_args() core_ip = socket.gethostbyname(args.host) # establish a synchronus connection to server - Connection.establish(core_ip) + Connection.establish(core_ip) server_caps = get_server_caps() @@ -310,6 +307,5 @@ def main(): run_pipeline(pipeline, args) - if __name__ == '__main__': main() diff --git a/example-scripts/gstreamer/source-background-loop.py b/example-scripts/gstreamer/source-background-loop.py index 2f13edf..4d1dae1 100755 --- a/example-scripts/gstreamer/source-background-loop.py +++ b/example-scripts/gstreamer/source-background-loop.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 -import os, sys, gi, signal +import os +import sys +import gi +import signal gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject @@ -8,66 +11,71 @@ from gi.repository import Gst, GObject GObject.threads_init() Gst.init([]) + class LoopSource(object): - def __init__(self, settings): - # it works much better with a local file - pipeline = """ - uridecodebin name=src uri=http://c3voc.mazdermind.de/testfiles/bg.ts ! - videoscale ! - videoconvert ! - video/x-raw,format=I420,width={WIDTH},height={HEIGHT},framerate={FRAMERATE}/1,pixel-aspect-ratio=1/1 ! - matroskamux ! - tcpclientsink host=localhost port=16000 - """.format_map(settings) - - print('starting pipeline '+pipeline) - self.senderPipeline = Gst.parse_launch(pipeline) - self.src = self.senderPipeline.get_by_name('src') - - # Binding End-of-Stream-Signal on Source-Pipeline - self.senderPipeline.bus.add_signal_watch() - self.senderPipeline.bus.connect("message::eos", self.on_eos) - self.senderPipeline.bus.connect("message::error", self.on_error) - - print("playing") - self.senderPipeline.set_state(Gst.State.PLAYING) - - - def on_eos(self, bus, message): - print('Received EOS-Signal, Seeking to start') - self.src.seek( - 1.0, # rate (float) - Gst.Format.TIME, # format (Gst.Format) - Gst.SeekFlags.FLUSH, # flags (Gst.SeekFlags) - Gst.SeekType.SET, # start_type (Gst.SeekType) - 0, # start (int) - Gst.SeekType.NONE, # stop_type (Gst.SeekType) - 0 # stop (int) - ) - - def on_error(self, bus, message): - print('Received Error-Signal') - (error, debug) = message.parse_error() - print('Error-Details: #%u: %s' % (error.code, debug)) - sys.exit(1) + + def __init__(self, settings): + # it works much better with a local file + pipeline = """ + uridecodebin name=src + uri=http://c3voc.mazdermind.de/testfiles/bg.ts ! + videoscale ! + videoconvert ! + video/x-raw,format=I420,width={WIDTH},height={HEIGHT}, + framerate={FRAMERATE}/1,pixel-aspect-ratio=1/1 ! + matroskamux ! + tcpclientsink host=localhost port=16000 + """.format_map(settings) + + print('starting pipeline ' + pipeline) + self.senderPipeline = Gst.parse_launch(pipeline) + self.src = self.senderPipeline.get_by_name('src') + + # Binding End-of-Stream-Signal on Source-Pipeline + self.senderPipeline.bus.add_signal_watch() + self.senderPipeline.bus.connect("message::eos", self.on_eos) + self.senderPipeline.bus.connect("message::error", self.on_error) + + print("playing") + self.senderPipeline.set_state(Gst.State.PLAYING) + + def on_eos(self, bus, message): + print('Received EOS-Signal, Seeking to start') + self.src.seek( + 1.0, # rate (float) + Gst.Format.TIME, # format (Gst.Format) + Gst.SeekFlags.FLUSH, # flags (Gst.SeekFlags) + Gst.SeekType.SET, # start_type (Gst.SeekType) + 0, # start (int) + Gst.SeekType.NONE, # stop_type (Gst.SeekType) + 0 # stop (int) + ) + + def on_error(self, bus, message): + print('Received Error-Signal') + (error, debug) = message.parse_error() + print('Error-Details: #%u: %s' % (error.code, debug)) + sys.exit(1) + def main(): - signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.SIG_DFL) - config = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../config.sh') - with open(config, 'r') as config: - lines = [ line.strip() for line in config if line[0] != '#' ] - pairs = [ line.split('=', 1) for line in lines ] - settings = { pair[0]: pair[1] for pair in pairs } + config = os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../config.sh') + with open(config, 'r') as config: + lines = [line.strip() for line in config if line[0] != '#'] + pairs = [line.split('=', 1) for line in lines] + settings = {pair[0]: pair[1] for pair in pairs} - src = LoopSource(settings) + src = LoopSource(settings) - mainloop = GObject.MainLoop() - try: - mainloop.run() - except KeyboardInterrupt: - print('Terminated via Ctrl-C') + mainloop = GObject.MainLoop() + try: + mainloop.run() + except KeyboardInterrupt: + print('Terminated via Ctrl-C') if __name__ == '__main__': - main() + main() diff --git a/example-scripts/gstreamer/source-nostream-music-from-folder.py b/example-scripts/gstreamer/source-nostream-music-from-folder.py index a176a7d..c804182 100755 --- a/example-scripts/gstreamer/source-nostream-music-from-folder.py +++ b/example-scripts/gstreamer/source-nostream-music-from-folder.py @@ -1,6 +1,12 @@ #!/usr/bin/env python3 -import os, sys, gi, signal, random -import argparse, logging, pyinotify +import os +import sys +import gi +import signal +import random +import argparse +import logging +import pyinotify gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject, GLib @@ -9,189 +15,206 @@ from gi.repository import Gst, GObject, GLib GObject.threads_init() Gst.init([]) -class Directory(object): - def __init__(self, path): - self.log = logging.getLogger('Directory') - self.path = path - self.scheduled = False - self.rescan() - - self.log.debug('setting up inotify watch for %s', self.path) - wm = pyinotify.WatchManager() - notifier = pyinotify.Notifier(wm, - timeout=10, - default_proc_fun=self.inotify_callback) - - wm.add_watch( - self.path, - #pyinotify.ALL_EVENTS, - pyinotify.IN_DELETE | pyinotify.IN_CREATE | pyinotify.IN_MODIFY, - rec=True) - - GLib.io_add_watch( - notifier._fd, - GLib.IO_IN, - self.io_callback, - notifier) - - def inotify_callback(self, notifier): - self.log.info('inotify callback %s: %s', notifier.maskname, notifier.pathname) - if not self.scheduled: - self.scheduled = True - GLib.timeout_add(100, self.rescan) - return True - - def io_callback(self, source, condition, notifier): - notifier.process_events() - while notifier.check_events(): - notifier.read_events() - notifier.process_events() - - return True - def is_playable_file(self, filepath): - root, ext = os.path.splitext(filepath) - return ext in ['.mp3', '.ogg', '.oga', '.wav', '.m4a', '.flac', 'self.opus'] - - def rescan(self): - self.log.info('scanning directory %s', self.path) - self.scheduled = False - - all_files = [] - - for root, dirs, files in os.walk(self.path): - files = filter(self.is_playable_file, files) - files = map(lambda f: os.path.join(root, f), files) - files = list(files) - - self.log.debug('found directory %s: %u playable file(s)', root, len(files)) - all_files.extend(files) +class Directory(object): - self.log.info('found %u playable files', len(all_files)) - self.files = all_files + def __init__(self, path): + self.log = logging.getLogger('Directory') + self.path = path + self.scheduled = False + self.rescan() + + self.log.debug('setting up inotify watch for %s', self.path) + wm = pyinotify.WatchManager() + notifier = pyinotify.Notifier( + wm, + timeout=10, + default_proc_fun=self.inotify_callback + ) + + wm.add_watch( + self.path, + # pyinotify.ALL_EVENTS, + pyinotify.IN_DELETE | pyinotify.IN_CREATE | pyinotify.IN_MODIFY, + rec=True + ) + + GLib.io_add_watch( + notifier._fd, + GLib.IO_IN, + self.io_callback, + notifier + ) + + def inotify_callback(self, notifier): + self.log.info('inotify callback %s: %s', + notifier.maskname, notifier.pathname) + if not self.scheduled: + self.scheduled = True + GLib.timeout_add(100, self.rescan) + return True + + def io_callback(self, source, condition, notifier): + notifier.process_events() + while notifier.check_events(): + notifier.read_events() + notifier.process_events() + + return True + + def is_playable_file(self, filepath): + root, ext = os.path.splitext(filepath) + return ext in ['.mp3', '.ogg', '.oga', '.wav', '.m4a', + '.flac', 'self.opus'] + + def rescan(self): + self.log.info('scanning directory %s', self.path) + self.scheduled = False + + all_files = [] + + for root, dirs, files in os.walk(self.path): + files = filter(self.is_playable_file, files) + files = map(lambda f: os.path.join(root, f), files) + files = list(files) + + self.log.debug('found directory %s: %u playable file(s)', + root, len(files)) + all_files.extend(files) + + self.log.info('found %u playable files', len(all_files)) + self.files = all_files + + def get_random_file(self): + return random.choice(self.files) + + def get_random_uri(self): + return 'file://' + self.get_random_file() - def get_random_file(self): - return random.choice(self.files) - def get_random_uri(self): - return 'file://'+self.get_random_file() +class LoopSource(object): + def __init__(self, directory): + self.log = logging.getLogger('LoopSource') + self.directory = directory + + pipeline = """ + audioresample name=join ! + audioconvert ! + audio/x-raw,format=S16LE,channels=2,rate=48000, + layout=interleaved ! + matroskamux ! + tcpclientsink host=localhost port=18000 + """ + + # Parsing Pipeline + self.log.debug('creating pipeline\n%s', pipeline) + self.pipeline = Gst.parse_launch(pipeline) + + # Selecting inital URI + inital_uri = self.directory.get_random_uri() + self.log.info('initial track %s', inital_uri) + + # Create decoder-element + self.src = Gst.ElementFactory.make('uridecodebin', None) + self.src.set_property('uri', inital_uri) + self.src.connect('pad-added', self.on_pad_added) + self.pipeline.add(self.src) + + # Save pad on the Join-Element + self.joinpad = self.pipeline.get_by_name('join').get_static_pad('sink') + + # Binding End-of-Stream-Signal on Source-Pipeline + self.pipeline.bus.add_signal_watch() + self.pipeline.bus.connect("message::eos", self.on_eos) + self.pipeline.bus.connect("message::error", self.on_error) + + self.log.debug('setting pipeline to playing') + self.pipeline.set_state(Gst.State.PLAYING) + + def on_pad_added(self, src, pad): + self.log.debug('new pad on decoder, setting pad-probe') + pad.add_probe( + Gst.PadProbeType.EVENT_DOWNSTREAM | Gst.PadProbeType.BLOCK, + self.on_pad_event + ) + if self.joinpad.is_linked(): + self.log.debug('unlinking with joinpad') + self.joinpad.unlink(self.joinpad.get_peer()) + + clock = self.pipeline.get_clock() + if clock: + runtime = clock.get_time() - self.pipeline.get_base_time() + self.log.debug('setting pad offset to pipeline runtime: %sns', + runtime) + pad.set_offset(runtime) + + self.log.debug('linking with joinpad') + pad.link(self.joinpad) + + def on_pad_event(self, pad, info): + event = info.get_event() + self.log.debug('event %s on pad %s', event.type, pad) + + if event.type == Gst.EventType.EOS: + self.log.debug('scheduling next track and dropping EOS-Event') + GObject.idle_add(self.next_track) + return Gst.PadProbeReturn.DROP + + return Gst.PadProbeReturn.PASS + + def next_track(self): + next_uri = self.directory.get_random_uri() + self.log.info('next track %s', next_uri) + + self.src.set_state(Gst.State.READY) + self.src.set_property('uri', next_uri) + self.src.set_state(Gst.State.PLAYING) + return False + + def on_eos(self, bus, message): + self.log.info('received EOS-Event on bus, exiting') + sys.exit(1) + + def on_error(self, bus, message): + self.log.warning('received Error-Event on bus, exiting') + (error, debug) = message.parse_error() + self.log.warning('Error-Details: #%u: %s', error.code, debug) + sys.exit(1) -class LoopSource(object): - def __init__(self, directory): - self.log = logging.getLogger('LoopSource') - self.directory = directory - - pipeline = """ - audioresample name=join ! - audioconvert ! - audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 ! - matroskamux ! - tcpclientsink host=localhost port=18000 - """ - - # Parsing Pipeline - self.log.debug('creating pipeline\n%s', pipeline) - self.pipeline = Gst.parse_launch(pipeline) - - # Selecting inital URI - inital_uri = self.directory.get_random_uri() - self.log.info('initial track %s', inital_uri) - - # Create decoder-element - self.src = Gst.ElementFactory.make('uridecodebin', None) - self.src.set_property('uri', inital_uri); - self.src.connect('pad-added', self.on_pad_added) - self.pipeline.add(self.src) - - # Save pad on the Join-Element - self.joinpad = self.pipeline.get_by_name('join').get_static_pad('sink') - - # Binding End-of-Stream-Signal on Source-Pipeline - self.pipeline.bus.add_signal_watch() - self.pipeline.bus.connect("message::eos", self.on_eos) - self.pipeline.bus.connect("message::error", self.on_error) - - self.log.debug('setting pipeline to playing') - self.pipeline.set_state(Gst.State.PLAYING) - - def on_pad_added(self, src, pad): - self.log.debug('new pad on decoder, setting pad-probe') - pad.add_probe(Gst.PadProbeType.EVENT_DOWNSTREAM | Gst.PadProbeType.BLOCK, self.on_pad_event) - if self.joinpad.is_linked(): - self.log.debug('unlinking with joinpad') - self.joinpad.unlink(self.joinpad.get_peer()) - - clock = self.pipeline.get_clock() - if clock: - runtime = clock.get_time() - self.pipeline.get_base_time() - self.log.debug('setting pad offset to pipeline runtime: %sns', runtime) - pad.set_offset(runtime) - - self.log.debug('linking with joinpad') - pad.link(self.joinpad) - - def on_pad_event(self, pad, info): - event = info.get_event() - self.log.debug('event %s on pad %s', event.type, pad) - - if event.type == Gst.EventType.EOS: - self.log.debug('scheduling next track and dropping EOS-Event') - GObject.idle_add(self.next_track) - return Gst.PadProbeReturn.DROP - - return Gst.PadProbeReturn.PASS - - def next_track(self): - next_uri = self.directory.get_random_uri() - self.log.info('next track %s', next_uri) - - self.src.set_state(Gst.State.READY) - self.src.set_property('uri', next_uri); - self.src.set_state(Gst.State.PLAYING) - return False - - def on_eos(self, bus, message): - self.log.info('received EOS-Event on bus, exiting') - sys.exit(1) - - def on_error(self, bus, message): - self.log.warning('received Error-Event on bus, exiting') - (error, debug) = message.parse_error() - self.log.warning('Error-Details: #%u: %s', error.code, debug) - sys.exit(1) def main(): - signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.SIG_DFL) - parser = argparse.ArgumentParser(description='Voctocore Music-Source') - parser.add_argument('directory') + parser = argparse.ArgumentParser(description='Voctocore Music-Source') + parser.add_argument('directory') - parser.add_argument('-v|-vv', '--verbose', action='count', default=0, - help="Also print INFO and DEBUG messages.") + parser.add_argument('-v|-vv', '--verbose', action='count', default=0, + help="Also print INFO and DEBUG messages.") - args = parser.parse_args() + args = parser.parse_args() - if args.verbose >= 2: - level = logging.DEBUG - elif args.verbose == 1: - level = logging.INFO - else: - level = logging.WARNING + if args.verbose >= 2: + level = logging.DEBUG + elif args.verbose == 1: + level = logging.INFO + else: + level = logging.WARNING - logging.basicConfig( - level=level, - format='%(levelname)8s %(name)s: %(message)s') + logging.basicConfig( + level=level, + format='%(levelname)8s %(name)s: %(message)s' + ) - directory = Directory(args.directory) - src = LoopSource(directory) + directory = Directory(args.directory) + src = LoopSource(directory) - mainloop = GObject.MainLoop() - try: - mainloop.run() - except KeyboardInterrupt: - print('Terminated via Ctrl-C') + mainloop = GObject.MainLoop() + try: + mainloop.run() + except KeyboardInterrupt: + print('Terminated via Ctrl-C') if __name__ == '__main__': - main() + main() diff --git a/example-scripts/gstreamer/source-remote-desktop-as-cam1.py b/example-scripts/gstreamer/source-remote-desktop-as-cam1.py index 7d1c7f2..4788554 100755 --- a/example-scripts/gstreamer/source-remote-desktop-as-cam1.py +++ b/example-scripts/gstreamer/source-remote-desktop-as-cam1.py @@ -1,6 +1,10 @@ #!/usr/bin/python3 -import os, sys, gi, signal -import argparse, socket +import os +import sys +import gi +import signal +import argparse +import socket gi.require_version('Gst', '1.0') from gi.repository import Gst, GstNet, GObject @@ -9,88 +13,97 @@ from gi.repository import Gst, GstNet, GObject GObject.threads_init() Gst.init([]) + class Source(object): - def __init__(self, settings): - pipeline = """ - ximagesrc use-damage=0 startx=0 starty=0 endx=1919 endy=1079 ! - queue ! - videoscale ! - videorate ! - timeoverlay ! - videoconvert ! - video/x-raw,format=I420,width={WIDTH},height={HEIGHT},framerate={FRAMERATE}/1,pixel-aspect-ratio=1/1 ! - queue ! - mux. - - pulsesrc ! - audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate={AUDIORATE} ! - queue ! - mux. - - matroskamux name=mux ! - tcpclientsink host={IP} port=10000 - """.format_map(settings) - - self.clock = GstNet.NetClientClock.new('voctocore', settings['IP'], 9998, 0) - print('obtained NetClientClock from host', self.clock) - - print('waiting for NetClientClock to sync…') - self.clock.wait_for_sync(Gst.CLOCK_TIME_NONE) - - print('starting pipeline '+pipeline) - self.senderPipeline = Gst.parse_launch(pipeline) - self.senderPipeline.use_clock(self.clock) - self.src = self.senderPipeline.get_by_name('src') - - # Binding End-of-Stream-Signal on Source-Pipeline - self.senderPipeline.bus.add_signal_watch() - self.senderPipeline.bus.connect("message::eos", self.on_eos) - self.senderPipeline.bus.connect("message::error", self.on_error) - - print("playing") - self.senderPipeline.set_state(Gst.State.PLAYING) - - - def on_eos(self, bus, message): - print('Received EOS-Signal') - sys.exit(1) - - def on_error(self, bus, message): - print('Received Error-Signal') - (error, debug) = message.parse_error() - print('Error-Details: #%u: %s' % (error.code, debug)) - sys.exit(1) + + def __init__(self, settings): + pipeline = """ + ximagesrc + use-damage=0 + startx=0 starty=0 endx=1919 endy=1079 ! + queue ! + videoscale ! + videorate ! + timeoverlay ! + videoconvert ! + video/x-raw,format=I420,width={WIDTH},height={HEIGHT}, + framerate={FRAMERATE}/1,pixel-aspect-ratio=1/1 ! + queue ! + mux. + + pulsesrc ! + audio/x-raw,format=S16LE,channels=2,rate={AUDIORATE}, + layout=interleaved ! + queue ! + mux. + + matroskamux name=mux ! + tcpclientsink host={IP} port=10000 + """.format_map(settings) + + self.clock = GstNet.NetClientClock.new('voctocore', + settings['IP'], 9998, + 0) + print('obtained NetClientClock from host', self.clock) + + print('waiting for NetClientClock to sync…') + self.clock.wait_for_sync(Gst.CLOCK_TIME_NONE) + + print('starting pipeline ' + pipeline) + self.senderPipeline = Gst.parse_launch(pipeline) + self.senderPipeline.use_clock(self.clock) + self.src = self.senderPipeline.get_by_name('src') + + # Binding End-of-Stream-Signal on Source-Pipeline + self.senderPipeline.bus.add_signal_watch() + self.senderPipeline.bus.connect("message::eos", self.on_eos) + self.senderPipeline.bus.connect("message::error", self.on_error) + + print("playing") + self.senderPipeline.set_state(Gst.State.PLAYING) + + def on_eos(self, bus, message): + print('Received EOS-Signal') + sys.exit(1) + + def on_error(self, bus, message): + print('Received Error-Signal') + (error, debug) = message.parse_error() + print('Error-Details: #%u: %s' % (error.code, debug)) + sys.exit(1) + def main(): - signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.SIG_DFL) - parser = argparse.ArgumentParser(description='Voctocore Remote-Source') - parser.add_argument('host') + parser = argparse.ArgumentParser(description='Voctocore Remote-Source') + parser.add_argument('host') - args = parser.parse_args() - print('Resolving hostname '+args.host) - addrs = [ str(i[4][0]) for i in socket.getaddrinfo(args.host, None) ] - if len(addrs) == 0: - print('Found no IPs') - sys.exit(1) + args = parser.parse_args() + print('Resolving hostname ' + args.host) + addrs = [str(i[4][0]) for i in socket.getaddrinfo(args.host, None)] + if len(addrs) == 0: + print('Found no IPs') + sys.exit(1) - print('Using IP '+addrs[0]) + print('Using IP ' + addrs[0]) - config = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../config.sh') - with open(config) as config: - lines = [ line.strip() for line in config if line[0] != '#' ] - pairs = [ line.split('=', 1) for line in lines ] - settings = { pair[0]: pair[1] for pair in pairs } + config = os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../config.sh') + with open(config) as config: + lines = [line.strip() for line in config if line[0] != '#'] + pairs = [line.split('=', 1) for line in lines] + settings = {pair[0]: pair[1] for pair in pairs} - settings['IP'] = addrs[0] + settings['IP'] = addrs[0] - src = Source(settings) - mainloop = GObject.MainLoop() - try: - mainloop.run() - except KeyboardInterrupt: - print('Terminated via Ctrl-C') + src = Source(settings) + mainloop = GObject.MainLoop() + try: + mainloop.run() + except KeyboardInterrupt: + print('Terminated via Ctrl-C') if __name__ == '__main__': - main() + main() diff --git a/example-scripts/gstreamer/source-remote-videotestsrc-as-cam1.py b/example-scripts/gstreamer/source-remote-videotestsrc-as-cam1.py index 5404add..e09f0ec 100755 --- a/example-scripts/gstreamer/source-remote-videotestsrc-as-cam1.py +++ b/example-scripts/gstreamer/source-remote-videotestsrc-as-cam1.py @@ -1,6 +1,10 @@ #!/usr/bin/python3 -import os, sys, gi, signal -import argparse, socket +import os +import sys +import gi +import signal +import argparse +import socket gi.require_version('Gst', '1.0') from gi.repository import Gst, GstNet, GObject @@ -9,83 +13,92 @@ from gi.repository import Gst, GstNet, GObject GObject.threads_init() Gst.init([]) -class Source(object): - def __init__(self, settings): - # it works much better with a local file - pipeline = """ - videotestsrc pattern=ball foreground-color=0x00ff0000 background-color=0x00440000 ! - timeoverlay ! - video/x-raw,format=I420,width=1280,height=720,framerate=25/1,pixel-aspect-ratio=1/1 ! - mux. - - audiotestsrc freq=330 ! - audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 ! - mux. - - matroskamux name=mux ! - tcpclientsink host={IP} port=10000 - """.format_map(settings) - - self.clock = GstNet.NetClientClock.new('voctocore', settings['IP'], 9998, 0) - print('obtained NetClientClock from host', self.clock) - - print('waiting for NetClientClock to sync…') - self.clock.wait_for_sync(Gst.CLOCK_TIME_NONE) - - print('starting pipeline '+pipeline) - self.senderPipeline = Gst.parse_launch(pipeline) - self.senderPipeline.use_clock(self.clock) - self.src = self.senderPipeline.get_by_name('src') - - # Binding End-of-Stream-Signal on Source-Pipeline - self.senderPipeline.bus.add_signal_watch() - self.senderPipeline.bus.connect("message::eos", self.on_eos) - self.senderPipeline.bus.connect("message::error", self.on_error) - - print("playing") - self.senderPipeline.set_state(Gst.State.PLAYING) +class Source(object): - def on_eos(self, bus, message): - print('Received EOS-Signal') - sys.exit(1) + def __init__(self, settings): + # it works much better with a local file + pipeline = """ + videotestsrc + pattern=ball + foreground-color=0x00ff0000 background-color=0x00440000 ! + timeoverlay ! + video/x-raw,format=I420,width=1280,height=720, + framerate=25/1,pixel-aspect-ratio=1/1 ! + mux. + + audiotestsrc freq=330 ! + audio/x-raw,format=S16LE,channels=2,rate=48000, + layout=interleaved ! + mux. + + matroskamux name=mux ! + tcpclientsink host={IP} port=10000 + """.format_map(settings) + + self.clock = GstNet.NetClientClock.new('voctocore', + settings['IP'], 9998, + 0) + print('obtained NetClientClock from host', self.clock) + + print('waiting for NetClientClock to sync…') + self.clock.wait_for_sync(Gst.CLOCK_TIME_NONE) + + print('starting pipeline ' + pipeline) + self.senderPipeline = Gst.parse_launch(pipeline) + self.senderPipeline.use_clock(self.clock) + self.src = self.senderPipeline.get_by_name('src') + + # Binding End-of-Stream-Signal on Source-Pipeline + self.senderPipeline.bus.add_signal_watch() + self.senderPipeline.bus.connect("message::eos", self.on_eos) + self.senderPipeline.bus.connect("message::error", self.on_error) + + print("playing") + self.senderPipeline.set_state(Gst.State.PLAYING) + + def on_eos(self, bus, message): + print('Received EOS-Signal') + sys.exit(1) + + def on_error(self, bus, message): + print('Received Error-Signal') + (error, debug) = message.parse_error() + print('Error-Details: #%u: %s' % (error.code, debug)) + sys.exit(1) - def on_error(self, bus, message): - print('Received Error-Signal') - (error, debug) = message.parse_error() - print('Error-Details: #%u: %s' % (error.code, debug)) - sys.exit(1) def main(): - signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.SIG_DFL) - parser = argparse.ArgumentParser(description='Voctocore Remote-Source') - parser.add_argument('host') + parser = argparse.ArgumentParser(description='Voctocore Remote-Source') + parser.add_argument('host') - args = parser.parse_args() - print('Resolving hostname '+args.host) - addrs = [ str(i[4][0]) for i in socket.getaddrinfo(args.host, None) ] - if len(addrs) == 0: - print('Found no IPs') - sys.exit(1) + args = parser.parse_args() + print('Resolving hostname ' + args.host) + addrs = [str(i[4][0]) for i in socket.getaddrinfo(args.host, None)] + if len(addrs) == 0: + print('Found no IPs') + sys.exit(1) - print('Using IP '+addrs[0]) + print('Using IP ' + addrs[0]) - config = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../config.sh') - with open(config) as config: - lines = [ line.strip() for line in config if line[0] != '#' ] - pairs = [ line.split('=', 1) for line in lines ] - settings = { pair[0]: pair[1] for pair in pairs } + config = os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../config.sh') + with open(config) as config: + lines = [line.strip() for line in config if line[0] != '#'] + pairs = [line.split('=', 1) for line in lines] + settings = {pair[0]: pair[1] for pair in pairs} - settings['IP'] = addrs[0] + settings['IP'] = addrs[0] - src = Source(settings) - mainloop = GObject.MainLoop() - try: - mainloop.run() - except KeyboardInterrupt: - print('Terminated via Ctrl-C') + src = Source(settings) + mainloop = GObject.MainLoop() + try: + mainloop.run() + except KeyboardInterrupt: + print('Terminated via Ctrl-C') if __name__ == '__main__': - main() + main() diff --git a/example-scripts/voctomidi/voctomidi.py b/example-scripts/voctomidi/voctomidi.py index b21f529..0de14aa 100755 --- a/example-scripts/voctomidi/voctomidi.py +++ b/example-scripts/voctomidi/voctomidi.py @@ -18,6 +18,7 @@ event_map = dict(map(lambda x: (int(x[0]), x[1]), Config.items("eventmap"))) class MidiInputHandler(object): + def __init__(self, port): self.port = port diff --git a/voctocore/lib/args.py b/voctocore/lib/args.py index d40cd75..66c298d 100644 --- a/voctocore/lib/args.py +++ b/voctocore/lib/args.py @@ -4,16 +4,19 @@ __all__ = ['Args'] parser = argparse.ArgumentParser(description='Voctocore') parser.add_argument('-v', '--verbose', action='count', default=0, - help="Also print INFO and DEBUG messages.") + help="Also print INFO and DEBUG messages.") -parser.add_argument('-c', '--color', action='store', choices=['auto', 'always', 'never'], default='auto', - help="Control the use of colors in the Log-Output") +parser.add_argument('-c', '--color', + action='store', + choices=['auto', 'always', 'never'], + default='auto', + help="Control the use of colors in the Log-Output") parser.add_argument('-t', '--timestamp', action='store_true', - help="Enable timestamps in the Log-Output") + help="Enable timestamps in the Log-Output") parser.add_argument('-i', '--ini-file', action='store', - help="Load a custom config.ini-File") + help="Load a custom config.ini-File") Args = parser.parse_args() diff --git a/voctocore/lib/audiomix.py b/voctocore/lib/audiomix.py index 6c1768a..37376fd 100644 --- a/voctocore/lib/audiomix.py +++ b/voctocore/lib/audiomix.py @@ -5,83 +5,87 @@ from enum import Enum from lib.config import Config from lib.clock import Clock + class AudioMix(object): - def __init__(self): - self.log = logging.getLogger('AudioMix') - - self.selectedSource = 0 - - self.caps = Config.get('mix', 'audiocaps') - self.names = Config.getlist('mix', 'sources') - self.log.info('Configuring Mixer for %u Sources', len(self.names)) - - pipeline = """ - audiomixer name=mix ! - {caps} ! - queue ! - tee name=tee - - tee. ! queue ! interaudiosink channel=audio_mix_out - """.format( - caps=self.caps - ) - - if Config.getboolean('previews', 'enabled'): - pipeline += """ - tee. ! queue ! interaudiosink channel=audio_mix_preview - """ - - if Config.getboolean('stream-blanker', 'enabled'): - pipeline += """ - tee. ! queue ! interaudiosink channel=audio_mix_streamblanker - """ - - for idx, name in enumerate(self.names): - pipeline += """ - interaudiosrc channel=audio_{name}_mixer ! - {caps} ! - mix. - """.format( - name=name, - caps=self.caps - ) - - self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline) - self.mixingPipeline = Gst.parse_launch(pipeline) - self.mixingPipeline.use_clock(Clock) - - self.log.debug('Binding Error & End-of-Stream-Signal on Mixing-Pipeline') - self.mixingPipeline.bus.add_signal_watch() - self.mixingPipeline.bus.connect("message::eos", self.on_eos) - self.mixingPipeline.bus.connect("message::error", self.on_error) - - self.log.debug('Initializing Mixer-State') - self.updateMixerState() - - self.log.debug('Launching Mixing-Pipeline') - self.mixingPipeline.set_state(Gst.State.PLAYING) - - def updateMixerState(self): - self.log.info('Updating Mixer-State') - - for idx, name in enumerate(self.names): - volume = int(idx == self.selectedSource) - - self.log.debug('Setting Mixerpad %u to volume=%0.2f', idx, volume) - mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_%u' % idx) - mixerpad.set_property('volume', volume) - - def setAudioSource(self, source): - self.selectedSource = source - self.updateMixerState() - - def getAudioSource(self): - return self.selectedSource - - def on_eos(self, bus, message): - self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline') - - def on_error(self, bus, message): - self.log.debug('Received Error-Signal on Mixing-Pipeline') - (error, debug) = message.parse_error() - self.log.debug('Error-Details: #%u: %s', error.code, debug) + + def __init__(self): + self.log = logging.getLogger('AudioMix') + + self.selectedSource = 0 + + self.caps = Config.get('mix', 'audiocaps') + self.names = Config.getlist('mix', 'sources') + self.log.info('Configuring Mixer for %u Sources', len(self.names)) + + pipeline = """ + audiomixer name=mix ! + {caps} ! + queue ! + tee name=tee + + tee. ! queue ! interaudiosink channel=audio_mix_out + """.format( + caps=self.caps + ) + + if Config.getboolean('previews', 'enabled'): + pipeline += """ + tee. ! queue ! interaudiosink channel=audio_mix_preview + """ + + if Config.getboolean('stream-blanker', 'enabled'): + pipeline += """ + tee. ! queue ! interaudiosink channel=audio_mix_streamblanker + """ + + for idx, name in enumerate(self.names): + pipeline += """ + interaudiosrc channel=audio_{name}_mixer ! + {caps} ! + mix. + """.format( + name=name, + caps=self.caps + ) + + self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline) + self.mixingPipeline = Gst.parse_launch(pipeline) + self.mixingPipeline.use_clock(Clock) + + self.log.debug('Binding Error & End-of-Stream-Signal ' + 'on Mixing-Pipeline') + self.mixingPipeline.bus.add_signal_watch() + self.mixingPipeline.bus.connect("message::eos", self.on_eos) + self.mixingPipeline.bus.connect("message::error", self.on_error) + + self.log.debug('Initializing Mixer-State') + self.updateMixerState() + + self.log.debug('Launching Mixing-Pipeline') + self.mixingPipeline.set_state(Gst.State.PLAYING) + + def updateMixerState(self): + self.log.info('Updating Mixer-State') + + for idx, name in enumerate(self.names): + volume = int(idx == self.selectedSource) + + self.log.debug('Setting Mixerpad %u to volume=%0.2f', idx, volume) + mixerpad = (self.mixingPipeline.get_by_name('mix') + .get_static_pad('sink_%u' % idx)) + mixerpad.set_property('volume', volume) + + def setAudioSource(self, source): + self.selectedSource = source + self.updateMixerState() + + def getAudioSource(self): + return self.selectedSource + + def on_eos(self, bus, message): + self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline') + + def on_error(self, bus, message): + self.log.debug('Received Error-Signal on Mixing-Pipeline') + (error, debug) = message.parse_error() + self.log.debug('Error-Details: #%u: %s', error.code, debug) diff --git a/voctocore/lib/avpreviewoutput.py b/voctocore/lib/avpreviewoutput.py index d5c2c66..749249b 100644 --- a/voctocore/lib/avpreviewoutput.py +++ b/voctocore/lib/avpreviewoutput.py @@ -5,84 +5,87 @@ from lib.config import Config from lib.tcpmulticonnection import TCPMultiConnection from lib.clock import Clock + class AVPreviewOutput(TCPMultiConnection): - def __init__(self, channel, port): - self.log = logging.getLogger('AVPreviewOutput['+channel+']') - super().__init__(port) - - self.channel = channel - - if Config.has_option('previews', 'videocaps'): - vcaps_out = Config.get('previews', 'videocaps') - else: - vcaps_out = Config.get('mix', 'videocaps') - - deinterlace = "" - if Config.getboolean('previews', 'deinterlace'): - deinterlace = "deinterlace mode=interlaced !" - - pipeline = """ - intervideosrc channel=video_{channel} ! - {vcaps_in} ! - {deinterlace} - videoscale ! - videorate ! - {vcaps_out} ! - jpegenc quality=90 ! - queue ! - mux. - - interaudiosrc channel=audio_{channel} ! - {acaps} ! - queue ! - mux. - - matroskamux - name=mux - streamable=true - writing-app=Voctomix-AVPreviewOutput ! - - multifdsink - blocksize=1048576 - buffers-max=500 - sync-method=next-keyframe - name=fd - """.format( - channel=self.channel, - acaps=Config.get('mix', 'audiocaps'), - vcaps_in=Config.get('mix', 'videocaps'), - vcaps_out=vcaps_out, - deinterlace=deinterlace - ) - - self.log.debug('Creating Output-Pipeline:\n%s', pipeline) - self.outputPipeline = Gst.parse_launch(pipeline) - self.outputPipeline.use_clock(Clock) - - self.log.debug('Binding Error & End-of-Stream-Signal on Output-Pipeline') - self.outputPipeline.bus.add_signal_watch() - self.outputPipeline.bus.connect("message::eos", self.on_eos) - self.outputPipeline.bus.connect("message::error", self.on_error) - - self.log.debug('Launching Output-Pipeline') - self.outputPipeline.set_state(Gst.State.PLAYING) - - def on_accepted(self, conn, addr): - self.log.debug('Adding fd %u to multifdsink', conn.fileno()) - fdsink = self.outputPipeline.get_by_name('fd') - fdsink.emit('add', conn.fileno()) - - def on_disconnect(multifdsink, fileno): - if fileno == conn.fileno(): - self.log.debug('fd %u removed from multifdsink', fileno) - self.close_connection(conn) - - fdsink.connect('client-fd-removed', on_disconnect) - - def on_eos(self, bus, message): - self.log.debug('Received End-of-Stream-Signal on Output-Pipeline') - - def on_error(self, bus, message): - self.log.debug('Received Error-Signal on Output-Pipeline') - (error, debug) = message.parse_error() - self.log.debug('Error-Details: #%u: %s', error.code, debug) + + def __init__(self, channel, port): + self.log = logging.getLogger('AVPreviewOutput[{}]'.format(channel)) + super().__init__(port) + + self.channel = channel + + if Config.has_option('previews', 'videocaps'): + vcaps_out = Config.get('previews', 'videocaps') + else: + vcaps_out = Config.get('mix', 'videocaps') + + deinterlace = "" + if Config.getboolean('previews', 'deinterlace'): + deinterlace = "deinterlace mode=interlaced !" + + pipeline = """ + intervideosrc channel=video_{channel} ! + {vcaps_in} ! + {deinterlace} + videoscale ! + videorate ! + {vcaps_out} ! + jpegenc quality=90 ! + queue ! + mux. + + interaudiosrc channel=audio_{channel} ! + {acaps} ! + queue ! + mux. + + matroskamux + name=mux + streamable=true + writing-app=Voctomix-AVPreviewOutput ! + + multifdsink + blocksize=1048576 + buffers-max=500 + sync-method=next-keyframe + name=fd + """.format( + channel=self.channel, + acaps=Config.get('mix', 'audiocaps'), + vcaps_in=Config.get('mix', 'videocaps'), + vcaps_out=vcaps_out, + deinterlace=deinterlace + ) + + self.log.debug('Creating Output-Pipeline:\n%s', pipeline) + self.outputPipeline = Gst.parse_launch(pipeline) + self.outputPipeline.use_clock(Clock) + + self.log.debug('Binding Error & End-of-Stream-Signal ' + 'on Output-Pipeline') + self.outputPipeline.bus.add_signal_watch() + self.outputPipeline.bus.connect("message::eos", self.on_eos) + self.outputPipeline.bus.connect("message::error", self.on_error) + + self.log.debug('Launching Output-Pipeline') + self.outputPipeline.set_state(Gst.State.PLAYING) + + def on_accepted(self, conn, addr): + self.log.debug('Adding fd %u to multifdsink', conn.fileno()) + fdsink = self.outputPipeline.get_by_name('fd') + fdsink.emit('add', conn.fileno()) + + def on_disconnect(multifdsink, fileno): + if fileno == conn.fileno(): + self.log.debug('fd %u removed from multifdsink', fileno) + self.close_connection(conn) + + fdsink.connect('client-fd-removed', on_disconnect) + + def on_eos(self, bus, message): + self.log.debug('Received End-of-Stream-Signal on Output-Pipeline') + + def on_error(self, bus, message): + self.log.debug('Received Error-Signal on Output-Pipeline') + (error, debug) = message.parse_error() + self.log.debug('Error-Details: #%u: %s', error.code, debug) diff --git a/voctocore/lib/avrawoutput.py b/voctocore/lib/avrawoutput.py index 5523a66..b044e24 100644 --- a/voctocore/lib/avrawoutput.py +++ b/voctocore/lib/avrawoutput.py @@ -5,76 +5,78 @@ from lib.config import Config from lib.tcpmulticonnection import TCPMultiConnection from lib.clock import Clock + class AVRawOutput(TCPMultiConnection): - def __init__(self, channel, port): - self.log = logging.getLogger('AVRawOutput['+channel+']') - super().__init__(port) - - self.channel = channel - - pipeline = """ - intervideosrc channel=video_{channel} ! - {vcaps} ! - queue ! - mux. - - interaudiosrc channel=audio_{channel} ! - {acaps} ! - queue ! - mux. - - matroskamux - name=mux - streamable=true - writing-app=Voctomix-AVRawOutput ! - - multifdsink - blocksize=1048576 - buffers-max={buffers_max} - sync-method=next-keyframe - name=fd - """.format( - channel=self.channel, - acaps=Config.get('mix', 'audiocaps'), - vcaps=Config.get('mix', 'videocaps'), - buffers_max= - Config.get('output-buffers', channel) - if Config.has_option('output-buffers', channel) - else 500, - ) - self.log.debug('Creating Output-Pipeline:\n%s', pipeline) - self.outputPipeline = Gst.parse_launch(pipeline) - self.outputPipeline.use_clock(Clock) - - self.log.debug('Binding Error & End-of-Stream-Signal on Output-Pipeline') - self.outputPipeline.bus.add_signal_watch() - self.outputPipeline.bus.connect("message::eos", self.on_eos) - self.outputPipeline.bus.connect("message::error", self.on_error) - - self.log.debug('Launching Output-Pipeline') - self.outputPipeline.set_state(Gst.State.PLAYING) - - def on_accepted(self, conn, addr): - self.log.debug('Adding fd %u to multifdsink', conn.fileno()) - fdsink = self.outputPipeline.get_by_name('fd') - fdsink.emit('add', conn.fileno()) - - def on_disconnect(multifdsink, fileno): - if fileno == conn.fileno(): - self.log.debug('fd %u removed from multifdsink', fileno) - self.close_connection(conn) - - def on_about_to_disconnect(multifdsink, fileno, status): - if fileno == conn.fileno() and status == 3: # Gst.MultiHandleSinkClientStatus.Slow - self.log.warning('about to remove fd %u from multifdsink because it is too slow!', fileno) - - fdsink.connect('client-fd-removed', on_disconnect) - fdsink.connect('client-removed', on_about_to_disconnect) - - def on_eos(self, bus, message): - self.log.debug('Received End-of-Stream-Signal on Output-Pipeline') - - def on_error(self, bus, message): - self.log.debug('Received Error-Signal on Output-Pipeline') - (error, debug) = message.parse_error() - self.log.debug('Error-Details: #%u: %s', error.code, debug) + + def __init__(self, channel, port): + self.log = logging.getLogger('AVRawOutput[{}]'.format(channel)) + super().__init__(port) + + self.channel = channel + + pipeline = """ + intervideosrc channel=video_{channel} ! + {vcaps} ! + queue ! + mux. + + interaudiosrc channel=audio_{channel} ! + {acaps} ! + queue ! + mux. + + matroskamux + name=mux + streamable=true + writing-app=Voctomix-AVRawOutput ! + + multifdsink + blocksize=1048576 + buffers-max={buffers_max} + sync-method=next-keyframe + name=fd + """.format( + channel=self.channel, + acaps=Config.get('mix', 'audiocaps'), + vcaps=Config.get('mix', 'videocaps'), + buffers_max=Config.get('output-buffers', channel, fallback=500) + ) + self.log.debug('Creating Output-Pipeline:\n%s', pipeline) + self.outputPipeline = Gst.parse_launch(pipeline) + self.outputPipeline.use_clock(Clock) + + self.log.debug('Binding Error & End-of-Stream-Signal ' + 'on Output-Pipeline') + self.outputPipeline.bus.add_signal_watch() + self.outputPipeline.bus.connect("message::eos", self.on_eos) + self.outputPipeline.bus.connect("message::error", self.on_error) + + self.log.debug('Launching Output-Pipeline') + self.outputPipeline.set_state(Gst.State.PLAYING) + + def on_accepted(self, conn, addr): + self.log.debug('Adding fd %u to multifdsink', conn.fileno()) + fdsink = self.outputPipeline.get_by_name('fd') + fdsink.emit('add', conn.fileno()) + + def on_disconnect(multifdsink, fileno): + if fileno == conn.fileno(): + self.log.debug('fd %u removed from multifdsink', fileno) + self.close_connection(conn) + + def on_about_to_disconnect(multifdsink, fileno, status): + # GST_CLIENT_STATUS_SLOW = 3, + if fileno == conn.fileno() and status == 3: + self.log.warning('about to remove fd %u from multifdsink ' + 'because it is too slow!', fileno) + + fdsink.connect('client-fd-removed', on_disconnect) + fdsink.connect('client-removed', on_about_to_disconnect) + + def on_eos(self, bus, message): + self.log.debug('Received End-of-Stream-Signal on Output-Pipeline') + + def on_error(self, bus, message): + self.log.debug('Received Error-Signal on Output-Pipeline') + (error, debug) = message.parse_error() + self.log.debug('Error-Details: #%u: %s', error.code, debug) diff --git a/voctocore/lib/avsource.py b/voctocore/lib/avsource.py index aa604b2..c2e08da 100644 --- a/voctocore/lib/avsource.py +++ b/voctocore/lib/avsource.py @@ -5,116 +5,126 @@ from lib.config import Config from lib.tcpsingleconnection import TCPSingleConnection from lib.clock import Clock + class AVSource(TCPSingleConnection): - def __init__(self, name, port, outputs=None, has_audio=True, has_video=True): - self.log = logging.getLogger('AVSource['+name+']') - super().__init__(port) - - if outputs is None: - outputs = [name] - - assert has_audio or has_video - - self.name = name - self.has_audio = has_audio - self.has_video = has_video - self.outputs = outputs - - def on_accepted(self, conn, addr): - pipeline = """ - fdsrc fd={fd} blocksize=1048576 ! - queue ! - matroskademux name=demux - """.format( - fd=conn.fileno() - ) - - if self.has_audio: - pipeline += """ - demux. ! - {acaps} ! - queue ! - tee name=atee - """.format( - acaps=Config.get('mix', 'audiocaps') - ) - - for output in self.outputs: - pipeline += """ - atee. ! queue ! interaudiosink channel=audio_{output} - """.format( - output=output - ) - - if self.has_video: - pipeline += """ - demux. ! - {vcaps} ! - queue ! - tee name=vtee - """.format( - vcaps=Config.get('mix', 'videocaps') - ) - - for output in self.outputs: - pipeline += """ - vtee. ! queue ! intervideosink channel=video_{output} - """.format( - output=output - ) - - self.log.debug('Launching Source-Pipeline:\n%s', pipeline) - self.receiverPipeline = Gst.parse_launch(pipeline) - self.receiverPipeline.use_clock(Clock) - - self.log.debug('Binding End-of-Stream-Signal on Source-Pipeline') - self.receiverPipeline.bus.add_signal_watch() - self.receiverPipeline.bus.connect("message::eos", self.on_eos) - self.receiverPipeline.bus.connect("message::error", self.on_error) - - self.all_video_caps = Gst.Caps.from_string('video/x-raw') - self.video_caps = Gst.Caps.from_string(Config.get('mix', 'videocaps')) - - self.all_audio_caps = Gst.Caps.from_string('audio/x-raw') - self.audio_caps = Gst.Caps.from_string(Config.get('mix', 'audiocaps')) - - demux = self.receiverPipeline.get_by_name('demux') - demux.connect('pad-added', self.on_pad_added) - - self.receiverPipeline.set_state(Gst.State.PLAYING) - - def on_pad_added(self, demux, src_pad): - caps = src_pad.query_caps(None) - self.log.debug('demuxer added pad w/ caps: %s', caps.to_string()) - if caps.can_intersect(self.all_audio_caps): - self.log.debug('new demuxer-pad is a audio-pad, testing against configured audio-caps') - if not caps.can_intersect(self.audio_caps): - self.log.warning('the incoming connection presented a video-stream that is not compatible to the configured caps') - self.log.warning(' incoming caps: %s', caps.to_string()) - self.log.warning(' configured caps: %s', self.audio_caps.to_string()) - - - elif caps.can_intersect(self.all_video_caps): - self.log.debug('new demuxer-pad is a video-pad, testing against configured video-caps') - if not caps.can_intersect(self.video_caps): - self.log.warning('the incoming connection presented a video-stream that is not compatible to the configured caps') - self.log.warning(' incoming caps: %s', caps.to_string()) - self.log.warning(' configured caps: %s', self.video_caps.to_string()) - - def on_eos(self, bus, message): - self.log.debug('Received End-of-Stream-Signal on Source-Pipeline') - if self.currentConnection is not None: - self.disconnect() - - def on_error(self, bus, message): - self.log.debug('Received Error-Signal on Source-Pipeline') - (error, debug) = message.parse_error() - self.log.debug('Error-Details: #%u: %s', error.code, debug) - - if self.currentConnection is not None: - self.disconnect() - - def disconnect(self): - self.receiverPipeline.set_state(Gst.State.NULL) - self.receiverPipeline = None - self.close_connection() + + def __init__(self, name, port, outputs=None, + has_audio=True, has_video=True): + self.log = logging.getLogger('AVSource[{}]'.format(name)) + super().__init__(port) + + if outputs is None: + outputs = [name] + + assert has_audio or has_video + + self.name = name + self.has_audio = has_audio + self.has_video = has_video + self.outputs = outputs + + def on_accepted(self, conn, addr): + pipeline = """ + fdsrc fd={fd} blocksize=1048576 ! + queue ! + matroskademux name=demux + """.format( + fd=conn.fileno() + ) + + if self.has_audio: + pipeline += """ + demux. ! + {acaps} ! + queue ! + tee name=atee + """.format( + acaps=Config.get('mix', 'audiocaps') + ) + + for output in self.outputs: + pipeline += """ + atee. ! queue ! interaudiosink channel=audio_{output} + """.format( + output=output + ) + + if self.has_video: + pipeline += """ + demux. ! + {vcaps} ! + queue ! + tee name=vtee + """.format( + vcaps=Config.get('mix', 'videocaps') + ) + + for output in self.outputs: + pipeline += """ + vtee. ! queue ! intervideosink channel=video_{output} + """.format( + output=output + ) + + self.log.debug('Launching Source-Pipeline:\n%s', pipeline) + self.receiverPipeline = Gst.parse_launch(pipeline) + self.receiverPipeline.use_clock(Clock) + + self.log.debug('Binding End-of-Stream-Signal on Source-Pipeline') + self.receiverPipeline.bus.add_signal_watch() + self.receiverPipeline.bus.connect("message::eos", self.on_eos) + self.receiverPipeline.bus.connect("message::error", self.on_error) + + self.all_video_caps = Gst.Caps.from_string('video/x-raw') + self.video_caps = Gst.Caps.from_string(Config.get('mix', 'videocaps')) + + self.all_audio_caps = Gst.Caps.from_string('audio/x-raw') + self.audio_caps = Gst.Caps.from_string(Config.get('mix', 'audiocaps')) + + demux = self.receiverPipeline.get_by_name('demux') + demux.connect('pad-added', self.on_pad_added) + + self.receiverPipeline.set_state(Gst.State.PLAYING) + + def on_pad_added(self, demux, src_pad): + caps = src_pad.query_caps(None) + self.log.debug('demuxer added pad w/ caps: %s', caps.to_string()) + if caps.can_intersect(self.all_audio_caps): + self.log.debug('new demuxer-pad is a audio-pad, ' + 'testing against configured audio-caps') + if not caps.can_intersect(self.audio_caps): + self.log.warning('the incoming connection presented ' + 'a video-stream that is not compatible ' + 'to the configured caps') + self.log.warning(' incoming caps: %s', caps.to_string()) + self.log.warning(' configured caps: %s', + self.audio_caps.to_string()) + + elif caps.can_intersect(self.all_video_caps): + self.log.debug('new demuxer-pad is a video-pad, ' + 'testing against configured video-caps') + if not caps.can_intersect(self.video_caps): + self.log.warning('the incoming connection presented ' + 'a video-stream that is not compatible ' + 'to the configured caps') + self.log.warning(' incoming caps: %s', caps.to_string()) + self.log.warning(' configured caps: %s', + self.video_caps.to_string()) + + def on_eos(self, bus, message): + self.log.debug('Received End-of-Stream-Signal on Source-Pipeline') + if self.currentConnection is not None: + self.disconnect() + + def on_error(self, bus, message): + self.log.debug('Received Error-Signal on Source-Pipeline') + (error, debug) = message.parse_error() + self.log.debug('Error-Details: #%u: %s', error.code, debug) + + if self.currentConnection is not None: + self.disconnect() + + def disconnect(self): + self.receiverPipeline.set_state(Gst.State.NULL) + self.receiverPipeline = None + self.close_connection() diff --git a/voctocore/lib/commands.py b/voctocore/lib/commands.py index a5b6ab1..34590b7 100644 --- a/voctocore/lib/commands.py +++ b/voctocore/lib/commands.py @@ -6,236 +6,240 @@ from lib.config import Config from lib.videomix import CompositeModes from lib.response import NotifyResponse, OkResponse -def decodeName(items, name_or_id): - try: - name_or_id = int(name_or_id) - if name_or_id < 0 or name_or_id >= len(items): - raise IndexError("unknown index %d" % name_or_id) - - return name_or_id - - except ValueError as e: - try: - return items.index(name_or_id) - - except ValueError as e: - raise IndexError("unknown name %s" % name_or_id) - -def decodeEnumName(enum, name_or_id): - try: - name_or_id = int(name_or_id) - if name_or_id < 0 or name_or_id >= len(enum): - raise IndexError("unknown index %d" % name_or_id) - - return name_or_id - - except ValueError as e: - try: - return enum[name_or_id] - - except KeyError as e: - raise IndexError("unknown name %s" % name_or_id) - -def encodeName(items, id): - try: - return items[id] - except IndexError as e: - raise IndexError("unknown index %d" % id) - -def encodeEnumName(enum, id): - try: - return enum(id).name - except ValueError as e: - raise IndexError("unknown index %d" % id) - -class ControlServerCommands(object): - def __init__(self, pipeline): - self.log = logging.getLogger('ControlServerCommands') - - self.pipeline = pipeline - self.sources = Config.getlist('mix', 'sources') - self.blankerSources = Config.getlist('stream-blanker', 'sources') - - # Commands are defined below. Errors are sent to the clients by throwing - # exceptions, they will be turned into messages outside. - - def message(self, *args): - """sends a message through the control-server, which can be received by - user-defined scripts. does not change the state of the voctocore.""" - return NotifyResponse('message', *args) - - def help(self): - helplines = [] - - helplines.append("Commands:") - for name, func in ControlServerCommands.__dict__.items(): - if name[0] == '_': - continue - - if not func.__code__: - continue - - params = inspect.signature(func).parameters - params = [str(info) for name, info in params.items()] - params = ', '.join(params[1:]) - - command_sig = '\t' + name - - if params: - command_sig += ': '+params - - if func.__doc__: - command_sig += '\n'+'\n'.join( - ['\t\t'+line.strip() for line in func.__doc__.splitlines()])+'\n' - - helplines.append(command_sig) +def decodeName(items, name_or_id): + try: + name_or_id = int(name_or_id) + if name_or_id < 0 or name_or_id >= len(items): + raise IndexError("unknown index %d" % name_or_id) - helplines.append('\t'+'quit / exit') + return name_or_id - helplines.append("\n") - helplines.append("Source-Names:") - for source in self.sources: - helplines.append("\t"+source) + except ValueError as e: + try: + return items.index(name_or_id) - helplines.append("\n") - helplines.append("Stream-Blanker Sources-Names:") - for source in self.blankerSources: - helplines.append("\t"+source) + except ValueError as e: + raise IndexError("unknown name %s" % name_or_id) - helplines.append("\n") - helplines.append("Composition-Modes:") - for mode in CompositeModes: - helplines.append("\t"+mode.name) - return OkResponse("\n".join(helplines)) +def decodeEnumName(enum, name_or_id): + try: + name_or_id = int(name_or_id) + if name_or_id < 0 or name_or_id >= len(enum): + raise IndexError("unknown index %d" % name_or_id) - def _get_video_status(self): - a = encodeName( self.sources, self.pipeline.vmix.getVideoSourceA() ) - b = encodeName( self.sources, self.pipeline.vmix.getVideoSourceB() ) - return [a, b] + return name_or_id - def get_video(self): - """gets the current video-status, consisting of the name of - video-source A and video-source B""" - status = self._get_video_status() - return OkResponse('video_status', *status) + except ValueError as e: + try: + return enum[name_or_id] - def set_video_a(self, src_name_or_id): - """sets the video-source A to the supplied source-name or source-id, - swapping A and B if the supplied source is currently used as - video-source B""" - src_id = decodeName(self.sources, src_name_or_id) - self.pipeline.vmix.setVideoSourceA(src_id) + except KeyError as e: + raise IndexError("unknown name %s" % name_or_id) - status = self._get_video_status() - return NotifyResponse('video_status', *status) - def set_video_b(self, src_name_or_id): - """sets the video-source B to the supplied source-name or source-id, - swapping A and B if the supplied source is currently used as - video-source A""" - src_id = decodeName(self.sources, src_name_or_id) - self.pipeline.vmix.setVideoSourceB(src_id) +def encodeName(items, id): + try: + return items[id] + except IndexError as e: + raise IndexError("unknown index %d" % id) - status = self._get_video_status() - return NotifyResponse('video_status', *status) +def encodeEnumName(enum, id): + try: + return enum(id).name + except ValueError as e: + raise IndexError("unknown index %d" % id) - def _get_audio_status(self): - src_id = self.pipeline.amix.getAudioSource() - return encodeName(self.sources, src_id) - def get_audio(self): - """gets the name of the current audio-source""" - status = self._get_audio_status() - return OkResponse('audio_status', status) +class ControlServerCommands(object): - def set_audio(self, src_name_or_id): - """sets the audio-source to the supplied source-name or source-id""" - src_id = decodeName(self.sources, src_name_or_id) - self.pipeline.amix.setAudioSource(src_id) + def __init__(self, pipeline): + self.log = logging.getLogger('ControlServerCommands') - status = self._get_audio_status() - return NotifyResponse('audio_status', status) + self.pipeline = pipeline + self.sources = Config.getlist('mix', 'sources') + self.blankerSources = Config.getlist('stream-blanker', 'sources') - def _get_composite_status(self): - mode = self.pipeline.vmix.getCompositeMode() - return encodeEnumName(CompositeModes, mode) + # Commands are defined below. Errors are sent to the clients by throwing + # exceptions, they will be turned into messages outside. - def get_composite_mode(self): - """gets the name of the current composite-mode""" - status = self._get_composite_status() - return OkResponse('composite_mode', status) + def message(self, *args): + """sends a message through the control-server, which can be received by + user-defined scripts. does not change the state of the voctocore.""" + return NotifyResponse('message', *args) - def set_composite_mode(self, mode_name_or_id): - """sets the name of the id of the composite-mode""" - mode = decodeEnumName(CompositeModes, mode_name_or_id) - self.pipeline.vmix.setCompositeMode(mode) + def help(self): + helplines = [] - composite_status = self._get_composite_status() - video_status = self._get_video_status() - return [ - NotifyResponse('composite_mode', composite_status), - NotifyResponse('video_status', *video_status) - ] - + helplines.append("Commands:") + for name, func in ControlServerCommands.__dict__.items(): + if name[0] == '_': + continue - def set_videos_and_composite(self, src_a_name_or_id, src_b_name_or_id, mode_name_or_id): - """sets the A- and the B-source synchronously with the composition-mode - all parametets can be set to "*" which will leave them unchanged.""" - if src_a_name_or_id != '*': - src_a_id = decodeName(self.sources, src_a_name_or_id) - self.pipeline.vmix.setVideoSourceA(src_a_id) + if not func.__code__: + continue - if src_b_name_or_id != '*': - src_b_id = decodeName(self.sources, src_b_name_or_id) - self.pipeline.vmix.setVideoSourceB(src_b_id) - - if mode_name_or_id != '*': - mode = decodeEnumName(CompositeModes, mode_name_or_id) - self.pipeline.vmix.setCompositeMode(mode) - - composite_status = self._get_composite_status() - video_status = self._get_video_status() - - return [ - NotifyResponse('composite_mode', composite_status), - NotifyResponse('video_status', *video_status) - ] - - - def _get_stream_status(self): - blankSource = self.pipeline.streamblanker.blankSource - if blankSource is None: - return ('live',) - - return 'blank', encodeName(self.blankerSources, blankSource) - - def get_stream_status(self): - """gets the current streamblanker-status""" - status = self._get_stream_status() - return OkResponse('stream_status', *status) - - def set_stream_blank(self, source_name_or_id): - """sets the streamblanker-status to blank with the specified - blanker-source-name or -id""" - src_id = decodeName(self.blankerSources, source_name_or_id) - self.pipeline.streamblanker.setBlankSource(src_id) - - status = self._get_stream_status() - return NotifyResponse('stream_status', *status) + params = inspect.signature(func).parameters + params = [str(info) for name, info in params.items()] + params = ', '.join(params[1:]) - def set_stream_live(self): - """sets the streamblanker-status to live""" - self.pipeline.streamblanker.setBlankSource(None) + command_sig = '\t' + name - status = self._get_stream_status() - return NotifyResponse('stream_status', *status) + if params: + command_sig += ': ' + params + + if func.__doc__: + command_sig += '\n\t\t{}\n'.format('\n\t\t'.join( + [line.strip() for line in func.__doc__.splitlines()] + )) + + helplines.append(command_sig) + + helplines.append('\t' + 'quit / exit') + + helplines.append("\n") + helplines.append("Source-Names:") + for source in self.sources: + helplines.append("\t" + source) + helplines.append("\n") + helplines.append("Stream-Blanker Sources-Names:") + for source in self.blankerSources: + helplines.append("\t" + source) + + helplines.append("\n") + helplines.append("Composition-Modes:") + for mode in CompositeModes: + helplines.append("\t" + mode.name) + + return OkResponse("\n".join(helplines)) - def get_config(self): - """returns the parsed server-config""" - confdict = {header: dict(section) for header, section in dict(Config).items()} - return OkResponse('server_config', json.dumps(confdict)) + def _get_video_status(self): + a = encodeName(self.sources, self.pipeline.vmix.getVideoSourceA()) + b = encodeName(self.sources, self.pipeline.vmix.getVideoSourceB()) + return [a, b] + + def get_video(self): + """gets the current video-status, consisting of the name of + video-source A and video-source B""" + status = self._get_video_status() + return OkResponse('video_status', *status) + + def set_video_a(self, src_name_or_id): + """sets the video-source A to the supplied source-name or source-id, + swapping A and B if the supplied source is currently used as + video-source B""" + src_id = decodeName(self.sources, src_name_or_id) + self.pipeline.vmix.setVideoSourceA(src_id) + + status = self._get_video_status() + return NotifyResponse('video_status', *status) + + def set_video_b(self, src_name_or_id): + """sets the video-source B to the supplied source-name or source-id, + swapping A and B if the supplied source is currently used as + video-source A""" + src_id = decodeName(self.sources, src_name_or_id) + self.pipeline.vmix.setVideoSourceB(src_id) + + status = self._get_video_status() + return NotifyResponse('video_status', *status) + + def _get_audio_status(self): + src_id = self.pipeline.amix.getAudioSource() + return encodeName(self.sources, src_id) + + def get_audio(self): + """gets the name of the current audio-source""" + status = self._get_audio_status() + return OkResponse('audio_status', status) + + def set_audio(self, src_name_or_id): + """sets the audio-source to the supplied source-name or source-id""" + src_id = decodeName(self.sources, src_name_or_id) + self.pipeline.amix.setAudioSource(src_id) + + status = self._get_audio_status() + return NotifyResponse('audio_status', status) + + def _get_composite_status(self): + mode = self.pipeline.vmix.getCompositeMode() + return encodeEnumName(CompositeModes, mode) + + def get_composite_mode(self): + """gets the name of the current composite-mode""" + status = self._get_composite_status() + return OkResponse('composite_mode', status) + + def set_composite_mode(self, mode_name_or_id): + """sets the name of the id of the composite-mode""" + mode = decodeEnumName(CompositeModes, mode_name_or_id) + self.pipeline.vmix.setCompositeMode(mode) + + composite_status = self._get_composite_status() + video_status = self._get_video_status() + return [ + NotifyResponse('composite_mode', composite_status), + NotifyResponse('video_status', *video_status) + ] + + def set_videos_and_composite(self, src_a_name_or_id, src_b_name_or_id, + mode_name_or_id): + """sets the A- and the B-source synchronously with the composition-mode + all parametets can be set to "*" which will leave them unchanged.""" + if src_a_name_or_id != '*': + src_a_id = decodeName(self.sources, src_a_name_or_id) + self.pipeline.vmix.setVideoSourceA(src_a_id) + + if src_b_name_or_id != '*': + src_b_id = decodeName(self.sources, src_b_name_or_id) + self.pipeline.vmix.setVideoSourceB(src_b_id) + + if mode_name_or_id != '*': + mode = decodeEnumName(CompositeModes, mode_name_or_id) + self.pipeline.vmix.setCompositeMode(mode) + + composite_status = self._get_composite_status() + video_status = self._get_video_status() + + return [ + NotifyResponse('composite_mode', composite_status), + NotifyResponse('video_status', *video_status) + ] + + def _get_stream_status(self): + blankSource = self.pipeline.streamblanker.blankSource + if blankSource is None: + return ('live',) + + return 'blank', encodeName(self.blankerSources, blankSource) + + def get_stream_status(self): + """gets the current streamblanker-status""" + status = self._get_stream_status() + return OkResponse('stream_status', *status) + + def set_stream_blank(self, source_name_or_id): + """sets the streamblanker-status to blank with the specified + blanker-source-name or -id""" + src_id = decodeName(self.blankerSources, source_name_or_id) + self.pipeline.streamblanker.setBlankSource(src_id) + + status = self._get_stream_status() + return NotifyResponse('stream_status', *status) + + def set_stream_live(self): + """sets the streamblanker-status to live""" + self.pipeline.streamblanker.setBlankSource(None) + + status = self._get_stream_status() + return NotifyResponse('stream_status', *status) + + def get_config(self): + """returns the parsed server-config""" + confdict = {header: dict(section) + for header, section in dict(Config).items()} + return OkResponse('server_config', json.dumps(confdict)) diff --git a/voctocore/lib/config.py b/voctocore/lib/config.py index df0ff9a..388778e 100644 --- a/voctocore/lib/config.py +++ b/voctocore/lib/config.py @@ -5,29 +5,32 @@ from lib.args import Args __all__ = ['Config'] + def getlist(self, section, option): - return [x.strip() for x in self.get(section, option).split(',')] + return [x.strip() for x in self.get(section, option).split(',')] SafeConfigParser.getlist = getlist files = [ - os.path.join(os.path.dirname(os.path.realpath(__file__)), '../default-config.ini'), - os.path.join(os.path.dirname(os.path.realpath(__file__)), '../config.ini'), - '/etc/voctomix/voctocore.ini', - '/etc/voctomix.ini', # deprecated - '/etc/voctocore.ini', - os.path.expanduser('~/.voctomix.ini'), # deprecated - os.path.expanduser('~/.voctocore.ini'), + os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../default-config.ini'), + os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../config.ini'), + '/etc/voctomix/voctocore.ini', + '/etc/voctomix.ini', # deprecated + '/etc/voctocore.ini', + os.path.expanduser('~/.voctomix.ini'), # deprecated + os.path.expanduser('~/.voctocore.ini'), ] if Args.ini_file is not None: - files.append(Args.ini_file) + files.append(Args.ini_file) Config = SafeConfigParser() readfiles = Config.read(files) log = logging.getLogger('ConfigParser') log.debug('considered config-files: \n%s', - "\n".join(["\t\t"+os.path.normpath(file) for file in files]) ) + "\n".join(["\t\t" + os.path.normpath(file) for file in files])) log.debug('successfully parsed config-files: \n%s', - "\n".join(["\t\t"+os.path.normpath(file) for file in readfiles]) ) + "\n".join(["\t\t" + os.path.normpath(file) for file in readfiles])) diff --git a/voctocore/lib/controlserver.py b/voctocore/lib/controlserver.py index 4c506be..42ae4b2 100644 --- a/voctocore/lib/controlserver.py +++ b/voctocore/lib/controlserver.py @@ -1,4 +1,6 @@ -import socket, logging, traceback +import socket +import logging +import traceback from queue import Queue from gi.repository import GObject @@ -6,164 +8,171 @@ from lib.commands import ControlServerCommands from lib.tcpmulticonnection import TCPMultiConnection from lib.response import NotifyResponse, OkResponse -class ControlServer(TCPMultiConnection): - def __init__(self, pipeline): - '''Initialize server and start listening.''' - self.log = logging.getLogger('ControlServer') - super().__init__(port=9999) - - self.command_queue = Queue() - self.commands = ControlServerCommands(pipeline) - - def on_accepted(self, conn, addr): - '''Asynchronous connection listener. Starts a handler for each connection.''' - self.log.debug('setting gobject io-watch on connection') - GObject.io_add_watch(conn, GObject.IO_IN, self.on_data, ['']) - - def on_data(self, conn, _, leftovers, *args): - '''Asynchronous connection handler. Pushes data from socket - into command queue linewise''' - close_after = False - try: - while True: - try: - leftovers.append(conn.recv(4096).decode(errors='replace')) - if len(leftovers[-1]) == 0: - self.log.info("Socket was closed") - leftovers.pop() - close_after = True - break - - except UnicodeDecodeError as e: - continue - except: - pass +class ControlServer(TCPMultiConnection): - data = "".join(leftovers) - del leftovers[:] + def __init__(self, pipeline): + '''Initialize server and start listening.''' + self.log = logging.getLogger('ControlServer') + super().__init__(port=9999) - lines = data.split('\n') - for line in lines[:-1]: - self.log.debug("got line: %r", line) + self.command_queue = Queue() + + self.commands = ControlServerCommands(pipeline) - line = line.strip() - # 'quit' = remote wants us to close the connection - if line == 'quit' or line == 'exit': - self.log.info("Client asked us to close the Connection") - self.close_connection(conn) - return False + def on_accepted(self, conn, addr): + '''Asynchronous connection listener. + Starts a handler for each connection.''' + self.log.debug('setting gobject io-watch on connection') + GObject.io_add_watch(conn, GObject.IO_IN, self.on_data, ['']) + + def on_data(self, conn, _, leftovers, *args): + '''Asynchronous connection handler. + Pushes data from socket into command queue linewise''' + close_after = False + try: + while True: + try: + leftovers.append(conn.recv(4096).decode(errors='replace')) + if len(leftovers[-1]) == 0: + self.log.info("Socket was closed") + leftovers.pop() + close_after = True + break - self.log.debug('re-starting on_loop scheduling') - GObject.idle_add(self.on_loop) + except UnicodeDecodeError as e: + continue + except: + pass - self.command_queue.put((line, conn)) + data = "".join(leftovers) + del leftovers[:] - if close_after: - self.close_connection(conn) - return False - - if lines[-1] != '': - self.log.debug("remaining %r", lines[-1]) - - leftovers.append(lines[-1]) - return True - - def on_loop(self): - '''Command handler. Processes commands in the command queue whenever - nothing else is happening (registered as GObject idle callback)''' - - self.log.debug('on_loop called') - - if self.command_queue.empty(): - self.log.debug('command_queue is empty again, stopping on_loop scheduling') - return False - - line, requestor = self.command_queue.get() - - words = line.split() - if len(words) < 1: - self.log.debug('command_queue is empty again, stopping on_loop scheduling') - return True - - command = words[0] - args = words[1:] - - self.log.info("processing command %r with args %s", command, args) - - response = None - try: - # deny calling private methods - if command[0] == '_': - self.log.info('private methods are not callable') - raise KeyError() - - command_function = self.commands.__class__.__dict__[command] - - except KeyError as e: - self.log.info("received unknown command %s", command) - response = "error unknown command %s\n" % command - - else: - try: - responseObject = command_function(self.commands, *args) - - except Exception as e: - message = str(e) or "<no message>" - response = "error %s\n" % message - - else: - if isinstance(responseObject, NotifyResponse): - responseObject = [ responseObject ] - - if isinstance(responseObject, list): - for obj in responseObject: - signal = "%s\n" % str(obj) - for conn in self.currentConnections: - self._schedule_write(conn, signal) - else: - response = "%s\n" % str(responseObject) - - finally: - if response is not None and requestor in self.currentConnections: - self._schedule_write(requestor, response) - - return False + lines = data.split('\n') + for line in lines[:-1]: + self.log.debug("got line: %r", line) - def _schedule_write(self, conn, message): - queue = self.currentConnections[conn] + line = line.strip() + # 'quit' = remote wants us to close the connection + if line == 'quit' or line == 'exit': + self.log.info("Client asked us to close the Connection") + self.close_connection(conn) + return False - self.log.debug('re-starting on_write[%u] scheduling', conn.fileno()) - GObject.io_add_watch(conn, GObject.IO_OUT, self.on_write) + self.log.debug('re-starting on_loop scheduling') + GObject.idle_add(self.on_loop) + + self.command_queue.put((line, conn)) + + if close_after: + self.close_connection(conn) + return False - queue.put(message) + if lines[-1] != '': + self.log.debug("remaining %r", lines[-1]) + + leftovers.append(lines[-1]) + return True + + def on_loop(self): + '''Command handler. Processes commands in the command queue whenever + nothing else is happening (registered as GObject idle callback)''' + + self.log.debug('on_loop called') + + if self.command_queue.empty(): + self.log.debug('command_queue is empty again, ' + 'stopping on_loop scheduling') + return False + + line, requestor = self.command_queue.get() + + words = line.split() + if len(words) < 1: + self.log.debug('command_queue is empty again, ' + 'stopping on_loop scheduling') + return True + + command = words[0] + args = words[1:] + + self.log.info("processing command %r with args %s", command, args) + + response = None + try: + # deny calling private methods + if command[0] == '_': + self.log.info('private methods are not callable') + raise KeyError() + + command_function = self.commands.__class__.__dict__[command] + + except KeyError as e: + self.log.info("received unknown command %s", command) + response = "error unknown command %s\n" % command + + else: + try: + responseObject = command_function(self.commands, *args) + + except Exception as e: + message = str(e) or "<no message>" + response = "error %s\n" % message + + else: + if isinstance(responseObject, NotifyResponse): + responseObject = [responseObject] + + if isinstance(responseObject, list): + for obj in responseObject: + signal = "%s\n" % str(obj) + for conn in self.currentConnections: + self._schedule_write(conn, signal) + else: + response = "%s\n" % str(responseObject) + + finally: + if response is not None and requestor in self.currentConnections: + self._schedule_write(requestor, response) + + return False + + def _schedule_write(self, conn, message): + queue = self.currentConnections[conn] - def on_write(self, conn, *args): - self.log.debug('on_write[%u] called', conn.fileno()) + self.log.debug('re-starting on_write[%u] scheduling', conn.fileno()) + GObject.io_add_watch(conn, GObject.IO_OUT, self.on_write) - try: - queue = self.currentConnections[conn] - except KeyError: - return False + queue.put(message) - if queue.empty(): - self.log.debug('write_queue[%u] is empty again, stopping on_write scheduling', conn.fileno()) - return False + def on_write(self, conn, *args): + self.log.debug('on_write[%u] called', conn.fileno()) - message = queue.get() - try: - conn.send(message.encode()) - except Exception as e: - self.log.warn(e) + try: + queue = self.currentConnections[conn] + except KeyError: + return False + + if queue.empty(): + self.log.debug('write_queue[%u] is empty again, ' + 'stopping on_write scheduling', + conn.fileno()) + return False - return True + message = queue.get() + try: + conn.send(message.encode()) + except Exception as e: + self.log.warn(e) + + return True - def notify_all(self, msg): - try: - words = msg.split() - words[-1] = self.commands.encodeSourceName(int(words[-1])) - msg = " ".join(words) + '\n' - for conn in self.currentConnections: - self._schedule_write(conn, msg) - except Exception as e: - self.log.debug("error during notify: %s", e) + def notify_all(self, msg): + try: + words = msg.split() + words[-1] = self.commands.encodeSourceName(int(words[-1])) + msg = " ".join(words) + '\n' + for conn in self.currentConnections: + self._schedule_write(conn, msg) + except Exception as e: + self.log.debug("error during notify: %s", e) diff --git a/voctocore/lib/loghandler.py b/voctocore/lib/loghandler.py index 2cc7ceb..6efb890 100644 --- a/voctocore/lib/loghandler.py +++ b/voctocore/lib/loghandler.py @@ -1,41 +1,57 @@ -import logging, time +import logging +import time -class LogFormatter(logging.Formatter): - def __init__(self, docolor, timestamps=False): - super().__init__() - self.docolor = docolor - self.timestamps = timestamps - - def formatMessage(self, record): - if self.docolor: - c_lvl = 33 - c_mod = 32 - c_msg = 0 - - if record.levelno == logging.WARNING: - c_lvl = 31 - #c_mod = 33 - c_msg = 33 - - elif record.levelno > logging.WARNING: - c_lvl = 31 - c_mod = 31 - c_msg = 31 - fmt = '\x1b['+str(c_lvl)+'m%(levelname)8s\x1b[0m \x1b['+str(c_mod)+'m%(name)s\x1b['+str(c_msg)+'m: %(message)s\x1b[0m' - else: - fmt = '%(levelname)8s %(name)s: %(message)s' - - if self.timestamps: - fmt = '%(asctime)s '+fmt - - if not 'asctime' in record.__dict__: - record.__dict__['asctime']=time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(record.__dict__['created'])) +class LogFormatter(logging.Formatter): - return fmt % record.__dict__ + def __init__(self, docolor, timestamps=False): + super().__init__() + self.docolor = docolor + self.timestamps = timestamps + + def formatMessage(self, record): + if self.docolor: + c_lvl = 33 + c_mod = 32 + c_msg = 0 + + if record.levelno == logging.WARNING: + c_lvl = 31 + # c_mod = 33 + c_msg = 33 + + elif record.levelno > logging.WARNING: + c_lvl = 31 + c_mod = 31 + c_msg = 31 + + fmt = ''.join([ + '\x1b[%dm' % c_lvl, # set levelname color + '%(levelname)8s', # print levelname + '\x1b[0m', # reset formatting + '\x1b[%dm' % c_mod, # set name color + ' %(name)s', # print name + '\x1b[%dm' % c_msg, # set message color + ': %(message)s', # print message + '\x1b[0m' # reset formatting + ]) + else: + fmt = '%(levelname)8s %(name)s: %(message)s' + + if self.timestamps: + fmt = '%(asctime)s ' + fmt + + if 'asctime' not in record.__dict__: + record.__dict__['asctime'] = time.strftime( + "%Y-%m-%d %H:%M:%S", + time.localtime(record.__dict__['created']) + ) + + return fmt % record.__dict__ class LogHandler(logging.StreamHandler): - def __init__(self, docolor, timestamps): - super().__init__() - self.setFormatter(LogFormatter(docolor,timestamps)) + + def __init__(self, docolor, timestamps): + super().__init__() + self.setFormatter(LogFormatter(docolor, timestamps)) diff --git a/voctocore/lib/pipeline.py b/voctocore/lib/pipeline.py index a13f1d2..1a1349e 100644 --- a/voctocore/lib/pipeline.py +++ b/voctocore/lib/pipeline.py @@ -10,93 +10,99 @@ from lib.videomix import VideoMix from lib.audiomix import AudioMix from lib.streamblanker import StreamBlanker -class Pipeline(object): - """mixing, streaming and encoding pipeline constuction and control""" - - def __init__(self): - self.log = logging.getLogger('Pipeline') - self.log.info('Video-Caps configured to: %s', Config.get('mix', 'videocaps')) - self.log.info('Audio-Caps configured to: %s', Config.get('mix', 'audiocaps')) - - names = Config.getlist('mix', 'sources') - if len(names) < 1: - raise RuntimeError("At least one AVSource must be configured!") - - self.sources = [] - self.mirrors = [] - self.previews = [] - self.sbsources = [] - - self.log.info('Creating %u Creating AVSources: %s', len(names), names) - for idx, name in enumerate(names): - port = 10000 + idx - self.log.info('Creating AVSource %s at tcp-port %u', name, port) - - outputs = [name+'_mixer', name+'_mirror'] - if Config.getboolean('previews', 'enabled'): - outputs.append(name+'_preview') - - source = AVSource(name, port, outputs=outputs) - self.sources.append(source) - - - port = 13000 + idx - self.log.info('Creating Mirror-Output for AVSource %s at tcp-port %u', name, port) - - mirror = AVRawOutput('%s_mirror' % name, port) - self.mirrors.append(mirror) - - - if Config.getboolean('previews', 'enabled'): - port = 14000 + idx - self.log.info('Creating Preview-Output for AVSource %s at tcp-port %u', name, port) - - preview = AVPreviewOutput('%s_preview' % name, port) - self.previews.append(preview) - - self.log.info('Creating Videmixer') - self.vmix = VideoMix() - - self.log.info('Creating Audiomixer') - self.amix = AudioMix() - - port = 16000 - self.log.info('Creating Mixer-Background VSource at tcp-port %u', port) - self.bgsrc = AVSource('background', port, has_audio=False) - - port = 11000 - self.log.info('Creating Mixer-Output at tcp-port %u', port) - self.mixout = AVRawOutput('mix_out', port) - - - if Config.getboolean('previews', 'enabled'): - port = 12000 - self.log.info('Creating Preview-Output for AVSource %s at tcp-port %u', name, port) - - self.mixpreview = AVPreviewOutput('mix_preview', port) - - if Config.getboolean('stream-blanker', 'enabled'): - names = Config.getlist('stream-blanker', 'sources') - if len(names) < 1: - raise RuntimeError("At least one StreamBlanker-Source must be configured or the StreamBlanker disabled!") - for idx, name in enumerate(names): - port = 17000 + idx - self.log.info('Creating StreamBlanker VSource %s at tcp-port %u', name, port) - - source = AVSource('%s_streamblanker' % name, port, has_audio=False) - self.sbsources.append(source) - - port = 18000 - self.log.info('Creating StreamBlanker ASource at tcp-port %u', port) - - source = AVSource('streamblanker', port, has_video=False) - self.sbsources.append(source) - - - self.log.info('Creating StreamBlanker') - self.streamblanker = StreamBlanker() - - port = 15000 - self.log.info('Creating StreamBlanker-Output at tcp-port %u', port) - self.streamout = AVRawOutput('streamblanker_out', port) +class Pipeline(object): + """mixing, streaming and encoding pipeline constuction and control""" + + def __init__(self): + self.log = logging.getLogger('Pipeline') + self.log.info('Video-Caps configured to: %s', + Config.get('mix', 'videocaps')) + self.log.info('Audio-Caps configured to: %s', + Config.get('mix', 'audiocaps')) + + names = Config.getlist('mix', 'sources') + if len(names) < 1: + raise RuntimeError("At least one AVSource must be configured!") + + self.sources = [] + self.mirrors = [] + self.previews = [] + self.sbsources = [] + + self.log.info('Creating %u Creating AVSources: %s', len(names), names) + for idx, name in enumerate(names): + port = 10000 + idx + self.log.info('Creating AVSource %s at tcp-port %u', name, port) + + outputs = [name + '_mixer', name + '_mirror'] + if Config.getboolean('previews', 'enabled'): + outputs.append(name + '_preview') + + source = AVSource(name, port, outputs=outputs) + self.sources.append(source) + + port = 13000 + idx + self.log.info('Creating Mirror-Output for AVSource %s ' + 'at tcp-port %u', name, port) + + mirror = AVRawOutput('%s_mirror' % name, port) + self.mirrors.append(mirror) + + if Config.getboolean('previews', 'enabled'): + port = 14000 + idx + self.log.info('Creating Preview-Output for AVSource %s ' + 'at tcp-port %u', name, port) + + preview = AVPreviewOutput('%s_preview' % name, port) + self.previews.append(preview) + + self.log.info('Creating Videmixer') + self.vmix = VideoMix() + + self.log.info('Creating Audiomixer') + self.amix = AudioMix() + + port = 16000 + self.log.info('Creating Mixer-Background VSource at tcp-port %u', port) + self.bgsrc = AVSource('background', port, has_audio=False) + + port = 11000 + self.log.info('Creating Mixer-Output at tcp-port %u', port) + self.mixout = AVRawOutput('mix_out', port) + + if Config.getboolean('previews', 'enabled'): + port = 12000 + self.log.info('Creating Preview-Output for AVSource %s ' + 'at tcp-port %u', name, port) + + self.mixpreview = AVPreviewOutput('mix_preview', port) + + if Config.getboolean('stream-blanker', 'enabled'): + names = Config.getlist('stream-blanker', 'sources') + if len(names) < 1: + raise RuntimeError('At least one StreamBlanker-Source must ' + 'be configured or the ' + 'StreamBlanker disabled!') + for idx, name in enumerate(names): + port = 17000 + idx + self.log.info('Creating StreamBlanker VSource %s ' + 'at tcp-port %u', name, port) + + source = AVSource('{}_streamblanker'.format(name), port, + has_audio=False) + self.sbsources.append(source) + + port = 18000 + self.log.info('Creating StreamBlanker ASource at tcp-port %u', + port) + + source = AVSource('streamblanker', port, has_video=False) + self.sbsources.append(source) + + self.log.info('Creating StreamBlanker') + self.streamblanker = StreamBlanker() + + port = 15000 + self.log.info('Creating StreamBlanker-Output at tcp-port %u', port) + self.streamout = AVRawOutput('streamblanker_out', port) diff --git a/voctocore/lib/response.py b/voctocore/lib/response.py index 9f4ad84..b5f7578 100644 --- a/voctocore/lib/response.py +++ b/voctocore/lib/response.py @@ -1,14 +1,16 @@ class Response(object): - def __init__(self, *args): - self.args = args - def __str__(self): - return " ".join(map(str, self.args)) + def __init__(self, *args): + self.args = args + + def __str__(self): + return " ".join(map(str, self.args)) class OkResponse(Response): - pass + pass + class NotifyResponse(Response): - pass + pass diff --git a/voctocore/lib/streamblanker.py b/voctocore/lib/streamblanker.py index fea3d6a..67af0cd 100644 --- a/voctocore/lib/streamblanker.py +++ b/voctocore/lib/streamblanker.py @@ -5,97 +5,104 @@ from enum import Enum from lib.config import Config from lib.clock import Clock -class StreamBlanker(object): - log = logging.getLogger('StreamBlanker') - - def __init__(self): - self.acaps = Config.get('mix', 'audiocaps') - self.vcaps = Config.get('mix', 'videocaps') - - self.names = Config.getlist('stream-blanker', 'sources') - self.log.info('Configuring StreamBlanker video %u Sources', len(self.names)) - - pipeline = """ - compositor name=vmix ! - {vcaps} ! - intervideosink channel=video_streamblanker_out - - audiomixer name=amix ! - {acaps} ! - interaudiosink channel=audio_streamblanker_out - - - intervideosrc channel=video_mix_streamblanker ! - {vcaps} ! - vmix. - - interaudiosrc channel=audio_mix_streamblanker ! - {acaps} ! - amix. - - - interaudiosrc channel=audio_streamblanker ! - {acaps} ! - amix. - """.format( - acaps=self.acaps, - vcaps=self.vcaps - ) - - for name in self.names: - pipeline += """ - intervideosrc channel=video_{name}_streamblanker ! - {vcaps} ! - vmix. - """.format( - name=name, - vcaps=self.vcaps - ) - - self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline) - self.mixingPipeline = Gst.parse_launch(pipeline) - self.mixingPipeline.use_clock(Clock) - self.log.debug('Binding Error & End-of-Stream-Signal on Mixing-Pipeline') - self.mixingPipeline.bus.add_signal_watch() - self.mixingPipeline.bus.connect("message::eos", self.on_eos) - self.mixingPipeline.bus.connect("message::error", self.on_error) - - self.log.debug('Initializing Mixer-State') - self.blankSource = None - self.applyMixerState() - - self.log.debug('Launching Mixing-Pipeline') - self.mixingPipeline.set_state(Gst.State.PLAYING) - - def on_eos(self, bus, message): - self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline') - - def on_error(self, bus, message): - self.log.debug('Received Error-Signal on Mixing-Pipeline') - (error, debug) = message.parse_error() - self.log.debug('Error-Details: #%u: %s', error.code, debug) - - - def applyMixerState(self): - self.applyMixerStateAudio() - self.applyMixerStateVideo() - - def applyMixerStateAudio(self): - mixpad = self.mixingPipeline.get_by_name('amix').get_static_pad('sink_0') - blankpad = self.mixingPipeline.get_by_name('amix').get_static_pad('sink_1') - - mixpad.set_property('volume', int(self.blankSource is None)) - blankpad.set_property('volume', int(self.blankSource is not None)) - - def applyMixerStateVideo(self): - mixpad = self.mixingPipeline.get_by_name('vmix').get_static_pad('sink_0') - mixpad.set_property('alpha', int(self.blankSource is None)) - - for idx, name in enumerate(self.names): - blankpad = self.mixingPipeline.get_by_name('vmix').get_static_pad('sink_%u' % (idx+1)) - blankpad.set_property('alpha', int(self.blankSource == idx)) - - def setBlankSource(self, source): - self.blankSource = source - self.applyMixerState() +class StreamBlanker(object): + log = logging.getLogger('StreamBlanker') + + def __init__(self): + self.acaps = Config.get('mix', 'audiocaps') + self.vcaps = Config.get('mix', 'videocaps') + + self.names = Config.getlist('stream-blanker', 'sources') + self.log.info('Configuring StreamBlanker video %u Sources', + len(self.names)) + + pipeline = """ + compositor name=vmix ! + {vcaps} ! + intervideosink channel=video_streamblanker_out + + audiomixer name=amix ! + {acaps} ! + interaudiosink channel=audio_streamblanker_out + + + intervideosrc channel=video_mix_streamblanker ! + {vcaps} ! + vmix. + + interaudiosrc channel=audio_mix_streamblanker ! + {acaps} ! + amix. + + + interaudiosrc channel=audio_streamblanker ! + {acaps} ! + amix. + """.format( + acaps=self.acaps, + vcaps=self.vcaps + ) + + for name in self.names: + pipeline += """ + intervideosrc channel=video_{name}_streamblanker ! + {vcaps} ! + vmix. + """.format( + name=name, + vcaps=self.vcaps + ) + + self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline) + self.mixingPipeline = Gst.parse_launch(pipeline) + self.mixingPipeline.use_clock(Clock) + + self.log.debug('Binding Error & End-of-Stream-Signal ' + 'on Mixing-Pipeline') + self.mixingPipeline.bus.add_signal_watch() + self.mixingPipeline.bus.connect("message::eos", self.on_eos) + self.mixingPipeline.bus.connect("message::error", self.on_error) + + self.log.debug('Initializing Mixer-State') + self.blankSource = None + self.applyMixerState() + + self.log.debug('Launching Mixing-Pipeline') + self.mixingPipeline.set_state(Gst.State.PLAYING) + + def on_eos(self, bus, message): + self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline') + + def on_error(self, bus, message): + self.log.debug('Received Error-Signal on Mixing-Pipeline') + (error, debug) = message.parse_error() + self.log.debug('Error-Details: #%u: %s', error.code, debug) + + def applyMixerState(self): + self.applyMixerStateAudio() + self.applyMixerStateVideo() + + def applyMixerStateAudio(self): + mixpad = (self.mixingPipeline.get_by_name('amix') + .get_static_pad('sink_0')) + blankpad = (self.mixingPipeline.get_by_name('amix') + .get_static_pad('sink_1')) + + mixpad.set_property('volume', int(self.blankSource is None)) + blankpad.set_property('volume', int(self.blankSource is not None)) + + def applyMixerStateVideo(self): + mixpad = (self.mixingPipeline.get_by_name('vmix') + .get_static_pad('sink_0')) + mixpad.set_property('alpha', int(self.blankSource is None)) + + for idx, name in enumerate(self.names): + blankpad = (self.mixingPipeline + .get_by_name('vmix') + .get_static_pad('sink_%u' % (idx + 1))) + blankpad.set_property('alpha', int(self.blankSource == idx)) + + def setBlankSource(self, source): + self.blankSource = source + self.applyMixerState() diff --git a/voctocore/lib/tcpmulticonnection.py b/voctocore/lib/tcpmulticonnection.py index 66fc33f..ac228a3 100644 --- a/voctocore/lib/tcpmulticonnection.py +++ b/voctocore/lib/tcpmulticonnection.py @@ -1,41 +1,48 @@ -import logging, socket +import logging +import socket from queue import Queue from gi.repository import GObject from lib.config import Config + class TCPMultiConnection(object): - def __init__(self, port): - if not hasattr(self, 'log'): - self.log = logging.getLogger('TCPMultiConnection') - self.boundSocket = None - self.currentConnections = dict() + def __init__(self, port): + if not hasattr(self, 'log'): + self.log = logging.getLogger('TCPMultiConnection') + + self.boundSocket = None + self.currentConnections = dict() - self.log.debug('Binding to Source-Socket on [::]:%u', port) - self.boundSocket = socket.socket(socket.AF_INET6) - self.boundSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.boundSocket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False) - self.boundSocket.bind(('::', port)) - self.boundSocket.listen(1) + self.log.debug('Binding to Source-Socket on [::]:%u', port) + self.boundSocket = socket.socket(socket.AF_INET6) + self.boundSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.boundSocket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, + False) + self.boundSocket.bind(('::', port)) + self.boundSocket.listen(1) - self.log.debug('Setting GObject io-watch on Socket') - GObject.io_add_watch(self.boundSocket, GObject.IO_IN, self.on_connect) + self.log.debug('Setting GObject io-watch on Socket') + GObject.io_add_watch(self.boundSocket, GObject.IO_IN, self.on_connect) - def on_connect(self, sock, *args): - conn, addr = sock.accept() - conn.setblocking(False) + def on_connect(self, sock, *args): + conn, addr = sock.accept() + conn.setblocking(False) - self.log.info("Incomming Connection from [%s]:%u (fd=%u)", addr[0], addr[1], conn.fileno()) + self.log.info("Incomming Connection from [%s]:%u (fd=%u)", + addr[0], addr[1], conn.fileno()) - self.currentConnections[conn] = Queue() - self.log.info('Now %u Receiver connected', len(self.currentConnections)) + self.currentConnections[conn] = Queue() + self.log.info('Now %u Receiver connected', + len(self.currentConnections)) - self.on_accepted(conn, addr) + self.on_accepted(conn, addr) - return True + return True - def close_connection(self, conn): - if conn in self.currentConnections: - del(self.currentConnections[conn]) - self.log.info('Now %u Receiver connected', len(self.currentConnections)) + def close_connection(self, conn): + if conn in self.currentConnections: + del(self.currentConnections[conn]) + self.log.info('Now %u Receiver connected', + len(self.currentConnections)) diff --git a/voctocore/lib/tcpsingleconnection.py b/voctocore/lib/tcpsingleconnection.py index bf89651..2d3c658 100644 --- a/voctocore/lib/tcpsingleconnection.py +++ b/voctocore/lib/tcpsingleconnection.py @@ -1,39 +1,45 @@ -import logging, socket, time +import logging +import socket +import time from gi.repository import GObject from lib.config import Config + class TCPSingleConnection(object): - def __init__(self, port): - if not hasattr(self, 'log'): - self.log = logging.getLogger('TCPMultiConnection') - self.log.debug('Binding to Source-Socket on [::]:%u', port) - self.boundSocket = socket.socket(socket.AF_INET6) - self.boundSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.boundSocket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False) - self.boundSocket.bind(('::', port)) - self.boundSocket.listen(1) + def __init__(self, port): + if not hasattr(self, 'log'): + self.log = logging.getLogger('TCPMultiConnection') + + self.log.debug('Binding to Source-Socket on [::]:%u', port) + self.boundSocket = socket.socket(socket.AF_INET6) + self.boundSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.boundSocket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, + False) + self.boundSocket.bind(('::', port)) + self.boundSocket.listen(1) - self.currentConnection = None + self.currentConnection = None - self.log.debug('Setting GObject io-watch on Socket') - GObject.io_add_watch(self.boundSocket, GObject.IO_IN, self.on_connect) + self.log.debug('Setting GObject io-watch on Socket') + GObject.io_add_watch(self.boundSocket, GObject.IO_IN, self.on_connect) - def on_connect(self, sock, *args): - conn, addr = sock.accept() - self.log.info("Incomming Connection from %s", addr) + def on_connect(self, sock, *args): + conn, addr = sock.accept() + self.log.info('Incomming Connection from %s', addr) - if self.currentConnection is not None: - self.log.warn("Another Source is already connected, closing existing pipeline") - self.disconnect() - time.sleep(1) + if self.currentConnection is not None: + self.log.warn('Another Source is already connected, ' + 'closing existing pipeline') + self.disconnect() + time.sleep(1) - self.on_accepted(conn, addr) - self.currentConnection = conn + self.on_accepted(conn, addr) + self.currentConnection = conn - return True + return True - def close_connection(self): - self.currentConnection = None - self.log.info('Connection closed') + def close_connection(self): + self.currentConnection = None + self.log.info('Connection closed') diff --git a/voctocore/lib/videomix.py b/voctocore/lib/videomix.py index 95d538d..4ab740f 100644 --- a/voctocore/lib/videomix.py +++ b/voctocore/lib/videomix.py @@ -5,377 +5,411 @@ from enum import Enum, unique from lib.config import Config from lib.clock import Clock + @unique class CompositeModes(Enum): - fullscreen = 0 - side_by_side_equal = 1 - side_by_side_preview = 2 - picture_in_picture = 3 - -class PadState(object): - def __init__(self): - self.reset() + fullscreen = 0 + side_by_side_equal = 1 + side_by_side_preview = 2 + picture_in_picture = 3 - def reset(self): - self.alpha = 1.0 - self.xpos = 0 - self.ypos = 0 - self.zorder = 1 - self.width = 0 - self.height = 0 -class VideoMix(object): - log = logging.getLogger('VideoMix') - - def __init__(self): - self.caps = Config.get('mix', 'videocaps') - - self.names = Config.getlist('mix', 'sources') - self.log.info('Configuring Mixer for %u Sources', len(self.names)) - - pipeline = """ - compositor name=mix ! - {caps} ! - identity name=sig ! - queue ! - tee name=tee +class PadState(object): - intervideosrc channel=video_background ! - {caps} ! - mix. - - tee. ! queue ! intervideosink channel=video_mix_out - """.format( - caps=self.caps - ) - - if Config.getboolean('previews', 'enabled'): - pipeline += """ - tee. ! queue ! intervideosink channel=video_mix_preview - """ - - if Config.getboolean('stream-blanker', 'enabled'): - pipeline += """ - tee. ! queue ! intervideosink channel=video_mix_streamblanker - """ - - for idx, name in enumerate(self.names): - pipeline += """ - intervideosrc channel=video_{name}_mixer ! - {caps} ! - mix. - """.format( - name=name, - caps=self.caps, - idx=idx - ) - - self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline) - self.mixingPipeline = Gst.parse_launch(pipeline) - self.mixingPipeline.use_clock(Clock) - - self.log.debug('Binding Error & End-of-Stream-Signal on Mixing-Pipeline') - self.mixingPipeline.bus.add_signal_watch() - self.mixingPipeline.bus.connect("message::eos", self.on_eos) - self.mixingPipeline.bus.connect("message::error", self.on_error) + def __init__(self): + self.reset() - self.log.debug('Binding Handoff-Handler for Synchronus mixer manipulation') - sig = self.mixingPipeline.get_by_name('sig') - sig.connect('handoff', self.on_handoff) - - self.padStateDirty = False - self.padState = list() - for idx, name in enumerate(self.names): - self.padState.append(PadState()) - - self.log.debug('Initializing Mixer-State') - self.compositeMode = CompositeModes.fullscreen - self.sourceA = 0 - self.sourceB = 1 - self.recalculateMixerState() - self.applyMixerState() + def reset(self): + self.alpha = 1.0 + self.xpos = 0 + self.ypos = 0 + self.zorder = 1 + self.width = 0 + self.height = 0 - bgMixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_0') - bgMixerpad.set_property('zorder', 0) - self.log.debug('Launching Mixing-Pipeline') - self.mixingPipeline.set_state(Gst.State.PLAYING) - - def getInputVideoSize(self): - caps = Gst.Caps.from_string(self.caps) - struct = caps.get_structure(0) - _, width = struct.get_int('width') - _, height = struct.get_int('height') - - return width, height - - def recalculateMixerState(self): - if self.compositeMode == CompositeModes.fullscreen: - self.recalculateMixerStateFullscreen() - - elif self.compositeMode == CompositeModes.side_by_side_equal: - self.recalculateMixerStateSideBySideEqual() - - elif self.compositeMode == CompositeModes.side_by_side_preview: - self.recalculateMixerStateSideBySidePreview() - - elif self.compositeMode == CompositeModes.picture_in_picture: - self.recalculateMixerStatePictureInPicture() - - self.log.debug('Marking Pad-State as Dirty') - self.padStateDirty = True - - def recalculateMixerStateFullscreen(self): - self.log.info('Updating Mixer-State for Fullscreen-Composition') - - for idx, name in enumerate(self.names): - pad = self.padState[idx] - - pad.reset() - pad.alpha = float(idx == self.sourceA) - - def recalculateMixerStateSideBySideEqual(self): - self.log.info('Updating Mixer-State for Side-by-side-Equal-Composition') - - width, height = self.getInputVideoSize() - self.log.debug('Video-Size parsed as %ux%u', width, height) - - try: - gutter = Config.getint('side-by-side-equal', 'gutter') - self.log.debug('Gutter configured to %u', gutter) - except: - gutter = int(width / 100) - self.log.debug('Gutter calculated to %u', gutter) - - targetWidth = int((width - gutter) / 2) - targetHeight = int(targetWidth / width * height) - - self.log.debug('Video-Size calculated to %ux%u', targetWidth, targetHeight) - - xa = 0 - xb = width - targetWidth - y = (height - targetHeight) / 2 - - try: - ya = Config.getint('side-by-side-equal', 'atop') - self.log.debug('A-Video Y-Pos configured to %u', ya) - except: - ya = y - self.log.debug('A-Video Y-Pos calculated to %u', ya) - - - try: - yb = Config.getint('side-by-side-equal', 'btop') - self.log.debug('B-Video Y-Pos configured to %u', yb) - except: - yb = y - self.log.debug('B-Video Y-Pos calculated to %u', yb) - - - for idx, name in enumerate(self.names): - pad = self.padState[idx] - pad.reset() - - pad.width = targetWidth - pad.height = targetHeight - - if idx == self.sourceA: - pad.xpos = xa - pad.ypos = ya - pad.zorder = 1 - - elif idx == self.sourceB: - pad.xpos = xb - pad.ypos = yb - pad.zorder = 2 - - else: - pad.alpha = 0 - - def recalculateMixerStateSideBySidePreview(self): - self.log.info('Updating Mixer-State for Side-by-side-Preview-Composition') - - width, height = self.getInputVideoSize() - self.log.debug('Video-Size parsed as %ux%u', width, height) - - try: - asize = [int(i) for i in Config.get('side-by-side-preview', 'asize').split('x', 1)] - self.log.debug('A-Video-Size configured to %ux%u', asize[0], asize[1]) - except: - asize = [ - int(width / 1.25), # 80% - int(height / 1.25) # 80% - ] - self.log.debug('A-Video-Size calculated to %ux%u', asize[0], asize[1]) - - try: - apos = [int(i) for i in Config.get('side-by-side-preview', 'apos').split('/', 1)] - self.log.debug('B-Video-Position configured to %u/%u', apos[0], apos[1]) - except: - apos = [ - int(width / 100), # 1% - int(width / 100) # 1% - ] - self.log.debug('B-Video-Position calculated to %u/%u', apos[0], apos[1]) - - try: - bsize = [int(i) for i in Config.get('side-by-side-preview', 'bsize').split('x', 1)] - self.log.debug('B-Video-Size configured to %ux%u', bsize[0], bsize[1]) - except: - bsize = [ - int(width / 4), # 25% - int(height / 4) # 25% - ] - self.log.debug('B-Video-Size calculated to %ux%u', bsize[0], bsize[1]) - - try: - bpos = [int(i) for i in Config.get('side-by-side-preview', 'bpos').split('/', 1)] - self.log.debug('B-Video-Position configured to %u/%u', bpos[0], bpos[1]) - except: - bpos = [ - width - int(width / 100) - bsize[0], - height - int(width / 100) - bsize[1] # 1% - ] - self.log.debug('B-Video-Position calculated to %u/%u', bpos[0], bpos[1]) - - for idx, name in enumerate(self.names): - pad = self.padState[idx] - pad.reset() - - if idx == self.sourceA: - pad.xpos, pad.ypos = apos - pad.width, pad.height = asize - pad.zorder = 1 - - elif idx == self.sourceB: - pad.xpos, pad.ypos = bpos - pad.width, pad.height = bsize - pad.zorder = 2 - - else: - pad.alpha = 0 - - def recalculateMixerStatePictureInPicture(self): - self.log.info('Updating Mixer-State for Picture-in-Picture-Composition') - - width, height = self.getInputVideoSize() - self.log.debug('Video-Size parsed as %ux%u', width, height) - - try: - pipsize = [int(i) for i in Config.get('picture-in-picture', 'pipsize').split('x', 1)] - self.log.debug('PIP-Size configured to %ux%u', pipsize[0], pipsize[1]) - except: - pipsize = [ - int(width / 4), # 25% - int(height / 4) # 25% - ] - self.log.debug('PIP-Size calculated to %ux%u', pipsize[0], pipsize[1]) - - try: - pippos = [int(i) for i in Config.get('picture-in-picture', 'pippos').split('/', 1)] - self.log.debug('PIP-Position configured to %u/%u', pippos[0], pippos[1]) - except: - pippos = [ - width - pipsize[0] - int(width / 100), # 1% - height - pipsize[1] -int(width / 100) # 1% - ] - self.log.debug('PIP-Position calculated to %u/%u', pippos[0], pippos[1]) - - for idx, name in enumerate(self.names): - pad = self.padState[idx] - pad.reset() - - if idx == self.sourceA: - pass - elif idx == self.sourceB: - pad.xpos, pad.ypos = pippos - pad.width, pad.height = pipsize - pad.zorder = 2 - - else: - pad.alpha = 0 - - def applyMixerState(self): - for idx, state in enumerate(self.padState): - # mixerpad 0 = background - mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_%u' % (idx+1)) - - self.log.debug('Reconfiguring Mixerpad %u to x/y=%u/%u, w/h=%u/%u alpha=%0.2f, zorder=%u', \ - idx, state.xpos, state.ypos, state.width, state.height, state.alpha, state.zorder) - mixerpad.set_property('xpos', state.xpos) - mixerpad.set_property('ypos', state.ypos) - mixerpad.set_property('width', state.width) - mixerpad.set_property('height', state.height) - mixerpad.set_property('alpha', state.alpha) - mixerpad.set_property('zorder', state.zorder) - - def selectCompositeModeDefaultSources(self): - sectionNames = { - CompositeModes.fullscreen: 'fullscreen', - CompositeModes.side_by_side_equal: 'side-by-side-equal', - CompositeModes.side_by_side_preview: 'side-by-side-preview', - CompositeModes.picture_in_picture: 'picture-in-picture' - } - - compositeModeName = self.compositeMode.name - sectionName = sectionNames[self.compositeMode] - - try: - defSource = Config.get(sectionName, 'default-a') - self.setVideoSourceA(self.names.index(defSource)) - self.log.info('Changing sourceA to default of Mode %s: %s', compositeModeName, defSource) - except Exception as e: - pass - - try: - defSource = Config.get(sectionName, 'default-b') - self.setVideoSourceB(self.names.index(defSource)) - self.log.info('Changing sourceB to default of Mode %s: %s', compositeModeName, defSource) - except Exception as e: - pass - - def on_handoff(self, object, buffer): - if self.padStateDirty: - self.padStateDirty = False - self.log.debug('[Streaming-Thread]: Pad-State is Dirty, applying new Mixer-State') - self.applyMixerState() - - def on_eos(self, bus, message): - self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline') - - def on_error(self, bus, message): - self.log.debug('Received Error-Signal on Mixing-Pipeline') - (error, debug) = message.parse_error() - self.log.debug('Error-Details: #%u: %s', error.code, debug) - - - def setVideoSourceA(self, source): - # swap if required - if self.sourceB == source: - self.sourceB = self.sourceA - - self.sourceA = source - self.recalculateMixerState() - - def getVideoSourceA(self): - return self.sourceA - - def setVideoSourceB(self, source): - # swap if required - if self.sourceA == source: - self.sourceA = self.sourceB - - self.sourceB = source - self.recalculateMixerState() - - def getVideoSourceB(self): - return self.sourceB - - def setCompositeMode(self, mode): - self.compositeMode = mode - - self.selectCompositeModeDefaultSources() - self.recalculateMixerState() - - def getCompositeMode(self): - return self.compositeMode +class VideoMix(object): + log = logging.getLogger('VideoMix') + + def __init__(self): + self.caps = Config.get('mix', 'videocaps') + + self.names = Config.getlist('mix', 'sources') + self.log.info('Configuring Mixer for %u Sources', len(self.names)) + + pipeline = """ + compositor name=mix ! + {caps} ! + identity name=sig ! + queue ! + tee name=tee + + intervideosrc channel=video_background ! + {caps} ! + mix. + + tee. ! queue ! intervideosink channel=video_mix_out + """.format( + caps=self.caps + ) + + if Config.getboolean('previews', 'enabled'): + pipeline += """ + tee. ! queue ! intervideosink channel=video_mix_preview + """ + + if Config.getboolean('stream-blanker', 'enabled'): + pipeline += """ + tee. ! queue ! intervideosink channel=video_mix_streamblanker + """ + + for idx, name in enumerate(self.names): + pipeline += """ + intervideosrc channel=video_{name}_mixer ! + {caps} ! + mix. + """.format( + name=name, + caps=self.caps, + idx=idx + ) + + self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline) + self.mixingPipeline = Gst.parse_launch(pipeline) + self.mixingPipeline.use_clock(Clock) + + self.log.debug('Binding Error & End-of-Stream-Signal ' + 'on Mixing-Pipeline') + self.mixingPipeline.bus.add_signal_watch() + self.mixingPipeline.bus.connect("message::eos", self.on_eos) + self.mixingPipeline.bus.connect("message::error", self.on_error) + + self.log.debug('Binding Handoff-Handler for ' + 'Synchronus mixer manipulation') + sig = self.mixingPipeline.get_by_name('sig') + sig.connect('handoff', self.on_handoff) + + self.padStateDirty = False + self.padState = list() + for idx, name in enumerate(self.names): + self.padState.append(PadState()) + + self.log.debug('Initializing Mixer-State') + self.compositeMode = CompositeModes.fullscreen + self.sourceA = 0 + self.sourceB = 1 + self.recalculateMixerState() + self.applyMixerState() + + bgMixerpad = (self.mixingPipeline.get_by_name('mix') + .get_static_pad('sink_0')) + bgMixerpad.set_property('zorder', 0) + + self.log.debug('Launching Mixing-Pipeline') + self.mixingPipeline.set_state(Gst.State.PLAYING) + + def getInputVideoSize(self): + caps = Gst.Caps.from_string(self.caps) + struct = caps.get_structure(0) + _, width = struct.get_int('width') + _, height = struct.get_int('height') + + return width, height + + def recalculateMixerState(self): + if self.compositeMode == CompositeModes.fullscreen: + self.recalculateMixerStateFullscreen() + + elif self.compositeMode == CompositeModes.side_by_side_equal: + self.recalculateMixerStateSideBySideEqual() + + elif self.compositeMode == CompositeModes.side_by_side_preview: + self.recalculateMixerStateSideBySidePreview() + + elif self.compositeMode == CompositeModes.picture_in_picture: + self.recalculateMixerStatePictureInPicture() + + self.log.debug('Marking Pad-State as Dirty') + self.padStateDirty = True + + def recalculateMixerStateFullscreen(self): + self.log.info('Updating Mixer-State for Fullscreen-Composition') + + for idx, name in enumerate(self.names): + pad = self.padState[idx] + + pad.reset() + pad.alpha = float(idx == self.sourceA) + + def recalculateMixerStateSideBySideEqual(self): + self.log.info('Updating Mixer-State for ' + 'Side-by-side-Equal-Composition') + + width, height = self.getInputVideoSize() + self.log.debug('Video-Size parsed as %ux%u', width, height) + + try: + gutter = Config.getint('side-by-side-equal', 'gutter') + self.log.debug('Gutter configured to %u', gutter) + except: + gutter = int(width / 100) + self.log.debug('Gutter calculated to %u', gutter) + + targetWidth = int((width - gutter) / 2) + targetHeight = int(targetWidth / width * height) + + self.log.debug('Video-Size calculated to %ux%u', + targetWidth, targetHeight) + + xa = 0 + xb = width - targetWidth + y = (height - targetHeight) / 2 + + try: + ya = Config.getint('side-by-side-equal', 'atop') + self.log.debug('A-Video Y-Pos configured to %u', ya) + except: + ya = y + self.log.debug('A-Video Y-Pos calculated to %u', ya) + + try: + yb = Config.getint('side-by-side-equal', 'btop') + self.log.debug('B-Video Y-Pos configured to %u', yb) + except: + yb = y + self.log.debug('B-Video Y-Pos calculated to %u', yb) + + for idx, name in enumerate(self.names): + pad = self.padState[idx] + pad.reset() + + pad.width = targetWidth + pad.height = targetHeight + + if idx == self.sourceA: + pad.xpos = xa + pad.ypos = ya + pad.zorder = 1 + + elif idx == self.sourceB: + pad.xpos = xb + pad.ypos = yb + pad.zorder = 2 + + else: + pad.alpha = 0 + + def recalculateMixerStateSideBySidePreview(self): + self.log.info('Updating Mixer-State for ' + 'Side-by-side-Preview-Composition') + + width, height = self.getInputVideoSize() + self.log.debug('Video-Size parsed as %ux%u', width, height) + + try: + asize = [int(i) for i in Config.get('side-by-side-preview', + 'asize').split('x', 1)] + self.log.debug('A-Video-Size configured to %ux%u', + asize[0], asize[1]) + except: + asize = [ + int(width / 1.25), # 80% + int(height / 1.25) # 80% + ] + self.log.debug('A-Video-Size calculated to %ux%u', + asize[0], asize[1]) + + try: + apos = [int(i) for i in Config.get('side-by-side-preview', + 'apos').split('/', 1)] + self.log.debug('B-Video-Position configured to %u/%u', + apos[0], apos[1]) + except: + apos = [ + int(width / 100), # 1% + int(width / 100) # 1% + ] + self.log.debug('B-Video-Position calculated to %u/%u', + apos[0], apos[1]) + + try: + bsize = [int(i) for i in Config.get('side-by-side-preview', + 'bsize').split('x', 1)] + self.log.debug('B-Video-Size configured to %ux%u', + bsize[0], bsize[1]) + except: + bsize = [ + int(width / 4), # 25% + int(height / 4) # 25% + ] + self.log.debug('B-Video-Size calculated to %ux%u', + bsize[0], bsize[1]) + + try: + bpos = [int(i) for i in Config.get('side-by-side-preview', + 'bpos').split('/', 1)] + self.log.debug('B-Video-Position configured to %u/%u', + bpos[0], bpos[1]) + except: + bpos = [ + width - int(width / 100) - bsize[0], + height - int(width / 100) - bsize[1] # 1% + ] + self.log.debug('B-Video-Position calculated to %u/%u', + bpos[0], bpos[1]) + + for idx, name in enumerate(self.names): + pad = self.padState[idx] + pad.reset() + + if idx == self.sourceA: + pad.xpos, pad.ypos = apos + pad.width, pad.height = asize + pad.zorder = 1 + + elif idx == self.sourceB: + pad.xpos, pad.ypos = bpos + pad.width, pad.height = bsize + pad.zorder = 2 + + else: + pad.alpha = 0 + + def recalculateMixerStatePictureInPicture(self): + self.log.info('Updating Mixer-State for ' + 'Picture-in-Picture-Composition') + + width, height = self.getInputVideoSize() + self.log.debug('Video-Size parsed as %ux%u', width, height) + + try: + pipsize = [int(i) for i in Config.get('picture-in-picture', + 'pipsize').split('x', 1)] + self.log.debug('PIP-Size configured to %ux%u', + pipsize[0], pipsize[1]) + except: + pipsize = [ + int(width / 4), # 25% + int(height / 4) # 25% + ] + self.log.debug('PIP-Size calculated to %ux%u', + pipsize[0], pipsize[1]) + + try: + pippos = [int(i) for i in Config.get('picture-in-picture', + 'pippos').split('/', 1)] + self.log.debug('PIP-Position configured to %u/%u', + pippos[0], pippos[1]) + except: + pippos = [ + width - pipsize[0] - int(width / 100), # 1% + height - pipsize[1] - int(width / 100) # 1% + ] + self.log.debug('PIP-Position calculated to %u/%u', + pippos[0], pippos[1]) + + for idx, name in enumerate(self.names): + pad = self.padState[idx] + pad.reset() + + if idx == self.sourceA: + pass + elif idx == self.sourceB: + pad.xpos, pad.ypos = pippos + pad.width, pad.height = pipsize + pad.zorder = 2 + + else: + pad.alpha = 0 + + def applyMixerState(self): + for idx, state in enumerate(self.padState): + # mixerpad 0 = background + mixerpad = (self.mixingPipeline + .get_by_name('mix') + .get_static_pad('sink_%u' % (idx + 1))) + + self.log.debug('Reconfiguring Mixerpad %u to ' + 'x/y=%u/%u, w/h=%u/%u alpha=%0.2f, zorder=%u', + idx, state.xpos, state.ypos, + state.width, state.height, + state.alpha, state.zorder) + mixerpad.set_property('xpos', state.xpos) + mixerpad.set_property('ypos', state.ypos) + mixerpad.set_property('width', state.width) + mixerpad.set_property('height', state.height) + mixerpad.set_property('alpha', state.alpha) + mixerpad.set_property('zorder', state.zorder) + + def selectCompositeModeDefaultSources(self): + sectionNames = { + CompositeModes.fullscreen: 'fullscreen', + CompositeModes.side_by_side_equal: 'side-by-side-equal', + CompositeModes.side_by_side_preview: 'side-by-side-preview', + CompositeModes.picture_in_picture: 'picture-in-picture' + } + + compositeModeName = self.compositeMode.name + sectionName = sectionNames[self.compositeMode] + + try: + defSource = Config.get(sectionName, 'default-a') + self.setVideoSourceA(self.names.index(defSource)) + self.log.info('Changing sourceA to default of Mode %s: %s', + compositeModeName, defSource) + except Exception as e: + pass + + try: + defSource = Config.get(sectionName, 'default-b') + self.setVideoSourceB(self.names.index(defSource)) + self.log.info('Changing sourceB to default of Mode %s: %s', + compositeModeName, defSource) + except Exception as e: + pass + + def on_handoff(self, object, buffer): + if self.padStateDirty: + self.padStateDirty = False + self.log.debug('[Streaming-Thread]: Pad-State is Dirty, ' + 'applying new Mixer-State') + self.applyMixerState() + + def on_eos(self, bus, message): + self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline') + + def on_error(self, bus, message): + self.log.debug('Received Error-Signal on Mixing-Pipeline') + (error, debug) = message.parse_error() + self.log.debug('Error-Details: #%u: %s', error.code, debug) + + def setVideoSourceA(self, source): + # swap if required + if self.sourceB == source: + self.sourceB = self.sourceA + + self.sourceA = source + self.recalculateMixerState() + + def getVideoSourceA(self): + return self.sourceA + + def setVideoSourceB(self, source): + # swap if required + if self.sourceA == source: + self.sourceA = self.sourceB + + self.sourceB = source + self.recalculateMixerState() + + def getVideoSourceB(self): + return self.sourceB + + def setCompositeMode(self, mode): + self.compositeMode = mode + + self.selectCompositeModeDefaultSources() + self.recalculateMixerState() + + def getCompositeMode(self): + return self.compositeMode diff --git a/voctocore/voctocore.py b/voctocore/voctocore.py index 1d04c4f..fd20163 100755 --- a/voctocore/voctocore.py +++ b/voctocore/voctocore.py @@ -1,91 +1,100 @@ #!/usr/bin/env python3 -import gi, signal, logging, sys +import gi +import signal +import logging +import sys # import GStreamer and GLib-Helper classes gi.require_version('Gst', '1.0') gi.require_version('GstNet', '1.0') from gi.repository import Gst, GstNet, GObject +# import local classes +from lib.args import Args +from lib.loghandler import LogHandler + # check min-version minGst = (1, 5) minPy = (3, 0) Gst.init([]) if Gst.version() < minGst: - raise Exception("GStreamer version", Gst.version(), 'is too old, at least', minGst, 'is required') + raise Exception('GStreamer version', Gst.version(), + 'is too old, at least', minGst, 'is required') if sys.version_info < minPy: - raise Exception("Python version", sys.version_info, 'is too old, at least', minPy, 'is required') + raise Exception('Python version', sys.version_info, + 'is too old, at least', minPy, 'is required') # init GObject & Co. before importing local classes GObject.threads_init() -# import local classes -from lib.args import Args -from lib.loghandler import LogHandler # main class class Voctocore(object): - def __init__(self): - # import local which use the config or the logging system - # this is required, so that we can cnfigure logging, before reading the config - from lib.pipeline import Pipeline - from lib.controlserver import ControlServer - self.log = logging.getLogger('Voctocore') - self.log.debug('creating GObject-MainLoop') - self.mainloop = GObject.MainLoop() + def __init__(self): + # import local which use the config or the logging system + # this is required, so that we can configure logging, + # before reading the config + from lib.pipeline import Pipeline + from lib.controlserver import ControlServer + + self.log = logging.getLogger('Voctocore') + self.log.debug('creating GObject-MainLoop') + self.mainloop = GObject.MainLoop() - # initialize subsystem - self.log.debug('creating A/V-Pipeline') - self.pipeline = Pipeline() + # initialize subsystem + self.log.debug('creating A/V-Pipeline') + self.pipeline = Pipeline() - self.log.debug('creating ControlServer') - self.controlserver = ControlServer(self.pipeline) + self.log.debug('creating ControlServer') + self.controlserver = ControlServer(self.pipeline) - def run(self): - self.log.info('running GObject-MainLoop') - try: - self.mainloop.run() - except KeyboardInterrupt: - self.log.info('Terminated via Ctrl-C') + def run(self): + self.log.info('running GObject-MainLoop') + try: + self.mainloop.run() + except KeyboardInterrupt: + self.log.info('Terminated via Ctrl-C') - def quit(self): - self.log.info('quitting GObject-MainLoop') - self.mainloop.quit() + def quit(self): + self.log.info('quitting GObject-MainLoop') + self.mainloop.quit() # run mainclass def main(): - # configure logging - docolor = (Args.color == 'always') or (Args.color == 'auto' and sys.stderr.isatty()) + # configure logging + docolor = (Args.color == 'always') or (Args.color == 'auto' and + sys.stderr.isatty()) - handler = LogHandler(docolor, Args.timestamp) - logging.root.addHandler(handler) + handler = LogHandler(docolor, Args.timestamp) + logging.root.addHandler(handler) - if Args.verbose >= 2: - level = logging.DEBUG - elif Args.verbose == 1: - level = logging.INFO - else: - level = logging.WARNING + if Args.verbose >= 2: + level = logging.DEBUG + elif Args.verbose == 1: + level = logging.INFO + else: + level = logging.WARNING - logging.root.setLevel(level) + logging.root.setLevel(level) - # make killable by ctrl-c - logging.debug('setting SIGINT handler') - signal.signal(signal.SIGINT, signal.SIG_DFL) + # make killable by ctrl-c + logging.debug('setting SIGINT handler') + signal.signal(signal.SIGINT, signal.SIG_DFL) - logging.info('Python Version: %s', sys.version_info) - logging.info('GStreamer Version: %s', Gst.version()) + logging.info('Python Version: %s', sys.version_info) + logging.info('GStreamer Version: %s', Gst.version()) - # init main-class and main-loop - logging.debug('initializing Voctocore') - voctocore = Voctocore() + # init main-class and main-loop + logging.debug('initializing Voctocore') + voctocore = Voctocore() - logging.debug('running Voctocore') - voctocore.run() + logging.debug('running Voctocore') + voctocore.run() if __name__ == '__main__': - main() + main() diff --git a/voctogui/lib/args.py b/voctogui/lib/args.py index 83cd40d..f50126f 100644 --- a/voctogui/lib/args.py +++ b/voctogui/lib/args.py @@ -4,21 +4,24 @@ __all__ = ['Args'] parser = argparse.ArgumentParser(description='Voctogui') parser.add_argument('-v', '--verbose', action='count', default=0, - help="Also print INFO and DEBUG messages.") + help="Also print INFO and DEBUG messages.") -parser.add_argument('-c', '--color', action='store', choices=['auto', 'always', 'never'], default='auto', - help="Control the use of colors in the Log-Output") +parser.add_argument('-c', '--color', + action='store', + choices=['auto', 'always', 'never'], + default='auto', + help="Control the use of colors in the Log-Output") parser.add_argument('-t', '--timestamp', action='store_true', - help="Enable timestamps in the Log-Output") + help="Enable timestamps in the Log-Output") parser.add_argument('-i', '--ini-file', action='store', - help="Load a custom config.ini-File") + help="Load a custom config.ini-File") parser.add_argument('-u', '--ui-file', action='store', - help="Load a custom .ui-File") + help="Load a custom .ui-File") parser.add_argument('-H', '--host', action='store', - help="Connect to this host instead of the configured one.") + help="Connect to this host instead of the configured one.") Args = parser.parse_args() diff --git a/voctogui/lib/audioleveldisplay.py b/voctogui/lib/audioleveldisplay.py index 1a4be14..bd8fb21 100644 --- a/voctogui/lib/audioleveldisplay.py +++ b/voctogui/lib/audioleveldisplay.py @@ -1,121 +1,127 @@ -import logging, math +import logging +import math from gi.repository import Gst, Gtk + class AudioLevelDisplay(object): - """ Displays a Level-Meter of another VideoDisplay into a GtkWidget """ - - def __init__(self, drawing_area): - self.log = logging.getLogger('AudioLevelDisplay[%s]' % drawing_area.get_name()) - - self.drawing_area = drawing_area - - self.levelrms = [] - self.levelpeak = [] - self.leveldecay = [] - - # register on_draw handler - self.drawing_area.connect('draw', self.on_draw) - - def on_draw(self, widget, cr): - # number of audio-channels - channels = len(self.levelrms) - - if channels == 0: - return - - width = self.drawing_area.get_allocated_width() - height = self.drawing_area.get_allocated_height() - - # space between the channels in px - margin = 2 - - # 1 channel -> 0 margins, 2 channels -> 1 margin, 3 channels… - channel_width = int( (width - (margin * (channels - 1))) / channels ) - - # self.log.debug( - # 'width: %upx filled with %u channels of each %upx ' - # 'and %ux margin of %upx', - # width, channels, channel_width, channels-1, margin) - - # normalize db-value to 0…1 and multiply with the height - rms_px = [ self.normalize_db(db) * height for db in self.levelrms ] - peak_px = [ self.normalize_db(db) * height for db in self.levelpeak ] - decay_px = [ self.normalize_db(db) * height for db in self.leveldecay ] - - # set the line-width >1, to get a nice overlap - cr.set_line_width(2) - - # iterate over all pixels - for y in range(0, height): - - # calculate our place in the color-gradient, clamp to 0…1 - # 0 -> green, 0.5 -> yellow, 1 -> red - color = self.clamp(((y / height) - 0.6) / 0.42) - - for channel in range(0, channels): - # start-coordinate for this channel - x = (channel * channel_width) + (channel * margin) - - # calculate the brightness based on whether this line is in the - # active region - - # default to 0.25, dark - bright = 0.25 - if int(y - decay_px[channel]) in range(0, 2): - # decay marker, 2px wide, extra bright - bright = 1.5 - elif y < rms_px[channel]: - # rms bar, full bright - bright = 1 - elif y < peak_px[channel]: - # peak bar, a little darker - bright = 0.75 - - # set the color with a little reduced green - cr.set_source_rgb( - color * bright, - (1-color) * bright * 0.75, - 0 - ) - - # draw the marker - cr.move_to(x, height-y) - cr.line_to(x + channel_width, height-y) - cr.stroke() - - # draw a black line for the margin - cr.set_source_rgb(0,0,0) - cr.move_to(x + channel_width, height-y) - cr.line_to(x + channel_width + margin, height-y) - cr.stroke() - - # draw db text-markers - cr.set_source_rgb(1, 1, 1) - for db in [-40, -20, -10, -5, -4, -3, -2, -1]: - text = str(db) - xbearing, ybearing, textwidth, textheight, xadvance, yadvance = ( - cr.text_extents(text)) - - y = self.normalize_db(db) * height - cr.move_to((width-textwidth) / 2, height - y - textheight) - cr.show_text(text) - - return True - - def normalize_db(self, db): - # -60db -> 1.00 (very quiet) - # -30db -> 0.75 - # -15db -> 0.50 - # -5db -> 0.25 - # -0db -> 0.00 (very loud) - logscale = 1 - math.log10(-0.15 * db + 1) - return self.clamp(logscale) - - def clamp(self, value, min_value=0, max_value=1): - return max(min(value, max_value), min_value) - - def level_callback(self, rms, peak, decay): - self.levelrms = rms - self.levelpeak = peak - self.leveldecay = decay - self.drawing_area.queue_draw() + """Displays a Level-Meter of another VideoDisplay into a GtkWidget""" + + def __init__(self, drawing_area): + self.log = logging.getLogger( + 'AudioLevelDisplay[{}]'.format(drawing_area.get_name()) + ) + + self.drawing_area = drawing_area + + self.levelrms = [] + self.levelpeak = [] + self.leveldecay = [] + + # register on_draw handler + self.drawing_area.connect('draw', self.on_draw) + + def on_draw(self, widget, cr): + # number of audio-channels + channels = len(self.levelrms) + + if channels == 0: + return + + width = self.drawing_area.get_allocated_width() + height = self.drawing_area.get_allocated_height() + + # space between the channels in px + margin = 2 + + # 1 channel -> 0 margins, 2 channels -> 1 margin, 3 channels… + channel_width = int((width - (margin * (channels - 1))) / channels) + + # self.log.debug( + # 'width: %upx filled with %u channels of each %upx ' + # 'and %ux margin of %upx', + # width, channels, channel_width, channels - 1, margin + # ) + + # normalize db-value to 0…1 and multiply with the height + rms_px = [self.normalize_db(db) * height for db in self.levelrms] + peak_px = [self.normalize_db(db) * height for db in self.levelpeak] + decay_px = [self.normalize_db(db) * height for db in self.leveldecay] + + # set the line-width >1, to get a nice overlap + cr.set_line_width(2) + + # iterate over all pixels + for y in range(0, height): + + # calculate our place in the color-gradient, clamp to 0…1 + # 0 -> green, 0.5 -> yellow, 1 -> red + color = self.clamp(((y / height) - 0.6) / 0.42) + + for channel in range(0, channels): + # start-coordinate for this channel + x = (channel * channel_width) + (channel * margin) + + # calculate the brightness based on whether this line is in the + # active region + + # default to 0.25, dark + bright = 0.25 + if int(y - decay_px[channel]) in range(0, 2): + # decay marker, 2px wide, extra bright + bright = 1.5 + elif y < rms_px[channel]: + # rms bar, full bright + bright = 1 + elif y < peak_px[channel]: + # peak bar, a little darker + bright = 0.75 + + # set the color with a little reduced green + cr.set_source_rgb( + color * bright, + (1 - color) * bright * 0.75, + 0 + ) + + # draw the marker + cr.move_to(x, height - y) + cr.line_to(x + channel_width, height - y) + cr.stroke() + + # draw a black line for the margin + cr.set_source_rgb(0, 0, 0) + cr.move_to(x + channel_width, height - y) + cr.line_to(x + channel_width + margin, height - y) + cr.stroke() + + # draw db text-markers + cr.set_source_rgb(1, 1, 1) + for db in [-40, -20, -10, -5, -4, -3, -2, -1]: + text = str(db) + (xbearing, ybearing, + textwidth, textheight, + xadvance, yadvance) = cr.text_extents(text) + + y = self.normalize_db(db) * height + cr.move_to((width - textwidth) / 2, height - y - textheight) + cr.show_text(text) + + return True + + def normalize_db(self, db): + # -60db -> 1.00 (very quiet) + # -30db -> 0.75 + # -15db -> 0.50 + # -5db -> 0.25 + # -0db -> 0.00 (very loud) + logscale = 1 - math.log10(-0.15 * db + 1) + return self.clamp(logscale) + + def clamp(self, value, min_value=0, max_value=1): + return max(min(value, max_value), min_value) + + def level_callback(self, rms, peak, decay): + self.levelrms = rms + self.levelpeak = peak + self.leveldecay = decay + self.drawing_area.queue_draw() diff --git a/voctogui/lib/audioselector.py b/voctogui/lib/audioselector.py index 4f8f9ec..c4be4d3 100644 --- a/voctogui/lib/audioselector.py +++ b/voctogui/lib/audioselector.py @@ -4,70 +4,72 @@ from gi.repository import Gst, Gdk, GLib from lib.config import Config import lib.connection as Connection + class AudioSelectorController(object): - """ Displays a Level-Meter of another VideoDisplay into a GtkWidget """ + """Displays a Level-Meter of another VideoDisplay into a GtkWidget""" - def __init__(self, drawing_area, win, uibuilder): - self.log = logging.getLogger('AudioSelectorController') + def __init__(self, drawing_area, win, uibuilder): + self.log = logging.getLogger('AudioSelectorController') - self.drawing_area = drawing_area - self.win = win + self.drawing_area = drawing_area + self.win = win - combo = uibuilder.find_widget_recursive(win, 'combo_audio') - combo.connect('changed', self.on_changed) - #combo.set_sensitive(True) - self.combo = combo + combo = uibuilder.find_widget_recursive(win, 'combo_audio') + combo.connect('changed', self.on_changed) + # combo.set_sensitive(True) + self.combo = combo - eventbox = uibuilder.find_widget_recursive(win, 'combo_audio_events') - eventbox.connect('button_press_event', self.on_button_press_event) - eventbox.set_property('above_child', True) - self.eventbox = eventbox + eventbox = uibuilder.find_widget_recursive(win, 'combo_audio_events') + eventbox.connect('button_press_event', self.on_button_press_event) + eventbox.set_property('above_child', True) + self.eventbox = eventbox - combo.remove_all() - for name in Config.getlist('mix', 'sources'): - combo.append(name, name) + combo.remove_all() + for name in Config.getlist('mix', 'sources'): + combo.append(name, name) - # connect event-handler and request initial state - Connection.on('audio_status', self.on_audio_status) - Connection.send('get_audio') + # connect event-handler and request initial state + Connection.on('audio_status', self.on_audio_status) + Connection.send('get_audio') - self.timer_iteration = 0 + self.timer_iteration = 0 - def on_audio_status(self, source): - self.log.info('on_audio_status callback w/ source: %s', source) - self.combo.set_active_id(source) + def on_audio_status(self, source): + self.log.info('on_audio_status callback w/ source: %s', source) + self.combo.set_active_id(source) - def on_button_press_event(self, combo, event): - if event.type != Gdk.EventType.DOUBLE_BUTTON_PRESS: - return + def on_button_press_event(self, combo, event): + if event.type != Gdk.EventType.DOUBLE_BUTTON_PRESS: + return - self.log.debug('double-clicked, unlocking') - self.set_enabled(True) - GLib.timeout_add_seconds(5, self.on_disabled_timer, self.timer_iteration) + self.log.debug('double-clicked, unlocking') + self.set_enabled(True) + GLib.timeout_add_seconds(5, self.on_disabled_timer, + self.timer_iteration) - def on_disabled_timer(self, timer_iteration): - if timer_iteration != self.timer_iteration: - self.log.debug('lock-timer fired late, ignoring') - return + def on_disabled_timer(self, timer_iteration): + if timer_iteration != self.timer_iteration: + self.log.debug('lock-timer fired late, ignoring') + return - self.log.debug('lock-timer fired, locking') - self.set_enabled(False) - return False + self.log.debug('lock-timer fired, locking') + self.set_enabled(False) + return False - def set_enabled(self, enable): - self.combo.set_sensitive(enable) - self.eventbox.set_property('above_child', not enable) + def set_enabled(self, enable): + self.combo.set_sensitive(enable) + self.eventbox.set_property('above_child', not enable) - def is_enabled(self): - return self.combo.get_sensitive() + def is_enabled(self): + return self.combo.get_sensitive() - def on_changed(self, combo): - if not self.is_enabled(): - return + def on_changed(self, combo): + if not self.is_enabled(): + return - self.timer_iteration += 1 + self.timer_iteration += 1 - value = combo.get_active_text() - self.log.info('changed to %s', value) - self.set_enabled(False) - Connection.send('set_audio', value) + value = combo.get_active_text() + self.log.info('changed to %s', value) + self.set_enabled(False) + Connection.send('set_audio', value) diff --git a/voctogui/lib/clock.py b/voctogui/lib/clock.py index 9075bdc..1a977ce 100644 --- a/voctogui/lib/clock.py +++ b/voctogui/lib/clock.py @@ -8,13 +8,14 @@ port = 9998 log = logging.getLogger('Clock') Clock = None + def obtainClock(host): - global log, Clock, SystemClock + global log, Clock, SystemClock - log.debug('obtaining NetClientClock from host %s', host) - Clock = GstNet.NetClientClock.new('voctocore', host, port, 0) - log.debug('obtained NetClientClock from host %s: %s', host, Clock) + log.debug('obtaining NetClientClock from host %s', host) + Clock = GstNet.NetClientClock.new('voctocore', host, port, 0) + log.debug('obtained NetClientClock from host %s: %s', host, Clock) - log.debug('waiting for NetClientClock to sync to host') - Clock.wait_for_sync(Gst.CLOCK_TIME_NONE) - log.info('successfully synced NetClientClock to host') + log.debug('waiting for NetClientClock to sync to host') + Clock.wait_for_sync(Gst.CLOCK_TIME_NONE) + log.info('successfully synced NetClientClock to host') diff --git a/voctogui/lib/config.py b/voctogui/lib/config.py index d4630bf..dc7c561 100644 --- a/voctogui/lib/config.py +++ b/voctogui/lib/config.py @@ -7,31 +7,35 @@ import lib.connection as Connection __all__ = ['Config'] + def getlist(self, section, option): - return [x.strip() for x in self.get(section, option).split(',')] + return [x.strip() for x in self.get(section, option).split(',')] + def fetchServerConfig(self): - log = logging.getLogger('Config') - log.info("reading server-config") + log = logging.getLogger('Config') + log.info("reading server-config") - server_config = Connection.fetchServerConfig() + server_config = Connection.fetchServerConfig() - log.info("merging server-config %s", server_config) - self.read_dict(server_config) + log.info("merging server-config %s", server_config) + self.read_dict(server_config) SafeConfigParser.getlist = getlist SafeConfigParser.fetchServerConfig = fetchServerConfig files = [ - os.path.join(os.path.dirname(os.path.realpath(__file__)), '../default-config.ini'), - os.path.join(os.path.dirname(os.path.realpath(__file__)), '../config.ini'), - '/etc/voctomix/voctogui.ini', - '/etc/voctogui.ini', - os.path.expanduser('~/.voctogui.ini'), + os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../default-config.ini'), + os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../config.ini'), + '/etc/voctomix/voctogui.ini', + '/etc/voctogui.ini', + os.path.expanduser('~/.voctogui.ini'), ] if Args.ini_file is not None: - files.append(Args.ini_file) + files.append(Args.ini_file) Config = SafeConfigParser() Config.read(files) diff --git a/voctogui/lib/connection.py b/voctogui/lib/connection.py index 6f8245f..500973d 100644 --- a/voctogui/lib/connection.py +++ b/voctogui/lib/connection.py @@ -12,140 +12,147 @@ port = 9999 command_queue = Queue() signal_handlers = {} + def establish(host): - global conn, port, log, ip + global conn, port, log, ip + + log.info('establishing Connection to %s', host) + conn = socket.create_connection((host, port)) + log.debug('Connection successful \o/') - log.info('establishing Connection to %s', host) - conn = socket.create_connection( (host, port) ) - log.debug('Connection successful \o/') + ip = conn.getpeername()[0] + log.debug('Remote-IP is %s', ip) - ip = conn.getpeername()[0] - log.debug('Remote-IP is %s', ip) def fetchServerConfig(): - global conn, log + global conn, log - log.info('reading server-config') - fd = conn.makefile('rw') - fd.write("get_config\n") - fd.flush() + log.info('reading server-config') + fd = conn.makefile('rw') + fd.write("get_config\n") + fd.flush() - while True: - line = fd.readline() - words = line.split(' ') + while True: + line = fd.readline() + words = line.split(' ') - signal = words[0] - args = words[1:] + signal = words[0] + args = words[1:] - if signal != 'server_config': - continue + if signal != 'server_config': + continue - server_config_json = " ".join(args) - server_config = json.loads(server_config_json) - return server_config + server_config_json = " ".join(args) + server_config = json.loads(server_config_json) + return server_config def enterNonblockingMode(): - global conn, log + global conn, log + + log.debug('entering nonblocking-mode') + conn.setblocking(False) + GObject.io_add_watch(conn, GObject.IO_IN, on_data, ['']) - log.debug('entering nonblocking-mode') - conn.setblocking(False) - GObject.io_add_watch(conn, GObject.IO_IN, on_data, ['']) def on_data(conn, _, leftovers, *args): - global log + global log + + '''Asynchronous connection handler. Pushes data from socket + into command queue linewise''' + try: + while True: + try: + leftovers.append(conn.recv(4096).decode(errors='replace')) + if len(leftovers[-1]) == 0: + log.info("Socket was closed") - '''Asynchronous connection handler. Pushes data from socket - into command queue linewise''' - try: - while True: - try: - leftovers.append(conn.recv(4096).decode(errors='replace')) - if len(leftovers[-1]) == 0: - log.info("Socket was closed") + # FIXME try to reconnect + conn.close() + Gtk.main_quit() + return False - # FIXME try to reconnect - conn.close() - Gtk.main_quit() - return False + except UnicodeDecodeError as e: + continue + except: + pass - except UnicodeDecodeError as e: - continue - except: - pass + data = "".join(leftovers) + del leftovers[:] - data = "".join(leftovers) - del leftovers[:] + lines = data.split('\n') + for line in lines[:-1]: + log.debug("got line: %r", line) - lines = data.split('\n') - for line in lines[:-1]: - log.debug("got line: %r", line) + line = line.strip() + log.debug('re-starting on_loop scheduling') + GObject.idle_add(on_loop) - line = line.strip() - log.debug('re-starting on_loop scheduling') - GObject.idle_add(on_loop) + command_queue.put((line, conn)) - command_queue.put((line, conn)) + if lines[-1] != '': + log.debug("remaining %r", lines[-1]) - if lines[-1] != '': - log.debug("remaining %r", lines[-1]) + leftovers.append(lines[-1]) + return True - leftovers.append(lines[-1]) - return True def on_loop(): - '''Command handler. Processes commands in the command queue whenever - nothing else is happening (registered as GObject idle callback)''' + '''Command handler. Processes commands in the command queue whenever + nothing else is happening (registered as GObject idle callback)''' - global command_queue + global command_queue - log.debug('on_loop called') + log.debug('on_loop called') - if command_queue.empty(): - log.debug('command_queue is empty again, stopping on_loop scheduling') - return False + if command_queue.empty(): + log.debug('command_queue is empty again, stopping on_loop scheduling') + return False - line, requestor = command_queue.get() + line, requestor = command_queue.get() - words = line.split() - if len(words) < 1: - log.debug('command_queue is empty again, stopping on_loop scheduling') - return True + words = line.split() + if len(words) < 1: + log.debug('command_queue is empty again, stopping on_loop scheduling') + return True - signal = words[0] - args = words[1:] + signal = words[0] + args = words[1:] - log.info('received signal %s, dispatching', signal) - if signal not in signal_handlers: - return True + log.info('received signal %s, dispatching', signal) + if signal not in signal_handlers: + return True - for handler in signal_handlers[signal]: - cb = handler['cb'] - if 'one' in handler and handler['one']: - log.debug('removing one-time handler') - del signal_handlers[signal] + for handler in signal_handlers[signal]: + cb = handler['cb'] + if 'one' in handler and handler['one']: + log.debug('removing one-time handler') + del signal_handlers[signal] - cb(*args) + cb(*args) + + return True - return True def send(command, *args): - global conn, log - if len(args) > 0: - command += ' '+(' '.join(args)) + global conn, log + if len(args) > 0: + command += ' ' + (' '.join(args)) + + command += '\n' - command += '\n' + conn.send(command.encode('ascii')) - conn.send(command.encode('ascii')) def on(signal, cb): - if signal not in signal_handlers: - signal_handlers[signal] = [] + if signal not in signal_handlers: + signal_handlers[signal] = [] + + signal_handlers[signal].append({'cb': cb}) - signal_handlers[signal].append({'cb': cb}) def one(signal, cb): - if signal not in signal_handlers: - signal_handlers[signal] = [] + if signal not in signal_handlers: + signal_handlers[signal] = [] - signal_handlers[signal].append({'cb': cb, 'one': True}) + signal_handlers[signal].append({'cb': cb, 'one': True}) diff --git a/voctogui/lib/loghandler.py b/voctogui/lib/loghandler.py index 2cc7ceb..6efb890 100644 --- a/voctogui/lib/loghandler.py +++ b/voctogui/lib/loghandler.py @@ -1,41 +1,57 @@ -import logging, time +import logging +import time -class LogFormatter(logging.Formatter): - def __init__(self, docolor, timestamps=False): - super().__init__() - self.docolor = docolor - self.timestamps = timestamps - - def formatMessage(self, record): - if self.docolor: - c_lvl = 33 - c_mod = 32 - c_msg = 0 - - if record.levelno == logging.WARNING: - c_lvl = 31 - #c_mod = 33 - c_msg = 33 - - elif record.levelno > logging.WARNING: - c_lvl = 31 - c_mod = 31 - c_msg = 31 - fmt = '\x1b['+str(c_lvl)+'m%(levelname)8s\x1b[0m \x1b['+str(c_mod)+'m%(name)s\x1b['+str(c_msg)+'m: %(message)s\x1b[0m' - else: - fmt = '%(levelname)8s %(name)s: %(message)s' - - if self.timestamps: - fmt = '%(asctime)s '+fmt - - if not 'asctime' in record.__dict__: - record.__dict__['asctime']=time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(record.__dict__['created'])) +class LogFormatter(logging.Formatter): - return fmt % record.__dict__ + def __init__(self, docolor, timestamps=False): + super().__init__() + self.docolor = docolor + self.timestamps = timestamps + + def formatMessage(self, record): + if self.docolor: + c_lvl = 33 + c_mod = 32 + c_msg = 0 + + if record.levelno == logging.WARNING: + c_lvl = 31 + # c_mod = 33 + c_msg = 33 + + elif record.levelno > logging.WARNING: + c_lvl = 31 + c_mod = 31 + c_msg = 31 + + fmt = ''.join([ + '\x1b[%dm' % c_lvl, # set levelname color + '%(levelname)8s', # print levelname + '\x1b[0m', # reset formatting + '\x1b[%dm' % c_mod, # set name color + ' %(name)s', # print name + '\x1b[%dm' % c_msg, # set message color + ': %(message)s', # print message + '\x1b[0m' # reset formatting + ]) + else: + fmt = '%(levelname)8s %(name)s: %(message)s' + + if self.timestamps: + fmt = '%(asctime)s ' + fmt + + if 'asctime' not in record.__dict__: + record.__dict__['asctime'] = time.strftime( + "%Y-%m-%d %H:%M:%S", + time.localtime(record.__dict__['created']) + ) + + return fmt % record.__dict__ class LogHandler(logging.StreamHandler): - def __init__(self, docolor, timestamps): - super().__init__() - self.setFormatter(LogFormatter(docolor,timestamps)) + + def __init__(self, docolor, timestamps): + super().__init__() + self.setFormatter(LogFormatter(docolor, timestamps)) diff --git a/voctogui/lib/toolbar/composition.py b/voctogui/lib/toolbar/composition.py index a2b1f29..2674260 100644 --- a/voctogui/lib/toolbar/composition.py +++ b/voctogui/lib/toolbar/composition.py @@ -3,54 +3,58 @@ from gi.repository import Gtk import lib.connection as Connection -class CompositionToolbarController(object): - """ Manages Accelerators and Clicks on the Composition Toolbar-Buttons """ - - def __init__(self, drawing_area, win, uibuilder): - self.log = logging.getLogger('CompositionToolbarController') - - accelerators = Gtk.AccelGroup() - win.add_accel_group(accelerators) - - composites = [ - 'fullscreen', - 'picture_in_picture', - 'side_by_side_equal', - 'side_by_side_preview' - ] - - self.composite_btns = {} - self.current_composition = None - - for idx, name in enumerate(composites): - key, mod = Gtk.accelerator_parse('F%u' % (idx+1)) - btn = uibuilder.find_widget_recursive(drawing_area, 'composite-'+name.replace('_', '-')) - btn.set_name(name) - - # Thanks to http://stackoverflow.com/a/19739855/1659732 - btn.get_child().add_accelerator('clicked', accelerators, key, mod, Gtk.AccelFlags.VISIBLE) - btn.connect('toggled', self.on_btn_toggled) - self.composite_btns[name] = btn - - # connect event-handler and request initial state - Connection.on('composite_mode', self.on_composite_mode) - Connection.send('get_composite_mode') - - - def on_btn_toggled(self, btn): - if not btn.get_active(): - return - - btn_name = btn.get_name() - if self.current_composition == btn_name: - self.log.info('composition-mode already active: %s', btn_name) - return - - self.log.info('composition-mode activated: %s', btn_name) - Connection.send('set_composite_mode', btn_name) - - def on_composite_mode(self, mode): - self.log.info('on_composite_mode callback w/ mode %s', mode) - self.current_composition = mode - self.composite_btns[mode].set_active(True) +class CompositionToolbarController(object): + """Manages Accelerators and Clicks on the Composition Toolbar-Buttons""" + + def __init__(self, drawing_area, win, uibuilder): + self.log = logging.getLogger('CompositionToolbarController') + + accelerators = Gtk.AccelGroup() + win.add_accel_group(accelerators) + + composites = [ + 'fullscreen', + 'picture_in_picture', + 'side_by_side_equal', + 'side_by_side_preview' + ] + + self.composite_btns = {} + self.current_composition = None + + for idx, name in enumerate(composites): + key, mod = Gtk.accelerator_parse('F%u' % (idx + 1)) + btn = uibuilder.find_widget_recursive( + drawing_area, + 'composite-' + name.replace('_', '-') + ) + btn.set_name(name) + + # Thanks to http://stackoverflow.com/a/19739855/1659732 + btn.get_child().add_accelerator('clicked', accelerators, + key, mod, Gtk.AccelFlags.VISIBLE) + btn.connect('toggled', self.on_btn_toggled) + + self.composite_btns[name] = btn + + # connect event-handler and request initial state + Connection.on('composite_mode', self.on_composite_mode) + Connection.send('get_composite_mode') + + def on_btn_toggled(self, btn): + if not btn.get_active(): + return + + btn_name = btn.get_name() + if self.current_composition == btn_name: + self.log.info('composition-mode already active: %s', btn_name) + return + + self.log.info('composition-mode activated: %s', btn_name) + Connection.send('set_composite_mode', btn_name) + + def on_composite_mode(self, mode): + self.log.info('on_composite_mode callback w/ mode %s', mode) + self.current_composition = mode + self.composite_btns[mode].set_active(True) diff --git a/voctogui/lib/toolbar/misc.py b/voctogui/lib/toolbar/misc.py index 9528b67..530bbad 100644 --- a/voctogui/lib/toolbar/misc.py +++ b/voctogui/lib/toolbar/misc.py @@ -6,30 +6,31 @@ import lib.connection as Connection class MiscToolbarController(object): - """ Manages Accelerators and Clicks Misc buttons """ + """Manages Accelerators and Clicks Misc buttons""" - def __init__(self, drawing_area, win, uibuilder): - self.log = logging.getLogger('MiscToolbarController') + def __init__(self, drawing_area, win, uibuilder): + self.log = logging.getLogger('MiscToolbarController') - # Accelerators - accelerators = Gtk.AccelGroup() - win.add_accel_group(accelerators) + # Accelerators + accelerators = Gtk.AccelGroup() + win.add_accel_group(accelerators) - closebtn = uibuilder.find_widget_recursive(drawing_area, 'close') - closebtn.set_visible( Config.getboolean('misc', 'close') ) - closebtn.connect('clicked', self.on_closebtn_clicked) + closebtn = uibuilder.find_widget_recursive(drawing_area, 'close') + closebtn.set_visible(Config.getboolean('misc', 'close')) + closebtn.connect('clicked', self.on_closebtn_clicked) - cutbtn = uibuilder.find_widget_recursive(drawing_area, 'cut') - cutbtn.set_visible( Config.getboolean('misc', 'cut') ) - cutbtn.connect('clicked', self.on_cutbtn_clicked) + cutbtn = uibuilder.find_widget_recursive(drawing_area, 'cut') + cutbtn.set_visible(Config.getboolean('misc', 'cut')) + cutbtn.connect('clicked', self.on_cutbtn_clicked) - key, mod = Gtk.accelerator_parse('t') - cutbtn.add_accelerator('clicked', accelerators, key, mod, Gtk.AccelFlags.VISIBLE) + key, mod = Gtk.accelerator_parse('t') + cutbtn.add_accelerator('clicked', accelerators, + key, mod, Gtk.AccelFlags.VISIBLE) - def on_closebtn_clicked(self, btn): - self.log.info('close-button clicked') - Gtk.main_quit() + def on_closebtn_clicked(self, btn): + self.log.info('close-button clicked') + Gtk.main_quit() - def on_cutbtn_clicked(self, btn): - self.log.info('cut-button clicked') - Connection.send('message', 'cut') + def on_cutbtn_clicked(self, btn): + self.log.info('cut-button clicked') + Connection.send('message', 'cut') diff --git a/voctogui/lib/toolbar/streamblank.py b/voctogui/lib/toolbar/streamblank.py index 717fadd..e627237 100644 --- a/voctogui/lib/toolbar/streamblank.py +++ b/voctogui/lib/toolbar/streamblank.py @@ -4,82 +4,88 @@ from gi.repository import Gtk from lib.config import Config import lib.connection as Connection + class StreamblankToolbarController(object): - """ Manages Accelerators and Clicks on the Composition Toolbar-Buttons """ + """Manages Accelerators and Clicks on the Composition Toolbar-Buttons""" - def __init__(self, drawing_area, win, uibuilder, warning_overlay): - self.log = logging.getLogger('StreamblankToolbarController') + def __init__(self, drawing_area, win, uibuilder, warning_overlay): + self.log = logging.getLogger('StreamblankToolbarController') - self.warning_overlay = warning_overlay + self.warning_overlay = warning_overlay - livebtn = uibuilder.find_widget_recursive(drawing_area, 'stream_live') - blankbtn = uibuilder.find_widget_recursive(drawing_area, 'stream_blank') + livebtn = uibuilder.find_widget_recursive(drawing_area, 'stream_live') + blankbtn = uibuilder.find_widget_recursive(drawing_area, + 'stream_blank') - blankbtn_pos = drawing_area.get_item_index(blankbtn) + blankbtn_pos = drawing_area.get_item_index(blankbtn) - if not Config.getboolean('stream-blanker', 'enabled'): - self.log.info('disabling stream-blanker features because the server does not support them: %s', Config.getboolean('stream-blanker', 'enabled')) + if not Config.getboolean('stream-blanker', 'enabled'): + self.log.info('disabling stream-blanker features ' + 'because the server does not support them: %s', + Config.getboolean('stream-blanker', 'enabled')) - drawing_area.remove(livebtn) - drawing_area.remove(blankbtn) - return + drawing_area.remove(livebtn) + drawing_area.remove(blankbtn) + return - blank_sources = Config.getlist('stream-blanker', 'sources') - self.status_btns = {} + blank_sources = Config.getlist('stream-blanker', 'sources') + self.status_btns = {} - self.current_status = None + self.current_status = None - livebtn.connect('toggled', self.on_btn_toggled) - livebtn.set_name('live') + livebtn.connect('toggled', self.on_btn_toggled) + livebtn.set_name('live') - self.livebtn = livebtn - self.blank_btns = {} + self.livebtn = livebtn + self.blank_btns = {} - for idx, name in enumerate(blank_sources): - if idx == 0: - new_btn = blankbtn - else: - new_icon = Gtk.Image.new_from_pixbuf(blankbtn.get_icon_widget().get_pixbuf()) - new_btn = Gtk.RadioToolButton(group=livebtn) - new_btn.set_icon_widget(new_icon) - drawing_area.insert(new_btn, blankbtn_pos+1) + for idx, name in enumerate(blank_sources): + if idx == 0: + new_btn = blankbtn + else: + new_icon = Gtk.Image.new_from_pixbuf(blankbtn.get_icon_widget() + .get_pixbuf()) + new_btn = Gtk.RadioToolButton(group=livebtn) + new_btn.set_icon_widget(new_icon) + drawing_area.insert(new_btn, blankbtn_pos + 1) - new_btn.set_label("Stream %s" % name) - new_btn.connect('toggled', self.on_btn_toggled) - new_btn.set_name(name) + new_btn.set_label("Stream %s" % name) + new_btn.connect('toggled', self.on_btn_toggled) + new_btn.set_name(name) - self.blank_btns[name] = new_btn + self.blank_btns[name] = new_btn - # connect event-handler and request initial state - Connection.on('stream_status', self.on_stream_status) - Connection.send('get_stream_status') + # connect event-handler and request initial state + Connection.on('stream_status', self.on_stream_status) + Connection.send('get_stream_status') - def on_btn_toggled(self, btn): - if not btn.get_active(): - return + def on_btn_toggled(self, btn): + if not btn.get_active(): + return - btn_name = btn.get_name() - if btn_name == 'live': - self.warning_overlay.disable() + btn_name = btn.get_name() + if btn_name == 'live': + self.warning_overlay.disable() - else: - self.warning_overlay.enable(btn_name) + else: + self.warning_overlay.enable(btn_name) - if self.current_status == btn_name: - self.log.info('stream-status already activate: %s', btn_name) - return + if self.current_status == btn_name: + self.log.info('stream-status already activate: %s', btn_name) + return - self.log.info('stream-status activated: %s', btn_name) - if btn_name == 'live': - Connection.send('set_stream_live') - else: - Connection.send('set_stream_blank', btn_name) + self.log.info('stream-status activated: %s', btn_name) + if btn_name == 'live': + Connection.send('set_stream_live') + else: + Connection.send('set_stream_blank', btn_name) - def on_stream_status(self, status, source = None): - self.log.info('on_stream_status callback w/ status %s and source %s', status, source) + def on_stream_status(self, status, source=None): + self.log.info('on_stream_status callback w/ status %s and source %s', + status, source) - self.current_status = source if source is not None else status - if status == 'live': - self.livebtn.set_active(True) - else: - self.blank_btns[source].set_active(True) + self.current_status = source if source is not None else status + if status == 'live': + self.livebtn.set_active(True) + else: + self.blank_btns[source].set_active(True) diff --git a/voctogui/lib/ui.py b/voctogui/lib/ui.py index 553682c..92d0692 100644 --- a/voctogui/lib/ui.py +++ b/voctogui/lib/ui.py @@ -1,4 +1,5 @@ -import gi, logging +import gi +import logging from gi.repository import Gtk, Gst, Gdk, GLib from lib.config import Config @@ -15,67 +16,76 @@ from lib.toolbar.composition import CompositionToolbarController from lib.toolbar.streamblank import StreamblankToolbarController from lib.toolbar.misc import MiscToolbarController -class Ui(UiBuilder): - def __init__(self, uifile): - self.log = logging.getLogger('Ui') - super().__init__(uifile) - - def setup(self): - self.log.info('Initializing Ui') - - # Aquire the Main-Window from the UI-File - self.win = self.get_check_widget('window') - - # Connect Close-Handler - self.win.connect('delete-event', Gtk.main_quit) - - - # Create Audio-Level Display - drawing_area = self.find_widget_recursive(self.win, 'audiolevel_main') - self.audio_level_display = AudioLevelDisplay(drawing_area) - - - # Create Main-Video Overlay Controller - drawing_area = self.find_widget_recursive(self.win, 'video_overlay_drawingarea') - self.video_warning_overlay = VideoWarningOverlay(drawing_area) - - - # Create Main-Video Display - drawing_area = self.find_widget_recursive(self.win, 'video_main') - self.main_video_display = VideoDisplay(drawing_area, - port=11000, - play_audio=Config.getboolean('mainvideo', 'playaudio'), - level_callback=self.audio_level_display.level_callback) - - - # Setup Preview Controller - drawing_area = self.find_widget_recursive(self.win, 'box_left') - self.video_previews_controller = VideoPreviewsController(drawing_area, - win=self.win, - uibuilder=self) - - drawing_area = self.find_widget_recursive(self.win, 'combo_audio') - self.audio_selector_controller = AudioSelectorController(drawing_area, - win=self.win, - uibuilder=self) - - - # Setup Toolbar Controllers - toolbar = self.find_widget_recursive(self.win, 'toolbar') - self.composition_toolbar_controller = CompositionToolbarController(toolbar, - win=self.win, - uibuilder=self) - - self.streamblank_toolbar_controller = StreamblankToolbarController(toolbar, - win=self.win, - uibuilder=self, - warning_overlay=self.video_warning_overlay) - - self.misc_controller = MiscToolbarController(toolbar, - win=self.win, - uibuilder=self) +class Ui(UiBuilder): - def show(self): - self.log.info('Showing Main-Window') - self.win.show_all() + def __init__(self, uifile): + self.log = logging.getLogger('Ui') + super().__init__(uifile) + + def setup(self): + self.log.info('Initializing Ui') + + # Aquire the Main-Window from the UI-File + self.win = self.get_check_widget('window') + + # Connect Close-Handler + self.win.connect('delete-event', Gtk.main_quit) + + # Create Audio-Level Display + drawing_area = self.find_widget_recursive(self.win, 'audiolevel_main') + self.audio_level_display = AudioLevelDisplay(drawing_area) + + # Create Main-Video Overlay Controller + drawing_area = self.find_widget_recursive(self.win, + 'video_overlay_drawingarea') + self.video_warning_overlay = VideoWarningOverlay(drawing_area) + + # Create Main-Video Display + drawing_area = self.find_widget_recursive(self.win, 'video_main') + self.main_video_display = VideoDisplay( + drawing_area, + port=11000, + play_audio=Config.getboolean('mainvideo', 'playaudio'), + level_callback=self.audio_level_display.level_callback + ) + + # Setup Preview Controller + drawing_area = self.find_widget_recursive(self.win, 'box_left') + self.video_previews_controller = VideoPreviewsController( + drawing_area, + win=self.win, + uibuilder=self + ) + + drawing_area = self.find_widget_recursive(self.win, 'combo_audio') + self.audio_selector_controller = AudioSelectorController( + drawing_area, + win=self.win, + uibuilder=self + ) + + # Setup Toolbar Controllers + toolbar = self.find_widget_recursive(self.win, 'toolbar') + self.composition_toolbar_controller = CompositionToolbarController( + toolbar, + win=self.win, + uibuilder=self + ) + + self.streamblank_toolbar_controller = StreamblankToolbarController( + toolbar, + win=self.win, + uibuilder=self, + warning_overlay=self.video_warning_overlay + ) + + self.misc_controller = MiscToolbarController( + toolbar, + win=self.win, + uibuilder=self + ) + + def show(self): + self.log.info('Showing Main-Window') + self.win.show_all() diff --git a/voctogui/lib/uibuilder.py b/voctogui/lib/uibuilder.py index 2a6b00e..8776480 100644 --- a/voctogui/lib/uibuilder.py +++ b/voctogui/lib/uibuilder.py @@ -1,47 +1,57 @@ -import gi, logging +import gi +import logging from gi.repository import Gtk, Gst -class UiBuilder(object): - def __init__(self, uifile): - if not self.log: - self.log = logging.getLogger('UiBuilder') - - self.uifile = uifile - - self.builder = Gtk.Builder() - self.builder.add_from_file(self.uifile) - - def find_widget_recursive(self, widget, name): - widget = self._find_widget_recursive(widget, name) - if not widget: - self.log.error('could find required widget "%s" by ID inside the parent %s', name, str(widget)) - raise Exception('Widget not found in parent') - - return widget - def _find_widget_recursive(self, widget, name): - if Gtk.Buildable.get_name(widget) == name: - return widget - - if hasattr(widget, 'get_children'): - for child in widget.get_children(): - widget = self._find_widget_recursive(child, name) - if widget: - return widget - - return None - - def get_check_widget(self, widget_id, clone=False): - if clone: - builder = Gtk.Builder() - builder.add_from_file(self.uifile) - else: - builder = self.builder - - self.log.debug('loading widget "%s" from the .ui-File', widget_id) - widget = builder.get_object(widget_id) - if not widget: - self.log.error('could not load required widget "%s" from the .ui-File', widget_id) - raise Exception('Widget not found in .ui-File') +class UiBuilder(object): - return widget + def __init__(self, uifile): + if not self.log: + self.log = logging.getLogger('UiBuilder') + + self.uifile = uifile + + self.builder = Gtk.Builder() + self.builder.add_from_file(self.uifile) + + def find_widget_recursive(self, widget, name): + widget = self._find_widget_recursive(widget, name) + if not widget: + self.log.error( + 'could find required widget "%s" by ID inside the parent %s', + name, + str(widget) + ) + raise Exception('Widget not found in parent') + + return widget + + def _find_widget_recursive(self, widget, name): + if Gtk.Buildable.get_name(widget) == name: + return widget + + if hasattr(widget, 'get_children'): + for child in widget.get_children(): + widget = self._find_widget_recursive(child, name) + if widget: + return widget + + return None + + def get_check_widget(self, widget_id, clone=False): + if clone: + builder = Gtk.Builder() + builder.add_from_file(self.uifile) + else: + builder = self.builder + + self.log.debug('loading widget "%s" from the .ui-File', widget_id) + widget = builder.get_object(widget_id) + if not widget: + self.log.error( + 'could not load required widget "%s" from the .ui-File', + widget_id + ) + raise Exception('Widget not found in .ui-File') + + return widget diff --git a/voctogui/lib/videodisplay.py b/voctogui/lib/videodisplay.py index 744e1aa..9259f6c 100644 --- a/voctogui/lib/videodisplay.py +++ b/voctogui/lib/videodisplay.py @@ -5,152 +5,152 @@ from lib.args import Args from lib.config import Config from lib.clock import Clock + class VideoDisplay(object): - """ Displays a Voctomix-Video-Stream into a GtkWidget """ - - def __init__(self, drawing_area, port, width=None, height=None, play_audio=False, level_callback=None): - self.log = logging.getLogger('VideoDisplay[%u]' % port) - - self.drawing_area = drawing_area - self.level_callback = level_callback - - caps = Config.get('mix', 'videocaps') - use_previews = Config.getboolean('previews', 'enabled') and Config.getboolean('previews', 'use') - - # Preview-Ports are Raw-Ports + 1000 - if use_previews: - self.log.info('using jpeg-previews instead of raw-video for gui') - port += 1000 - else: - self.log.info('using raw-video instead of jpeg-previews for gui') - - # Setup Server-Connection, Demuxing and Decoding - pipeline = """ - tcpclientsrc host={host} port={port} blocksize=1048576 ! - queue ! - matroskademux name=demux - """ - - if use_previews: - pipeline += """ - demux. ! - image/jpeg ! - jpegdec ! - {previewcaps} ! - queue ! - """ - - else: - pipeline += """ - demux. ! - {vcaps} ! - queue ! - """ - - # Video Display - videosystem = Config.get('videodisplay', 'system') - self.log.debug('Configuring for Video-System %s', videosystem) - if videosystem == 'gl': - pipeline += """ - glupload ! - glcolorconvert ! - glimagesinkelement - """ - - elif videosystem == 'xv': - pipeline += """ - xvimagesink - """ - - elif videosystem == 'x': - prescale_caps = 'video/x-raw' - if width and height: - prescale_caps += ',width=%u,height=%u' % (width, height) - - pipeline += """ - videoconvert ! - videoscale ! - {prescale_caps} ! - ximagesink - """.format( - prescale_caps=prescale_caps - ) - - else: - raise Exception('Invalid Videodisplay-System configured: %s' % videosystem) - - - - # If an Audio-Path is required, add an Audio-Path through a level-Element - if self.level_callback or play_audio: - pipeline += """ - demux. ! - {acaps} ! - queue ! - level name=lvl interval=50000000 ! - """ - - # If Playback is requested, push fo pulseaudio - if play_audio: - pipeline += """ - pulsesink - """ - - # Otherwise just trash the Audio - else: - pipeline += """ - fakesink - """ - - pipeline = pipeline.format( - acaps=Config.get('mix', 'audiocaps'), - vcaps=Config.get('mix', 'videocaps'), - previewcaps=Config.get('previews', 'videocaps'), - host=Args.host if Args.host else Config.get('server', 'host'), - port=port, - ) - - self.log.debug('Creating Display-Pipeline:\n%s', pipeline) - self.pipeline = Gst.parse_launch(pipeline) - self.pipeline.use_clock(Clock) - - self.drawing_area.realize() - self.xid = self.drawing_area.get_property('window').get_xid() - self.log.debug('Realized Drawing-Area with xid %u', self.xid) - - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.enable_sync_message_emission() - - bus.connect('message::error', self.on_error) - bus.connect("sync-message::element", self.on_syncmsg) - - if self.level_callback: - bus.connect("message::element", self.on_level) - - self.log.debug('Launching Display-Pipeline') - self.pipeline.set_state(Gst.State.PLAYING) - - - def on_syncmsg(self, bus, msg): - if msg.get_structure().get_name() == "prepare-window-handle": - self.log.info('Setting imagesink window-handle to %s', self.xid) - msg.src.set_window_handle(self.xid) - - def on_error(self, bus, message): - self.log.debug('Received Error-Signal on Display-Pipeline') - (error, debug) = message.parse_error() - self.log.debug('Error-Details: #%u: %s', error.code, debug) - - - def on_level(self, bus, msg): - if msg.src.name != 'lvl': - return - - if msg.type != Gst.MessageType.ELEMENT: - return - - rms = msg.get_structure().get_value('rms') - peak = msg.get_structure().get_value('peak') - decay = msg.get_structure().get_value('decay') - self.level_callback(rms, peak, decay) + """Displays a Voctomix-Video-Stream into a GtkWidget""" + + def __init__(self, drawing_area, port, width=None, height=None, + play_audio=False, level_callback=None): + self.log = logging.getLogger('VideoDisplay[%u]' % port) + + self.drawing_area = drawing_area + self.level_callback = level_callback + + caps = Config.get('mix', 'videocaps') + use_previews = (Config.getboolean('previews', 'enabled') and + Config.getboolean('previews', 'use')) + + # Preview-Ports are Raw-Ports + 1000 + if use_previews: + self.log.info('using jpeg-previews instead of raw-video for gui') + port += 1000 + else: + self.log.info('using raw-video instead of jpeg-previews for gui') + + # Setup Server-Connection, Demuxing and Decoding + pipeline = """ + tcpclientsrc host={host} port={port} blocksize=1048576 ! + queue ! + matroskademux name=demux + """ + + if use_previews: + pipeline += """ + demux. ! + image/jpeg ! + jpegdec ! + {previewcaps} ! + queue ! + """ + + else: + pipeline += """ + demux. ! + {vcaps} ! + queue ! + """ + + # Video Display + videosystem = Config.get('videodisplay', 'system') + self.log.debug('Configuring for Video-System %s', videosystem) + if videosystem == 'gl': + pipeline += """ + glupload ! + glcolorconvert ! + glimagesinkelement + """ + + elif videosystem == 'xv': + pipeline += """ + xvimagesink + """ + + elif videosystem == 'x': + prescale_caps = 'video/x-raw' + if width and height: + prescale_caps += ',width=%u,height=%u' % (width, height) + + pipeline += """ + videoconvert ! + videoscale ! + {prescale_caps} ! + ximagesink + """.format(prescale_caps=prescale_caps) + + else: + raise Exception( + 'Invalid Videodisplay-System configured: %s' % videosystem + ) + + # If an Audio-Path is required, + # add an Audio-Path through a level-Element + if self.level_callback or play_audio: + pipeline += """ + demux. ! + {acaps} ! + queue ! + level name=lvl interval=50000000 ! + """ + + # If Playback is requested, push fo pulseaudio + if play_audio: + pipeline += """ + pulsesink + """ + + # Otherwise just trash the Audio + else: + pipeline += """ + fakesink + """ + + pipeline = pipeline.format( + acaps=Config.get('mix', 'audiocaps'), + vcaps=Config.get('mix', 'videocaps'), + previewcaps=Config.get('previews', 'videocaps'), + host=Args.host if Args.host else Config.get('server', 'host'), + port=port, + ) + + self.log.debug('Creating Display-Pipeline:\n%s', pipeline) + self.pipeline = Gst.parse_launch(pipeline) + self.pipeline.use_clock(Clock) + + self.drawing_area.realize() + self.xid = self.drawing_area.get_property('window').get_xid() + self.log.debug('Realized Drawing-Area with xid %u', self.xid) + + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.enable_sync_message_emission() + + bus.connect('message::error', self.on_error) + bus.connect("sync-message::element", self.on_syncmsg) + + if self.level_callback: + bus.connect("message::element", self.on_level) + + self.log.debug('Launching Display-Pipeline') + self.pipeline.set_state(Gst.State.PLAYING) + + def on_syncmsg(self, bus, msg): + if msg.get_structure().get_name() == "prepare-window-handle": + self.log.info('Setting imagesink window-handle to %s', self.xid) + msg.src.set_window_handle(self.xid) + + def on_error(self, bus, message): + self.log.debug('Received Error-Signal on Display-Pipeline') + (error, debug) = message.parse_error() + self.log.debug('Error-Details: #%u: %s', error.code, debug) + + def on_level(self, bus, msg): + if msg.src.name != 'lvl': + return + + if msg.type != Gst.MessageType.ELEMENT: + return + + rms = msg.get_structure().get_value('rms') + peak = msg.get_structure().get_value('peak') + decay = msg.get_structure().get_value('decay') + self.level_callback(rms, peak, decay) diff --git a/voctogui/lib/videopreviews.py b/voctogui/lib/videopreviews.py index 3490a4d..9adb76f 100644 --- a/voctogui/lib/videopreviews.py +++ b/voctogui/lib/videopreviews.py @@ -5,133 +5,140 @@ from lib.config import Config from lib.videodisplay import VideoDisplay import lib.connection as Connection -class VideoPreviewsController(object): - """ Displays Video-Previews and selection Buttons for them """ - - def __init__(self, drawing_area, win, uibuilder): - self.log = logging.getLogger('VideoPreviewsController') - self.drawing_area = drawing_area - self.win = win - - self.sources = Config.getlist('mix', 'sources') - self.preview_players = {} - self.previews = {} - self.a_btns = {} - self.b_btns = {} +class VideoPreviewsController(object): + """Displays Video-Previews and selection Buttons for them""" - self.current_source = {'a': None, 'b': None} + def __init__(self, drawing_area, win, uibuilder): + self.log = logging.getLogger('VideoPreviewsController') - try: - width = Config.getint('previews', 'width') - self.log.debug('Preview-Width configured to %u', width) - except: - width = 320 - self.log.debug('Preview-Width selected as %u', width) + self.drawing_area = drawing_area + self.win = win - try: - height = Config.getint('previews', 'height') - self.log.debug('Preview-Height configured to %u', height) - except: - height = width*9/16 - self.log.debug('Preview-Height calculated to %u', height) + self.sources = Config.getlist('mix', 'sources') + self.preview_players = {} + self.previews = {} + self.a_btns = {} + self.b_btns = {} - # Accelerators - accelerators = Gtk.AccelGroup() - win.add_accel_group(accelerators) + self.current_source = {'a': None, 'b': None} - group_a = None - group_b = None + try: + width = Config.getint('previews', 'width') + self.log.debug('Preview-Width configured to %u', width) + except: + width = 320 + self.log.debug('Preview-Width selected as %u', width) - for idx, source in enumerate(self.sources): - self.log.info('Initializing Video Preview %s', source) + try: + height = Config.getint('previews', 'height') + self.log.debug('Preview-Height configured to %u', height) + except: + height = width * 9 / 16 + self.log.debug('Preview-Height calculated to %u', height) - preview = uibuilder.get_check_widget('widget_preview', clone=True) - video = uibuilder.find_widget_recursive(preview, 'video') + # Accelerators + accelerators = Gtk.AccelGroup() + win.add_accel_group(accelerators) - video.set_size_request(width, height) - drawing_area.pack_start(preview, fill=False, expand=False, padding=0) + group_a = None + group_b = None - player = VideoDisplay(video, port=13000 + idx, width=width, height=height) + for idx, source in enumerate(self.sources): + self.log.info('Initializing Video Preview %s', source) - uibuilder.find_widget_recursive(preview, 'label').set_label(source) - btn_a = uibuilder.find_widget_recursive(preview, 'btn_a') - btn_b = uibuilder.find_widget_recursive(preview, 'btn_b') + preview = uibuilder.get_check_widget('widget_preview', clone=True) + video = uibuilder.find_widget_recursive(preview, 'video') - btn_a.set_name("%c %u" % ('a', idx)) - btn_b.set_name("%c %u" % ('b', idx)) + video.set_size_request(width, height) + drawing_area.pack_start(preview, fill=False, + expand=False, padding=0) - if not group_a: - group_a = btn_a - else: - btn_a.join_group(group_a) + player = VideoDisplay(video, port=13000 + idx, + width=width, height=height) + uibuilder.find_widget_recursive(preview, 'label').set_label(source) + btn_a = uibuilder.find_widget_recursive(preview, 'btn_a') + btn_b = uibuilder.find_widget_recursive(preview, 'btn_b') - if not group_b: - group_b = btn_b - else: - btn_b.join_group(group_b) + btn_a.set_name("%c %u" % ('a', idx)) + btn_b.set_name("%c %u" % ('b', idx)) + if not group_a: + group_a = btn_a + else: + btn_a.join_group(group_a) - btn_a.connect('toggled', self.btn_toggled) - btn_b.connect('toggled', self.btn_toggled) + if not group_b: + group_b = btn_b + else: + btn_b.join_group(group_b) - key, mod = Gtk.accelerator_parse('%u' % (idx+1)) - btn_a.add_accelerator('activate', accelerators, key, mod, Gtk.AccelFlags.VISIBLE) + btn_a.connect('toggled', self.btn_toggled) + btn_b.connect('toggled', self.btn_toggled) - key, mod = Gtk.accelerator_parse('<Ctrl>%u' % (idx+1)) - btn_b.add_accelerator('activate', accelerators, key, mod, Gtk.AccelFlags.VISIBLE) + key, mod = Gtk.accelerator_parse('%u' % (idx + 1)) + btn_a.add_accelerator('activate', accelerators, + key, mod, Gtk.AccelFlags.VISIBLE) - btn_fullscreen = uibuilder.find_widget_recursive(preview, 'btn_fullscreen') - btn_fullscreen.set_name("%c %u" % ('f', idx)) + key, mod = Gtk.accelerator_parse('<Ctrl>%u' % (idx + 1)) + btn_b.add_accelerator('activate', accelerators, + key, mod, Gtk.AccelFlags.VISIBLE) - btn_fullscreen.connect('clicked', self.btn_fullscreen_clicked) + btn_fullscreen = uibuilder.find_widget_recursive(preview, + 'btn_fullscreen') + btn_fullscreen.set_name("%c %u" % ('f', idx)) - key, mod = Gtk.accelerator_parse('<Alt>%u' % (idx+1)) - btn_fullscreen.add_accelerator('activate', accelerators, key, mod, Gtk.AccelFlags.VISIBLE) + btn_fullscreen.connect('clicked', self.btn_fullscreen_clicked) - self.preview_players[source] = player - self.previews[source] = preview - self.a_btns[source] = btn_a - self.b_btns[source] = btn_b + key, mod = Gtk.accelerator_parse('<Alt>%u' % (idx + 1)) + btn_fullscreen.add_accelerator('activate', accelerators, + key, mod, Gtk.AccelFlags.VISIBLE) + self.preview_players[source] = player + self.previews[source] = preview + self.a_btns[source] = btn_a + self.b_btns[source] = btn_b - # connect event-handler and request initial state - Connection.on('video_status', self.on_video_status) - Connection.send('get_video') + # connect event-handler and request initial state + Connection.on('video_status', self.on_video_status) + Connection.send('get_video') - def btn_toggled(self, btn): - if not btn.get_active(): - return + def btn_toggled(self, btn): + if not btn.get_active(): + return - btn_name = btn.get_name() - self.log.debug('btn_toggled: %s', btn_name) + btn_name = btn.get_name() + self.log.debug('btn_toggled: %s', btn_name) - channel, idx = btn_name.split(' ')[:2] - source_name = self.sources[int(idx)] + channel, idx = btn_name.split(' ')[:2] + source_name = self.sources[int(idx)] - if self.current_source[channel] == source_name: - self.log.info('video-channel %s already on %s', channel, source_name) - return + if self.current_source[channel] == source_name: + self.log.info('video-channel %s already on %s', + channel, source_name) + return - self.log.info('video-channel %s changed to %s', channel, source_name) - Connection.send('set_video_'+channel, source_name) + self.log.info('video-channel %s changed to %s', channel, source_name) + Connection.send('set_video_' + channel, source_name) - def btn_fullscreen_clicked(self, btn): - btn_name = btn.get_name() - self.log.debug('btn_fullscreen_clicked: %s', btn_name) + def btn_fullscreen_clicked(self, btn): + btn_name = btn.get_name() + self.log.debug('btn_fullscreen_clicked: %s', btn_name) - channel, idx = btn_name.split(' ')[:2] - source_name = self.sources[int(idx)] + channel, idx = btn_name.split(' ')[:2] + source_name = self.sources[int(idx)] - self.log.info('selcting video %s for fullscreen', source_name) - Connection.send('set_videos_and_composite', source_name, '*', 'fullscreen') + self.log.info('selcting video %s for fullscreen', source_name) + Connection.send('set_videos_and_composite', + source_name, '*', 'fullscreen') - def on_video_status(self, source_a, source_b): - self.log.info('on_video_status callback w/ sources: %s and %s', source_a, source_b) + def on_video_status(self, source_a, source_b): + self.log.info('on_video_status callback w/ sources: %s and %s', + source_a, source_b) - self.current_source['a'] = source_a - self.current_source['b'] = source_b + self.current_source['a'] = source_a + self.current_source['b'] = source_b - self.a_btns[source_a].set_active(True) - self.b_btns[source_b].set_active(True) + self.a_btns[source_a].set_active(True) + self.b_btns[source_b].set_active(True) diff --git a/voctogui/lib/warningoverlay.py b/voctogui/lib/warningoverlay.py index bf2c2cd..f4f7f24 100644 --- a/voctogui/lib/warningoverlay.py +++ b/voctogui/lib/warningoverlay.py @@ -3,59 +3,64 @@ from gi.repository import GLib, Gst, cairo from lib.config import Config + class VideoWarningOverlay(object): - """ Displays a Warning-Overlay above the Video-Feed of another VideoDisplay """ + """Displays a Warning-Overlay above the Video-Feed + of another VideoDisplay""" - def __init__(self, drawing_area): - self.log = logging.getLogger('VideoWarningOverlay') + def __init__(self, drawing_area): + self.log = logging.getLogger('VideoWarningOverlay') - self.drawing_area = drawing_area - self.drawing_area.connect("draw", self.draw_callback) + self.drawing_area = drawing_area + self.drawing_area.connect("draw", self.draw_callback) - self.text = None - self.blink_state = False + self.text = None + self.blink_state = False - GLib.timeout_add_seconds(1, self.on_blink_callback) + GLib.timeout_add_seconds(1, self.on_blink_callback) - def on_blink_callback(self): - self.blink_state = not self.blink_state - self.drawing_area.queue_draw() - return True + def on_blink_callback(self): + self.blink_state = not self.blink_state + self.drawing_area.queue_draw() + return True - def enable(self, text=None): - self.text = text - self.drawing_area.show() - self.drawing_area.queue_draw() + def enable(self, text=None): + self.text = text + self.drawing_area.show() + self.drawing_area.queue_draw() - def set_text(self, text=None): - self.text = text - self.drawing_area.queue_draw() + def set_text(self, text=None): + self.text = text + self.drawing_area.queue_draw() - def disable(self): - self.drawing_area.hide() - self.drawing_area.queue_draw() + def disable(self): + self.drawing_area.hide() + self.drawing_area.queue_draw() - def draw_callback(self, area, cr): - w = self.drawing_area.get_allocated_width(); - h = self.drawing_area.get_allocated_height(); + def draw_callback(self, area, cr): + w = self.drawing_area.get_allocated_width() + h = self.drawing_area.get_allocated_height() - self.log.debug('draw_callback: w/h=%u/%u, blink_state=%u', w, h, self.blink_state) + self.log.debug('draw_callback: w/h=%u/%u, blink_state=%u', + w, h, self.blink_state) - if self.blink_state: - cr.set_source_rgba(1.0, 0.0, 0.0, 0.8) - else: - cr.set_source_rgba(1.0, 0.5, 0.0, 0.8) + if self.blink_state: + cr.set_source_rgba(1.0, 0.0, 0.0, 0.8) + else: + cr.set_source_rgba(1.0, 0.5, 0.0, 0.8) - cr.rectangle(0, 0, w, h) - cr.fill() + cr.rectangle(0, 0, w, h) + cr.fill() - text = "Stream is Blanked" - if self.text: - text += ": "+self.text + text = "Stream is Blanked" + if self.text: + text += ": " + self.text - cr.set_font_size(h*0.75) - xbearing, ybearing, txtwidth, txtheight, xadvance, yadvance = cr.text_extents(text) + cr.set_font_size(h * 0.75) + (xbearing, ybearing, + txtwidth, txtheight, + xadvance, yadvance) = cr.text_extents(text) - cr.move_to(w/2 - txtwidth/2, h*0.75) - cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) - cr.show_text(text) + cr.move_to(w / 2 - txtwidth / 2, h * 0.75) + cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) + cr.show_text(text) diff --git a/voctogui/voctogui.py b/voctogui/voctogui.py index 9ae3af5..0e953b5 100755 --- a/voctogui/voctogui.py +++ b/voctogui/voctogui.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 -import gi, signal, logging, sys, os +import gi +import signal +import logging +import sys +import os # import GStreamer and GLib-Helper classes gi.require_version('Gtk', '3.0') @@ -15,11 +19,12 @@ minPy = (3, 0) Gst.init([]) if Gst.version() < minGst: - raise Exception("GStreamer version", Gst.version(), 'is too old, at least', minGst, 'is required') + raise Exception('GStreamer version', Gst.version(), + 'is too old, at least', minGst, 'is required') if sys.version_info < minPy: - raise Exception("Python version", sys.version_info, 'is too old, at least', minPy, 'is required') - + raise Exception('Python version', sys.version_info, + 'is too old, at least', minPy, 'is required') # init GObject & Co. before importing local classes GObject.threads_init() @@ -35,112 +40,125 @@ from lib.loghandler import LogHandler import lib.connection as Connection import lib.clock as ClockManager + # main class class Voctogui(object): - def __init__(self): - self.log = logging.getLogger('Voctogui') - - # Uf a UI-File was specified on the Command-Line, load it - if Args.ui_file: - self.log.info('loading ui-file from file specified on command-line: %s', self.options.ui_file) - self.ui = Ui(Args.ui_file) - - else: - # Paths to look for the gst-switch UI-File - paths = [ - os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ui/voctogui.ui'), - '/usr/lib/voctogui/ui/voctogui.ui' - ] - - # Look for a gst-switch UI-File and load it - for path in paths: - self.log.debug('trying to load ui-file from file %s', path) - - if os.path.isfile(path): - self.log.info('loading ui-file from file %s', path) - self.ui = Ui(path) - break - - if self.ui is None: - raise Exception("Can't find any .ui-Files to use (searched %s)" % (', '.join(paths))) - - self.ui.setup() - - - def run(self): - self.log.info('setting UI visible') - self.ui.show() - - try: - self.log.info('running Gtk-MainLoop') - Gtk.main() - self.log.info('Gtk-MainLoop ended') - except KeyboardInterrupt: - self.log.info('Terminated via Ctrl-C') - def quit(self): - self.log.info('quitting Gtk-MainLoop') - Gtk.main_quit() + def __init__(self): + self.log = logging.getLogger('Voctogui') + + # Uf a UI-File was specified on the Command-Line, load it + if Args.ui_file: + self.log.info( + 'loading ui-file from file specified on command-line: %s', + Args.ui_file + ) + self.ui = Ui(Args.ui_file) + else: + # Paths to look for the gst-switch UI-File + paths = [ + os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'ui/voctogui.ui'), + '/usr/lib/voctogui/ui/voctogui.ui' + ] + + # Look for a gst-switch UI-File and load it + self.ui = None + for path in paths: + self.log.debug('trying to load ui-file from file %s', path) + + if os.path.isfile(path): + self.log.info('loading ui-file from file %s', path) + self.ui = Ui(path) + break + + if self.ui is None: + raise Exception("Can't find any .ui-Files to use " + "(searched {})".format(', '.join(paths))) + + self.ui.setup() + + def run(self): + self.log.info('setting UI visible') + self.ui.show() + + try: + self.log.info('running Gtk-MainLoop') + Gtk.main() + self.log.info('Gtk-MainLoop ended') + except KeyboardInterrupt: + self.log.info('Terminated via Ctrl-C') + + def quit(self): + self.log.info('quitting Gtk-MainLoop') + Gtk.main_quit() # run mainclass def main(): - # configure logging - docolor = (Args.color == 'always') or (Args.color == 'auto' and sys.stderr.isatty()) - - handler = LogHandler(docolor, Args.timestamp) - logging.root.addHandler(handler) - - if Args.verbose >= 2: - level = logging.DEBUG - elif Args.verbose == 1: - level = logging.INFO - else: - level = logging.WARNING - - logging.root.setLevel(level) - - # make killable by ctrl-c - logging.debug('setting SIGINT handler') - signal.signal(signal.SIGINT, signal.SIG_DFL) - - logging.info('Python Version: %s', sys.version_info) - logging.info('GStreamer Version: %s', Gst.version()) - - # establish a synchronus connection to server - Connection.establish( - Args.host if Args.host else Config.get('server', 'host')) - - # fetch config from server - Config.fetchServerConfig() - - # Warn when connecting to a non-local core without preview-encoders enabled - # the list-comparison is not complete (one could use a local hostname or the local system ip) - # but it's only here to warn that one might be making a mistake - use_previews = Config.getboolean('previews', 'enabled') and Config.getboolean('previews', 'use') - looks_like_localhost = Config.get('server', 'host') in ['::1', '127.0.0.1', 'localhost'] - if not use_previews and not looks_like_localhost: - logging.warn( - 'Connecting to `%s` (which looks like a remote host) might not work without enabeling ' - 'the preview encoders (set `[previews] enabled=true` on the core) or it might saturate ' - 'your ethernet link between the two machines.', - Config.get('server', 'host') - ) - - # obtain network-clock - ClockManager.obtainClock(Connection.ip) - - # switch connection to nonblocking, event-driven mode - Connection.enterNonblockingMode() - - # init main-class and main-loop - # (this binds all event-hander on the Connection) - logging.debug('initializing Voctogui') - voctogui = Voctogui() - - # start the Mainloop and show the Window - logging.debug('running Voctogui') - voctogui.run() + # configure logging + docolor = (Args.color == 'always') or (Args.color == 'auto' and + sys.stderr.isatty()) + + handler = LogHandler(docolor, Args.timestamp) + logging.root.addHandler(handler) + + if Args.verbose >= 2: + level = logging.DEBUG + elif Args.verbose == 1: + level = logging.INFO + else: + level = logging.WARNING + + logging.root.setLevel(level) + + # make killable by ctrl-c + logging.debug('setting SIGINT handler') + signal.signal(signal.SIGINT, signal.SIG_DFL) + + logging.info('Python Version: %s', sys.version_info) + logging.info('GStreamer Version: %s', Gst.version()) + + # establish a synchronus connection to server + Connection.establish( + Args.host if Args.host else Config.get('server', 'host') + ) + + # fetch config from server + Config.fetchServerConfig() + + # Warn when connecting to a non-local core without preview-encoders enabled + # The list-comparison is not complete + # (one could use a local hostname or the local system ip), + # but it's only here to warn that one might be making a mistake + use_previews = (Config.getboolean('previews', 'enabled') and + Config.getboolean('previews', 'use')) + looks_like_localhost = Config.get('server', 'host') in ['::1', + '127.0.0.1', + 'localhost'] + if not use_previews and not looks_like_localhost: + logging.warn( + 'Connecting to `%s` (which looks like a remote host) ' + 'might not work without enabeling the preview encoders ' + '(set `[previews] enabled=true` on the core) or it might saturate ' + 'your ethernet link between the two machines.', + Config.get('server', 'host') + ) + + # obtain network-clock + ClockManager.obtainClock(Connection.ip) + + # switch connection to nonblocking, event-driven mode + Connection.enterNonblockingMode() + + # init main-class and main-loop + # (this binds all event-hander on the Connection) + logging.debug('initializing Voctogui') + voctogui = Voctogui() + + # start the Mainloop and show the Window + logging.debug('running Voctogui') + voctogui.run() if __name__ == '__main__': - main() + main() |