aboutsummaryrefslogtreecommitdiff
path: root/voctocore/lib
diff options
context:
space:
mode:
Diffstat (limited to 'voctocore/lib')
-rw-r--r--voctocore/lib/args.py15
-rw-r--r--voctocore/lib/config.py2
-rw-r--r--voctocore/lib/distributor.py33
-rw-r--r--voctocore/lib/failaudiosrc.py25
-rw-r--r--voctocore/lib/failvideosrc.py34
-rw-r--r--voctocore/lib/pipeline.py449
-rw-r--r--voctocore/lib/quadmix.py190
-rw-r--r--voctocore/lib/shmsrc.py125
-rw-r--r--voctocore/lib/videomix.py2
-rw-r--r--voctocore/lib/videosrc.py92
-rw-r--r--voctocore/lib/videosrcmirror.py66
11 files changed, 200 insertions, 833 deletions
diff --git a/voctocore/lib/args.py b/voctocore/lib/args.py
new file mode 100644
index 0000000..6699679
--- /dev/null
+++ b/voctocore/lib/args.py
@@ -0,0 +1,15 @@
+import argparse
+
+__all__ = ['Args']
+
+parser = argparse.ArgumentParser(description='Voctocore')
+parser.add_argument('-v', '--verbose', action='store_true',
+ 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('-i', '---config-ini', action='store',
+ help="Load a custom config.ini-File")
+
+Args = parser.parse_args()
diff --git a/voctocore/lib/config.py b/voctocore/lib/config.py
index 76f67b0..88e713c 100644
--- a/voctocore/lib/config.py
+++ b/voctocore/lib/config.py
@@ -1,6 +1,8 @@
import os.path
from configparser import SafeConfigParser
+__all__ = ['Config']
+
def getlist(self, section, option):
return [x.strip() for x in self.get(section, option).split(',')]
diff --git a/voctocore/lib/distributor.py b/voctocore/lib/distributor.py
deleted file mode 100644
index 8282839..0000000
--- a/voctocore/lib/distributor.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/python3
-import time, logging
-from gi.repository import GLib, Gst
-
-from lib.config import Config
-
-class TimesTwoDistributor(Gst.Bin):
- log = logging.getLogger('TimesTwoDistributor')
-
- def __init__(self):
- super().__init__()
-
- self.tee = Gst.ElementFactory.make('tee', None)
- self.queue_a = Gst.ElementFactory.make('queue', 'queue-a')
- self.queue_b = Gst.ElementFactory.make('queue', 'queue-b')
-
- self.add(self.tee)
- self.add(self.queue_a)
- self.add(self.queue_b)
-
- self.tee.link(self.queue_a)
- self.tee.link(self.queue_b)
-
- # Add Ghost Pads
- self.add_pad(
- Gst.GhostPad.new('sink', self.tee.get_static_pad('sink'))
- )
- self.add_pad(
- Gst.GhostPad.new('src_a', self.queue_a.get_static_pad('src'))
- )
- self.add_pad(
- Gst.GhostPad.new('src_b', self.queue_b.get_static_pad('src'))
- )
diff --git a/voctocore/lib/failaudiosrc.py b/voctocore/lib/failaudiosrc.py
deleted file mode 100644
index 0ce885e..0000000
--- a/voctocore/lib/failaudiosrc.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/python3
-import time, logging
-from gi.repository import GLib, Gst
-
-from lib.config import Config
-
-class FailAudioSrc(Gst.Bin):
- log = logging.getLogger('FailAudioSrc')
-
- def __init__(self, idx, name):
- super().__init__()
-
- # Create elements
- self.failsrc = Gst.ElementFactory.make('audiotestsrc', None)
-
- # Add elements to Bin
- self.add(self.failsrc)
-
- # Set properties
- self.failsrc.set_property('freq', 400+idx*50)
-
- # Add Ghost Pads
- self.add_pad(
- Gst.GhostPad.new('src', self.failsrc.get_static_pad('src'))
- )
diff --git a/voctocore/lib/failvideosrc.py b/voctocore/lib/failvideosrc.py
deleted file mode 100644
index 500603a..0000000
--- a/voctocore/lib/failvideosrc.py
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/python3
-import time, logging
-from gi.repository import GLib, Gst
-
-from lib.config import Config
-
-class FailVideoSrc(Gst.Bin):
- log = logging.getLogger('FailVideoSrc')
- colors = [
- 0xffff0000, # red
- 0xff00ff00, # green
- 0xff0000ff, # blue
- 0xffffff00, # yellow
- 0xff00ffff, # cyan
- 0xffff00ff, # magenta
- 0xffffffff, # white
- ]
-
- def __init__(self, idx, name):
- super().__init__()
-
- # Create elements
- self.failsrc = Gst.ElementFactory.make('videotestsrc', None)
-
- # Add elements to Bin
- self.add(self.failsrc)
-
- # Set properties
- self.failsrc.set_property('foreground-color', self.colors[idx % len(self.colors)])
-
- # Add Ghost Pads
- self.add_pad(
- Gst.GhostPad.new('src', self.failsrc.get_static_pad('src'))
- )
diff --git a/voctocore/lib/pipeline.py b/voctocore/lib/pipeline.py
index 6267e9e..6c207fe 100644
--- a/voctocore/lib/pipeline.py
+++ b/voctocore/lib/pipeline.py
@@ -1,444 +1,43 @@
#!/usr/bin/python3
-import os, errno, time, logging
-from gi.repository import GLib, Gst
+import logging
+from gi.repository import Gst
# import controlserver annotation
from lib.controlserver import controlServerEntrypoint
# import library components
from lib.config import Config
-from lib.quadmix import QuadMix
-from lib.videomix import VideoMix
-from lib.audiomix import AudioMix
-from lib.distributor import TimesTwoDistributor
-from lib.shmsrc import FailsafeShmSrc
-from lib.failvideosrc import FailVideoSrc
-from lib.failaudiosrc import FailAudioSrc
+from lib.videosrc import VideoSrc
+from lib.videosrcmirror import VideoSrcMirror
-class Pipeline(Gst.Pipeline):
+class Pipeline(object):
"""mixing, streaming and encoding pipeline constuction and control"""
log = logging.getLogger('Pipeline')
- videonames = []
- audionames = []
- def __init__(self):
- super().__init__()
-
- self.log.debug('Creating Video-Mixer')
- # create audio and video mixer
- self.quadmixer = QuadMix()
- self.add(self.quadmixer)
-
- self.videomixer = VideoMix()
- self.add(self.videomixer)
-
- self.audiomixer = AudioMix()
- self.add(self.audiomixer)
-
- # read the path where the shm-control-sockets are located and ensure it exists
- socketpath = Config.get('sources', 'socketpath')
- self.log.info('Ensuring the configured socketpath exists: %s', socketpath)
- try:
- os.makedirs(socketpath)
- except OSError as exception:
- if exception.errno != errno.EEXIST:
- raise
-
- self.videonames = Config.getlist('sources', 'video')
- self.audionames = Config.getlist('sources', 'audio')
-
- caps = Gst.Caps.from_string(Config.get('sources', 'videocaps'))
- self.log.debug('parsing videocaps from config: %s', caps.to_string())
-
- for idx, name in enumerate(self.videonames):
- socket = os.path.join(socketpath, 'v-'+name)
-
- self.log.info('Creating video-source "%s" at socket-path %s', name, socket)
- sourcebin = FailsafeShmSrc(socket, caps, FailVideoSrc(idx, name))
- self.add(sourcebin)
-
- distributor = TimesTwoDistributor()
- self.add(distributor)
- sourcebin.link(distributor)
-
- mixerpad = self.quadmixer.request_mixer_pad()
- distributor.get_static_pad('src_a').link(mixerpad)
-
- mixerpad = self.videomixer.request_mixer_pad()
- distributor.get_static_pad('src_b').link(mixerpad)
-
-
- caps = Gst.Caps.from_string(Config.get('sources', 'audiocaps'))
- self.log.debug('parsing videocaps from config: %s', caps.to_string())
-
- for idx, name in enumerate(self.audionames):
- socket = os.path.join(socketpath, 'a-'+name)
-
- self.log.info('Creating audio-source "%s" at socket-path %s', name, socket)
- sourcebin = FailsafeShmSrc(socket, caps, FailAudioSrc(idx, name))
- self.add(sourcebin)
-
- mixerpad = self.audiomixer.request_mixer_pad()
- sourcebin.get_static_pad('src').link(mixerpad)
-
- # tell the quadmix that this were all sources and no more sources will come after this
- self.quadmixer.finalize()
-
- self.quadmixer.set_active(0)
- self.videomixer.set_active(0)
- self.audiomixer.set_active(0)
-
- self.audioconv = Gst.ElementFactory.make('audioconvert', 'audioconv')
- self.audioenc = Gst.ElementFactory.make('avenc_mp2', 'audioenc')
-
- self.videoconv = Gst.ElementFactory.make('videoconvert', 'videoconv')
- self.videoenc = Gst.ElementFactory.make('avenc_mpeg2video', 'videoenc')
-
- self.mux = Gst.ElementFactory.make('mpegtsmux', 'mux')
- self.sink = Gst.ElementFactory.make('filesink', 'sink')
-
- self.add(self.audioconv)
- self.add(self.audioenc)
- self.add(self.videoconv)
- self.add(self.videoenc)
-
- self.add(self.mux)
- self.add(self.sink)
-
- self.videomixer.link(self.videoconv)
- self.videoconv.link(self.videoenc)
-
- self.audiomixer.link(self.audioconv)
- self.audioconv.link(self.audioenc)
-
- self.videoenc.link(self.mux)
- self.audioenc.link(self.mux)
-
- self.mux.link(self.sink)
-
- self.sink.set_property('location', '/home/peter/test.ts')
-
-
- self.quadmixsink = Gst.ElementFactory.make('autovideosink', 'quadmixsink')
- self.quadmixsink.set_property('sync', False)
- self.add(self.quadmixsink)
- self.quadmixer.link(self.quadmixsink)
-
- def run(self):
- self.set_state(Gst.State.PAUSED)
- time.sleep(0.5)
- self.set_state(Gst.State.PLAYING)
-
- def quit(self):
- self.set_state(Gst.State.NULL)
-
-
-
-
- # # collection of video-sources to connect to the quadmix
- # quadmixSources = []
-
- # # create camera sources
- # for camberabin in self.createDummyCamSources():
- # # link camerasource to audiomixer
- # camberabin.get_by_name('audio_src').link(self.pipeline.get_by_name('liveaudio'))
-
- # # inject a ×2 distributor and link one end to the live-mixer
- # distributor = self.createDistributor(camberabin.get_by_name('video_src'), camberabin.get_name())
- # distributor.get_by_name('a').link(self.pipeline.get_by_name('livevideo'))
-
- # # collect the other end to add it later to the quadmix
- # quadmixSources.append(distributor.get_by_name('b'))
-
- # # TODO: generate pause & slides with another generator here which only
- # # yields if the respective files are present and which only have a video-pad
-
- # # add all video-sources to the quadmix-monitor-screen
- # self.addVideosToQuadmix(quadmixSources, self.pipeline.get_by_name('quadmix'))
-
- # # initialize to known defaults
- # # TODO: make configurable
- # self.switchVideo(0)
- # self.switchAudio(0)
-
- # Gst.debug_bin_to_dot_file(self.pipeline, Gst.DebugGraphDetails.ALL, 'test')
- # self.pipeline.set_state(Gst.State.PLAYING)
-
-
-
-
-
-
- # def createMixer(self):
- # """create audio and video mixer"""
- # # create mixer-pipeline from string
- # mixerbin = Gst.parse_bin_from_description("""
- # videomixer name=livevideo ! autovideosink
- # input-selector name=liveaudio ! autoaudiosink
-
- # videotestsrc pattern="solid-color" foreground-color=0x808080 ! capsfilter name=filter ! videomixer name=quadmix ! autovideosink
- # """, False)
-
- # # define caps for the videotestsrc which generates the background-color for the quadmix
- # bgcaps = Gst.Caps.new_empty_simple('video/x-raw')
- # bgcaps.set_value('width', round(self.monitorSize[0]))
- # bgcaps.set_value('height', round(self.monitorSize[1]))
- # mixerbin.get_by_name('filter').set_property('caps', bgcaps)
-
- # # name the bin, add and return it
- # mixerbin.set_name('mixerbin')
- # self.pipeline.add(mixerbin)
- # return mixerbin
-
- # def addVideosToQuadmix(self, videosources, quadmix):
- # """add all avaiable videosources to the quadmix"""
- # count = len(videosources)
-
- # # coordinate of the cell where we place the next video
- # place = [0, 0]
-
- # # number of cells in the quadmix-monitor
- # grid = [0, 0]
- # grid[0] = math.ceil(math.sqrt(count))
- # grid[1] = math.ceil(count / grid[0])
-
- # # size of each cell in the quadmix-monitor
- # cellSize = (
- # self.monitorSize[0] / grid[0],
- # self.monitorSize[1] / grid[1]
- # )
-
- # print("showing {} videosources in a {}×{} grid in a {}×{} px window, which gives cells of {}×{} px per videosource".format(
- # count, grid[0], grid[1], self.monitorSize[0], self.monitorSize[1], cellSize[0], cellSize[1]))
-
- # # iterate over all video-sources
- # for idx, videosource in enumerate(videosources):
- # # generate a pipeline for this videosource which
- # # - scales the video to the request
- # # - remove n px of the video (n = 5 if the video is highlighted else 0)
- # # - add a colored border of n px of the video (n = 5 if the video is highlighted else 0)
- # # - overlay the index of the video as text in the top left corner
- # # - known & named output
- # previewbin = Gst.parse_bin_from_description("""
- # videoscale name=in !
- # capsfilter name=caps !
- # videobox name=crop top=0 left=0 bottom=0 right=0 !
- # videobox fill=red top=-0 left=-0 bottom=-0 right=-0 name=add !
- # textoverlay color=0xFFFFFFFF halignment=left valignment=top xpad=10 ypad=5 font-desc="sans 35" name=text !
- # identity name=out
- # """, False)
-
- # # name the bin and add it
- # previewbin.set_name('previewbin-{}'.format(idx))
- # self.pipeline.add(previewbin)
- # self.previewbins.append(previewbin)
-
- # # set the overlay-text
- # previewbin.get_by_name('text').set_property('text', str(idx))
-
- # # query the video-source caps and extract its size
- # caps = videosource.get_static_pad('src').query_caps(None)
- # capsstruct = caps.get_structure(0)
- # srcSize = (
- # capsstruct.get_int('width')[1],
- # capsstruct.get_int('height')[1],
- # )
-
- # # calculate the ideal scale factor and scale the sizes
- # f = max(srcSize[0] / cellSize[0], srcSize[1] / cellSize[1])
- # scaleSize = (
- # srcSize[0] / f,
- # srcSize[1] / f,
- # )
-
- # # calculate the top/left coordinate
- # coord = (
- # place[0] * cellSize[0] + (cellSize[0] - scaleSize[0]) / 2,
- # place[1] * cellSize[1] + (cellSize[1] - scaleSize[1]) / 2,
- # )
-
- # print("placing videosource {} of size {}×{} scaled by {} to {}×{} in a cell {}×{} px cell ({}/{}) at position ({}/{})".format(
- # idx, srcSize[0], srcSize[1], f, scaleSize[0], scaleSize[1], cellSize[0], cellSize[1], place[0], place[1], coord[0], coord[1]))
-
- # # link the videosource to the input of the preview-bin
- # videosource.link(previewbin.get_by_name('in'))
-
- # # create and set the caps for the preview-scaler
- # scalecaps = Gst.Caps.new_empty_simple('video/x-raw')
- # scalecaps.set_value('width', round(scaleSize[0]))
- # scalecaps.set_value('height', round(scaleSize[1]))
- # previewbin.get_by_name('caps').set_property('caps', scalecaps)
-
- # # request a pad from the quadmixer and configure x/y position
- # sinkpad = quadmix.get_request_pad('sink_%u')
- # sinkpad.set_property('xpos', round(coord[0]))
- # sinkpad.set_property('ypos', round(coord[1]))
-
- # # link the output of the preview-bin to the mixer
- # previewbin.get_by_name('out').link(quadmix)
-
- # # increment grid position
- # place[0] += 1
- # if place[0] >= grid[0]:
- # place[1] += 1
- # place[0] = 0
-
- # def createDistributor(self, videosource, name):
- # """create a simple ×2 distributor"""
- # distributor = Gst.parse_bin_from_description("""
- # tee name=t
- # t. ! queue name=a
- # t. ! queue name=b
- # """, False)
-
- # # set a name and add to pipeline
- # distributor.set_name('distributor({0})'.format(name))
- # self.pipeline.add(distributor)
-
- # # link input to the tee
- # videosource.link(distributor.get_by_name('t'))
- # return distributor
-
- # def createDummyCamSources(self):
- # """create test-video-sources from files or urls"""
-
- # # TODO make configurable
- # uris = ('file:///home/peter/122.mp4', 'file:///home/peter/10025.mp4',)
- # for idx, uri in enumerate(uris):
- # # create a bin for a simulated camera input
- # # force the input resolution to 1024x576 because that way the following elements
- # # in the pipeline cam know the size even if the file is not yet loaded. the quadmixer
- # # is not resize-capable
- # camberabin = Gst.parse_bin_from_description("""
- # uridecodebin name=input
- # input. ! videoconvert ! videoscale ! videorate ! video/x-raw,width=1024,height=576,framerate=25/1 ! identity name=video_src
- # input. ! audioconvert name=audio_src
- # """, False)
-
- # # set name and uri
- # camberabin.set_name('dummy-camberabin({0})'.format(uri))
- # camberabin.get_by_name('input').set_property('uri', uri)
-
- # # add to pipeline and pass the bin upstream
- # self.pipeline.add(camberabin)
- # yield camberabin
-
-
- # def createCamSources(self):
- # """create real-video-sources from the bmd-drivers"""
-
- # # TODO make number of installed cams configurable
- # for cam in range(2):
- # # create a bin for camera input
- # camberabin = Gst.parse_bin_from_description("""
- # decklinksrc name=input input=sdi input-mode=1080p25
- # input. ! videoconvert ! videoscale ! videorate ! video/x-raw,width=1920,height=1080,framerate=25/1 ! identity name=video_src
- # input. ! audioconvert name=audio_src
- # """, False)
-
- # # set name and subdevice
- # camberabin.set_name('camberabin({0})'.format(cam))
- # camberabin.get_by_name('input').set_property('subdevice', cam)
-
- # # add to pipeline and pass the bin upstream
- # self.pipeline.add(camberabin)
- # yield camberabin
-
-
-
-
- ### below are access-methods for the ControlServer
- @controlServerEntrypoint
- def status(self):
- """System Status Query"""
- raise NotImplementedError("status command is not implemented yet")
-
- @controlServerEntrypoint
- def numAudioSources(self):
- """return number of available audio sources"""
- raise NotImplementedError("audio is not implemented yet")
-
-
- @controlServerEntrypoint
- def switchAudio(self, audiosource):
- """switch audio to the selected audio"""
- idx = int(audiosource)
- if idx >= len(self.audionames):
- return 'unknown audio-source: %s' % (audiosource)
-
- self.log.info("switching mixer to audio-source %u", idx)
- self.audiomixer.set_active(idx)
-
-
-
- @controlServerEntrypoint
- def numVideoSources(self):
- """return number of available video sources"""
- livevideo = self.pipeline.get_by_name('livevideo')
- return str(len(self.videonames))
-
-
- @controlServerEntrypoint
- def switchVideo(self, videosource):
- """switch video to the selected video"""
- idx = int(videosource)
- if idx >= len(self.videonames):
- return 'unknown video-source: %s' % (videosource)
-
- self.log.info("switching mixer to video-source %u", idx)
- self.quadmixer.set_active(idx)
- self.videomixer.set_active(idx)
-
-
- @controlServerEntrypoint
- def fadeVideo(self, videosource):
- """fade video to the selected video"""
- raise NotImplementedError("fade command is not implemented yet")
-
-
- @controlServerEntrypoint
- def setPipVideo(self, videosource):
- """switch video-source in the PIP to the selected video"""
- raise NotImplementedError("pip commands are not implemented yet")
-
-
- @controlServerEntrypoint
- def fadePipVideo(self, videosource):
- """fade video-source in the PIP to the selected video"""
- raise NotImplementedError("pip commands are not implemented yet")
-
-
- class PipPlacements:
- """enumeration of possible PIP-Placements"""
- TopLeft, TopRight, BottomLeft, BottomRight = range(4)
-
-
- @controlServerEntrypoint
- def setPipPlacement(self, placement):
- """place PIP in the selected position"""
- assert(isinstance(placement, PipPlacements))
- raise NotImplementedError("pip commands are not implemented yet")
+ vsources = []
+ vmirrors = []
+ def __init__(self):
+ # self.log.debug('Creating A/V-Mixer')
+ # self.videomixer = VideoMix()
+ # self.add(self.videomixer)
- @controlServerEntrypoint
- def setPipStatus(self, enabled):
- """show or hide PIP"""
- raise NotImplementedError("pip commands are not implemented yet")
+ # self.audiomixer = AudioMix()
+ # self.add(self.audiomixer)
+ caps = Config.get('mix', 'videocaps')
+ self.log.info('Video-Caps configured to: %s', caps)
- @controlServerEntrypoint
- def fadePipStatus(self, enabled):
- """fade PIP in our out"""
- raise NotImplementedError("pip commands are not implemented yet")
+ for idx, name in enumerate(Config.getlist('sources', 'video')):
+ port = 10000 + idx
+ self.log.info('Creating Video-Source %s at tcp-port %u', name, port)
+ source = VideoSrc(name, port, caps)
+ self.vsources.append(source)
- class StreamContents:
- """enumeration of possible PIP-Placements"""
- Live, Pause, NoStream = range(3)
+ port = 13000 + idx
+ self.log.info('Creating Mirror-Output for Video-Source %s at tcp-port %u', name, port)
- @controlServerEntrypoint
- def selectStreamContent(self, content):
- """switch the livestream-content between selected mixer output, pause-image or nostream-imag"""
- assert(isinstance(content, StreamContents))
- raise NotImplementedError("pause/nostream switching is not implemented yet")
+ mirror = VideoSrcMirror(name, port, caps)
+ self.vmirrors.append(mirror)
diff --git a/voctocore/lib/quadmix.py b/voctocore/lib/quadmix.py
deleted file mode 100644
index 8d69304..0000000
--- a/voctocore/lib/quadmix.py
+++ /dev/null
@@ -1,190 +0,0 @@
-#!/usr/bin/python3
-import math, logging
-from gi.repository import GLib, Gst
-
-from lib.helper import iteratorHelper
-from lib.config import Config
-
-class QuadMix(Gst.Bin):
- log = logging.getLogger('QuadMix')
- previewbins = []
- mixerpads = []
-
- def __init__(self):
- super().__init__()
-
- caps = Gst.Caps.from_string(Config.get('mix', 'monitorcaps'))
- self.log.debug('parsing monitorcaps from config: %s', caps.to_string())
- struct = caps.get_structure(0)
-
- self.monitorSize = [struct.get_int('width')[1], struct.get_int('height')[1]]
-
- self.bgsrc = Gst.ElementFactory.make('videotestsrc', 'bgsrc')
- self.mixer = Gst.ElementFactory.make('videomixer', 'mixer')
- self.scale = Gst.ElementFactory.make('videoscale', 'scale')
-
- self.add(self.bgsrc)
- self.add(self.mixer)
- self.add(self.scale)
-
- self.bgsrc.link_filtered(self.mixer, caps)
- self.mixer.link_filtered(self.scale, caps)
-
- self.bgsrc.set_property('pattern', 'solid-color')
- self.bgsrc.set_property('foreground-color', 0x808080)
-
- self.add_pad(
- Gst.GhostPad.new('src', self.scale.get_static_pad('src'))
- )
-
- def request_mixer_pad(self):
- previewbin = QuadMixPreview()
- self.add(previewbin)
- self.previewbins.append(previewbin)
-
- srcpad = previewbin.get_static_pad('src')
- sinkpad = previewbin.get_static_pad('sink')
-
- mixerpad = self.mixer.get_request_pad('sink_%u')
- self.mixerpads.append(mixerpad)
- srcpad.link(mixerpad)
-
- self.log.info('requested mixerpad %u (named %s)', len(self.mixerpads) - 1, mixerpad.get_name())
- ghostpad = Gst.GhostPad.new(mixerpad.get_name(), sinkpad)
- self.add_pad(ghostpad)
- return ghostpad
-
- def finalize(self):
- self.log.debug('all sources linked, calculating layout')
-
- # number of placed sources
- count = len(self.previewbins)
-
- # coordinate of the cell where we place the next video
- place = [0, 0]
-
- # number of cells in the quadmix-monitor
- grid = [0, 0]
- grid[0] = math.ceil(math.sqrt(count))
- grid[1] = math.ceil(count / grid[0])
-
- # size of each cell in the quadmix-monitor
- cellSize = (
- self.monitorSize[0] / grid[0],
- self.monitorSize[1] / grid[1]
- )
-
- # report calculation results
- self.log.info('showing %u videosources in a %u×%u grid in a %u×%u px window, which gives cells of %u×%u px per videosource',
- count, grid[0], grid[1], self.monitorSize[0], self.monitorSize[1], cellSize[0], cellSize[1])
-
- # iterate over all previewbins
- for idx, previewbin in enumerate(self.previewbins):
- # request srcpad to query videosize and aspect from it
- srcpad = previewbin.get_static_pad('src')
-
- # select the mixerpad responsible for this previewbin from the videomixer
- mixerpad = self.mixerpads[idx]
-
- # query the video-source caps and extract its size
- caps = srcpad.query_caps(None)
- capsstruct = caps.get_structure(0)
- srcSize = (
- capsstruct.get_int('width')[1],
- capsstruct.get_int('height')[1],
- )
-
- # calculate the ideal scale factor and scale the sizes
- f = max(srcSize[0] / cellSize[0], srcSize[1] / cellSize[1])
- scaleSize = (
- srcSize[0] / f,
- srcSize[1] / f,
- )
-
- # calculate the top/left coordinate
- coord = (
- place[0] * cellSize[0] + (cellSize[0] - scaleSize[0]) / 2,
- place[1] * cellSize[1] + (cellSize[1] - scaleSize[1]) / 2,
- )
-
- self.log.info('placing mixerpad %u of size %u×%u scaled by %u to %u×%u in a cell %u×%u px cell (%u/%u) at position (%u/%u)',
- idx, srcSize[0], srcSize[1], f, scaleSize[0], scaleSize[1], cellSize[0], cellSize[1], place[0], place[1], coord[0], coord[1])
-
- # request a pad from the quadmixer and configure x/y position
- mixerpad.set_property('xpos', round(coord[0]))
- mixerpad.set_property('ypos', round(coord[1]))
-
- previewbin.set_size(scaleSize)
- previewbin.set_idx(idx)
-
- # increment grid position
- place[0] += 1
- if place[0] >= grid[0]:
- place[1] += 1
- place[0] = 0
-
- def set_active(self, target):
- self.log.info('setting videosource %u active, disabling other', target)
- for idx, previewbin in enumerate(self.previewbins):
- previewbin.set_active(target == idx)
-
-class QuadMixPreview(Gst.Bin):
- log = logging.getLogger('QuadMixPreview')
- strokeWidth = 5
-
- def __init__(self):
- super().__init__()
-
- self.scale = Gst.ElementFactory.make('videoscale', 'scale')
- self.caps = Gst.ElementFactory.make('capsfilter', 'caps')
- self.cropbox = Gst.ElementFactory.make('videobox', 'cropbox')
- self.strokebox = Gst.ElementFactory.make('videobox', 'strokebox')
- self.textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')
-
- self.add(self.scale)
- self.add(self.caps)
- self.add(self.cropbox)
- self.add(self.strokebox)
- self.add(self.textoverlay)
-
- self.strokebox.set_property('fill', 'green')
-
- self.textoverlay.set_property('color', 0xFFFFFFFF)
- self.textoverlay.set_property('halignment', 'left')
- self.textoverlay.set_property('valignment', 'top')
- self.textoverlay.set_property('xpad', 10)
- self.textoverlay.set_property('ypad', 5)
- self.textoverlay.set_property('font-desc', 'sans 35')
-
- self.scale.link(self.caps)
- self.caps.link(self.cropbox)
- self.cropbox.link(self.strokebox)
- self.strokebox.link(self.textoverlay)
-
- self.set_active(False)
-
- # Add Ghost Pads
- self.add_pad(
- Gst.GhostPad.new('sink', self.scale.get_static_pad('sink'))
- )
- self.add_pad(
- Gst.GhostPad.new('src', self.textoverlay.get_static_pad('src'))
- )
-
- def set_size(self, scaleSize):
- caps = Gst.Caps.new_empty_simple('video/x-raw')
- caps.set_value('width', round(scaleSize[0]))
- caps.set_value('height', round(scaleSize[1]))
- self.caps.set_property('caps', caps)
-
- def set_idx(self, idx):
- self.textoverlay.set_property('text', str(idx))
-
- def set_active(self, active):
- self.log.info("switching active-state to %u", active)
- for side in ('top', 'left', 'right', 'bottom'):
- self.cropbox.set_property(side, self.strokeWidth if active else 0)
- self.strokebox.set_property(side, -self.strokeWidth if active else 0)
-
- def set_color(self, color):
- self.strokebox.set_property('fill', color)
diff --git a/voctocore/lib/shmsrc.py b/voctocore/lib/shmsrc.py
deleted file mode 100644
index 4e3c402..0000000
--- a/voctocore/lib/shmsrc.py
+++ /dev/null
@@ -1,125 +0,0 @@
-#!/usr/bin/python3
-import time, logging
-from gi.repository import GLib, Gst
-
-from lib.config import Config
-
-class FailsafeShmSrc(Gst.Bin):
- log = logging.getLogger('FailsafeShmSrc')
- last_buffer_arrived = 0
- last_restart_retry = 0
- is_in_failstate = True
-
- def __init__(self, socket, caps, failsrc):
- super().__init__()
-
- # Create elements
- self.shmsrc = Gst.ElementFactory.make('shmsrc', None)
- self.depay = Gst.ElementFactory.make('gdpdepay', None)
- self.capsfilter = Gst.ElementFactory.make('capsfilter', None)
- self.failsrcsyncer = Gst.ElementFactory.make('identity', None)
- self.switch = Gst.ElementFactory.make('input-selector', None)
- self.failsrc = failsrc
- self.capsstr = caps.to_string()
-
- if not self.shmsrc or not self.capsfilter or not self.failsrcsyncer or not self.switch or not self.failsrc:
- self.log.error('could not create elements')
-
- # Add elements to Bin
- self.add(self.shmsrc)
- self.add(self.depay)
- self.add(self.capsfilter)
- self.add(self.failsrcsyncer)
- self.add(self.switch)
- self.add(self.failsrc)
-
- # Get Switcher-Pads
- self.goodpad = self.switch.get_request_pad('sink_%u')
- self.failpad = self.switch.get_request_pad('sink_%u')
-
- # Set properties
- self.shmsrc.set_property('socket-path', socket)
- self.shmsrc.link(self.depay)
- self.switch.set_property('active-pad', self.failpad)
- self.failsrcsyncer.set_property('sync', True)
- self.capsfilter.set_property('caps', caps)
-
- # Link elements
- self.depay.link(self.capsfilter)
- self.capsfilter.get_static_pad('src').link(self.goodpad)
-
- self.failsrc.link_filtered(self.failsrcsyncer, caps)
- self.failsrcsyncer.get_static_pad('src').link(self.failpad)
-
- # Install pad probes
- self.shmsrc.get_static_pad('src').add_probe(Gst.PadProbeType.BLOCK | Gst.PadProbeType.EVENT_DOWNSTREAM, self.event_probe, None)
- self.shmsrc.get_static_pad('src').add_probe(Gst.PadProbeType.BLOCK | Gst.PadProbeType.BUFFER, self.data_probe, None)
-
- # Install Watchdog
- if self.capsstr.startswith('audio'):
- timeoutms = 1000
- else:
- timeoutms = 250
-
- GLib.timeout_add(timeoutms, self.watchdog)
-
- # Add Ghost Pads
- self.add_pad(
- Gst.GhostPad.new('src', self.switch.get_static_pad('src'))
- )
-
- def do_handle_message(self, msg):
- if msg.type == Gst.MessageType.ERROR and msg.src == self.shmsrc:
- (err, debug) = msg.parse_error()
- self.log.warning('received error-message from ShmSrc, dropping: %s', err)
- self.log.debug(' debug-info from shmsrc: %s', debug)
- else:
- Gst.Bin.do_handle_message(self, msg)
-
- def event_probe(self, pad, info, ud):
- e = info.get_event()
- if e.type == Gst.EventType.EOS:
- self.log.warning('received EOS-event on event-probe, dropping')
- self.switch_to_failstate()
- return Gst.PadProbeReturn.DROP
-
- return Gst.PadProbeReturn.PASS
-
- def data_probe(self, pad, info, ud):
- self.last_buffer_arrived = time.time()
- self.switch_to_goodstate()
- return Gst.PadProbeReturn.PASS
-
- def watchdog(self):
- t = time.time()
- if self.last_buffer_arrived + 0.1 < t:
- self.log.warning('watchdog encountered a timeout')
- self.switch_to_failstate()
-
- if self.is_in_failstate and self.last_restart_retry + 1 < t:
- self.last_restart_retry = t
- self.restart()
-
- return True
-
- def restart(self):
- self.log.warning('restarting ShmSrc')
- self.shmsrc.set_state(Gst.State.NULL)
- self.shmsrc.set_base_time(self.get_parent().get_base_time())
- self.shmsrc.set_state(Gst.State.PLAYING)
-
- def switch_to_goodstate(self):
- if not self.is_in_failstate:
- return
-
- self.log.warning('switching output to goodstate')
- self.is_in_failstate = False
- self.switch.set_property('active-pad', self.goodpad)
-
- def switch_to_failstate(self):
- if self.is_in_failstate:
- return
-
- self.log.warning('switching output to failstate')
- self.is_in_failstate = True
- self.switch.set_property('active-pad', self.failpad)
diff --git a/voctocore/lib/videomix.py b/voctocore/lib/videomix.py
index 3f2dd7e..9207723 100644
--- a/voctocore/lib/videomix.py
+++ b/voctocore/lib/videomix.py
@@ -4,7 +4,7 @@ from gi.repository import GLib, Gst
from lib.config import Config
-class VideoMix(Gst.Bin):
+class VideoMix(object):
log = logging.getLogger('VideoMix')
mixerpads = []
diff --git a/voctocore/lib/videosrc.py b/voctocore/lib/videosrc.py
new file mode 100644
index 0000000..10b63fb
--- /dev/null
+++ b/voctocore/lib/videosrc.py
@@ -0,0 +1,92 @@
+#!/usr/bin/python3
+import logging, socket
+from gi.repository import GObject, Gst
+
+from lib.config import Config
+
+class VideoSrc(object):
+ log = logging.getLogger('VideoSrc')
+
+ name = None
+ port = None
+ caps = None
+
+ distributionPipeline = None
+ receiverPipeline = None
+
+ boundSocket = None
+ currentConnection = None
+
+ def __init__(self, name, port, caps):
+ self.log = logging.getLogger('VideoSrc['+name+']')
+
+ self.name = name
+ self.port = port
+ self.caps = caps
+
+ pipeline = """
+ intervideosrc channel={name}_in !
+ {caps} !
+ timeoverlay halignment=left valignment=top !
+ textoverlay text={name}_in halignment=left valignment=top ypad=75 !
+ queue !
+ tee name=tee
+
+ tee. ! queue ! intervideosink channel={name}_mirror
+ tee. ! queue ! intervideosink channel={name}_preview
+ tee. ! queue ! intervideosink channel={name}_mixer
+ """.format(
+ name=self.name,
+ caps=self.caps
+ )
+
+ self.log.debug('Launching Source-Distribution-Pipeline:\n%s', pipeline)
+ self.distributionPipeline = Gst.parse_launch(pipeline)
+ self.distributionPipeline.set_state(Gst.State.PLAYING)
+
+ 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)
+
+ 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")
+ return True
+
+ pipeline = """
+ fdsrc fd={fd} !
+ gdpdepay !
+ {caps} !
+ intervideosink channel={name}_in
+ """.format(
+ fd=conn.fileno(),
+ name=self.name,
+ caps=self.caps
+ )
+ self.log.debug('Launching Source-Receiver-Pipeline:\n%s', pipeline)
+ self.receiverPipeline = Gst.parse_launch(pipeline)
+
+ self.log.debug('Binding End-of-Stream-Signal on Source-Receiver-Pipeline')
+ self.receiverPipeline.bus.add_signal_watch()
+ self.receiverPipeline.bus.connect("message::eos", self.on_disconnect)
+
+ self.receiverPipeline.set_state(Gst.State.PLAYING)
+
+ self.currentConnection = conn
+ return True
+
+ def on_disconnect(self, bus, message):
+ self.log.info('Received End-of-Stream-Signal on Source-Receiver-Pipeline')
+ self.receiverPipeline.set_state(Gst.State.NULL)
+ self.receiverPipeline = None
+
+ self.currentConnection = None
diff --git a/voctocore/lib/videosrcmirror.py b/voctocore/lib/videosrcmirror.py
new file mode 100644
index 0000000..8fb911d
--- /dev/null
+++ b/voctocore/lib/videosrcmirror.py
@@ -0,0 +1,66 @@
+#!/usr/bin/python3
+import logging, socket
+from gi.repository import GObject, Gst
+
+from lib.config import Config
+
+class VideoSrcMirror(object):
+ log = logging.getLogger('VideoSrcMirror')
+
+ name = None
+ port = None
+ caps = None
+
+ boundSocket = None
+
+ receiverPipelines = []
+ currentConnections = []
+
+ def __init__(self, name, port, caps):
+ self.log = logging.getLogger('VideoSrcMirror['+name+']')
+
+ self.name = name
+ self.port = port
+ self.caps = caps
+
+ self.log.debug('Binding to Mirror-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)
+
+ def on_connect(self, sock, *args):
+ conn, addr = sock.accept()
+ self.log.info("incomming connection from %s", addr)
+
+ pipeline = """
+ intervideosrc channel={name}_mirror !
+ {caps} !
+ textoverlay text={name}_mirror halignment=left valignment=top ypad=125 !
+ gdppay !
+ fdsink fd={fd}
+ """.format(
+ fd=conn.fileno(),
+ name=self.name,
+ caps=self.caps
+ )
+ self.log.debug('Launching Mirror-Receiver-Pipeline:\n%s', pipeline)
+ receiverPipeline = Gst.parse_launch(pipeline)
+
+ self.log.debug('Binding End-of-Stream-Signal on Source-Receiver-Pipeline')
+ receiverPipeline.bus.add_signal_watch()
+ receiverPipeline.bus.connect("message::eos", self.on_disconnect)
+
+ receiverPipeline.set_state(Gst.State.PLAYING)
+
+ self.receiverPipelines.append(receiverPipeline)
+ self.currentConnections.append(conn)
+
+ return True
+
+ def on_disconnect(self, bus, message):
+ self.log.info('Received End-of-Stream-Signal on Source-Receiver-Pipeline')