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