pax_global_header00006660000000000000000000000064144617543730014530gustar00rootroot0000000000000052 comment=5464683d1507e0e2b2364704c63eccbdffb76f11 voctomix-outcasts-1.0.0/000077500000000000000000000000001446175437300152415ustar00rootroot00000000000000voctomix-outcasts-1.0.0/.gitignore000066400000000000000000000013761446175437300172400ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ # lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints voctomix-outcasts-1.0.0/LICENSE000066400000000000000000000020001446175437300162360ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. voctomix-outcasts-1.0.0/NEWS.txt000066400000000000000000000015551446175437300165640ustar00rootroot00000000000000July 10,2020 ingest.py has a new parameter: --src it can be used instead of these 6: ``` --video-source {dv,hdv,udp_h264,hdmi2usb,blackmagic,ximage,png,file,test,spacescope} --video-attribs VIDEO_ATTRIBS --video-elements VIDEO_ELEMENTS --audio-source {dv,hdv,file,alsa,pulse,blackmagic,test} --audio-attribs AUDIO_ATTRIBS --audio-elements AUDIO_ELEMENTS ``` The above 6 parameters are used to construct a gst pipeline string, --src takes that same string as a parameter. or some other string that can't be constructed, like for a simple logitech webcam: `ingest.py --src "v4l2src ! queue ! videoconvert ! videorate ! videoscale ! {videocaps} ! mux. audiotestsrc wave=ticks freq=330 ! {audiocaps} ! queue ! matroskamux name=mux"' {videocaps} and {audiocaps} come from voctocore, or they can be overridden with static values, not sure when one would need that. voctomix-outcasts-1.0.0/README.md000066400000000000000000000023321446175437300165200ustar00rootroot00000000000000# Voctomix-Outcasts Components used with Voctomix. # Install notes (Debian) for fbdevsink: sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-bad # Getting started / Demo: Install the dependencies listed here: https://github.com/voc/voctomix#installation ``` git clone https://github.com/voc/voctomix.git git clone https://github.com/CarlFK/voctomix-outcasts.git cd voctomix-outcasts cd tests ./test1.sh ``` You should see the gui with 2 bouncing ball test feeds. It is saving to foo.ts and logging cuts to cut-list.log. # Production Voctomix is a small set of components that need to be assembled and configured with additional components. We try to make all of this open and public, but in general everyone needs to define a system appropriate to their needs. For example, not everyone does a live stream so there is no point in expecting a streaming server to be available. That said, here are things used in production: https://anonscm.debian.org/git/debconf-video/ansible.git https://github.com/xfxf/av-foss-stack https://github.com/CarlFK/dvsmon/blob/master/vocto-prod1.py https://anonscm.debian.org/git/debconf-video/debconf-video.git/tree/src/hdmi2usb/hdmi2usb.py and I think there are some udev rules around there too. voctomix-outcasts-1.0.0/configs/000077500000000000000000000000001446175437300166715ustar00rootroot00000000000000voctomix-outcasts-1.0.0/configs/lca.ini000066400000000000000000000004501446175437300201300ustar00rootroot00000000000000[mix] videocaps=video/x-raw,format=I420,width=1280,height=720,framerate=30000/1001,pixel-aspect-ratio=1/1 sources=cam1,grabber [mainvideo] playaudio=true [previews] enabled=false videocaps=video/x-raw,width=1280,height=720,framerate=30000/1001 [videodisplay] # system=gl system=xv # system=x voctomix-outcasts-1.0.0/configs/test1.ini000066400000000000000000000006751446175437300204420ustar00rootroot00000000000000[mix] videocaps=video/x-raw,format=I420,width=853,height=480,framerate=30000/1001,pixel-aspect-ratio=1/1 sources=cam1,cam2,grabber [cam1] # not referenced in test1.sh [cam2] video-source=test video-attribs=pattern=circular horizontal-speed=2 port=10001 [grabber] port=10002 [mainvideo] playaudio=true [previews] enabled=false videocaps=video/x-raw,width=320,height=200,framerate=30000/1001 [videodisplay] # system=gl system=xv # system=x voctomix-outcasts-1.0.0/configs/test_720p.ini000066400000000000000000000004501446175437300211200ustar00rootroot00000000000000[mix] videocaps=video/x-raw,format=I420,width=1280,height=720,framerate=30000/1001,pixel-aspect-ratio=1/1 sources=cam1,grabber [mainvideo] playaudio=true [previews] enabled=false videocaps=video/x-raw,width=1280,height=720,framerate=30000/1001 [videodisplay] # system=gl system=xv # system=x voctomix-outcasts-1.0.0/configs/voctolight.ini000066400000000000000000000012271446175437300215560ustar00rootroot00000000000000[server] host=localhost [light] ;; Camera name to use, case sensitive. cam=Camera ;; Plugin to use for output, chose one: ;; Write tally light state to stdout plugin=stdout ;; Raspberry Pi GPIO ;; ;; Requires RPi.GPIO ; plugin=rpi_gpio ;; Serial DTR ; plugin=serial_dtr ;; Tomu Simple LED ;; ;; Requires pyusb ;; ;; usb_simple application required from: ;; https://github.com/im-tomu/tomu-samples/tree/master/usb_simple ; plugin=tomu [rpi] ;; GPIO that has the desired light gpio=11 [serial_dtr] path=/dev/ttyS0 [tomu] ;; Value to send to usb_simple is a bitmask: ;; ;; 0: no light ;; 1: green light ;; 2: red light ;; 3: both lights on=2 off=0 voctomix-outcasts-1.0.0/debian/000077500000000000000000000000001446175437300164635ustar00rootroot00000000000000voctomix-outcasts-1.0.0/debian/changelog000066400000000000000000000007521446175437300203410ustar00rootroot00000000000000voctomix-outcasts (0.3-1ubuntu1) wily; urgency=medium * Add example scripts to OS path -- Carl F Karsten Wed, 18 May 2016 02:47:44 -0500 voctomix-outcasts (0.2-1ubuntu1) xenial; urgency=medium * Added scripts and configs from lca -- Carl F Karsten Thu, 05 May 2016 11:28:30 -0500 voctomix-outcasts (0.1-1) wily; urgency=medium * Initial release. -- Carl F Karsten Sun, 01 May 2016 21:11:53 -0400 voctomix-outcasts-1.0.0/debian/compat000066400000000000000000000000021446175437300176610ustar00rootroot000000000000009 voctomix-outcasts-1.0.0/debian/control000066400000000000000000000016431446175437300200720ustar00rootroot00000000000000Source: voctomix-outcasts Section: video Priority: optional Maintainer: Carl F. Karsten Build-Depends: debhelper (>=9), python3, python3:any, dh-python, pandoc Standards-Version: 3.9.8 Homepage: https://github.com/CarlFK/voctomix-outcasts # Vcs-Git: git://anonscm.debian.org/collab-maint/voctomix.git # Vcs-Browser: https://anonscm.debian.org/git/collab-maint/voctomix.git X-Python-Version: >= 3 Package: voctomix-outcasts Architecture: all Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3, python3-gi, gstreamer1.0-plugins-base, gstreamer1.0-plugins-good, libgstreamer1.0-0, gstreamer1.0-tools Suggests: gstreamer1.0-libav, gstreamer1.0-plugins-bad, gstreamer1.0-plugins-ugly Description: Additional files used with Voctomix . This package does not depend on any of Voctomix, it can be installed on a client machine that connects to a 2nd machine running Voctomix-core voctomix-outcasts-1.0.0/debian/copyright000066400000000000000000000041261446175437300204210ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: voctomix-outcasts Source: https://github.com/CarlFK/voctomix-outcasts Files: * Copyright: 2015,2016 Carl F. Karsten License: MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Files: debian/* Copyright: 2016 Holger Levsen License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". voctomix-outcasts-1.0.0/debian/rules000077500000000000000000000021501446175437300175410ustar00rootroot00000000000000#!/usr/bin/make -f # Uncomment this to turn on verbose mode. # #export DH_VERBOSE=1 # %: dh $@ --with python3 override_dh_auto_build: pandoc -o voctomix-outcasts.html README.md override_dh_auto_install: dh_auto_install # voctomix-outcasts cp -r configs/* \ $(CURDIR)/debian/voctomix-outcasts/etc/voctomix/ cp -r ingest.py generate-cut-list.py record-mixed-av.sh \ record-timestamp.sh \ lib/ tests/ \ $(CURDIR)/debian/voctomix-outcasts/usr/share/voctomix/outcasts/ ln -s /usr/share/voctomix/outcasts/ingest.py \ $(CURDIR)/debian/voctomix-outcasts/usr/bin/ingest ln -s /usr/share/voctomix/outcasts/generate-cut-list.py \ $(CURDIR)/debian/voctomix-outcasts/usr/bin/generate-cut-list ln -s /usr/share/voctomix/outcasts/record-mixed-av.sh \ $(CURDIR)/debian/voctomix-outcasts/usr/bin/record-mixed-av ln -s /usr/share/voctomix/outcasts/record-timestamp.sh \ $(CURDIR)/debian/voctomix-outcasts/usr/bin/record-timestamp override_dh_installdocs: dh_installdocs override_dh_python3: # override_dh_clean: dh_clean rm -f voctomix-outcasts.html debian/debhelper-build-stamp rm -rf ./lib/__pycache__ voctomix-outcasts-1.0.0/debian/source/000077500000000000000000000000001446175437300177635ustar00rootroot00000000000000voctomix-outcasts-1.0.0/debian/source/format000066400000000000000000000000141446175437300211710ustar00rootroot000000000000003.0 (quilt) voctomix-outcasts-1.0.0/debian/voctomix-outcasts.dirs000066400000000000000000000000621446175437300230570ustar00rootroot00000000000000etc/voctomix/ usr/share/voctomix/outcasts usr/bin voctomix-outcasts-1.0.0/generate-cut-list.py000077500000000000000000000034611446175437300211560ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright: 2015,2016 Carl F. Karsten # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. import argparse import socket import datetime import sys def capture_cuts(sock): """Listen to a voctocore control socket, and yield on cuts""" fd = sock.makefile('rw') for line in fd: words = line.rstrip('\n').split(' ') signal = words[0] args = words[1:] if signal == 'message' and args[0] == 'cut': yield def main(): p = argparse.ArgumentParser() p.add_argument('--host', default='localhost', help='Hostname of voctocore') p.add_argument('--port', type=int, default=9999, help='Port to connect to, on voctocore') p.add_argument('--file', type=argparse.FileType('a'), help='Filename to write cuts to') args = p.parse_args() sock = socket.create_connection((args.host, args.port)) for cut in capture_cuts(sock): ts = datetime.datetime.now().strftime("%Y-%m-%d/%H_%M_%S") print(ts) sys.stdout.flush() if args.file: args.file.write('%s\n' % ts) args.file.flush() if __name__ == '__main__': try: main() except KeyboardInterrupt: sys.exit(1) voctomix-outcasts-1.0.0/ingest.py000077500000000000000000000335201446175437300171120ustar00rootroot00000000000000#!/usr/bin/env python3 """ ingest.py: source client for Voctomix. Copyright: 2015-2020 Carl F. Karsten , Ryan Verner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Features: Retrieves audio and video-caps config from core. and client config Uses core's clock. Mix and match audio and video sources muxed into one stream. Can display video locally, including frame count and fps. Defaults to test audio and video sent to local core. """ import argparse from pprint import pprint import socket import sys import gi gi.require_version('Gst', '1.0') gi.require_version('GstNet', '1.0') from gi.repository import Gst, GstNet, GObject, GLib from lib.connection import Connection def mk_video_src(args, videocaps): # make video source part of pipeline # args come from command line # videocaps come from voctocore if args.video_source == 'dv': video_src = """ dv1394src name=videosrc {attribs} ! dvdemux name=demux ! queue max-size-time=4000000000 ! dvdec ! """ elif args.video_source == 'hdv': video_src = """ hdv1394src {attribs} name=videosrc ! tsdemux ! queue max-size-time=4000000000 ! decodebin ! """ elif args.video_source in ('hdmi2usb', 'uvc-mjpeg'): video_src = """ v4l2src {attribs} name=videosrc ! """ video_src += """ queue max-size-time=4000000000 ! image/jpeg,{caps} ! jpegdec ! """.format(caps=extract_caps(videocaps)) elif args.video_source == 'uvc-raw': video_src = """ v4l2src {attribs} name=videosrc ! """ video_src += """ video/x-raw,{caps} ! """.format( caps=extract_caps(videocaps)) elif args.video_source == 'ximage': # startx=0 starty=0 endx=1919 endy=1079 ! video_src = """ ximagesrc {attribs} name=videosrc use-damage=false ! """ elif args.video_source == 'blackmagic': video_src = """ decklinkvideosrc name=videosrc {attribs} ! """ elif args.video_source == 'png': video_src = """ multifilesrc {attribs} caps="image/png" ! pngdec ! """ # tip: --video-attribs "loop=true" # tip: --video-attribs "location=sc1204a.png" elif args.video_source == 'file': video_src = """ multifilesrc {attribs} ! decodebin name=src src. ! queue ! """ elif args.video_source == 'rtmp': video_src = """ rtmpsrc {attribs} ! decodebin name=src src. ! queue ! """ elif args.video_source == 'test': video_src = """ videotestsrc name=videosrc {attribs} ! """ # things to render as text ontop of test video video_src += """ clockoverlay text="Source: {hostname}\nCaps: {videocaps}\nAttribs: {attribs}\n" halignment=left line-alignment=left ! """.format(hostname=socket.gethostname(), videocaps=videocaps, attribs=args.video_attribs) elif args.video_source == 'spacescope': # Stereo visualizer # pair up with test beep for a handy AV sync test. video_src = """ audio_tee. ! queue ! spacescope shader=none style=lines {attribs} ! """ if args.monitor: videosink=args.monitor video_src += f""" tee name=t ! queue ! videoconvert ! {videosink} sync=false t. ! queue ! """ if args.video_elements: video_src += args.video_elements + " !\n" video_src += videocaps + " !\n" video_src = video_src.format(attribs=args.video_attribs) return video_src def mk_audio_src(args, audiocaps): d = { 'attribs': args.audio_attribs, 'base_audio_attribs': 'provide-clock=false slave-method=re-timestamp', 'audiocaps': audiocaps, } 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.audio ! queue ! """ elif args.audio_source in ('file', 'rtmp'): # this only works if video is from ... # some gst source that gets demux ed, I guess. audio_src = """ src. ! queue ! """ elif args.audio_source == 'pulse': audio_src = """ pulsesrc {attribs} {base_audio_attribs} name=audiosrc ! queue max-size-time=4000000000 ! """ elif args.audio_source == 'alsa': audio_src = """ alsasrc {attribs} {base_audio_attribs} name=audiosrc ! queue max-size-time=4000000000 ! """ elif args.audio_source == 'blackmagic': audio_src = """ decklinkaudiosrc name=audiosrc {attribs} ! """ elif args.audio_source == 'test': audio_src = """ audiotestsrc wave=ticks freq=330 {attribs} name=audiosrc ! """ if args.audio_elements: audio_src += args.audio_elements + " !\n" audio_src += """ {audiocaps} ! tee name=audio_tee audio_tee. ! queue ! """ audio_src = audio_src.format(**d) return audio_src def extract_caps(videocaps, caps=("width", "height")): """Filter out caps (list of keys) from a videocaps strings""" parts = videocaps.split(',') filtered = [cap for cap in parts if cap.split('=')[0] in caps] return ','.join(filtered) def mk_client(core_ip, port): client = "tcpclientsink host={host} port={port}".format( host=core_ip, port=port) return client def mk_pipeline(args, server_caps, core_ip): if args.src: src = args.src.format(**server_caps) else: video_src = mk_video_src(args, server_caps['videocaps']) audio_src = mk_audio_src(args, server_caps['audiocaps']) src = """ {video_src} mux. {audio_src} mux. matroskamux name=mux ! """.format(video_src=video_src, audio_src=audio_src) client = mk_client(core_ip, args.port) pipeline = """ {src} {client} """.format(src=src, client=client) # remove blank lines to make it more human readable while "\n\n" in pipeline: pipeline = pipeline.replace("\n\n", "\n") print(pipeline) if args.debug: # print something to run in a shell gst_cmd = "gst-launch-1.0 {}".format(pipeline) # escape the ! because bash # asl2: ! is interpreted as a command history metacharacter gst_cmd = gst_cmd.replace("!", " \! ") # remove all the \n to make it easy to cut/paste into shell gst_cmd = gst_cmd.replace("\n", " ") while " " in gst_cmd: gst_cmd = gst_cmd.replace(" ", " ") print("-"*78) print(gst_cmd) print("-"*78) return pipeline def get_server_conf(core_ip, source_id, args): # establish a synchronus connection to server conn = Connection(core_ip) # fetch config from server server_config = conn.fetch_config() # Pull out the configs relevant to this client server_conf = { 'videocaps': server_config['mix']['videocaps'], 'audiocaps': server_config['mix']['audiocaps'] } if source_id is not None: # get conf from server for this source, d=server_config[source_id] if args.debug: pprint(d) # stomp all over command line values # this is backwards: command line should override conf file. for k in d: if args.debug: print('--{}="{}"'.format(k,d[k])) # python argparse converts a-b to a_b, so we will to too. args.__setattr__(k.replace("-", "_"),d[k]) return server_conf, args def get_clock(core_ip, core_clock_port=9998): clock = GstNet.NetClientClock.new( 'voctocore', core_ip, core_clock_port, 0) print('obtained NetClientClock from host: {ip}:{port}'.format( ip=core_ip, port=core_clock_port)) print('waiting for NetClientClock to sync...') clock.wait_for_sync(Gst.CLOCK_TIME_NONE) print('synced with NetClientClock.') return clock def run_pipeline(pipeline, clock, audio_delay=0, video_delay=0): def on_eos(bus, message): print('Received EOS-Signal') sys.exit(0) def on_error(bus, message): print('Received Error-Signal') (error, debug) = message.parse_error() print('Error-Details: #%u: %s' % (error.code, debug)) sys.exit(2) print('starting pipeline...') senderPipeline = Gst.parse_launch(pipeline) if clock is not None: senderPipeline.use_clock(clock) # Delay video/audio if required NS_TO_MS = 100000 if video_delay > 0: print('Adjusting video sync: [{} milliseconds]'.format(video_delay)) video_delay = video_delay * NS_TO_MS videosrc = senderPipeline.get_by_name('videosrc') videosrc.get_static_pad('src').set_offset(video_delay) if audio_delay > 0: print('Adjusting audio sync: [{} milliseconds]'.format(audio_delay)) audio_delay = audio_delay * NS_TO_MS audiosrc = senderPipeline.get_by_name('audiosrc') audiosrc.get_static_pad('src').set_offset(audio_delay) # Binding End-of-Stream-Signal on Source-Pipeline senderPipeline.bus.add_signal_watch() senderPipeline.bus.connect("message::eos", on_eos) senderPipeline.bus.connect("message::error", on_error) print("playing...") senderPipeline.set_state(Gst.State.PLAYING) mainloop = GLib.MainLoop() try: mainloop.run() except KeyboardInterrupt: print('Terminated via Ctrl-C') print('Shutting down...') senderPipeline.set_state(Gst.State.NULL) print('Done.') return def get_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, 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.") parser.add_argument('--source-id', action='store', help="get config from server using this id.") parser.add_argument( '--src', action='store', default='', help="gst source pipeline") parser.add_argument( '--video-source', action='store', choices=[ 'dv', 'hdv', 'udp_h264', 'hdmi2usb', 'uvc-mjpeg', 'uvc-raw', 'blackmagic', 'ximage', 'png', 'file', 'rtmp', 'test', 'spacescope'], default='test', help="Where to get video from") parser.add_argument( '--video-attribs', action='store', default='', help="misc video attributes for gst") parser.add_argument( '--video-delay', action='store', default=0, type=int, help="delay video by this many milliseconds") parser.add_argument( '--video-elements', action='store', default='videoconvert ! deinterlace ! videorate ! videoscale', help="gst video elments after src.") parser.add_argument( '--audio-source', action='store', choices=['dv', 'hdv', 'file', 'alsa', 'pulse', 'blackmagic', 'rtmp', 'test',], default='test', help="Where to get audio from") parser.add_argument( '--audio-attribs', action='store', default='', help="misc audio attributes for gst") parser.add_argument( '--audio-delay', action='store', default=0, type=int, help="delay audio by this many milliseconds") parser.add_argument( '--audio-elements', action='store', default="audioconvert ! audioresample ! audiorate", help="gst audio elments after src.") parser.add_argument( '-m', '--monitor', # action='store_true', help="local display sink (autovideosink, fbdevsink, fpsdisplaysink...") 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( '--no-clock', action='store_true', help="Don't use core's clock. (danger)") parser.add_argument( '--debug', action='store_true', help="debugging things, like dump a gst-launch-1.0 command") args = parser.parse_args() return args def main(): Gst.init([]) args = get_args() core_ip = socket.gethostbyname(args.host) server_caps, args = get_server_conf(core_ip, args.source_id, args) pipeline = mk_pipeline(args, server_caps, core_ip) if args.no_clock: clock = None else: clock = get_clock(core_ip) run_pipeline(pipeline, clock, args.audio_delay, args.video_delay) if __name__ == '__main__': main() voctomix-outcasts-1.0.0/lib/000077500000000000000000000000001446175437300160075ustar00rootroot00000000000000voctomix-outcasts-1.0.0/lib/config.py000066400000000000000000000020231446175437300176230ustar00rootroot00000000000000from configparser import ConfigParser from os.path import join, dirname, realpath, expanduser __all__ = ['Config'] class Config(ConfigParser): config_files = [ join(dirname(realpath(__file__)), 'default-config.ini'), join(dirname(realpath(__file__)), 'config.ini'), '/etc/voctomix/voctolight.ini', '/etc/voctolight.ini', expanduser('~/.voctolight.ini'), ] def __init__(self, cmd_line_config=None): super().__init__() self.cmd_line_config = cmd_line_config self._read_config() def _read_config(self): self.read(self.config_files) if self.cmd_line_config: self.read_file(self.cmd_line_config) self.cmd_line_config.seek(0) def setup_with_server_config(self, server_config): self.clear() self._read_config() self.read_dict(server_config) def getlist(self, section, option): return [x.strip() for x in self.get(section, option).split(',')] voctomix-outcasts-1.0.0/lib/connection.py000066400000000000000000000030641446175437300205230ustar00rootroot00000000000000import json import logging import socket """ Copyright: 2015,2016 Carl F. Karsten , Ryan Verner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. """ log = logging.getLogger('Connection') # TODO: Replace with asyncio class Connection(object): def __init__(self, host, port=9999): self.host = host self.port = port log.info('Establishing connection to %s', host) self.sock = socket.create_connection((host, port)) log.debug('Connection successful \o/') ip = self.sock.getpeername()[0] log.debug('Remote-IP is %s', ip) def fetch_config(self): log.info('Reading server-config') fd = self.sock.makefile('rw') fd.write('get_config\n') fd.flush() while True: line = fd.readline() signal, _, args = line.partition(' ') if signal != 'server_config': continue server_config = json.loads(args) return server_config voctomix-outcasts-1.0.0/lib/plugins/000077500000000000000000000000001446175437300174705ustar00rootroot00000000000000voctomix-outcasts-1.0.0/lib/plugins/all_plugins.py000066400000000000000000000011411446175437300223500ustar00rootroot00000000000000from .base_plugin import BasePlugin from .rpi_gpio import RpiGpio from .serial_dtr import SerialDtr from .stdout import Stdout from .tomu import Tomu PLUGINS = { 'rpi_gpio': RpiGpio, 'serial_dtr': SerialDtr, 'stdout': Stdout, 'tomu': Tomu, } def get_plugin(config) -> BasePlugin: """Creates an instance of a plugin named in Voctolight's configuration file.""" plugin_name = config.get('light', 'plugin') plugin_cls = PLUGINS.get(plugin_name, None) if plugin_cls is None: raise ValueError(f'{plugin_name} is not a valid plugin name') return plugin_cls(config) voctomix-outcasts-1.0.0/lib/plugins/base_plugin.py000066400000000000000000000007171446175437300223370ustar00rootroot00000000000000from abc import ABC, abstractmethod __all__ = ['BasePlugin'] class BasePlugin(ABC): """An abstract plugin class, that all other plugins inherit from.""" def __init__(self, config): ... @abstractmethod def tally_on(self) -> None: """Called when the tally light should be turned on.""" ... @abstractmethod def tally_off(self) -> None: """Called when the tally light should be turned off.""" ... voctomix-outcasts-1.0.0/lib/plugins/rpi_gpio.py000066400000000000000000000014271446175437300216560ustar00rootroot00000000000000""" Plugin to provide a tally light interface for a Raspberry Pi's GPIO. It requires RPi.GPIO. """ from .base_plugin import BasePlugin try: import RPi.GPIO as GPIO GPIO.setmode(GPIO.BOARD) except ImportError: # We are probably not running on a Raspberry Pi. GPIO = None __all__ = ['RpiGpio'] class RpiGpio(BasePlugin): def __init__(self, config): if not GPIO: raise NotImplementedError('RpiGpio will not work on this platform. Is RPi.GPIO installed?') self.gpio_port = int(config.get('rpi', 'gpio')) GPIO.setup(self.gpio_port, GPIO.OUT) GPIO.output(self.gpio_port, GPIO.HIGH) def tally_on(self): GPIO.output(self.gpio_port, GPIO.LOW) def tally_off(self): GPIO.output(self.gpio_port, GPIO.HIGH) voctomix-outcasts-1.0.0/lib/plugins/serial_dtr.py000066400000000000000000000010311446175437300221650ustar00rootroot00000000000000# Turning on a light opens a serial port, which pulls the DTR line high. # Turning off a light closes the serial port, pulling DTR low. from .base_plugin import BasePlugin __all__ = ['SerialDtr'] class SerialDtr(BasePlugin): def __init__(self, config): self.fn = config.get('serial_dtr', 'port') self.fd = None def tally_on(self): self.fd = open(self.fn) def tally_off(self): if self.fd: self.fd.close() self.fd = None def __del__(self): tally_off() voctomix-outcasts-1.0.0/lib/plugins/stdout.py000066400000000000000000000005041446175437300213630ustar00rootroot00000000000000""" Plugin to provide a tally light interface via stdout. This is an example that can be used to build other plugins. """ from .base_plugin import BasePlugin __all__ = ['Stdout'] class Stdout(BasePlugin): def tally_on(self): print('Tally light on') def tally_off(self): print('Tally light off') voctomix-outcasts-1.0.0/lib/plugins/tomu.py000066400000000000000000000015361446175437300210330ustar00rootroot00000000000000# Makes a tomu.im USB Simple sample as a tally light. # https://github.com/im-tomu/tomu-samples/tree/master/usb_simple from .base_plugin import BasePlugin try: import usb.core except ImportError: usb = None class Tomu(BasePlugin): def __init__(self, config): if not usb: raise ValueError('USB support not available. Install pyusb') self.on = int(config.get('tomu', 'on')) self.off = int(config.get('tomu', 'off')) self.device = usb.core.find(idVendor=0x1209, idProduct=0x70b1) if self.device is None: raise ValueError('Device not found, flash usb_simple on the tomu.') self.device.set_configuration() def tally_on(self): self.device.ctrl_transfer(0x40, 0, self.on, 0, '') def tally_off(self): self.device.ctrl_transfer(0x40, 0, self.off, 0, '') voctomix-outcasts-1.0.0/notes.txt000066400000000000000000000031311446175437300171300ustar00rootroot00000000000000--src examples same as voctomix-ingest (test sources): voctomix-ingest --src "videotestsrc name=videosrc ! videoconvert ! videorate ! videoscale ! video/x-raw,format=I420,width=1280,height=720,framerate=30/1,pixel-aspect-ratio=1/1 ! mux. audiotestsrc wave=ticks freq=330 name=audiosrc ! audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 ! tee name=audio_tee audio_tee.! queue ! mux. matroskamux name=mux !" some webcam: --src "v4l2src ! queue ! videoconvert ! videorate ! videoscale ! {videocaps} ! mux. audiotestsrc wave=ticks freq=330 ! {audiocaps} ! queue ! matroskamux name=mux !" if --monoitor throws ERROR: from element /GstPipeline:pipeline0/GstAutoVideoSink:autovideosink0/GstVaapiSink:autovideosink0-actual-sink-vaapi: Internal error: could not render surface vaapi/gstvaapisink.c(1467): gst_vaapisink_show_frame_unlocked (): /GstPipeline:pipeline0/GstAutoVideoSink:autovideosink0/GstVaapiSink:autovideosink0-actual-sink-vaapi --monitor fpsdisplaysink --debug shows: tee name=t \! queue \! videoconvert \! fpsdisplaysink sync=false t. # add vaapipostproc before the videosink: vaapipostproc ! autovideosink # retult: voctomix-ingest --src "videotestsrc name=videosrc ! tee name=t \! queue \! videoconvert \! fpsdisplaysink sync=false t. ! videoconvert ! videorate ! videoscale ! video/x-raw,format=I420,width=1280,height=720,framerate=30/1,pixel-aspect-ratio=1/1 ! mux. audiotestsrc wave=ticks freq=330 name=audiosrc ! audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 ! tee name=audio_tee audio_tee.! queue ! mux. matroskamux name=mux !" To run on Wayland: GDK_BACKEND=x11 voctomix-outcasts-1.0.0/record-mixed-av.sh000077500000000000000000000033271446175437300205730ustar00rootroot00000000000000#!/bin/sh # Copyright: 2015,2016 Carl F. Karsten # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # $1 - destination dir. default: ~/Videos # files will be $dest_dir/$date/$time.gs.ts # (.gs to keep these apart from the files created by record-timestamp.sh) # NB: does not chunk files. keeps saving untill the process is killed. # TODO: https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good/html/gst-plugins-good-plugins-splitmuxsink.html # splitmuxsink — Muxer wrapper for splitting output stream by size or time # https://gstreamer.freedesktop.org/documentation/multifile/splitmuxsink.html?gi-language=c#splitmuxsink dest_dir=${1:-~/Videos}/$(date +%Y-%m-%d) mkdir -p $dest_dir gst-launch-1.0 \ tcpclientsrc host=localhost port=11000 !\ \ matroskademux name=demux \ \ demux. !\ queue !\ videoconvert !\ avenc_mpeg2video bitrate=5000000 max-key-interval=0 !\ queue !\ mux. \ \ demux. !\ queue !\ audioconvert !\ avenc_mp2 bitrate=192000 !\ queue !\ mux. \ \ mpegtsmux name=mux !\ filesink location="$dest_dir/$(date +%H_%M_%S).gs.ts" voctomix-outcasts-1.0.0/record-timestamp.sh000077500000000000000000000030571446175437300210640ustar00rootroot00000000000000#!/bin/bash -ex # Copyright: 2015,2016 Carl F. Karsten # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # note: this script will crash at midnight. # [segment @ 0x13bb960] Failed to open segment '.../2017-02-07/00_17_55.ts' # av_interleaved_write_frame(): No such file or directory # $1 - destination dir. default: ~/Videos # files will be $dest_dir/$date/$time.ts # dest_dir=${1:-~/Videos}/$(date +%Y-%m-%d) dest_dir=${1:-~/Videos} segment_time=1800 # 30 min mkdir -p $dest_dir/$(date +%Y-%m-%d) ffmpeg \ -nostdin -y \ -analyzeduration 10000 \ -thread_queue_size 512 \ -i tcp://localhost:11000?timeout=3000000 \ -aspect 16:9 \ -map 0:v -c:v:0 mpeg2video -pix_fmt:v:0 yuv420p -qscale:v:0 4 -qmin:v:0 4 -qmax:v:0 4 -keyint_min:v:0 5 -bf:v:0 0 -g:v:0 5 -me_method:v:0 dia \ -map 0:a -c:a mp2 -b:a 384k -ac:a 2 -ar:a 48000 \ -flags +global_header \ -f segment -segment_time $segment_time \ -segment_format mpegts \ -strftime 1 "$dest_dir/%Y-%m-%d/%H_%M_%S.ts" voctomix-outcasts-1.0.0/scripts/000077500000000000000000000000001446175437300167305ustar00rootroot00000000000000voctomix-outcasts-1.0.0/scripts/hdmi2usb-ingest.sh000077500000000000000000000017361446175437300223020ustar00rootroot00000000000000#!/bin/sh set -euf # 300 frames of test pattern and beeps: /home/juser/voctomix-outcasts/ingest.py \ --host cnt6 \ --port 10001 \ --video-source test \ --video-attribs "pattern=smpte75 num-buffers=200" \ --audio-source test \ --audio-attribs "num-buffers=200" \ --no-clock # Sometimes the uvcvideo driver gets stuck waiting for the HDMI2USB firmware to # respond but it never will. Removing the module and adding it back will reset # these timeouts getting things working again faster. # https://github.com/xfxf/lca2017-av/issues/33 if [ $(id -u) -eq 0 ]; then rmmod uvcvideo || true modprobe uvcvideo fi CMD="/home/juser/voctomix-outcasts/ingest.py --host cnt6 --port 10001 --video-source hdmi2usb --video-attribs device=/dev/hdmi2usb/by-num/all0/video " # if there is a sound card, use it. # and dump the clock becuase of my c2 cpu is to slow :( if [ -d /proc/asound/card1 ]; then CMD="$CMD --audio-source alsa --audio-attribs device=hw:1 --no-clock " fi exec $CMD voctomix-outcasts-1.0.0/setup/000077500000000000000000000000001446175437300164015ustar00rootroot00000000000000voctomix-outcasts-1.0.0/setup/voc_gits_path.sh000077500000000000000000000012101446175437300215630ustar00rootroot00000000000000#!/bin/bash -ex # git clone git://github.com/CarlFK/voctomix-outcasts.git # put voctomix and dvsmon next to voctomix-outcasts dir cd ../.. gitdirs=$PWD git clone https://github.com/voc/voctomix.git git clone git://github.com/CarlFK/dvsmon.git # create symlinks cd ~/bin ln -s $gitdirs/voctomix/voctocore/voctocore.py voctocore ln -s $gitdirs/voctomix/voctogui/voctogui.py voctogui ln -s $gitdirs/voctomix-outcasts/ingest.py ingest ln -s $gitdirs/voctomix-outcasts/record-timestamp.sh record-timestamp ln -s $gitdirs/voctomix-outcasts/record-mixed-av.sh record-mixed-av ln -s $gitdirs/voctomix-outcasts/generate-cut-list.py generate-cut-list voctomix-outcasts-1.0.0/testlight.py000077500000000000000000000015761446175437300176360ustar00rootroot00000000000000#!/usr/bin/env python3 # Test driver for Voctolight plugins. from argparse import ArgumentParser, FileType from sys import exit from lib.config import Config from lib.plugins.all_plugins import get_plugin def main(): parser = ArgumentParser( description='Test program for testing tallylight plugins') parser.add_argument( '-c', '--config', type=FileType('r'), help='Use a specific config file') args = parser.parse_args() config = Config(cmd_line_config=args.config) plugin = get_plugin(config) try: while True: print('Tally light on. Press ENTER to turn off, ^C to stop.') plugin.tally_on() input() print('Tally light off. Press ENTER to turn on, ^C to stop.') plugin.tally_off() input() except KeyboardInterrupt: pass if __name__ in '__main__': main() voctomix-outcasts-1.0.0/tests/000077500000000000000000000000001446175437300164035ustar00rootroot00000000000000voctomix-outcasts-1.0.0/tests/README.md000066400000000000000000000007601446175437300176650ustar00rootroot00000000000000Testing ingest.py (and other related bits.) ingest.py is a client, it needs a server to connect to, like voctocore or dummy-server.py ``` ./dummy-server.py ./ingest.py ``` A window should open and you see the gst test pattern. That's how you know it is working. ^c dummy-server.py and run it again: ``` ./ingest.py --video-source spacescope ./ingest.py --video-source hdmi2usb --video-attribs "device=/dev/video1" ./ingest.py --video-source file --video-attribs "location=17_58_47.ts" ``` voctomix-outcasts-1.0.0/tests/coupler-server.sh000077500000000000000000000003341446175437300217170ustar00rootroot00000000000000#!/bin/bash -ex # dummy-server.sh # something for vocto clients to connect to, # displays stream in a local window gst-launch-1.0 \ tcpserversrc host=0.0.0.0 port=4953 ! \ tcpserversink host=0.0.0.0 port=4954 voctomix-outcasts-1.0.0/tests/dummy-server-src.sh000077500000000000000000000003641446175437300221710ustar00rootroot00000000000000#!/bin/bash -x gst-launch-1.0 \ videotestsrc name=videosrc ! \ mux. \ audiotestsrc name=audiosrc ! \ mux. \ matroskamux streamable=true name=mux ! \ tcpserversink host=127.0.0.1 port=4954 voctomix-outcasts-1.0.0/tests/dummy-server.py000077500000000000000000000055471446175437300214320ustar00rootroot00000000000000#!/usr/bin/env python3 import asyncio import json import gbulb import gi # sudo apt install pkg-config libgirepository1.0-dev python3-venv # sudo apt build-dep python3-cairo # python3 -m venv venv # . venv/bin/activate # pip3 install gbulb PyGObject wheel gi.require_version('Gst', '1.0') gi.require_version('GstNet', '1.0') from gi.repository import Gst, GstNet CONFIG = { 'mix': { 'videocaps': 'video/x-raw,format=I420,width=1280,height=720,' 'framerate=30000/1001,pixel-aspect-ratio=1/1', 'audiocaps': 'audio/x-raw,format=S16LE,channels=2,' 'layout=interleaved,rate=48000', } } class VoctoMixProtocol(asyncio.Protocol): def connection_made(self, transport): peername = transport.get_extra_info('peername') print('Control connection from {}'.format(peername)) self.transport = transport def data_received(self, data): message = data.decode() if message == 'get_config\n': self.transport.write( 'server_config {}\n' .format(json.dumps(CONFIG)).encode('utf-8') ) else: print('Unknown control command: {!r}'.format(message)) class NetTimeClock(object): def __init__(self): clock = Gst.SystemClock().obtain() self.ntp = GstNet.NetTimeProvider.new(clock, '0.0.0.0', 9998) print('Clock listening on UDP port {}'.format(9998)) def stop(self): del self.ntp # FIXME: How is one supposed to shut it down? class VideoSink(object): def __init__(self): print('Listening for stream on port {}'.format(10000)) self.start() def start(self): self.pipeline = Gst.parse_launch( 'tcpserversrc host=0.0.0.0 port=10000 name=server ! ' 'decodebin ! videoconvert ! xvimagesink' ) self.pipeline.bus.add_signal_watch() self.pipeline.bus.connect("message::eos", self.on_eos) self.pipeline.set_state(Gst.State.PLAYING) def stop(self): self.pipeline.set_state(Gst.State.NULL) del self.pipeline def on_eos(self, bus, message): self.stop() self.start() def main(): Gst.init([]) gbulb.install() clock = NetTimeClock() sink = VideoSink() loop = asyncio.get_event_loop() coro = loop.create_server(VoctoMixProtocol, '0.0.0.0', 9999) server = loop.run_until_complete(coro) print('Control server listening on {}'.format( server.sockets[0].getsockname())) try: loop.run_forever() except KeyboardInterrupt: pass server.close() clock.stop() sink.stop() try: loop.run_until_complete(server.wait_closed()) except KeyboardInterrupt: # Gets carried through to here under glib main loop, for some reason... pass loop.close() if __name__ == '__main__': main() voctomix-outcasts-1.0.0/tests/server-file.sh000077500000000000000000000012161446175437300211650ustar00rootroot00000000000000#!/bin/bash -x # file sink server gst-launch-1.0 \ tcpserversrc host=127.0.0.1 port=4953 !\ matroskademux name=demux \ demux. !\ queue !\ videoconvert !\ avenc_mpeg2video bitrate=5000000 max-key-interval=0 !\ queue !\ mux. \ demux. !\ queue !\ audioconvert !\ avenc_mp2 bitrate=192000 !\ queue !\ mux. \ mpegtsmux name=mux !\ filesink location="$(mktemp --suffix=.ts)" exit # test source client gst-launch-1.0 \ videotestsrc name=videosrc ! \ mux. \ audiotestsrc name=audiosrc ! \ mux. \ matroskamux streamable=true name=mux ! \ tcpclientsink host=127.0.0.1 port=4953 voctomix-outcasts-1.0.0/tests/show_hdmi2usb.sh000077500000000000000000000001471446175437300215210ustar00rootroot00000000000000#!/bin/bash -x gst-launch-1.0 \ -v \ v4l2src device=$1 ! jpegdec ! autovideosink sync=false voctomix-outcasts-1.0.0/tests/test1.sh000077500000000000000000000007361446175437300200100ustar00rootroot00000000000000#!/bin/bash -ex # Mostly to confirm the basics is installed and setup properly. cd ../../voctomix ./voctocore/voctocore.py -i ../voctomix-outcasts/configs/test1.ini & sleep 5 ./voctogui/voctogui.py & ../voctomix-outcasts/ingest.py & ../voctomix-outcasts/ingest.py --source-id cam2 & ../voctomix-outcasts/ingest.py \ --video-attribs "pattern=ball" --source-id grabber & ../voctomix-outcasts/record-timestamp.sh & ../voctomix-outcasts/generate-cut-list.py --file test1.log voctomix-outcasts-1.0.0/tests/test2.sh000077500000000000000000000004671446175437300200120ustar00rootroot00000000000000#!/bin/bash -ex # Mostly to confirm the basics is installed and setup properly. # test1.sh expects git checkouts, test2 installed on $PATH voctocore -i /etc/voctomix/light.ini & sleep 5 voctogui & voctomix-ingest & sleep 1 voctomix-ingest --port 10001 & voctomix-record-timestamp & voctomix-generate-cut-list voctomix-outcasts-1.0.0/tests/test_720p.sh000077500000000000000000000006531446175437300204750ustar00rootroot00000000000000#!/bin/bash -ex # Mostly to confirm the basics is installed and setup properly. cd ../../voctomix ./voctocore/voctocore.py -i ../voctomix-outcasts/configs/test_720p.ini & sleep 5 ./voctogui/voctogui.py & ../voctomix-outcasts/ingest.py --debug & ../voctomix-outcasts/ingest.py --video-attribs "pattern=ball" --port 10001 & ./example-scripts/gstreamer/record-mixed-av.sh & ./example-scripts/control-server/generate-cut-list.py voctomix-outcasts-1.0.0/tests/test_file.sh000077500000000000000000000017661446175437300207320ustar00rootroot00000000000000# gst-launch-1.0 videotestsrc name=videosrc \! clockoverlay text="Source:twist Caps:video/x-raw,format=I420,width=1280,height=720,framerate=30000/1001,pixel-aspect-ratio=1/1 Attribs: " halignment=left line-alignment=left \! video/x-raw,format=I420,width=1280,height=720,framerate=30000/1001,pixel-aspect-ratio=1/1 \! mux. audiotestsrc name=audiosrc freq=330 \! audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 \! mux. matroskamux name=mux \! tcpclientsink host=127.0.0.1 port=10000 gst-launch-1.0 \ videotestsrc name=videosrc !\ video/x-raw,format=I420,width=1280,height=720,framerate=30000/1001,pixel-aspect-ratio=1/1 !\ avenc_mpeg2video bitrate=5000000 max-key-interval=0 !\ queue !\ mux. \ audiotestsrc name=audiosrc freq=330 ! \ audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 ! \ avenc_mp2 bitrate=192000 !\ queue !\ mux. \ matroskamux name=mux ! \ filesink location=test.ts voctomix-outcasts-1.0.0/voctolight.py000077500000000000000000000103351446175437300200020ustar00rootroot00000000000000#!/usr/bin/env python3 # Based upon: # https://github.com/voc/voctomix/tree/voctopanel/example-scripts/voctolight from argparse import ArgumentParser, FileType import asyncio from enum import Enum import json from sys import exit from lib.config import Config from lib.plugins.all_plugins import get_plugin class Connection(asyncio.Protocol): def __init__(self, interpreter): self.interpreter = interpreter def connect(self, host, port=9999): self.loop = asyncio.get_event_loop() self.conn = self.loop.create_connection(lambda: self, host, port) self.loop.run_until_complete(conn.conn) def send(self, message): self.transport.write(message.encode()) self.transport.write('\n'.encode()) def connection_made(self, transport): self.transport = transport def data_received(self, data): # print('data received: {}'.format(data.decode())) lines = data.decode().rstrip('\n').split('\n') for line in lines: interpreter.handler(line) def connection_lost(self, exc): print('server closed the connection') asyncio.get_event_loop().stop() # FIXME: Duplicate from videomix.py class CompositeModes(Enum): fullscreen = 0 side_by_side_equal = 1 side_by_side_preview = 2 picture_in_picture = 3 class Interpreter(object): a_or_b = False primary = False composite_mode = CompositeModes.fullscreen def __init__(self, actor, config, debug=False): self.config = config self.actor = actor self.debug = debug actor.tally_off() if self.debug: print('LED has been reset to off') def compute_state(self): if self.composite_mode == CompositeModes.fullscreen: return self.primary else: return self.a_or_b def handler(self, response): words = response.split() signal = words[0] args = words[1:] try: handler = getattr(self, 'handle_{}'.format(signal)) except AttributeError: print('Ignoring signal', signal) else: handler(args) if interpreter.compute_state(): print('LED has been switched on') actor.tally_on() else: print('LED has been switched off') actor.tally_off() def handle_video_status(self, cams): mycam = self.config.get('light', 'cam') if mycam in cams: self.a_or_b = True else: self.a_or_b = False self.primary = (cams[0] == mycam) # print ('Is primary?', self.primary) def handle_composite_mode(self, mode_list): mode = mode_list[0] if mode == 'fullscreen': self.composite_mode = CompositeModes.fullscreen elif mode == 'side_by_side_equal': self.composite_mode = CompositeModes.side_by_side_equal elif mode == 'side_by_side_preview': self.composite_mode = CompositeModes.side_by_side_preview elif mode == 'picture_in_picture': self.composite_mode = CompositeModes.picture_in_picture else: print('Cannot handle', mode, 'of type', type(mode)) def handle_server_config(self, args): server_config_json = ' '.join(args) server_config = json.loads(server_config_json) self.config.setup_with_server_config(server_config) if __name__ == '__main__': parser = ArgumentParser( description='Tallylight controlling daemon for voctomix.') parser.add_argument( '-c', '--config', type=FileType('r'), help='Use a specific config file') parser.add_argument( '-d', '--debug', action='store_true', help='Show what would be done in addition to toggling lights') args = parser.parse_args() config = Config(cmd_line_config=args.config) actor = get_plugin(config) interpreter = Interpreter(actor, config, debug=args.debug) conn = Connection(interpreter) conn.connect(config.get('server', 'host')) conn.send('get_config') conn.send('get_composite_mode') conn.send('get_video') try: conn.loop.run_forever() except KeyboardInterrupt: print('Quitting (Ctrl-C)') actor.reset_led()