From a93c71be7f309265658cd0cbc8bd63f22d7ce04f Mon Sep 17 00:00:00 2001 From: MaZderMind Date: Sun, 10 May 2015 23:07:11 +0200 Subject: Add controll-server and control mixer --- voctocore/README.md | 23 ++++ voctocore/lib/commands.py | 39 +++++++ voctocore/lib/controlserver.py | 118 ++++++++++++++------- voctocore/lib/pipeline.py | 8 +- voctocore/lib/video/mix.py | 63 ++++++++--- voctocore/scripts/set-composite-none.sh | 2 + voctocore/scripts/set-composite-pip.sh | 2 + .../scripts/set-composite-side-by-side-equal.sh | 2 + voctocore/scripts/set-video-cam1.sh | 2 + voctocore/scripts/set-video-cam2.sh | 2 + 10 files changed, 204 insertions(+), 57 deletions(-) create mode 100644 voctocore/lib/commands.py create mode 100755 voctocore/scripts/set-composite-none.sh create mode 100755 voctocore/scripts/set-composite-pip.sh create mode 100755 voctocore/scripts/set-composite-side-by-side-equal.sh create mode 100755 voctocore/scripts/set-video-cam1.sh create mode 100755 voctocore/scripts/set-video-cam2.sh diff --git a/voctocore/README.md b/voctocore/README.md index 40bb793..7fa069a 100644 --- a/voctocore/README.md +++ b/voctocore/README.md @@ -1,3 +1,4 @@ +# Server-Pipeline Structure ```` /-> Encoder -> PreviewPort 12000 /-> VideoMix --> OutputPort 11000 @@ -11,3 +12,25 @@ 20000… AudioSrc --> MirrorPort 23000… \-> Encoder -> PreviewPort 24000… ```` + +# Control Protocol +TCP-Port 9999 +```` +< set_video_a cam1 +> ok + +< set_composite_mode side_by_side_equal +> ok + +< get_video_output_port +> ok 11000 + +< get_video_a +> ok 0 cam1 + +< set_composite_mode +> ok side_by_side_equal + +< set_video_a blafoo +> error "blafoo" is no known src +```` diff --git a/voctocore/lib/commands.py b/voctocore/lib/commands.py new file mode 100644 index 0000000..f3243f3 --- /dev/null +++ b/voctocore/lib/commands.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +import logging + +from lib.config import Config + +class ControlServerCommands(): + log = logging.getLogger('ControlServerCommands') + + pipeline = None + vnames = [] + + def __init__(self, pipeline): + self.pipeline = pipeline + self.vnames = Config.getlist('sources', 'video') + + def decodeVideoSrcName(self, src_name_or_id): + if isinstance(src_name_or_id, str): + try: + return self.vnames.index(src_name_or_id) + except Exception as e: + raise IndexError("video-source %s unknown" % src_name_or_id) + + if src_name_or_id < 0 or src_name_or_id >= len(self.vnames): + raise IndexError("video-source %s unknown" % src_name_or_id) + + + def set_video_a(self, src_name_or_id): + src_id = self.decodeVideoSrcName(src_name_or_id) + self.pipeline.vmixer.setVideoA(src_id) + return True + + def set_video_b(self, src_name_or_id): + src_id = self.decodeVideoSrcName(src_name_or_id) + self.pipeline.vmixer.setVideoB(src_id) + return True + + def set_composite_mode(self, composite_mode): + self.pipeline.vmixer.set_composite_mode(src_name_or_id) + return True diff --git a/voctocore/lib/controlserver.py b/voctocore/lib/controlserver.py index fffeda8..e267ca7 100644 --- a/voctocore/lib/controlserver.py +++ b/voctocore/lib/controlserver.py @@ -1,65 +1,109 @@ -import socket, threading, queue, logging +#!/usr/bin/python3 +import socket, logging from gi.repository import GObject -def controlServerEntrypoint(f): - # mark the method as something that requires view's class - f.is_control_server_entrypoint = True - return f +from lib.commands import ControlServerCommands class ControlServer(): log = logging.getLogger('ControlServer') - def __init__(self, videomix): + + boundSocket = None + + def __init__(self, pipeline): '''Initialize server and start listening.''' - self.videomix = videomix + self.commands = ControlServerCommands(pipeline) - sock = socket.socket() - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(('0.0.0.0', 23000)) - sock.listen(1) + port = 9999 + self.log.debug('Binding to Command-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) - # register socket for callback inside the GTK-Mainloop - GObject.io_add_watch(sock, GObject.IO_IN, self.listener) + self.log.debug('Setting GObject io-watch on Socket') + GObject.io_add_watch(self.boundSocket, GObject.IO_IN, self.on_connect) - def listener(self, sock, *args): + def on_connect(self, sock, *args): '''Asynchronous connection listener. Starts a handler for each connection.''' conn, addr = sock.accept() - self.log.info("Connection from %s", addr) + self.log.info("Incomming Connection from %s", addr) - # register data-received handler inside the GTK-Mainloop - GObject.io_add_watch(conn, GObject.IO_IN, self.handler) + self.log.debug('Setting GObject io-watch on Connection') + GObject.io_add_watch(conn, GObject.IO_IN, self.on_data) return True - def handler(self, conn, *args): + def on_data(self, conn, *args): '''Asynchronous connection handler. Processes each line from the socket.''' - line = conn.recv(4096) - if not len(line): - self.log.debug("Connection closed.") + # construct a file-like object fro mthe socket + # to be able to read linewise and in utf-8 + filelike = conn.makefile('rw') + + # read a line from the socket + line = filelike.readline().strip() + + # no data = remote closed connection + if len(line) == 0: + self.log.info("Connection closed.") return False - r = self.processLine(line.decode('utf-8')) - if isinstance(r, str): - conn.send((r+'\n').encode('utf-8')) + # 'quit' = remote wants us to close the connection + if line == 'quit': + self.log.info("Client asked us to close the Connection") return False - conn.send('OK\n'.encode('utf-8')) + # process the received line + success, msg = self.processLine(line) + + # success = False -> error + if success == False: + # on error-responses the message is mandatory + if msg is None: + msg = '' + + # respond with 'error' and the message + filelike.write('error '+msg+'\n') + self.log.info("Function-Call returned an Error: %s", msg) + + # keep on listening on that connection + return True + + # success = True and not message + if msg is None: + # respond with a simple 'ok' + filelike.write('ok\n') + else: + # respond with the returned message + filelike.write('ok '+msg+'\n') return True - - - def processLine(self, line): - command, argstring = (line.strip()+' ').split(' ', 1) + # split line into command and optional args + command, argstring = (line+' ').split(' ', 1) args = argstring.strip().split() - self.log.info(command % args) - if not hasattr(self.videomix, command): - return 'unknown command {}'.format(command) + # log function-call as parsed + self.log.info("Read Function-Call from Socket: %s( %s )", command, args) + + # check that the function-call is a known Command + if not hasattr(self.commands, command): + return False, 'unknown command %s' % command - f = getattr(self.videomix, command) - if not hasattr(f, 'is_control_server_entrypoint'): - return 'method {} not callable from controlserver'.format(command) try: - return f(*args) + # fetch the function-pointer + f = getattr(self.commands, command) + + # call the function + ret = f(*args) + + # if it returned an iterable, probably (Success, Message), pass that on + if hasattr(ret, '__iter__'): + return ret + else: + # otherwise construct a tuple + return (ret, None) + except Exception as e: - return str(e) + # In case of an Exception, return that + return False, str(e) diff --git a/voctocore/lib/pipeline.py b/voctocore/lib/pipeline.py index fb9b846..a4ada04 100644 --- a/voctocore/lib/pipeline.py +++ b/voctocore/lib/pipeline.py @@ -2,9 +2,6 @@ import logging from gi.repository import Gst -# import controlserver annotation -from lib.controlserver import controlServerEntrypoint - # import library components from lib.config import Config from lib.video.src import VideoSrc @@ -22,8 +19,11 @@ class Pipeline(object): vmixerout = None def __init__(self): + self.log.debug('creating Video-Pipeline') self.initVideo() + self.log.debug('creating Control-Server') + def initVideo(self): caps = Config.get('mix', 'videocaps') self.log.info('Video-Caps configured to: %s', caps) @@ -47,7 +47,7 @@ class Pipeline(object): self.vmirrors.append(mirror) self.log.debug('Creating Video-Mixer') - self.videomixer = VideoMix() + self.vmixer = VideoMix() port = 11000 self.log.debug('Creating Video-Mixer-Output at tcp-port %u', port) diff --git a/voctocore/lib/video/mix.py b/voctocore/lib/video/mix.py index 8bb7036..a9e1513 100644 --- a/voctocore/lib/video/mix.py +++ b/voctocore/lib/video/mix.py @@ -1,19 +1,30 @@ #!/usr/bin/python3 import logging from gi.repository import Gst +from enum import Enum from lib.config import Config +class ComposteModes(Enum): + fullscreen = 0 + class VideoMix(object): log = logging.getLogger('VideoMix') mixingPipeline = None + caps = None + names = [] + + compositeMode = ComposteModes.fullscreen + sourceA = 0 + sourceB = 1 + def __init__(self): - caps = Config.get('mix', 'videocaps') + self.caps = Config.get('mix', 'videocaps') - names = Config.getlist('sources', 'video') - self.log.info('Configuring Mixer for %u Sources', len(names)) + self.names = Config.getlist('sources', 'video') + self.log.info('Configuring Mixer for %u Sources', len(self.names)) pipeline = """ videomixer name=mix ! @@ -21,10 +32,10 @@ class VideoMix(object): textoverlay text=mixer halignment=left valignment=top ypad=175 ! intervideosink channel=video_mix """.format( - caps=caps + caps=self.caps ) - for idx, name in enumerate(names): + for idx, name in enumerate(self.names): pipeline += """ intervideosrc channel=video_{name}_mixer ! {caps} ! @@ -33,22 +44,42 @@ class VideoMix(object): mix. """.format( name=name, - caps=caps, + caps=self.caps, idx=idx ) - self.log.debug('Launching Mixing-Pipeline:\n%s', pipeline) + self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline) self.mixingPipeline = Gst.parse_launch(pipeline) + + self.log.debug('Initializing Mixer-State') + self.updateMixerState() + + self.log.debug('Launching Mixing-Pipeline:\n%s', pipeline) self.mixingPipeline.set_state(Gst.State.PLAYING) - mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_0') - mixerpad.set_property('alpha', 0.5) - mixerpad.set_property('xpos', 64) - mixerpad.set_property('ypos', 64) + def updateMixerState(self): + if self.compositeMode == ComposteModes.fullscreen: + self.updateMixerStateFullscreen() + + def updateMixerStateFullscreen(self): + self.log.info('Updating Mixer-State for Fullscreen-Composition') + for idx, name in enumerate(self.names): + alpha = int(idx == self.sourceA) + + self.log.debug('Setting Mixerpad %u to x/y=0 and alpha=%u', idx, alpha) + mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_%u' % idx) + mixerpad.set_property('alpha', alpha ) + mixerpad.set_property('xpos', 0) + mixerpad.set_property('ypos', 0) + + self.log.debug('Resetting Scaler %u to non-scaling', idx) + capsfilter = self.mixingPipeline.get_by_name('caps_%u' % idx) + capsfilter.set_property('caps', Gst.Caps.from_string(self.caps)) - mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_1') - mixerpad.set_property('alpha', 0.2) + def setVideoA(self, source): + self.sourceA = source + self.updateMixerState() - capsilter = self.mixingPipeline.get_by_name('caps_1') - capsilter.set_property('caps', Gst.Caps.from_string( - 'video/x-raw,width=320,height=180')) + def setVideoB(self, source): + self.sourceN = source + self.updateMixerState() diff --git a/voctocore/scripts/set-composite-none.sh b/voctocore/scripts/set-composite-none.sh new file mode 100755 index 0000000..4ac2a66 --- /dev/null +++ b/voctocore/scripts/set-composite-none.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo set_composite_mode none | nc localhost 9999 diff --git a/voctocore/scripts/set-composite-pip.sh b/voctocore/scripts/set-composite-pip.sh new file mode 100755 index 0000000..04e41df --- /dev/null +++ b/voctocore/scripts/set-composite-pip.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo set_composite_mode pip | nc localhost 9999 diff --git a/voctocore/scripts/set-composite-side-by-side-equal.sh b/voctocore/scripts/set-composite-side-by-side-equal.sh new file mode 100755 index 0000000..d3d81df --- /dev/null +++ b/voctocore/scripts/set-composite-side-by-side-equal.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo set_composite_mode side-by-side-equal | nc localhost 9999 diff --git a/voctocore/scripts/set-video-cam1.sh b/voctocore/scripts/set-video-cam1.sh new file mode 100755 index 0000000..bc59e80 --- /dev/null +++ b/voctocore/scripts/set-video-cam1.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo set_video_a cam1 | nc localhost 9999 diff --git a/voctocore/scripts/set-video-cam2.sh b/voctocore/scripts/set-video-cam2.sh new file mode 100755 index 0000000..9bc0988 --- /dev/null +++ b/voctocore/scripts/set-video-cam2.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo set_video_a cam2 | nc localhost 9999 -- cgit v1.2.3