diff options
-rw-r--r-- | voctogui/lib/args.py | 17 | ||||
-rw-r--r-- | voctogui/lib/audioleveldisplay.py | 242 | ||||
-rw-r--r-- | voctogui/lib/audioselector.py | 100 | ||||
-rw-r--r-- | voctogui/lib/clock.py | 15 | ||||
-rw-r--r-- | voctogui/lib/config.py | 28 | ||||
-rw-r--r-- | voctogui/lib/connection.py | 187 | ||||
-rw-r--r-- | voctogui/lib/loghandler.py | 86 | ||||
-rw-r--r-- | voctogui/lib/toolbar/composition.py | 104 | ||||
-rw-r--r-- | voctogui/lib/toolbar/misc.py | 41 | ||||
-rw-r--r-- | voctogui/lib/toolbar/streamblank.py | 120 | ||||
-rw-r--r-- | voctogui/lib/ui.py | 136 | ||||
-rw-r--r-- | voctogui/lib/uibuilder.py | 96 | ||||
-rw-r--r-- | voctogui/lib/videodisplay.py | 296 | ||||
-rw-r--r-- | voctogui/lib/videopreviews.py | 195 | ||||
-rw-r--r-- | voctogui/lib/warningoverlay.py | 85 | ||||
-rwxr-xr-x | voctogui/voctogui.py | 226 |
16 files changed, 1037 insertions, 937 deletions
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() |