diff options
-rw-r--r-- | example-scripts/voctomidi/README.md | 10 | ||||
-rw-r--r-- | example-scripts/voctomidi/default-config.ini | 19 | ||||
-rw-r--r-- | example-scripts/voctomidi/lib/config.py | 16 | ||||
-rwxr-xr-x | example-scripts/voctomidi/voctomidi.py | 86 | ||||
-rw-r--r-- | voctocore/default-config.ini | 1 | ||||
-rw-r--r-- | voctocore/lib/avpreviewoutput.py | 11 | ||||
-rw-r--r-- | voctocore/lib/commands.py | 67 | ||||
-rw-r--r-- | voctocore/lib/config.py | 9 | ||||
-rw-r--r-- | voctocore/lib/videomix.py | 2 | ||||
-rwxr-xr-x | voctocore/voctocore.py | 7 |
10 files changed, 219 insertions, 9 deletions
diff --git a/example-scripts/voctomidi/README.md b/example-scripts/voctomidi/README.md new file mode 100644 index 0000000..b171f8a --- /dev/null +++ b/example-scripts/voctomidi/README.md @@ -0,0 +1,10 @@ +# Voctomidi - Control Voctocore from a MIDI controller + +## Configuration +Set the `device` option in the `midi` section to the device +you want to connect to. This can be either a device name, or port. +If this is unset or the device can't be found a list of connected devices will +be provided for you to choose from. + +In the `eventmap` section MIDI NOTE ON events are mapped to Voctocore layouts. +The syntax is `<note>=<srcA> <srcB> <mode>`. diff --git a/example-scripts/voctomidi/default-config.ini b/example-scripts/voctomidi/default-config.ini new file mode 100644 index 0000000..8e68bf3 --- /dev/null +++ b/example-scripts/voctomidi/default-config.ini @@ -0,0 +1,19 @@ +[server] +host=localhost + +[midi] +device=nanoPAD + +# nanoPAD Layout: +# Scene 1: +# 39, 48, 45, 43, 51, 49, +# 36, 38, 40, 42, 44, 46 +[eventmap] +39=grabber cam1 side_by_side_preview +48=grabber cam2 side_by_side_preview +45=grabber cam1 side_by_side_equal +43=grabber cam2 side_by_side_equal +51=cam1 cam2 side_by_side_equal +36=cam1 * fullscreen +38=cam2 * fullscreen +40=grabber * fullscreen diff --git a/example-scripts/voctomidi/lib/config.py b/example-scripts/voctomidi/lib/config.py new file mode 100644 index 0000000..44dd8da --- /dev/null +++ b/example-scripts/voctomidi/lib/config.py @@ -0,0 +1,16 @@ +import os.path +from configparser import SafeConfigParser + +__all__ = ['Config'] + +modulepath = os.path.dirname(os.path.realpath(__file__)) +files = [ + os.path.join(modulepath, '../default-config.ini'), + os.path.join(modulepath, '../config.ini'), + '/etc/voctomix/voctomidi.ini', + '/etc/voctomidi.ini', + os.path.expanduser('~/.voctomidi.ini'), +] + +Config = SafeConfigParser() +Config.read(files) diff --git a/example-scripts/voctomidi/voctomidi.py b/example-scripts/voctomidi/voctomidi.py new file mode 100755 index 0000000..b21f529 --- /dev/null +++ b/example-scripts/voctomidi/voctomidi.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +import atexit +import socket +import sys +import time +from rtmidi.midiutil import open_midiport + +from lib.config import Config + +NOTE_ON = 144 + +host = Config.get("server", "host") +port = 9999 + +device = Config.get("midi", "device") + +event_map = dict(map(lambda x: (int(x[0]), x[1]), Config.items("eventmap"))) + + +class MidiInputHandler(object): + def __init__(self, port): + self.port = port + + def __call__(self, event, data=None): + global conn + message, _deltatime = event + if message[0] != NOTE_ON: + return + if message[1] in event_map: + note = message[1] + msg = "set_videos_and_composite " + event_map[note] + print("Sending: '{}'".format(msg)) + try: + conn.sendall(msg.encode('ascii') + b"\n") + except BrokenPipeError: + print("voctocore disconnected, trying to reconnect") + try: + conn = socket.create_connection((host, port)) + print("Reconnected to voctocore") + except: + pass + else: + print("[{}]: Unhandled NOTE ON event {}".format(self.port, + message[1])) + + +@atexit.register +def kthxbye(): + print("Exit") + +conn, midiin = None, None + +try: + conn = socket.create_connection((host, port)) +except (ConnectionRefusedError, KeyboardInterrupt): + print("Could not connect to voctocore") + sys.exit() + + +@atexit.register +def close_conn(): + global conn + conn and conn.close() + +try: + midiin, port_name = open_midiport(device) +except (EOFError, KeyboardInterrupt): + print("Opening midi port failed") + sys.exit() + + +@atexit.register +def close_midi(): + global midiin + midiin and midiin.close_port() + del midiin + +midiin.set_callback(MidiInputHandler(port_name)) + +print("Entering main loop. Press Control-C to exit.") +try: + # just wait for keyboard interrupt in main thread + while True: + time.sleep(1) +except KeyboardInterrupt: + print("") diff --git a/voctocore/default-config.ini b/voctocore/default-config.ini index d5d89fa..31a49ff 100644 --- a/voctocore/default-config.ini +++ b/voctocore/default-config.ini @@ -60,6 +60,7 @@ mix_out=10000 [previews] ; disable if ui & server run on the same computer and can exchange uncompressed video frames enabled=false +deinterlace=false ; default to mix-videocaps, only applicable if enabled=true ; you can change the framerate and the width/height, but nothing else diff --git a/voctocore/lib/avpreviewoutput.py b/voctocore/lib/avpreviewoutput.py index a0bb951..d5c2c66 100644 --- a/voctocore/lib/avpreviewoutput.py +++ b/voctocore/lib/avpreviewoutput.py @@ -17,12 +17,14 @@ class AVPreviewOutput(TCPMultiConnection): else: vcaps_out = Config.get('mix', 'videocaps') + deinterlace = "" + if Config.getboolean('previews', 'deinterlace'): + deinterlace = "deinterlace mode=interlaced !" + pipeline = """ intervideosrc channel=video_{channel} ! {vcaps_in} ! - capssetter caps="video/x-raw,interlace-mode=interlaced" ! - deinterlace ! - video/x-raw,interlace-mode=progressive ! + {deinterlace} videoscale ! videorate ! {vcaps_out} ! @@ -49,7 +51,8 @@ class AVPreviewOutput(TCPMultiConnection): channel=self.channel, acaps=Config.get('mix', 'audiocaps'), vcaps_in=Config.get('mix', 'videocaps'), - vcaps_out=vcaps_out + vcaps_out=vcaps_out, + deinterlace=deinterlace ) self.log.debug('Creating Output-Pipeline:\n%s', pipeline) diff --git a/voctocore/lib/commands.py b/voctocore/lib/commands.py index bfe3168..92db70e 100644 --- a/voctocore/lib/commands.py +++ b/voctocore/lib/commands.py @@ -1,5 +1,6 @@ import logging import json +import inspect from lib.config import Config from lib.videomix import CompositeModes @@ -60,8 +61,54 @@ class ControlServerCommands(object): # 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) + + helplines.append('\t'+'quit') + + 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_video_status(self): a = encodeName( self.sources, self.pipeline.vmix.getVideoSourceA() ) @@ -69,10 +116,15 @@ class ControlServerCommands(object): 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) @@ -80,6 +132,9 @@ class ControlServerCommands(object): 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) @@ -92,10 +147,12 @@ class ControlServerCommands(object): 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) @@ -108,10 +165,12 @@ class ControlServerCommands(object): 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) @@ -122,7 +181,10 @@ class ControlServerCommands(object): 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) @@ -152,10 +214,13 @@ class ControlServerCommands(object): 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) @@ -163,6 +228,7 @@ class ControlServerCommands(object): 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() @@ -170,5 +236,6 @@ class ControlServerCommands(object): 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 3075f61..df0ff9a 100644 --- a/voctocore/lib/config.py +++ b/voctocore/lib/config.py @@ -1,4 +1,5 @@ import os.path +import logging from configparser import SafeConfigParser from lib.args import Args @@ -23,4 +24,10 @@ if Args.ini_file is not None: files.append(Args.ini_file) Config = SafeConfigParser() -Config.read(files) +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]) ) +log.debug('successfully parsed config-files: \n%s', + "\n".join(["\t\t"+os.path.normpath(file) for file in readfiles]) ) diff --git a/voctocore/lib/videomix.py b/voctocore/lib/videomix.py index aa2bedc..95d538d 100644 --- a/voctocore/lib/videomix.py +++ b/voctocore/lib/videomix.py @@ -325,7 +325,6 @@ class VideoMix(object): self.setVideoSourceA(self.names.index(defSource)) self.log.info('Changing sourceA to default of Mode %s: %s', compositeModeName, defSource) except Exception as e: - print(e) pass try: @@ -333,7 +332,6 @@ class VideoMix(object): self.setVideoSourceB(self.names.index(defSource)) self.log.info('Changing sourceB to default of Mode %s: %s', compositeModeName, defSource) except Exception as e: - print(e) pass def on_handoff(self, object, buffer): diff --git a/voctocore/voctocore.py b/voctocore/voctocore.py index fa712f7..1d04c4f 100755 --- a/voctocore/voctocore.py +++ b/voctocore/voctocore.py @@ -23,13 +23,16 @@ GObject.threads_init() # import local classes from lib.args import Args -from lib.pipeline import Pipeline -from lib.controlserver import ControlServer 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() |