diff --git a/.gitignore b/.gitignore index e94e83e..19a7e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,14 @@ *.o *.lo *.la +*.pyc +*.gcda +*.gcov +*.txt +imgurbash.sh.* +.coverage +.gdb_history +.idea /INSTALL /Makefile /Makefile.in @@ -17,7 +25,16 @@ /config.sub /configure /depcomp +/docs/_build +/gstreamer +/gst-plugins-base +/gst-plugins-good +/gst-plugins-bad +/gst-plugins-base +/gst-switch.sublime-* +/libvpx /html +/htmlcov /install-sh /libtool /ltmain.sh @@ -27,6 +44,10 @@ /plugins/.libs/ /plugins/Makefile /plugins/Makefile.in +/python-api/imgurbash.sh* +/python-api/.cache/ +/python-api/reports/ +/python-api/htmlcov/ /stamp-h1 /tools/.deps/ /tools/Makefile @@ -45,7 +66,9 @@ /timestamped* /frames /transition-strips -/record*.dat -*.data -*.log /tools/gst-switch-ptz +test-driver +*.dat* +*.avi +*.mkv +*.log diff --git a/.travis-run.sh b/.travis-run.sh new file mode 100755 index 0000000..03ad9d6 --- /dev/null +++ b/.travis-run.sh @@ -0,0 +1,67 @@ +#! /bin/bash -ex + +./autogen.sh --prefix=/usr || { + printf "Failed to do autogen!!!\n" + exit -1 +} +make clean +make || { + printf "make of gst-switch failed!!!\n" + exit -1 +} +sudo make install || { + printf "make install of gst-switch failed!!!\n" + exit -1 +} + +cd python-api + +case $TEST in + + python-api ) + case $TYPE in + unittest ) + make unittests || { + printf "Unittests failed!\n" + exit -1 + } + coveralls || { + printf "Coveralls failed!\n" + exit -1 + } + ;; + integration ) + make integration || { + printf "Integration tests failed!\n" + exit -1 + } + coveralls || { + printf "Coveralls failed!\n" + exit -1 + } + ;; + esac + ;; + module ) + case $TYPE in + python ) + make test || { + printf "Tests failed!\n" + exit -1 + } + coveralls || { + printf "Coveralls failed!\n" + exit -1 + } + ;; + c ) + make test || { + printf "Tests failed!\n" + exit -1 + } + coveralls -n -r ../tools + esac +esac || { + printf "Failed!\n!\n" + exit -1 +} diff --git a/.travis-setup.sh b/.travis-setup.sh new file mode 100644 index 0000000..378760e --- /dev/null +++ b/.travis-setup.sh @@ -0,0 +1,28 @@ +#! /bin/bash -ex + +# Update / upgrade everything +sudo apt-get -y update && sudo apt-get -y upgrade + +# gst-switch compile stuff +sudo apt-get -y install build-essential dh-autoreconf +sudo apt-get -y install libgtk-3-dev +sudo apt-get -y install gstreamer1.0.* libgstreamer.*1.0.* + +# Python stuff for gst-switch API +sudo apt-get -y install python-software-properties python-pip +sudo apt-get -y install libglib2.0-dev gir1.2-glib-2.0 libgirepository1.0-dev libglib2.0-0 python-gi +sudo apt-get -y install python-scipy +sudo pip install mock --upgrade +sudo pip install pytest-cov --upgrade +sudo pip install pytest-pep8 --upgrade +sudo pip install pylint --upgrade + +# Needed for tests +sudo apt-get -y install wget libav-tools + +# Needed to get coverage output +if [ $TYPE == 'c' ]; then + sudo pip install cpp-coveralls +else + sudo pip install python-coveralls +fi diff --git a/.travis.yml b/.travis.yml index 869dbef..2f520a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,23 @@ -language: c +language: + - c compiler: - - clang - gcc -before_install: - - echo 'yes' | sudo add-apt-repository ppa:gstreamer-developers/ppa - - sudo apt-get update - - sudo apt-get install .*gstreamer.*1\.0.* libgtk-3-dev - - ./autogen.sh +env: + matrix: + - TEST=python-api TYPE=unittest + - TEST=python-api TYPE=integration + - TEST=module TYPE=python + - TEST=module TYPE=c + +install: + - wget -q -O- https://raw.githubusercontent.com/mithro/travis-trusty/master/setup.sh | bash + - chmod a+rx $PWD/.travis-*.sh + - /trusty/run.py $PWD/.travis-setup.sh + +script: + - /trusty/run.py $PWD/.travis-run.sh notifications: email: diff --git a/Makefile.am b/Makefile.am index 74e55df..1dc5fb1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,8 +1,8 @@ -SUBDIRS = plugins tools tests +SUBDIRS = plugins tools tests/unit tests EXTRA_DIST = autogen.sh -ACLOCAL_AMFLAGS = -I m4 +ACLOCAL_AMFLAGS = -I build include tests/names.mk test: update-test-suite clean-test-data $(TESTS) diff --git a/README b/README deleted file mode 100644 index bfaf97a..0000000 --- a/README +++ /dev/null @@ -1,6 +0,0 @@ -GstSwitch - -This project is intended to be a replacement of DV-switch base on GStreamer. - -See README.md. - diff --git a/README b/README new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/README @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/README.md b/README.md index b435a10..909996b 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,402 @@ -# GstSwitch +# Build Status +[![Build Status](https://travis-ci.org/timvideos/gst-switch.png?branch=master)](https://travis-ci.org/timvideos/gst-switch) +[![Coverage Status](https://coveralls.io/repos/timvideos/gst-switch/badge.png)](https://coveralls.io/r/timvideos/gst-switch) -## Purpose +# gst-switch -This project is intended to be a replacement of DV-switch base on GStreamer. +gst-switch is a software based live video mixing and capture platform designed +for fast, reliable and fault tolerant mixing, switching and capture. It is +intended to replace expensive hardware mixing solutions needed when mixing HD +video streams. -## Components +gst-switch was developed as a HD be a replacement of +[dvswitch](http://dvswitch.alioth.debian.org/wiki/). -GstSwitch contains two parts, *gst-switch-srv* and *gst-switch-ui* -## Quick Manual +# Quick Start -### The GstSwitch Server + * Use Ubuntu 14.04 (trusty) + * Check out code + * Run `build-min-trusty.sh` + * Run `run-demo.sh` -The GstSwitch server will open at least three ports for video/audio input, and +---- +---- + +# Design + +gst-switch is split into multiple parts. + +The video mixing is done in a server process which is controlled via an API +available on DBUS. + +Video and Audio is feed to the server using the gstreamer data protocol (GDP) +over TCP sockets. The resulting mixed output is written to disk and can also be +read out of the server again using GDP over a TCP socket. + +The UI talks to the server via the API and displays the results. The UI is a +totally separate process (which could run on a different machine) meaning that +any crashes, stalls or other issues do not effect the mixing or recording. + +The Python API exists to allow easy scripting on the server. It has +functionality for controlling the starting and stopping of the server and +video/audio feeds. It also can control the mixing, doing anything that the GUI +can do. The Python API is used extensively for testing, but should also be +useful for semi and full-automated mixing. + +![System design diagram](https://docs.google.com/drawings/d/18sU5uECrPgQzxLWzc7RaYWRs9F3aocx0chf16-yxBEY/pub?w=1871&h=999) + +# Installing + +gst-switch is only **tested** on the current +[Ubuntu LTS](https://wiki.ubuntu.com/LTS). + +It **should** work on other Linux versions which include the dependencies (such +as Debian, RedHat, etc) but is not regularly tested on them. + +## Dependencies + +gst-switch has minimal dependencies when used just for basic video switching. + +The requirements are; + * A current version of [gstreamer1.0](http://gstreamer.freedesktop.org/). + (Used to do all the actual hard work.) + * A current version of dbus. (Used to communicate between the server and UI.) + * A current version of gtk3.0 (Used for the GUI.) + +The Python API is highly recommended but optional part of gst-switch. The +Python API requirements are; + * python-gi (and thus gir1.2-glib-2.0 and libglib2.0-0). + (Used to call gstreamer and dbus Python bindings.) + * python-scipy + (Used to create and manipulate images.) + + +The test suite requirements are; + * pytest (including pytest-pep8 and pytest-cov) + * pylint + * ffmpeg or avconv command line tools (ffmpeg and libav-tools). + * *FIXME: These should be removed and replaced with gst-launch commands.* + +There are some optional dependencies for the more advanced features like +speaker tracking. *FIXME: Add the dependency information here.* + +## Ubuntu Trusty + +Either run the `build-min-trusty.sh` script or follow the instructions in the +file. + +## Ubuntu Precise + +**We are currently in the process of dropping support for Precise in preference +of Trusty.** + +Ubuntu Precise does not ship with gstreamer1.0, so it needs to be build from +source. The `scripts/install2.sh` script will do this for you and then builds +gst-switch too. + +---- +---- + +# Manual + +gst-switch contains two parts, *gst-switch-srv* and *gst-switch-ui* + +## The gst-switch UI +The gst-switch UI is a graphical controller for controlling the input streams. +The UI must be started *after* the server is running. + +Example gst-switch-ui command line; +```bash +gst-switch-ui +``` + +### Controls + + * Left Click - Set video as primary video. + * Right Click - Set video as secondary video. + +| Key | Function | +| --------------------- | -------------------------------------------- | +| R | Start recording to new file | +| Tab | Cycle composite modes | +| Esc | No composite mode | +| F1 or P | Compositing mode - Picture-in-Picture | +| F2 or D | Compositing mode - Side-by-side (preview) | +| F3 or S | Compositing mode - Side-by-side (equal) | +| A | When compositing, change the primary video | +| B | When compositing, change the secondary video | +| Up/Down | When compositing, select the video | +| Ctrl + Arrows | When compositing PIP, Adjust PIP position | +| Ctrl + Shift + Arrows | When compositing PIP, Adjust PIP size | + + +## The gst-switch Server + +The gst-switch server will open at least three ports for video/audio input, and command controls. -#### Video Input +Example gst-switch command line: +```bash +gst-switch-srv +``` +*FIXME: Add examples of the recording and resolution options to the above +example.* + +Once the gst-switch server is running, you will then need to feed the server +audio and video streams. Helper scripts for doing this can be found in the XXXX +directory. + +*FIXME: Add example feed information here.* + + +### Command Line Options + +``` +Usage: + gst-switch-srv [OPTION...] + +Help Options: + -h, --help Show help options + --help-all Show all help options + --help-gst Show GStreamer Options + +Application Options: + -v, --verbose Prompt more messages + -t, --test-switch=OUTPUT Perform switch test + -r, --record=FILENAME Enable recorder and record into the specified FILENAME + -p, --video-input-port=NUM Specify the video input listen port. + -a, --audio-input-port=NUM Specify the audio input listen port. + --control-port=NUM Specify the control port. +``` + +### Video Input + +The default TCP port for video data is *3000*. + +Video data should be sent using gstreamer but there are multiple methods for +doing so (see the sections below). + +All video sent **must** be in the resolution the exact aspect ratio, resolution +and frame rates the server was configured with when starting. + +The color space / pixel format must be in [I420](http://www.fourcc.org/yuv.php#IYUV). +(This limitation comes from the usage of the inter gstreamer elements.) + +You can use the following gstreamer plug-ins to convert the video on the +sender's side; + * [videobox](http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good-plugins/html/gst-plugins-good-plugins-videobox.html), + to add borders or crop a video (thus changing the resolution or aspect ratio). + + * [videoscale](http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-videoscale.html), + to change the resolution of video via scaling. + + * [videoconvert](http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-videoconvert.html), + to convert the pixel format and color space. + + * [videorate](http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-videorate.html), + to adjust the frame rate (fps). + +All video **must** be deinterlaced, use the following plug-ins to convert +interlaced video on the sender's side; + * [deinterlace](http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good-plugins/html/gst-plugins-good-plugins-deinterlace.html), + the basic, well coded, stable, deinterlacer included in gstreamer-plugins-good. + + * `avdeinterlace`, the high performance and feature rich deinterlacer from + libav (a fork of ffmpeg). Previously called `ffdeinterlace`. + +#### Using `gst-launch-1.0` for video input + +*FIXME: Put some example gst-launch-1.0 examples here.* + +*FIXME: Write some example scripts which show gst-launch-1.0 usage for capturing +from DV, v4l, decklink cards, etc.* + +#### Using gst-switch Python API + +*FIXME: Put some Python API examples here.* + +#### Using gst-switch-cap for video input + +gst-switch-cap is a custom program needed if you wish to use the speaker +tracking functionality. + +Example command line; +```bash +gst-switch-cap --device='/dev/ttyUSB0' --protocol='visca' +``` + +#### Supported video formats + +*FIXME: Actually make sure the tests cover the following formats listed below.* + +gst-switch is tested with the following video resolution; + * 300x200 - A low resolution only used in testing to increase test speed. + * 1024x768 - A 4:3 resolution almost universally supported by computers and + projector equipment. Sometimes called XGA. + * 1280x720 - Lowest resolution 16:9 HD format, often called + [720p](http://en.wikipedia.org/wiki/720p). + * 1920x1080 - Most common 16:9 HD format, often called + [1080p](http://en.wikipedia.org/wiki/1080p). + * 4096x2160 - Ultra HD format with 19:10 (1.9:1) aspect ratio. Sometimes + called [4k](http://en.wikipedia.org/wiki/4K_resolution) video or + 2160p. + **WARNING: Even the fastest computers struggle at this resolution!** + + +gst-switch is tested with the following video frame rates; + * 24fps - Common "film" format. + * 25fps - "Full" frame rate of TV in PAL based countries (Europe, Australia, etc). + * 30fps - "Full" frame rate of TV in NTSC based countries (USA, Japan, etc). + * 60fps - Most common frame rate of computer monitors. + * 120fps - Frame rate of "High FPS" computer monitors and TVs. -The video input port is *3000*. Supported input video format: I420 -(video/x-raw), 1280x720 (for debug mode, the video size could be 300x200). #### Audio Input Port -The audio input port is *4000*. +The default audio input port is *4000*. + +Audio input must be in; +``` + audio/x-raw + format: S16LE + rate: 48000 + channels: 2 + layout: interleaved +``` + +##### Example test audio feed + +``` +gst-launch-1.0 audiotestsrc is-live=true \ + ! audioconvert \ + ! audio/x-raw,rate=48000,channels=2,format=S16LE,layout=interleaved \ + ! gdppay \ + ! tcpclientsink port=4000 +``` + +##### Example real audio feed + + #### Control Port -The command control port is *5000*. +The default command control port is *5000*. -### Controls +*FIXME: This is a DBUS port of some type?* + + +# gstreamer plug-ins usage + +This section lists the plug-ins used by gst-switch system. + +*FIXME: Generate this bit automagically. The following command line is kind of a +start;* +```shell +ack --type=cc "g_string_new|g_string_append" * | grep '"' | grep ".c" | sed -e's/.c:[^"]*"/ /' -e's/"[^"]*$//' | sort | uniq > output +``` + +### GUI + +#### Audio Display + +tools/gstaudiovisual - Renders a video of the audio. + * *FIXME: Is this GUI or server side?* + * audioconvert + * videoconvert + * faad + * gdpdepay + * monoscope + * queue2 + * tcpclientsrc + * tee + * textoverlay + * xvimagesink + +tools/gstvideodisp - Displays the video on the screen. + * cairooverlay + * gdpdepay + * tcpclientsrc + * videoconvert + * xvimagesink + +### Server + +tools/gstswitchserver + * gdppay + * intervideosrc + * tcpserversink + +tools/gstcase + * *FIXME: What is this?* + * faac + * gdpdepay + * gdpdepay + * gdppay + * giostreamsrc + * interaudiosink + * interaudiosrc + * intervideosink + * intervideosrc + * queue2 + * tcpserversink + * tee + +tools/gstcomposite - The actual mixer? + * *FIXME: I'm sure there is probably more here...* + * identity + * intervideosink + * queue2 + * tee - - - +#### Recording - +The output format is an AVI file with VP8 (webm) video and faac audio. - +tools/gstrecorder - Recording to disk. + * avimux + * voaacenc + * filesink + * gdppay + * interaudiosrc + * intervideosrc + * queue2 + * tcpserversink + * tee + * vp8enc - +### Speaker Track - +tools/gstswitchcapture and tools/gstswitchptz - +### Tests - +Everything under the tests/ directory - -
Key BindingsFunction
Ctrl+Arrow - Adjust PIP position. -
Ctrl+Shift+Arrow - Adjust PIP size. -
Arrow Up/Down - Changge selection of video previews. Selecting a video is the first step for - switching video/audio input. -
A - Switch video input for channel A or activate audio input to the selected - video/audio. -
B - Switch video input for channel B to the selected video. -
Tab - Change a mode of A/B compositing, stroking it repeatly will cycle within modes - and off. -
Esc - Turn off A/B videos compositing. -
r - Start a new recording, the *--record _name_* will be used as a template, - e.g. *record 2013-01-23 131139.dat* -
+ * audiotestsrc + * avidemux + * faad + * fakesink + * filesrc + * gdpdepay + * gdppay + * goom2k1 + * monoscope + * tcpclientsrc + * textoverlay + * videoconvert + * videotestsrc + * xvimagesink +###The gst-switch PTZ -# Current Build Status +Implements Pan-Tilt-Zoom Camera controller UI. +```bash +gst-switch-ptz --device='/dev/ttyUSB0' --protocol='visca' --video='/dev/video0' +``` -[![Build Status](https://travis-ci.org/timsvideo/gst-switch.png?branch=master)](https://travis-ci.org/timsvideo/gst-switch) +*FIXME: Remove this tool and put it into a gst-plugins-ptz repository with the +associated plug-ins.* diff --git a/build-min-trusty.sh b/build-min-trusty.sh new file mode 100755 index 0000000..5bf8a5a --- /dev/null +++ b/build-min-trusty.sh @@ -0,0 +1,17 @@ +#!/bin/bash -ex + +sudo apt-get install \ + build-essential \ + libglib2.0-dev \ + libgtk-3-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + gstreamer1.0-plugins-bad \ + autoconf \ + libtool + + + +./autogen.sh +make + diff --git a/m4/.gitignore b/build/.gitignore similarity index 100% rename from m4/.gitignore rename to build/.gitignore diff --git a/build/Makefile.am.gitignore b/build/Makefile.am.gitignore new file mode 100644 index 0000000..42992a3 --- /dev/null +++ b/build/Makefile.am.gitignore @@ -0,0 +1,33 @@ +# this file should only be used in directories that generate test +# or example binaries through noinst_PROGRAMS; it is *not* a full +# generator of Git ignore files, and it's not meant to be used as +# the top-level Git ignore file generator. + +$(srcdir)/.gitignore: Makefile.am $(top_srcdir)/build/Makefile.am.gitignore + $(AM_V_GEN)( \ + echo "*.o" ; \ + echo ".gitignore" ; \ + echo "*.trs" ; \ + echo "*.log" ; \ + echo "Makefile" ; \ + echo "Makefile.in" ; \ + echo ".deps" ; \ + ) > $(srcdir)/.gitignore ; \ + for p in $(noinst_PROGRAMS); do \ + echo "/$$p" >> $(srcdir)/.gitignore ; \ + done ; \ + for p in $(check_PROGRAMS); do \ + echo "/$$p" >> $(srcdir)/.gitignore ; \ + done + + +gitignore: $(srcdir)/.gitignore + +gitignore-clean: + @rm -f $(srcdir)/.gitignore + +.PHONY: gitignore gitignore-clean + +all: gitignore + +maintainer-clean: gitignore-clean diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..89ea825 --- /dev/null +++ b/build/README.md @@ -0,0 +1,6 @@ + + * [glib-tap.mk](https://git.gnome.org/browse/glib/tree/glib-tap.mk?id=7f2f4ab12df6ddb501900846896f496520871d16) (modified) + * [glibtests.m4](https://git.gnome.org/browse/glib/tree/m4macros/glibtests.m4?id=7f2f4ab12df6ddb501900846896f496520871d16) + * [tap-driver.sh](https://git.gnome.org/browse/glib/tree/tap-driver.sh?id=7f2f4ab12df6ddb501900846896f496520871d16) + * [tap-test](https://git.gnome.org/browse/glib/tree/tap-driver.sh?id=7f2f4ab12df6ddb501900846896f496520871d16) + * [Makefile.am.gitignore](https://git.gnome.org/browse/json-glib/tree/build/autotools/Makefile.am.gitignore?id=4ccb9ab6119b81dde81b04208b98c89fc3a7884c) (modified) diff --git a/build/glib-tap.mk b/build/glib-tap.mk new file mode 100644 index 0000000..11a34ab --- /dev/null +++ b/build/glib-tap.mk @@ -0,0 +1,135 @@ +# GLIB - Library of useful C routines + +TESTS_ENVIRONMENT= \ + G_TEST_SRCDIR="$(abs_srcdir)" \ + G_TEST_BUILDDIR="$(abs_builddir)" \ + G_DEBUG=gc-friendly \ + MALLOC_CHECK_=2 \ + MALLOC_PERTURB_=$$(($${RANDOM:-256} % 256)) +LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/build/tap-driver.sh +LOG_COMPILER = $(top_srcdir)/build/tap-test + +NULL = + +# initialize variables for unconditional += appending +BUILT_SOURCES = +BUILT_EXTRA_DIST = +CLEANFILES = *.log *.trs +DISTCLEANFILES = +MAINTAINERCLEANFILES = +EXTRA_DIST = +TESTS = + +installed_test_LTLIBRARIES = +installed_test_PROGRAMS = +installed_test_SCRIPTS = +nobase_installed_test_DATA = + +noinst_LTLIBRARIES = +noinst_PROGRAMS = +noinst_SCRIPTS = +noinst_DATA = + +check_LTLIBRARIES = +check_PROGRAMS = +check_SCRIPTS = +check_DATA = + +# We support a fairly large range of possible variables. It is expected that all types of files in a test suite +# will belong in exactly one of the following variables. +# +# First, we support the usual automake suffixes, but in lowercase, with the customary meaning: +# +# test_programs, test_scripts, test_data, test_ltlibraries +# +# The above are used to list files that are involved in both uninstalled and installed testing. The +# test_programs and test_scripts are taken to be actual testcases and will be run as part of the test suite. +# Note that _data is always used with the nobase_ automake variable name to ensure that installed test data is +# installed in the same way as it appears in the package layout. +# +# In order to mark a particular file as being only for one type of testing, use 'installed' or 'uninstalled', +# like so: +# +# installed_test_programs, uninstalled_test_programs +# installed_test_scripts, uninstalled_test_scripts +# installed_test_data, uninstalled_test_data +# installed_test_ltlibraries, uninstalled_test_ltlibraries +# +# Additionally, we support 'extra' infixes for programs and scripts. This is used for support programs/scripts +# that should not themselves be run as testcases (but exist to be used from other testcases): +# +# test_extra_programs, installed_test_extra_programs, uninstalled_test_extra_programs +# test_extra_scripts, installed_test_extra_scripts, uninstalled_test_extra_scripts +# +# Additionally, for _scripts and _data, we support the customary dist_ prefix so that the named script or data +# file automatically end up in the tarball. +# +# dist_test_scripts, dist_test_data, dist_test_extra_scripts +# dist_installed_test_scripts, dist_installed_test_data, dist_installed_test_extra_scripts +# dist_uninstalled_test_scripts, dist_uninstalled_test_data, dist_uninstalled_test_extra_scripts +# +# Note that no file is automatically disted unless it appears in one of the dist_ variables. This follows the +# standard automake convention of not disting programs scripts or data by default. +# +# test_programs, test_scripts, uninstalled_test_programs and uninstalled_test_scripts (as well as their disted +# variants) will be run as part of the in-tree 'make check'. These are all assumed to be runnable under +# gtester. That's a bit strange for scripts, but it's possible. + +TESTS += $(test_programs) $(test_scripts) $(uninstalled_test_programs) $(uninstalled_test_scripts) \ + $(dist_test_scripts) $(dist_uninstalled_test_scripts) + +# Note: build even the installed-only targets during 'make check' to ensure that they still work. +# We need to do a bit of trickery here and manage disting via EXTRA_DIST instead of using dist_ prefixes to +# prevent automake from mistreating gmake functions like $(wildcard ...) and $(addprefix ...) as if they were +# filenames, including removing duplicate instances of the opening part before the space, eg. '$(addprefix'. +all_test_programs = $(test_programs) $(uninstalled_test_programs) $(installed_test_programs) \ + $(test_extra_programs) $(uninstalled_test_extra_programs) $(installed_test_extra_programs) +all_test_scripts = $(test_scripts) $(uninstalled_test_scripts) $(installed_test_scripts) \ + $(test_extra_scripts) $(uninstalled_test_extra_scripts) $(installed_test_extra_scripts) +all_dist_test_scripts = $(dist_test_scripts) $(dist_uninstalled_test_scripts) $(dist_installed_test_scripts) \ + $(dist_test_extra_scripts) $(dist_uninstalled_test_extra_scripts) $(dist_installed_test_extra_scripts) +all_test_scripts += $(all_dist_test_scripts) +EXTRA_DIST += $(all_dist_test_scripts) +all_test_data = $(test_data) $(uninstalled_test_data) $(installed_test_data) +all_dist_test_data = $(dist_test_data) $(dist_uninstalled_test_data) $(dist_installed_test_data) +all_test_data += $(all_dist_test_data) +EXTRA_DIST += $(all_dist_test_data) +all_test_ltlibs = $(test_ltlibraries) $(uninstalled_test_ltlibraries) $(installed_test_ltlibraries) + +if ENABLE_ALWAYS_BUILD_TESTS +noinst_LTLIBRARIES += $(all_test_ltlibs) +noinst_PROGRAMS += $(all_test_programs) +noinst_SCRIPTS += $(all_test_scripts) +noinst_DATA += $(all_test_data) +else +check_LTLIBRARIES += $(all_test_ltlibs) +check_PROGRAMS += $(all_test_programs) +check_SCRIPTS += $(all_test_scripts) +check_DATA += $(all_test_data) +endif + +if ENABLE_INSTALLED_TESTS +installed_test_PROGRAMS += $(test_programs) $(installed_test_programs) \ + $(test_extra_programs) $(installed_test_extra_programs) +installed_test_SCRIPTS += $(test_scripts) $(installed_test_scripts) \ + $(test_extra_scripts) $(test_installed_extra_scripts) +installed_test_SCRIPTS += $(dist_test_scripts) $(dist_test_extra_scripts) \ + $(dist_installed_test_scripts) $(dist_installed_test_extra_scripts) +nobase_installed_test_DATA += $(test_data) $(installed_test_data) +nobase_installed_test_DATA += $(dist_test_data) $(dist_installed_test_data) +installed_test_LTLIBRARIES += $(test_ltlibraries) $(installed_test_ltlibraries) +installed_testcases = $(test_programs) $(installed_test_programs) \ + $(test_scripts) $(installed_test_scripts) \ + $(dist_test_scripts) $(dist_installed_test_scripts) + +installed_test_meta_DATA = $(installed_testcases:=.test) + +%.test: %$(EXEEXT) Makefile + $(AM_V_GEN) (echo '[Test]' > $@.tmp; \ + echo 'Type=session' >> $@.tmp; \ + echo 'Exec=$(installed_testdir)/$(notdir $<) --tap' >> $@.tmp; \ + echo 'Output=TAP' >> $@.tmp; \ + mv $@.tmp $@) + +CLEANFILES += $(installed_test_meta_DATA) +endif diff --git a/build/glibtests.m4 b/build/glibtests.m4 new file mode 100644 index 0000000..7d5920a --- /dev/null +++ b/build/glibtests.m4 @@ -0,0 +1,28 @@ +dnl GLIB_TESTS +dnl + +AC_DEFUN([GLIB_TESTS], +[ + AC_ARG_ENABLE(installed-tests, + AS_HELP_STRING([--enable-installed-tests], + [Enable installation of some test cases]), + [case ${enableval} in + yes) ENABLE_INSTALLED_TESTS="1" ;; + no) ENABLE_INSTALLED_TESTS="" ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-installed-tests]) ;; + esac]) + AM_CONDITIONAL([ENABLE_INSTALLED_TESTS], test "$ENABLE_INSTALLED_TESTS" = "1") + AC_ARG_ENABLE(always-build-tests, + AS_HELP_STRING([--enable-always-build-tests], + [Enable always building tests during 'make all']), + [case ${enableval} in + yes) ENABLE_ALWAYS_BUILD_TESTS="1" ;; + no) ENABLE_ALWAYS_BUILD_TESTS="" ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-always-build-tests]) ;; + esac]) + AM_CONDITIONAL([ENABLE_ALWAYS_BUILD_TESTS], test "$ENABLE_ALWAYS_BUILD_TESTS" = "1") + if test "$ENABLE_INSTALLED_TESTS" = "1"; then + AC_SUBST(installed_test_metadir, [${datadir}/installed-tests/]AC_PACKAGE_NAME) + AC_SUBST(installed_testdir, [${libexecdir}/installed-tests/]AC_PACKAGE_NAME) + fi +]) diff --git a/build/tap-driver.sh b/build/tap-driver.sh new file mode 100755 index 0000000..19aa531 --- /dev/null +++ b/build/tap-driver.sh @@ -0,0 +1,652 @@ +#! /bin/sh +# Copyright (C) 2011-2013 Free Software Foundation, Inc. +# +# This program 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, or (at your option) +# any later version. +# +# This program 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 . + +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +# This file is maintained in Automake, please report +# bugs to or send patches to +# . + +scriptversion=2011-12-27.17; # UTC + +# Make unconditional expansion of undefined variables an error. This +# helps a lot in preventing typo-related bugs. +set -u + +me=tap-driver.sh + +fatal () +{ + echo "$me: fatal: $*" >&2 + exit 1 +} + +usage_error () +{ + echo "$me: $*" >&2 + print_usage >&2 + exit 2 +} + +print_usage () +{ + cat < + # + trap : 1 3 2 13 15 + if test $merge -gt 0; then + exec 2>&1 + else + exec 2>&3 + fi + "$@" + echo $? + ) | LC_ALL=C ${AM_TAP_AWK-awk} \ + -v me="$me" \ + -v test_script_name="$test_name" \ + -v log_file="$log_file" \ + -v trs_file="$trs_file" \ + -v expect_failure="$expect_failure" \ + -v merge="$merge" \ + -v ignore_exit="$ignore_exit" \ + -v comments="$comments" \ + -v diag_string="$diag_string" \ +' +# FIXME: the usages of "cat >&3" below could be optimized when using +# FIXME: GNU awk, and/on on systems that supports /dev/fd/. + +# Implementation note: in what follows, `result_obj` will be an +# associative array that (partly) simulates a TAP result object +# from the `TAP::Parser` perl module. + +## ----------- ## +## FUNCTIONS ## +## ----------- ## + +function fatal(msg) +{ + print me ": " msg | "cat >&2" + exit 1 +} + +function abort(where) +{ + fatal("internal error " where) +} + +# Convert a boolean to a "yes"/"no" string. +function yn(bool) +{ + return bool ? "yes" : "no"; +} + +function add_test_result(result) +{ + if (!test_results_index) + test_results_index = 0 + test_results_list[test_results_index] = result + test_results_index += 1 + test_results_seen[result] = 1; +} + +# Whether the test script should be re-run by "make recheck". +function must_recheck() +{ + for (k in test_results_seen) + if (k != "XFAIL" && k != "PASS" && k != "SKIP") + return 1 + return 0 +} + +# Whether the content of the log file associated to this test should +# be copied into the "global" test-suite.log. +function copy_in_global_log() +{ + for (k in test_results_seen) + if (k != "PASS") + return 1 + return 0 +} + +# FIXME: this can certainly be improved ... +function get_global_test_result() +{ + if ("ERROR" in test_results_seen) + return "ERROR" + if ("FAIL" in test_results_seen || "XPASS" in test_results_seen) + return "FAIL" + all_skipped = 1 + for (k in test_results_seen) + if (k != "SKIP") + all_skipped = 0 + if (all_skipped) + return "SKIP" + return "PASS"; +} + +function stringify_result_obj(result_obj) +{ + if (result_obj["is_unplanned"] || result_obj["number"] != testno) + return "ERROR" + + if (plan_seen == LATE_PLAN) + return "ERROR" + + if (result_obj["directive"] == "TODO") + return result_obj["is_ok"] ? "XPASS" : "XFAIL" + + if (result_obj["directive"] == "SKIP") + return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL; + + if (length(result_obj["directive"])) + abort("in function stringify_result_obj()") + + return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL +} + +function decorate_result(result) +{ + color_name = color_for_result[result] + if (color_name) + return color_map[color_name] "" result "" color_map["std"] + # If we are not using colorized output, or if we do not know how + # to colorize the given result, we should return it unchanged. + return result +} + +function report(result, details) +{ + if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/) + { + msg = ": " test_script_name + add_test_result(result) + } + else if (result == "#") + { + msg = " " test_script_name ":" + } + else + { + abort("in function report()") + } + if (length(details)) + msg = msg " " details + # Output on console might be colorized. + print decorate_result(result) msg + # Log the result in the log file too, to help debugging (this is + # especially true when said result is a TAP error or "Bail out!"). + print result msg | "cat >&3"; +} + +function testsuite_error(error_message) +{ + report("ERROR", "- " error_message) +} + +function handle_tap_result() +{ + details = result_obj["number"]; + if (length(result_obj["description"])) + details = details " " result_obj["description"] + + if (plan_seen == LATE_PLAN) + { + details = details " # AFTER LATE PLAN"; + } + else if (result_obj["is_unplanned"]) + { + details = details " # UNPLANNED"; + } + else if (result_obj["number"] != testno) + { + details = sprintf("%s # OUT-OF-ORDER (expecting %d)", + details, testno); + } + else if (result_obj["directive"]) + { + details = details " # " result_obj["directive"]; + if (length(result_obj["explanation"])) + details = details " " result_obj["explanation"] + } + + report(stringify_result_obj(result_obj), details) +} + +# `skip_reason` should be empty whenever planned > 0. +function handle_tap_plan(planned, skip_reason) +{ + planned += 0 # Avoid getting confused if, say, `planned` is "00" + if (length(skip_reason) && planned > 0) + abort("in function handle_tap_plan()") + if (plan_seen) + { + # Error, only one plan per stream is acceptable. + testsuite_error("multiple test plans") + return; + } + planned_tests = planned + # The TAP plan can come before or after *all* the TAP results; we speak + # respectively of an "early" or a "late" plan. If we see the plan line + # after at least one TAP result has been seen, assume we have a late + # plan; in this case, any further test result seen after the plan will + # be flagged as an error. + plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN) + # If testno > 0, we have an error ("too many tests run") that will be + # automatically dealt with later, so do not worry about it here. If + # $plan_seen is true, we have an error due to a repeated plan, and that + # has already been dealt with above. Otherwise, we have a valid "plan + # with SKIP" specification, and should report it as a particular kind + # of SKIP result. + if (planned == 0 && testno == 0) + { + if (length(skip_reason)) + skip_reason = "- " skip_reason; + report("SKIP", skip_reason); + } +} + +function extract_tap_comment(line) +{ + if (index(line, diag_string) == 1) + { + # Strip leading `diag_string` from `line`. + line = substr(line, length(diag_string) + 1) + # And strip any leading and trailing whitespace left. + sub("^[ \t]*", "", line) + sub("[ \t]*$", "", line) + # Return what is left (if any). + return line; + } + return ""; +} + +# When this function is called, we know that line is a TAP result line, +# so that it matches the (perl) RE "^(not )?ok\b". +function setup_result_obj(line) +{ + # Get the result, and remove it from the line. + result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0) + sub("^(not )?ok[ \t]*", "", line) + + # If the result has an explicit number, get it and strip it; otherwise, + # automatically assing the next progresive number to it. + if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/) + { + match(line, "^[0-9]+") + # The final `+ 0` is to normalize numbers with leading zeros. + result_obj["number"] = substr(line, 1, RLENGTH) + 0 + line = substr(line, RLENGTH + 1) + } + else + { + result_obj["number"] = testno + } + + if (plan_seen == LATE_PLAN) + # No further test results are acceptable after a "late" TAP plan + # has been seen. + result_obj["is_unplanned"] = 1 + else if (plan_seen && testno > planned_tests) + result_obj["is_unplanned"] = 1 + else + result_obj["is_unplanned"] = 0 + + # Strip trailing and leading whitespace. + sub("^[ \t]*", "", line) + sub("[ \t]*$", "", line) + + # This will have to be corrected if we have a "TODO"/"SKIP" directive. + result_obj["description"] = line + result_obj["directive"] = "" + result_obj["explanation"] = "" + + if (index(line, "#") == 0) + return # No possible directive, nothing more to do. + + # Directives are case-insensitive. + rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*" + + # See whether we have the directive, and if yes, where. + pos = match(line, rx "$") + if (!pos) + pos = match(line, rx "[^a-zA-Z0-9_]") + + # If there was no TAP directive, we have nothing more to do. + if (!pos) + return + + # Let`s now see if the TAP directive has been escaped. For example: + # escaped: ok \# SKIP + # not escaped: ok \\# SKIP + # escaped: ok \\\\\# SKIP + # not escaped: ok \ # SKIP + if (substr(line, pos, 1) == "#") + { + bslash_count = 0 + for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--) + bslash_count += 1 + if (bslash_count % 2) + return # Directive was escaped. + } + + # Strip the directive and its explanation (if any) from the test + # description. + result_obj["description"] = substr(line, 1, pos - 1) + # Now remove the test description from the line, that has been dealt + # with already. + line = substr(line, pos) + # Strip the directive, and save its value (normalized to upper case). + sub("^[ \t]*#[ \t]*", "", line) + result_obj["directive"] = toupper(substr(line, 1, 4)) + line = substr(line, 5) + # Now get the explanation for the directive (if any), with leading + # and trailing whitespace removed. + sub("^[ \t]*", "", line) + sub("[ \t]*$", "", line) + result_obj["explanation"] = line +} + +function get_test_exit_message(status) +{ + if (status == 0) + return "" + if (status !~ /^[1-9][0-9]*$/) + abort("getting exit status") + if (status < 127) + exit_details = "" + else if (status == 127) + exit_details = " (command not found?)" + else if (status >= 128 && status <= 255) + exit_details = sprintf(" (terminated by signal %d?)", status - 128) + else if (status > 256 && status <= 384) + # We used to report an "abnormal termination" here, but some Korn + # shells, when a child process die due to signal number n, can leave + # in $? an exit status of 256+n instead of the more standard 128+n. + # Apparently, both behaviours are allowed by POSIX (2008), so be + # prepared to handle them both. See also Austing Group report ID + # 0000051 + exit_details = sprintf(" (terminated by signal %d?)", status - 256) + else + # Never seen in practice. + exit_details = " (abnormal termination)" + return sprintf("exited with status %d%s", status, exit_details) +} + +function write_test_results() +{ + print ":global-test-result: " get_global_test_result() > trs_file + print ":recheck: " yn(must_recheck()) > trs_file + print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file + for (i = 0; i < test_results_index; i += 1) + print ":test-result: " test_results_list[i] > trs_file + close(trs_file); +} + +BEGIN { + +## ------- ## +## SETUP ## +## ------- ## + +'"$init_colors"' + +# Properly initialized once the TAP plan is seen. +planned_tests = 0 + +COOKED_PASS = expect_failure ? "XPASS": "PASS"; +COOKED_FAIL = expect_failure ? "XFAIL": "FAIL"; + +# Enumeration-like constants to remember which kind of plan (if any) +# has been seen. It is important that NO_PLAN evaluates "false" as +# a boolean. +NO_PLAN = 0 +EARLY_PLAN = 1 +LATE_PLAN = 2 + +testno = 0 # Number of test results seen so far. +bailed_out = 0 # Whether a "Bail out!" directive has been seen. + +# Whether the TAP plan has been seen or not, and if yes, which kind +# it is ("early" is seen before any test result, "late" otherwise). +plan_seen = NO_PLAN + +## --------- ## +## PARSING ## +## --------- ## + +is_first_read = 1 + +while (1) + { + # Involutions required so that we are able to read the exit status + # from the last input line. + st = getline + if (st < 0) # I/O error. + fatal("I/O error while reading from input stream") + else if (st == 0) # End-of-input + { + if (is_first_read) + abort("in input loop: only one input line") + break + } + if (is_first_read) + { + is_first_read = 0 + nextline = $0 + continue + } + else + { + curline = nextline + nextline = $0 + $0 = curline + } + # Copy any input line verbatim into the log file. + print | "cat >&3" + # Parsing of TAP input should stop after a "Bail out!" directive. + if (bailed_out) + continue + + # TAP test result. + if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/) + { + testno += 1 + setup_result_obj($0) + handle_tap_result() + } + # TAP plan (normal or "SKIP" without explanation). + else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/) + { + # The next two lines will put the number of planned tests in $0. + sub("^1\\.\\.", "") + sub("[^0-9]*$", "") + handle_tap_plan($0, "") + continue + } + # TAP "SKIP" plan, with an explanation. + else if ($0 ~ /^1\.\.0+[ \t]*#/) + { + # The next lines will put the skip explanation in $0, stripping + # any leading and trailing whitespace. This is a little more + # tricky in truth, since we want to also strip a potential leading + # "SKIP" string from the message. + sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "") + sub("[ \t]*$", ""); + handle_tap_plan(0, $0) + } + # "Bail out!" magic. + # Older versions of prove and TAP::Harness (e.g., 3.17) did not + # recognize a "Bail out!" directive when preceded by leading + # whitespace, but more modern versions (e.g., 3.23) do. So we + # emulate the latter, "more modern" behaviour. + else if ($0 ~ /^[ \t]*Bail out!/) + { + bailed_out = 1 + # Get the bailout message (if any), with leading and trailing + # whitespace stripped. The message remains stored in `$0`. + sub("^[ \t]*Bail out![ \t]*", ""); + sub("[ \t]*$", ""); + # Format the error message for the + bailout_message = "Bail out!" + if (length($0)) + bailout_message = bailout_message " " $0 + testsuite_error(bailout_message) + } + # Maybe we have too look for dianogtic comments too. + else if (comments != 0) + { + comment = extract_tap_comment($0); + if (length(comment)) + report("#", comment); + } + } + +## -------- ## +## FINISH ## +## -------- ## + +# A "Bail out!" directive should cause us to ignore any following TAP +# error, as well as a non-zero exit status from the TAP producer. +if (!bailed_out) + { + if (!plan_seen) + { + testsuite_error("missing test plan") + } + else if (planned_tests != testno) + { + bad_amount = testno > planned_tests ? "many" : "few" + testsuite_error(sprintf("too %s tests run (expected %d, got %d)", + bad_amount, planned_tests, testno)) + } + if (!ignore_exit) + { + # Fetch exit status from the last line. + exit_message = get_test_exit_message(nextline) + if (exit_message) + testsuite_error(exit_message) + } + } + +write_test_results() + +exit 0 + +} # End of "BEGIN" block. +' + +# TODO: document that we consume the file descriptor 3 :-( +} 3>"$log_file" + +test $? -eq 0 || fatal "I/O or internal error" + +# Local Variables: +# mode: shell-script +# sh-indentation: 2 +# eval: (add-hook 'write-file-hooks 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC" +# time-stamp-end: "; # UTC" +# End: diff --git a/build/tap-test b/build/tap-test new file mode 100755 index 0000000..481e333 --- /dev/null +++ b/build/tap-test @@ -0,0 +1,5 @@ +#! /bin/sh + +# run a GTest in tap mode. The test binary is passed as $1 + +$1 -k --tap diff --git a/confdefs.h b/confdefs.h new file mode 100644 index 0000000..aa9bb9f --- /dev/null +++ b/confdefs.h @@ -0,0 +1,21 @@ +/* confdefs.h */ +#define PACKAGE_NAME "gst-switch" +#define PACKAGE_TARNAME "gst-switch" +#define PACKAGE_VERSION "0.0.1" +#define PACKAGE_STRING "gst-switch 0.0.1" +#define PACKAGE_BUGREPORT "" +#define PACKAGE_URL "" +#define PACKAGE "gst-switch" +#define VERSION "0.0.1" +#define STDC_HEADERS 1 +#define HAVE_SYS_TYPES_H 1 +#define HAVE_SYS_STAT_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STRING_H 1 +#define HAVE_MEMORY_H 1 +#define HAVE_STRINGS_H 1 +#define HAVE_INTTYPES_H 1 +#define HAVE_STDINT_H 1 +#define HAVE_UNISTD_H 1 +#define HAVE_DLFCN_H 1 +#define LT_OBJDIR ".libs/" diff --git a/configure.ac b/configure.ac index e2cb1b2..4484930 100644 --- a/configure.ac +++ b/configure.ac @@ -8,13 +8,18 @@ dnl required versions of gstreamer and plugins-base GLIB_REQUIRED=2.32.0 GST_REQUIRED=1.0.1 GSTPB_REQUIRED=1.0.1 +GTK_REQUIRED=3.10.8 AC_CONFIG_SRCDIR([plugins/gstswitch.c]) AC_CONFIG_HEADERS([config.h]) dnl required version of automake -AM_INIT_AUTOMAKE([1.10]) -AC_CONFIG_MACRO_DIR([m4]) +AM_INIT_AUTOMAKE([-Wno-portability 1.11 no-dist-gzip dist-xz tar-ustar]) +dnl use pretty build output with automake >= 1.11 +m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])], + [AM_DEFAULT_VERBOSITY=1 + AC_SUBST(AM_DEFAULT_VERBOSITY)]) +AC_CONFIG_MACRO_DIR([build]) dnl enable mainainer mode by default AM_MAINTAINER_MODE([enable]) @@ -33,6 +38,7 @@ AC_CHECK_PROG(HAVE_PKGCONFIG, pkg-config, [ ], [ AC_MSG_ERROR([You need to have pkg-config installed!]) ]) +dnl === Glib + GIO ============================================================ PKG_CHECK_MODULES(GIO, [ gio-2.0 >= 2.25.0 ], [ @@ -49,55 +55,37 @@ PKG_CHECK_MODULES(GLIB, [ AC_MSG_ERROR([ You need to install or upgrade the glib development packages on your system. The minimum version required - is $GST_REQUIRED. + is $GLIB_REQUIRED. ]) ]) -dnl GTK -HAVE_GTK=NO -GTK2_REQ=2.14.0 -GTK3_REQ=2.91.3 -AC_MSG_CHECKING([which gtk+ version to compile examples against (optional)]) -AC_ARG_WITH([gtk], - AS_HELP_STRING([--with-gtk=3.0|2.0],[which gtk+ version to compile against (default: 3.0)]), - [case "$with_gtk" in - 2.0) GTK_REQ=$GTK2_REQ ;; - 3.0) GTK_REQ=$GTK3_REQ ;; - *) AC_MSG_ERROR([invalid gtk+ version specified]);; - esac], - [with_gtk=3.0 - GTK_REQ=$GTK3_REQ]) -AC_MSG_RESULT([$with_gtk (>= $GTK_REQ)]) -PKG_CHECK_MODULES(GTK, gtk+-x11-$with_gtk >= $GTK_REQ, HAVE_GTK=yes, HAVE_GTK=no) -AC_SUBST(GTK_LIBS) -AC_SUBST(GTK_CFLAGS) -AM_CONDITIONAL(HAVE_GTK, test "x$HAVE_GTK" = "xyes") - -CFLAGS="$GTK_CFLAGS" -CPPFLAGS="$GTK_CFLAGS" -AC_CHECK_HEADER([gdk/gdkx.h], [], - [AC_MSG_ERROR([GDK X11 header (gdk/gdkx.h) is required])]) +PKG_CHECK_MODULES(GTK, [ + gtk+-3.0 >= $GTK_REQUIRED +], [ + AC_SUBST(GTK_CFLAGS) + AC_SUBST(GTK_LIBS) +], [ + AC_MSG_ERROR([ + You need to install or upgrade the gtk development + packages on your system. The minimum version required + is $GTK_REQUIRED. + ]) +]) -AC_ARG_ENABLE(gcov, - AS_HELP_STRING([--enable-gcov],[enable gcov building]), - [ - case "${enableval}" in - yes) BUILD_GCOV=yes ;; - no) BUILD_GCOV=no ;; - esac - ], - [BUILD_GCOV=yes]) -AC_ARG_ENABLE(gprof, - AS_HELP_STRING([--enable-gprof],[enable gprof building]), - [ - case "${enableval}" in - yes) BUILD_GPROF=yes ;; - no) BUILD_GPROF=no ;; - esac - ], - [BUILD_GPROF=yes]) -AM_CONDITIONAL(GCOV_ENABLED, test "x$BUILD_GCOV" = "xyes") -AM_CONDITIONAL(GPROF_ENABLED, test "x$BUILD_GPROF" = "xyes") +PKG_CHECK_MODULES(GDK, [ + gdk-3.0 >= $GTK_REQUIRED +], [ + AC_SUBST(GDK_CFLAGS) + AC_SUBST(GDK_LIBS) +], [ + AC_MSG_ERROR([ + You need to install or upgrade the gdk development + packages on your system. The minimum version required + is $GTK_REQUIRED. + ]) +]) + +dnl === gstreamer related options ============================================= dnl Check for the required version of GStreamer core (and gst-plugins-base) dnl This will export GST_CFLAGS and GST_LIBS variables for use in Makefile.am @@ -141,6 +129,17 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ ], [ ])], [ AC_MSG_RESULT([no]) ]) +dnl check if compiler understands -Werror (if yes, add -Werror to GST_CFLAGS) +AC_MSG_CHECKING([to see if compiler understands -Werror]) +save_CFLAGS="$CFLAGS" +CFLAGS="$CFLAGS -Werror" +AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ ], [ ])], [ + GST_CFLAGS="$GST_CFLAGS -Werror" + AC_MSG_RESULT([yes]) +], [ + AC_MSG_RESULT([no]) +]) + dnl set the plugindir where plugins should be installed (for plugins/Makefile.am) if test "x${prefix}" = "x$HOME"; then plugindir="$HOME/.gstreamer-1.0/plugins" @@ -153,10 +152,68 @@ dnl set proper LDFLAGS for plugins GST_PLUGIN_LDFLAGS='-module -avoid-version -export-symbols-regex [_]*\(gst_\|Gst\|GST_\).*' AC_SUBST(GST_PLUGIN_LDFLAGS) +dnl === Testing =============================================================== + +GLIB_TESTS + +dnl FIXME: Add checking for the testing requirements here. + +dnl === Coverage and profiling options ======================================== +AC_ARG_ENABLE(gcov, + AS_HELP_STRING([--enable-gcov],[enable gcov building]), + [ + case "${enableval}" in + yes) BUILD_GCOV=yes ;; + no) BUILD_GCOV=no ;; + esac + ], + [BUILD_GCOV=yes]) +AM_CONDITIONAL(GCOV_ENABLED, test "x$BUILD_GCOV" = "xyes") + +AC_ARG_ENABLE(gprof, + AS_HELP_STRING([--enable-gprof],[enable gprof building]), + [ + case "${enableval}" in + yes) BUILD_GPROF=yes ;; + no) BUILD_GPROF=no ;; + esac + ], + [BUILD_GPROF=yes]) +AM_CONDITIONAL(GPROF_ENABLED, test "x$BUILD_GPROF" = "xyes") + +dnl === Local options ========================================================= +AC_ARG_ENABLE(speakertrack, + AS_HELP_STRING([--enable-speakertrack],[enable speakertrack]), + [ + case "${enableval}" in + yes) BUILD_SPEAKERTRACK=yes ;; + no) BUILD_SPEAKERTRACK=no ;; + esac + ], + [BUILD_SPEAKERTRACK=no]) + AM_CONDITIONAL(SPEAKERTRACK_ENABLED, test "x$BUILD_SPEAKERTRACK" = "xyes") + +dnl FIXME: Add the Python-API as an option here + +dnl === Output ================================================================ + +AC_ARG_ENABLE(debug, +AS_HELP_STRING([--enable-debug], + [enable debugging, default: no]), +[case "${enableval}" in + yes) debug=true ;; + no) debug=false ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-debug]) ;; +esac], +[debug=false]) + +AM_CONDITIONAL(DEBUG, test x"$debug" = x"true") + AC_CONFIG_FILES( Makefile plugins/Makefile tests/Makefile +tests/unit/Makefile tools/Makefile ) diff --git a/docs/FORKING.md b/docs/FORKING.md new file mode 100644 index 0000000..ba6ad36 --- /dev/null +++ b/docs/FORKING.md @@ -0,0 +1,19 @@ +##Forking Gst-Switch + +To fork this project, click the "Fork" button at [Gst-Switch Github Page](https://github.com/timvideos/gst-switch ""). +![forking](https://github-images.s3.amazonaws.com/help/Bootcamp-Fork.png "forking") + +Refer to the [Github Documentation](https://help.github.com/articles/fork-a-repo "") for further details. + +##Running Tests + +* Install gst-switch using instructions at [README.md](https://github.com/timvideos/gst-switch/blob/master/README.md ""). +* `cd python-api` +* make test +* For specific tests refer to [Python-API Readme](https://github.com/timvideos/gst-switch/blob/master/python-api/README.md ""). + +##Sending Pull Requests for Changes + +* Click the **Compare and Review** button +* Ensure that the Travis message shows **Success: The Travis Build Passed** +* Send the pull request diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..210cb4e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/gstswitch.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/gstswitch.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/gstswitch" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/gstswitch" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/TEST-RESULTS.md b/docs/TEST-RESULTS.md new file mode 100644 index 0000000..e247cd7 --- /dev/null +++ b/docs/TEST-RESULTS.md @@ -0,0 +1,81 @@ +##Coverage +The Coverage reports can be found at [coveralls.io](https://coveralls.io/r/hyades/gst-switch). The build is currently failing, therefore the coverage of the integration tests and the c code coverage cannot be found in the coveralls report. The coverage reports are generated for the inetgration test and C code. They are not reported to coveralls. +The coverage are present at the build page of travis. + +##Python Integration Tests Coverage +The current coverage reports are: +```bash +Name Stmts Miss Cover +------------------------------------------ +gstswitch/__init__ 0 0 100% +gstswitch/connection 183 67 63% +gstswitch/controller 163 41 75% +gstswitch/exception 15 0 100% +gstswitch/helpers 129 44 66% +gstswitch/server 143 36 75% +gstswitch/testsource 312 64 79% +------------------------------------------ +TOTAL 945 252 73% +``` + +##C Coverage +The C Coverage is: +```bash +File 'gstcase.c' +Lines executed:76.65% of 227 +Creating 'gstcase.c.gcov' + +File 'gstcomposite.c' +Lines executed:63.34% of 371 +Creating 'gstcomposite.c.gcov' + +File 'gstrecorder.c' +Lines executed:70.18% of 114 +Creating 'gstrecorder.c.gcov' + +File 'gstswitchcontroller.c' +Lines executed:70.12% of 338 +Creating 'gstswitchcontroller.c.gcov' + +File 'gstswitchserver.c' +Lines executed:69.93% of 765 +Creating 'gstswitchserver.c.gcov' + +File 'gstworker.c' +Lines executed:66.67% of 348 +Creating 'gstworker.c.gcov' +``` + +The files which are tested are: +* gstswitchserver.c +* gstrecorder.c +* gstcomposite.c +* gstswitchcontroller.c +* gstworker.c +* gstcase.c + +The files left untested are: +* gstvideodisp.c +* gstswitchclient.c +* gstswitchptz.c +* gstswitchcapture.c +* gstswitchui.c +* gstswitchclient.c +* gstaudiovisual.c + +##Untested +The gst-switch controller can have three roles: +* UI +* Capture +* None + +The Python-API communicates with the gst-switch controller over DBus. Hence the role that is selected is None. The methods which use the roles UI and Capture are not tested by the API. + +The file gstswitchui.c handles the gst-switch-ui. +The file gstswitchcapture.c handles the gst-switch-cap. +The file gstswitchptz.c handles the gst-switch-ptz. +Since the above executables are not executed by the API, they are left untested. + +The files gstvideodisp.c and gstaudiodisp.c handle the video/audio contain functions dealing with the gst-switch-ui. + +The gstswitchclient.c is also used by gst-switch-ui and hence remains untested. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..041783a --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +# +# gstswitch documentation build configuration file, created by +# sphinx-quickstart on Mon Sep 2 03:55:42 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os +sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('../python-api')) +sys.path.insert(0, os.path.abspath('../python-api/gstswitch')) +sys.path.insert(0, os.path.abspath('../python-api/tests')) +sys.path.insert(0, os.path.abspath('../python-api/tests/unittests')) +sys.path.insert(0, os.path.abspath('../python-api/tests/integrationtests')) +sys.path.insert(0, os.path.abspath('fake-lib')) +# print sys.path +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'gstswitch' +copyright = u'2013, hyades' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '' +# The full version, including alpha/beta/rc tags. +release = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'gstswitchdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'gstswitch.tex', u'gstswitch Documentation', + u'hyades', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'gstswitch', u'gstswitch Documentation', + [u'hyades'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'gstswitch', u'gstswitch Documentation', + u'hyades', 'gstswitch', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'gstswitch' +epub_author = u'hyades' +epub_publisher = u'hyades' +epub_copyright = u'2013, hyades' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# A sequence of (type, uri, title) tuples for the guide element of content.opf. +#epub_guide = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + +# Fix unsupported image types using the PIL. +#epub_fix_images = False + +# Scale large images. +#epub_max_image_width = 0 + +# If 'no', URL addresses will not be shown. +#epub_show_urls = 'inline' + +# If false, no index is generated. +#epub_use_index = True diff --git a/docs/fake-lib/README b/docs/fake-lib/README new file mode 100644 index 0000000..b939a74 --- /dev/null +++ b/docs/fake-lib/README @@ -0,0 +1,3 @@ +This directory exists so to build documentation without having things +like gobject-introspection library installed, since they're +difficult to get working in a virtualenv. diff --git a/docs/fake-lib/gi/__init__.py b/docs/fake-lib/gi/__init__.py new file mode 100644 index 0000000..779bda1 --- /dev/null +++ b/docs/fake-lib/gi/__init__.py @@ -0,0 +1,2 @@ +def require_version(namespace, version): + pass \ No newline at end of file diff --git a/docs/fake-lib/gi/repository/GLib.py b/docs/fake-lib/gi/repository/GLib.py new file mode 100644 index 0000000..3a15192 --- /dev/null +++ b/docs/fake-lib/gi/repository/GLib.py @@ -0,0 +1,9 @@ +threads_init = lambda *a, **k: None + +class Source(object): + pass + +class IOCondition: + IN = 1 + ERR = 8 + HUP = 16 diff --git a/docs/fake-lib/gi/repository/GObject.py b/docs/fake-lib/gi/repository/GObject.py new file mode 100644 index 0000000..3a15192 --- /dev/null +++ b/docs/fake-lib/gi/repository/GObject.py @@ -0,0 +1,9 @@ +threads_init = lambda *a, **k: None + +class Source(object): + pass + +class IOCondition: + IN = 1 + ERR = 8 + HUP = 16 diff --git a/docs/fake-lib/gi/repository/Gio.py b/docs/fake-lib/gi/repository/Gio.py new file mode 100644 index 0000000..96e7d9f --- /dev/null +++ b/docs/fake-lib/gi/repository/Gio.py @@ -0,0 +1,9 @@ +threads_init = lambda *a, **k: None + +class DBusConnection(object): + pass + +class DBusConnectionFlags(object): + AUTHENTICATION_CLIENT = 1 + pass + diff --git a/docs/fake-lib/gi/repository/Gst.py b/docs/fake-lib/gi/repository/Gst.py new file mode 100644 index 0000000..32aace4 --- /dev/null +++ b/docs/fake-lib/gi/repository/Gst.py @@ -0,0 +1,19 @@ +threads_init = lambda *a, **k: None + +def init(a): + pass + +class Source(object): + pass + +class Pipeline(object): + pass + +class ElementFactory(object): + def make(self, a, b): + pass + +class IOCondition: + IN = 1 + ERR = 8 + HUP = 16 diff --git a/docs/fake-lib/gi/repository/__init__.py b/docs/fake-lib/gi/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/fake-lib/mock.py b/docs/fake-lib/mock.py new file mode 100644 index 0000000..45bdaf0 --- /dev/null +++ b/docs/fake-lib/mock.py @@ -0,0 +1,2 @@ +class Mock: + pass \ No newline at end of file diff --git a/docs/fake-lib/pytest.py b/docs/fake-lib/pytest.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/fake-lib/scipy/misc/imread.py b/docs/fake-lib/scipy/misc/imread.py new file mode 100644 index 0000000..3c11ff3 --- /dev/null +++ b/docs/fake-lib/scipy/misc/imread.py @@ -0,0 +1,2 @@ +def imread(): + pass \ No newline at end of file diff --git a/docs/gstswitch.rst b/docs/gstswitch.rst new file mode 100644 index 0000000..5878adf --- /dev/null +++ b/docs/gstswitch.rst @@ -0,0 +1,59 @@ +gst-switch python API +===================== + +:mod:`gstswitch` Package +------------------------ + +.. automodule:: gstswitch.__init__ + :members: + :undoc-members: + :show-inheritance: + +:mod:`connection` Module +------------------------ + +.. automodule:: gstswitch.connection + :members: + :undoc-members: + :show-inheritance: + +:mod:`controller` Module +------------------------ + +.. automodule:: gstswitch.controller + :members: + :undoc-members: + :show-inheritance: + +:mod:`exception` Module +----------------------- + +.. automodule:: gstswitch.exception + :members: + :undoc-members: + :show-inheritance: + +:mod:`helpers` Module +--------------------- + +.. automodule:: gstswitch.helpers + :members: + :undoc-members: + :show-inheritance: + +:mod:`server` Module +-------------------- + +.. automodule:: gstswitch.server + :members: + :undoc-members: + :show-inheritance: + +:mod:`testsource` Module +------------------------ + +.. automodule:: gstswitch.testsource + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c038cd4 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,35 @@ +.. gstswitch documentation master file, created by + sphinx-quickstart on Sun Sep 8 17:54:44 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +gst-switch +========== + +A replacement of DVSwitch based on gstreamer +-------------------------------------------- + +Python - API: + +.. toctree:: + :maxdepth: 4 + + gstswitch + +Tests: + +.. toctree:: + :maxdepth: 4 + + unittests + + integrationtests + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/integrationtests.rst b/docs/integrationtests.rst new file mode 100644 index 0000000..7ebb60d --- /dev/null +++ b/docs/integrationtests.rst @@ -0,0 +1,51 @@ +Integration Tests for gstswitch and Python-API +============================================== + +:mod:`integrationtests` Package +------------------------------- + +.. automodule:: integrationtests.__init__ + :members: + :undoc-members: + :show-inheritance: + +:mod:`compare` Module +--------------------- + +.. automodule:: integrationtests.compare + :members: + :undoc-members: + :show-inheritance: + +:mod:`generate_reference_frames` Module +--------------------------------------- + +.. automodule:: integrationtests.generate_reference_frames + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_controller` Module +----------------------------- + +.. automodule:: integrationtests.test_controller + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_helpers` Module +-------------------------- + +.. automodule:: integrationtests.test_helpers + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_server` Module +------------------------- + +.. automodule:: integrationtests.test_server + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..94dbb24 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\gstswitch.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\gstswitch.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/unittests.rst b/docs/unittests.rst new file mode 100644 index 0000000..a854f66 --- /dev/null +++ b/docs/unittests.rst @@ -0,0 +1,51 @@ +Unittest for Python-API +======================= + +:mod:`unittests` Package +------------------------ + +.. automodule:: unittests.__init__ + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_connection_unit` Module +---------------------------------- + +.. automodule:: unittests.test_connection_unit + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_controller_unit` Module +---------------------------------- + +.. automodule:: unittests.test_controller_unit + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_helpers_unit` Module +------------------------------- + +.. automodule:: unittests.test_helpers_unit + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_server_unit` Module +------------------------------ + +.. automodule:: unittests.test_server_unit + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_testsource_unit` Module +---------------------------------- + +.. automodule:: unittests.test_testsource_unit + :members: + :undoc-members: + :show-inheritance: + diff --git a/plugins/gstassess.h b/plugins/gstassess.h index 94a8877..d3491f1 100644 --- a/plugins/gstassess.h +++ b/plugins/gstassess.h @@ -63,4 +63,4 @@ GType gst_assess_get_type (void); G_END_DECLS -#endif//__GST_ASSESS_H__ +#endif //__GST_ASSESS_H__ diff --git a/plugins/gstconvbin.h b/plugins/gstconvbin.h index da7fecb..aeb2524 100644 --- a/plugins/gstconvbin.h +++ b/plugins/gstconvbin.h @@ -66,4 +66,4 @@ GType gst_conv_bin_get_type (void); G_END_DECLS -#endif//__GST_CONV_BIN_H__ +#endif //__GST_CONV_BIN_H__ diff --git a/plugins/gstswitch.h b/plugins/gstswitch.h index 443aa0b..ba371b9 100644 --- a/plugins/gstswitch.h +++ b/plugins/gstswitch.h @@ -63,4 +63,4 @@ GType gst_switch_get_type (void); G_END_DECLS -#endif//__GST_SWITCH_H__ +#endif //__GST_SWITCH_H__ diff --git a/plugins/gsttcpmixsrc.c b/plugins/gsttcpmixsrc.c index 620755a..e8a9bd5 100644 --- a/plugins/gsttcpmixsrc.c +++ b/plugins/gsttcpmixsrc.c @@ -53,7 +53,7 @@ GST_DEBUG_CATEGORY_STATIC (tcpmixsrc_debug); #define TCP_BACKLOG 1 /* client connection queue */ #define TCP_HIGHEST_PORT 65535 #define TCP_DEFAULT_PORT 4953 -#define TCP_DEFAULT_HOST "localhost" +#define TCP_DEFAULT_HOST "0.0.0.0" #define TCP_DEFAULT_LISTEN_HOST NULL /* listen on all interfaces */ #define MAX_READ_SIZE 4 * 1024 diff --git a/plugins/gsttcpmixsrc.h b/plugins/gsttcpmixsrc.h index fe45e58..4df7ec8 100644 --- a/plugins/gsttcpmixsrc.h +++ b/plugins/gsttcpmixsrc.h @@ -82,4 +82,4 @@ GType gst_tcp_mix_src_get_type (void); G_END_DECLS -#endif /* __GST_TCP_MIX_SRC_H__ */ +#endif //__GST_TCP_MIX_SRC_H__ diff --git a/python-api/.pylintrc_gstswitch b/python-api/.pylintrc_gstswitch new file mode 100644 index 0000000..e70d8bb --- /dev/null +++ b/python-api/.pylintrc_gstswitch @@ -0,0 +1,248 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). +# * E0712(raising-non-exception) - pylint is unable to see glib.GError +# inherits from Exception. +disable=E0611,E0202,R0801,E1101,E1002,R0904,E0712,raising-non-exception + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__ + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=10 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=20 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/python-api/.pylintrc_integrationtests b/python-api/.pylintrc_integrationtests new file mode 100644 index 0000000..8cf9d92 --- /dev/null +++ b/python-api/.pylintrc_integrationtests @@ -0,0 +1,246 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). +disable=E0611,E0202,R0801,E1101,E1002,R0201,F0401,W0141,R0904,too-few-public-methods + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__ + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=10 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=20 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/python-api/.pylintrc_unittests b/python-api/.pylintrc_unittests new file mode 100644 index 0000000..a2c3f9f --- /dev/null +++ b/python-api/.pylintrc_unittests @@ -0,0 +1,248 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). +# * E0712(raising-non-exception) - pylint is unable to see glib.GError +# inherits from Exception. +disable=E0611,E0202,R0801,E1101,E1002,R0201,W0613,W0212,R0923,R0904,raising-non-exception + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__ + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=10 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=20 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/python-api/Makefile b/python-api/Makefile new file mode 100644 index 0000000..79337f8 --- /dev/null +++ b/python-api/Makefile @@ -0,0 +1,56 @@ +.PHONY: lint unittests pytest integration test clean all + +lint: export PYTHONWARNINGS := ignore +lint: export PYTHONPATH := ${PYTHONPATH}:${PWD} +lint: + @rm -rf tests/unittests/__pycache__ + @rm -rf tests/integrationtests/__pycache__ + @rm -rf reports + @echo PYTHONWARNINGS=$$PYTHONWARNINGS + @echo PYTHONPATH=$$PYTHONPATH + pylint --rcfile=.pylintrc_gstswitch gstswitch + pylint --rcfile=.pylintrc_unittests tests/unittests/* + pylint --rcfile=.pylintrc_integrationtests tests/integrationtests/*.py + @echo "Code style is okay!!" + +unittests: + @rm -rf reports + @mkdir -p reports/coverage/unittests + py.test --cov gstswitch tests/unittests/ --pep8 -v -s + +pep8: + pep8 gstswitch + @echo "gstswitch is pep8 clean!!" + pep8 tests/unittests + @echo "unittests are pep8 clean!!" + pep8 tests/integrationtests + @echo "integration tests are pep8 clean!!" + +integration: + @rm -f ../tools/*.c.gcov + @rm -f ../tools/*.gcda + -wget http://imgur.com/tools/imgurbash.sh -O imgurbash.sh + -chmod +x imgurbash.sh + @rm -rf reports + @mkdir -p reports/coverage/integration + py.test --cov gstswitch tests/integrationtests/ --pep8 -s -v + -@mv htmlcov/*.* reports/coverage/integration + -rm imgurbash.sh* + +performance: + py.test tests/performancetests/*.py -v -s + make clean + +clean: + rm -rf .cache + rm -rf reports + rm -f *.data* + rm -f tests/integrationtests/*.data + rm -f *.log + rm -f *.html + rm -f *.sh.* + rm -f *.sh + rm -f .coverage + + +test: lint unittests integration clean diff --git a/python-api/README.md b/python-api/README.md new file mode 100644 index 0000000..43a6ec5 --- /dev/null +++ b/python-api/README.md @@ -0,0 +1,29 @@ +##Running Tests +```bash +cd python-api +``` + +####Run Unittests +```bash +make unittests +``` + +####Run Integration Tests +```bash +make integration +``` + +####Lint and pep8 Tests +```bash +make lint +make pep8 +``` +####Running everything together +```bash +make test +``` + + +##Python API Sphinx Docs +Sphinx documentation can be found at [http://gst-switch.readthedocs.org/en/latest/](http://gst-switch.readthedocs.org/en/latest/). + diff --git a/python-api/WRITING_TESTS.md b/python-api/WRITING_TESTS.md new file mode 100644 index 0000000..be83752 --- /dev/null +++ b/python-api/WRITING_TESTS.md @@ -0,0 +1,69 @@ +##Writing Tests Using Python-API +###Import the Modules +Ensure that gst-switch/python-api/gstswitch is in PYTHONPATH: +```python +import sys +sys.path.insert(0, install_dir + 'gst-switch/python-api/gstswitch') +# install_dir is the path where the installation started +``` +###Start the gst-switch Server +```python +from gstswitch.server import Server + +PATH = '/usr/bin' +# The default location is '/usr/bin'. Change to wherever the gst-switch executables are located +serv = Server(path=PATH, video_port=3000, audio_port=4000) +serv.run() +``` + +###Add Some Sources +```python +from gstswitch.helpers import TestSources + +sources = TestSources(video_port=video_port, audio_port=audio_port) +sources.new_test_video() +sources.new_test_audio() +``` + +###Add a Preview to See the Output!! +```python +from gstswitch.helpers import PreviewSinks + +preview = PreviewSinks(video_port=3000, audio_port=4000) +preview.run +``` + +###Remote Method Call Over DBus + +####Initialize the Controller +```python +from gstswitch.controller import Controller + +controller = Controller() +controller.establish_connection() +``` +####Calling Remote Methods +* Get Compose Port: `port = controller.get_compose_port()` +* Get Encode Port: `port = controller.get_encode_port()` +* Get Audio Port: `port = controller.get_audio_port()` +* Get All Preview Ports: `ports = controller.get_preview_ports()` +* Change the PIP Mode: `result = controller.set_composite_mode(mode=1)` +* Move or Adjust the PIP: `result = controller.adjust_pip(xpos=50, ypos=50, 0, 0)` +* Switch the Channel. Channel is specified as `ord('A')`, `ord('a')` or `ord('b')`. `result = controller.switch(channel=ord('A'), port=3004)` +* Start a New Record: `result = controller.new_record()` + +###Terminating the gst-switch Server +The `server.run()` should always be kept in a try/finally block, so that if any Exception is caught, all processes terminate safely. +i.e. +```python +server = Server(path=PATH, video_port=3000, audio_port=4000) +try: + server.run() + # rest of the code comes here + # ...... + server.terminate() +finally: + if server.proc: + server.terminate() +``` +`server.terminate()` can be replaced by `server.kill()`. In the latter SIGKILL will be sent to the process. diff --git a/python-api/gstswitch/__init__.py b/python-api/gstswitch/__init__.py new file mode 100644 index 0000000..9e7ab89 --- /dev/null +++ b/python-api/gstswitch/__init__.py @@ -0,0 +1,3 @@ +""" +Python API for gst-switch +""" diff --git a/python-api/gstswitch/connection.py b/python-api/gstswitch/connection.py new file mode 100644 index 0000000..b5d80ab --- /dev/null +++ b/python-api/gstswitch/connection.py @@ -0,0 +1,485 @@ +""" +connection deals with all low level method calls over dbus +The Connection object is capable of invoking remote methods over dbus +""" + +from gi.repository import Gio, GLib +from .exception import ConnectionError +import sys + +__all__ = ["Connection", ] + + +class Connection(object): + + """Class which makes all remote object class. + Deals with lower level connection and remote method invoking + + :default bus-address: unix:abstract=gstswitch + + :param: None + """ + CONNECTION_FLAGS = Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT + + def __init__( + self, + address="unix:abstract=gstswitch", + bus_name='info.duzy.gst.switch.SwitchController', + object_path="/info/duzy/gst/switch/SwitchController", + default_interface=("info.duzy.gst.switch." + "SwitchControllerInterface")): + + super(Connection, self).__init__() + self.connection = None + self._address = None + self._bus_name = None + self._object_path = None + self._default_interface = None + + self.address = address + self.bus_name = bus_name + self.object_path = object_path + self.default_interface = default_interface + + @property + def address(self): + """Get the address + """ + return self._address + + @address.setter + def address(self, address): + """Set the Address + http://dbus.freedesktop.org/doc/dbus-specification.html#addresses + """ + if not address: + raise ValueError("Address '{0}' cannot be blank") + else: + adr = str(address) + if adr.find(':') > 0: + self._address = adr + else: + raise ValueError("Address must follow specifications mentioned" + " at http://dbus.freedesktop.org/doc/" + "dbus-specification.html#addresses") + + @property + def bus_name(self): + """Get the bus name""" + if self._bus_name is None: + return None + return self._bus_name + + @bus_name.setter + def bus_name(self, bus_name): + """Set the Bus Name + http://dbus.freedesktop.org/doc/dbus-specification.html\ + #message-protocol-names-bus + """ + if bus_name is None: + self._bus_name = None + return + bus = str(bus_name) + self._bus_name = bus + + @property + def object_path(self): + """Get the object path""" + return self._object_path + + @object_path.setter + def object_path(self, object_path): + """Set the object_path + http://dbus.freedesktop.org/doc/dbus-specification.html\ + #message-protocol-marshaling-object-path + """ + if not object_path: + raise ValueError("object_path '{0} cannot be blank'") + else: + obj = str(object_path) + if obj[0] == '/': + self._object_path = obj + else: + raise ValueError("object_path must follow specifications" + " mentioned at " + "http://dbus.freedesktop.org/doc/" + "dbus-specification.html" + "#message-protocol-marshaling-object-path""") + + @property + def default_interface(self): + """Get the default interface""" + return self._default_interface + + @default_interface.setter + def default_interface(self, default_interface): + """Set the default_interface + http://dbus.freedesktop.org/doc/dbus-specification.html\ + #message-protocol-names-interface + """ + if not default_interface: + raise ValueError("default_interface '{0} cannot be blank'") + else: + intr = str(default_interface) + if intr.count('.') > 1: + self._default_interface = intr + else: + raise ValueError("default_interface must follow " + "specifications mentioned at " + "http://dbus.freedesktop.org/" + "doc/dbus-specification.html" + "#message-protocol-names-interface") + + def connect_dbus(self): + """Make a new connection using the parameters belonging to the class + to the gst-switch-srv over dbus. + Sets the self.connection + + :params: None + :returns: Nothing + :raises ConnectionError: GError occurs while making a connection + """ + try: + connection = Gio.DBusConnection.new_for_address_sync( + self.address, + self.CONNECTION_FLAGS, + None, + None) + self.connection = connection + except GLib.GError as error: + message = error.message + new_message = "{1} ({0})".format(message, self.address) + raise ConnectionError(new_message) + + def get_compose_port(self): + """get_compose_port(out i port); + Calls get_compose_port remotely + + :param: None + :returns: tuple with first element compose port number + """ + try: + args = None + connection = self.connection + port = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'get_compose_port', + args, GLib.VariantType.new("(i)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return port + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "get_compose_port") + raise ConnectionError(new_message) + + def get_encode_port(self): + """get_encode_port(out i port); + Calls get_encode_port remotely + + :param: None + :returns: tuple with first element encode port number + """ + try: + args = None + connection = self.connection + port = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'get_encode_port', + args, + GLib.VariantType.new("(i)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return port + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "get_encode_port") + raise ConnectionError(new_message) + + def get_audio_port(self): + """get_audio_port(out i port); + Calls get_audio_port remotely + + :param: None + :returns: tuple wit first element audio port number + """ + try: + args = None + connection = self.connection + port = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'get_audio_port', + args, + GLib.VariantType.new("(i)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return port + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "get_audio_port") + raise ConnectionError(new_message) + + def get_preview_ports(self): + """get_preview_ports(out s ports); + Calls get_preview_ports remotely + + :param: None + :returns: tuple with first element a string in the form of + '[(3002, 1, 7), (3003, 1, 8)]' + """ + try: + args = None + connection = self.connection + ports = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'get_preview_ports', + args, + GLib.VariantType.new("(s)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return ports + except GLib.GError as error: + error = sys.exc_info()[1] + message = error.message + new_message = "{0}: {1}".format(message, "get_preview_ports") + raise ConnectionError(new_message) + + def set_composite_mode(self, mode): + """set_composite_mode(in i channel, + out b result); + Calls set_composite_mode remotely + + :param mode: new composite mode + :returns: tuple with first element True if requested + """ + try: + args = GLib.Variant('(i)', (mode,)) + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'set_composite_mode', + args, + GLib.VariantType.new("(b)"), + Gio.DBusCallFlags.NONE, -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "set_composite_mode") + raise ConnectionError(new_message) + + def set_encode_mode(self, channel): + """set_encode_mode(in i channel, + out b result); + Calls set_encode_mode remotely + **Does not do anything** + + :param: channel + :returns: tuple with first element True if requested + """ + try: + args = GLib.Variant('(i)', (channel,)) + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'set_encode_mode', + args, + GLib.VariantType.new("(b)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "set_encode_mode") + raise ConnectionError(new_message) + + def new_record(self): + """new_record(out b result); + Calls new_record remotely + + :param: None: + returns: tuple with first element True if requested + """ + try: + args = None + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'new_record', + args, + GLib.VariantType.new("(b)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "new_record") + raise ConnectionError(new_message) + + def adjust_pip(self, xpos, ypos, width, height): + """adjust_pip(in i dx, + in i dy, + in i dw, + in i dh, + out u result); + Calls adjust_pip remotely + + :param xpos: the X position of the PIP + :param ypos: the Y position of the PIP + :param width: the width of the PIP + :param height: the height of the PIP + :returns: tuple with first element as result - + PIP has been changed succefully + """ + try: + args = GLib.Variant('(iiii)', (xpos, ypos, width, height,)) + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'adjust_pip', + args, + GLib.VariantType.new("(u)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "adjust_pip") + raise ConnectionError(new_message) + + def switch(self, channel, port): + """switch(in i channel, + in i port, + out b result); + Calls switch remotely + + :param channel: The channel to be switched, 'A', 'B', 'a' + :param port: The target port number + :returns: tuple with first element True if requested + """ + try: + args = GLib.Variant('(ii)', (channel, port,)) + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'switch', + args, + GLib.VariantType.new("(b)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "switch") + raise ConnectionError(new_message) + + def click_video(self, xpos, ypos, width, height): + """click_video(in i x, + in i y, + in i fw, + in i fh, + out b result); + Calls click_video remotely + + :param xpos: + :param ypos: + :param width: + :param height: + :returns: tuple with first element True if requested + """ + try: + args = GLib.Variant('(iiii)', (xpos, ypos, width, height,)) + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'click_video', + args, + GLib.VariantType.new("(b)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "click_video") + raise ConnectionError(new_message) + + def mark_face(self, faces): + """mark_face(in a(iiii) faces); + Calls mark_face remotely + + :param faces: tuple having four elements + :returns: tuple with first element True if requested + """ + try: + args = GLib.Variant('a(iiii)', faces) + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'mark_face', + args, + GLib.VariantType.new("(b)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "mark_face") + raise ConnectionError(new_message) + + def mark_tracking(self, faces): + """mark_tracking(in a(iiii) faces); + Calls mark_tracking remotely + + :param faces: tuple having four elements + :returns: tuple with first element True if requested + """ + try: + args = GLib.Variant('a(iiii)', faces) + connection = self.connection + result = connection.call_sync( + self.bus_name, + self.object_path, + self.default_interface, + 'mark_tracking', + args, + GLib.VariantType.new("(b)"), + Gio.DBusCallFlags.NONE, + -1, + None) + return result + except GLib.GError as error: + message = error.message + new_message = "{0}: {1}".format(message, "mark_tracking") + raise ConnectionError(new_message) diff --git a/python-api/gstswitch/controller.py b/python-api/gstswitch/controller.py new file mode 100644 index 0000000..8e2a6d2 --- /dev/null +++ b/python-api/gstswitch/controller.py @@ -0,0 +1,368 @@ +""" +The controller is the interface for all remote method calls over dbus. +The Controller class creates the controller, +which can be used to invoke the remote methods. +""" + +import ast +from .connection import Connection +from .exception import ConnectionReturnError + +__all__ = ["Controller", ] + + +class Controller(object): + + """A Class to control all interactions with the gst-switch-srv over dbus. + Provides the interface for higher level interactions + + :param: None + """ + + def __init__( + self, + address="unix:abstract=gstswitch", + bus_name='info.duzy.gst.switch.SwitchController', + object_path="/info/duzy/gst/switch/SwitchController", + default_interface="info.duzy.gst.switch.SwitchControllerInterface" + ): + + super(Controller, self).__init__() + + self._address = None + self._bus_name = None + self._object_path = None + self._default_interface = None + self.connection = None + + self.address = address + self.bus_name = bus_name + self.object_path = object_path + self.default_interface = default_interface + + @property + def address(self): + """ + Get the address + """ + return self._address + + @address.setter + def address(self, address): + """Set the Address + http://dbus.freedesktop.org/doc/dbus-specification.html#addresses + """ + if not address: + raise ValueError("Address '{0}' cannot be blank") + else: + adr = str(address) + if adr.find(':') > 0: + self._address = adr + else: + raise ValueError("Address must follow specifications mentioned" + " at http://dbus.freedesktop.org/doc/" + "dbus-specification.html#addresses") + + @property + def bus_name(self): + """Get the bus name + """ + if self._bus_name is None: + return None + return self._bus_name + + @bus_name.setter + def bus_name(self, bus_name): + """Set the Bus Name + http://dbus.freedesktop.org/doc/dbus-specification.html\ + #message-protocol-names-bus + """ + if bus_name is None: + self._bus_name = None + return + bus = str(bus_name) + self._bus_name = bus + + @property + def object_path(self): + """Get the object path + """ + return self._object_path + + @object_path.setter + def object_path(self, object_path): + """Set the object_path + http://dbus.freedesktop.org/doc/dbus-specification.html\ + #message-protocol-marshaling-object-path + """ + if not object_path: + raise ValueError("object_path '{0} cannot be blank'") + else: + obj = str(object_path) + if obj[0] == '/': + self._object_path = obj + else: + raise ValueError("object_path must follow specifications" + " mentioned at " + "http://dbus.freedesktop.org/doc/" + "dbus-specification.html" + "#message-protocol-marshaling-object-path""") + + @property + def default_interface(self): + """Get the default interface + """ + return self._default_interface + + @default_interface.setter + def default_interface(self, default_interface): + """Set the default_interface + http://dbus.freedesktop.org/doc/dbus-specification.html\ + #message-protocol-names-interface + """ + if not default_interface: + raise ValueError("default_interface '{0} cannot be blank'") + else: + intr = str(default_interface) + if intr.count('.') > 1: + self._default_interface = intr + else: + raise ValueError("default_interface must follow " + "specifications mentioned at " + "http://dbus.freedesktop.org/" + "doc/dbus-specification.html" + "#message-protocol-names-interface") + + def establish_connection(self): + """Establishes a fresh connection to the dbus + Connection stored as self.connection + + :param: None + :returns: None + """ + self.connection = Connection( + address=self.address, + bus_name=self.bus_name, + object_path=self.object_path, + default_interface=self.default_interface) + + self.connection.connect_dbus() + + def get_compose_port(self): + """Get the compose port number + + :param: None + :returns: compose port number + """ + conn = self.connection.get_compose_port() + try: + compose_port = conn.unpack()[0] + return compose_port + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values.' + 'Should return a GVariant tuple') + + def get_encode_port(self): + """Get the encode port number + + :param: None + :returns: encode port number + """ + conn = self.connection.get_encode_port() + try: + encode_port = conn.unpack()[0] + return encode_port + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values.' + ' Should return a GVariant tuple') + + def get_audio_port(self): + """Get the audio port number + + :param: None + :returns: audio port number + """ + conn = self.connection.get_audio_port() + try: + audio_port = conn.unpack()[0] + return audio_port + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values. ' + 'Should return a GVariant tuple') + + def get_preview_ports(self): + """Get all the preview ports + + :param: None + :returns: list of all preview ports + """ + conn = self.connection.get_preview_ports() + try: + res = conn.unpack()[0] + preview_ports = self.parse_preview_ports(res) + return preview_ports + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values. ' + 'Should return a GVariant tuple') + + def set_composite_mode(self, mode): + """Set the current composite mode. Modes between 0 and 3 are allowed. + + :param mode: new composite mode + :returns: True when requested + """ + self.establish_connection() + # only modes from 0 to 3 are supported + if mode >= 0 and mode <= 3: + try: + conn = self.connection.set_composite_mode(mode) + print conn + res = conn.unpack()[0] + if res is True: + print "Set composite mode to %s" % (str(mode)) + except AttributeError: + raise ConnectionReturnError('Connection returned invalid ' + 'values. Should return a ' + 'GVariant tuple') + else: + pass + # raise some Exception + return res + + def set_encode_mode(self, channel): + """Set the encode mode + WARNING: THIS DOES NOT WORK. + + :param: channel + :returns: True when requested + """ + self.establish_connection() + try: + conn = self.connection.set_encode_mode(channel) + res = conn.unpack()[0] + if res is True: + print "Set encode mode to %s" % (str(channel)) + else: + pass + # raise some exception + return res + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values. ' + 'Should return a GVariant tuple') + + def new_record(self): + """Start a new recording + + :param: None + """ + self.establish_connection() + try: + conn = self.connection.new_record() + res = conn.unpack()[0] + if res is True: + # logging + print "New record" + else: + pass + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values. ' + 'Should return a GVariant tuple') + return res + + def adjust_pip(self, xpos, ypos, width, height): + """Change the PIP position and size + + :param xpos: the x position of the PIP + :param ypos: the y position of the PIP + :param width: the width of the PIP + :param height: the height of the PIP + :returns: result - PIP has been changed succefully + """ + self.establish_connection() + try: + conn = self.connection.adjust_pip(xpos, ypos, width, height) + res = conn.unpack()[0] + print "adjust pip xpos:%s ypos:%s w:%s h:%s" % ( + str(xpos), str(ypos), str(width), str(height)) + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values. ' + 'Should return a GVariant tuple') + # to-do - parse + return res + + def switch(self, channel, port): + """Switch the channel to the target port + + :param channel: The channel to be switched, 'A', 'B', 'a' + :param port: The target port number + :returns: True when requested + """ + self.establish_connection() + try: + conn = self.connection.switch(channel, port) + res = conn.unpack()[0] + if res is True: + print "Switch channel:%s port:%s" % (str(channel), str(port)) + else: + pass + return res + except AttributeError: + raise ConnectionReturnError('Connection returned invalid values. ' + 'Should return a GVariant tuple') + + def click_video(self, xpos, ypos, width, height): + """User click on the video + + :param xpos: + :param ypos: + :param width: + :param height: + :returns: True when requested + """ + self.establish_connection() + try: + conn = self.connection.click_video(xpos, ypos, width, height) + res = conn.unpack()[0] + if res is True: + print "Click video: xpos:%s ypos:%s width:%s height:%s" % ( + str(xpos), str(ypos), str(width), str(height)) + else: + pass + except: + raise ConnectionReturnError('Connection returned invalid values. ' + 'Should return a GVariant tuple') + return res + + def mark_face(self, faces): + """Mark faces + + :param faces: tuple having four elements + :returns: True when requested + """ + # faces is list of a tuple of four elements + self.establish_connection() + self.connection.mark_face(faces) + + def mark_tracking(self, faces): + """Mark tracking + + :param faces: tuple having four elements + :returns: True when requested + """ + self.establish_connection() + self.connection.mark_tracking(faces) + + @classmethod + def parse_preview_ports(cls, res): + """Parses the preview_ports string""" + # res = '[(a, b, c), (a, b, c)*]' + try: + liststr = ast.literal_eval(res) + except (ValueError, SyntaxError): + raise ConnectionReturnError(("Connection returned " + "invalid values:{0}") + .format(res)) + preview_ports = [] + for tupl in liststr: + preview_ports.append(int(tupl[0])) + return preview_ports diff --git a/python-api/gstswitch/exception.py b/python-api/gstswitch/exception.py new file mode 100644 index 0000000..6189f38 --- /dev/null +++ b/python-api/gstswitch/exception.py @@ -0,0 +1,50 @@ +""" +All custom exceptions come here +""" + +__all__ = [ + 'BaseError', 'PathError', 'ServerProcessError', 'ConnectionError', + 'ConnectionReturnError', 'RangeError', 'InvalidIndexError', +] + + +class BaseError(Exception): + + """docstring for BaseError""" + pass + + +class PathError(BaseError): + + """docstring for PathError""" + pass + + +class ServerProcessError(BaseError): + + """docstring for ServerProcessError""" + pass + + +class ConnectionError(BaseError): + + """docstring for ConnectionError""" + pass + + +class ConnectionReturnError(BaseError): + + """docstring for ConnectionReturnError""" + pass + + +class RangeError(BaseError): + + """docstring for RangeError""" + pass + + +class InvalidIndexError(BaseError): + + """docstring for InvalidIndexError""" + pass diff --git a/python-api/gstswitch/helpers.py b/python-api/gstswitch/helpers.py new file mode 100644 index 0000000..09ce231 --- /dev/null +++ b/python-api/gstswitch/helpers.py @@ -0,0 +1,282 @@ +""" +Has helper classes which create test video and audio sources. +It is also possible to create a preview out source showing the +compose port output. +""" + +from gstswitch import testsource +from .exception import RangeError, InvalidIndexError + + +__all__ = ["TestSources", "PreviewSinks"] + + +class TestSources(object): + + """A Controller of test sources feeding into the + gst-switch-srv + :param width: The width of the output video + :param height: The height of the output video + :param pattern: The videotestsrc pattern of the output video + :param timeoverlay: True to enable a running time over video + :param clockoverlay: True to enable current clock time over video + """ + + def __init__(self, video_port=None, audio_port=None): + super(TestSources, self).__init__() + self._running_tests_video = [] + self._running_tests_audio = [] + self._audio_port = None + self._video_port = None + + if video_port: + self.video_port = video_port + if audio_port: + self.audio_port = audio_port + + @property + def video_port(self): + """Get the video port""" + return self._video_port + + @video_port.setter + def video_port(self, video_port): + """Set the video port + :raises RangeError: Video Port must be range 1 to 65535 + :raises TypeError: should be a integer or should be able to + covert to an integer + """ + try: + i = int(video_port) + if i < 1 or i > 65535: + raise RangeError('video_port must be in range 1 to 65535') + else: + self._video_port = video_port + except TypeError: + raise TypeError("video_port must be a string or number," + "not, '{0}'".format(type(video_port))) + except ValueError: + raise TypeError("Port must be a valid number") + + @property + def audio_port(self): + """Get the audio port""" + return self._audio_port + + @audio_port.setter + def audio_port(self, audio_port): + """Set the audio port + :raises RangeError: audio port must be range 1 to 65535 + :raises TypeError: should be a integer or should be able to + covert to an integer + """ + try: + i = int(audio_port) + if i < 1 or i > 65535: + raise RangeError('audio port must be in range 1 to 65535') + else: + self._audio_port = audio_port + except TypeError: + raise TypeError("audio port must be a string or number," + "not, '{0}'".format(type(audio_port))) + except ValueError: + raise TypeError("Port must be a valid number") + + @property + def running_tests_video(self): + """Get the currently running test video list""" + return self._running_tests_video + + @running_tests_video.setter + def running_tests_video(self, tests): + """Set the running test video list + """ + self._running_tests_video = tests + + @property + def running_tests_audio(self): + """Get the currently running test audio list""" + return self._running_tests_audio + + @running_tests_audio.setter + def running_tests_audio(self, tests): + """Set the currently running test audio list""" + self._running_tests_audio = tests + + def new_test_video(self, + width=300, + height=200, + pattern=None, + timeoverlay=False, + clockoverlay=False): + """Start a new test video + :param port: The port of where the TCP stream will be sent + Should be same as video port of gst-switch-src + :param width: The width of the output video + :param height: The height of the output video + :param pattern: The videotestsrc pattern of the output video + :param timeoverlay: True to enable a running time over video + :param clockoverlay: True to enable current clock time over video + """ + testsrc = testsource.VideoSrc( + self.video_port, + width, + height, + pattern, + timeoverlay, + clockoverlay) + testsrc.run() + self._running_tests_video.append(testsrc) + + def get_test_video(self): + """Returns a list of processes acting as video test sources running + :returns: A list containing all video test sources running + """ + i = 0 + for test in self._running_tests_video: + print i, "pattern:", test.pattern + i += 1 + return self._running_tests_video + + def terminate_index_video(self, index): + """Terminate video test source specified by index + :param index: The index of the video source to terminate + Use get_test_video for finding the index + """ + try: + testsrc = self._running_tests_video[int(index)] + except IndexError: + raise InvalidIndexError( + ("No video source with index:{0}," + "use get_test_video() to determine index") + .format(index)) + except ValueError: + raise InvalidIndexError("Index should be a valid integer") + except TypeError: + raise InvalidIndexError("Index should be a valid integer") + + print 'End source with pattern %s' % (str(testsrc.pattern)) + testsrc.end() + self._running_tests_video.remove(self._running_tests_video[index]) + + def terminate_video(self): + """Terminate all test video sources + """ + print 'TESTS:', self._running_tests_video + for _ in range(len(self._running_tests_video)): + self.terminate_index_video(0) + + def new_test_audio(self, freq=110, wave=None): + """Start a new test audio + :param port: The port of where the TCP stream will be sent + Should be same as audio port of gst-switch-src + :param width: The width of the output audio + :param height: The height of the output audio + :param pattern: The audiotestsrc pattern of the output audio + :param timeoverlay: True to enable a running time over audio + :param clockoverlay: True to enable current clock time over audio + """ + testsrc = testsource.AudioSrc( + self.audio_port, + freq, + wave) + testsrc.run() + # print testsrc + self._running_tests_audio.append(testsrc) + # print self._running_tests_audio + + def get_test_audio(self): + """Returns a list of processes acting as audio test sources running + :returns: A list containing all audio test sources running + """ + i = 0 + for test in self._running_tests_audio: + print i, "wave:", test.wave + i += 1 + return self._running_tests_audio + + def terminate_index_audio(self, index): + """Terminate audio test source specified by index + :param index: The index of the audio source to terminate + Use get_test_audio for finding the index + """ + try: + testsrc = self._running_tests_audio[int(index)] + except IndexError: + raise InvalidIndexError( + ("No audio source with index:{0}," + "use get_test_audio() to determine index") + .format(index)) + except ValueError: + raise InvalidIndexError("Index should be a valid integer") + except TypeError: + raise InvalidIndexError("Index should be a valid integer") + + print 'End source with wave %s' % (str(testsrc.wave)) + testsrc.end() + self._running_tests_audio.remove(self._running_tests_audio[index]) + + def terminate_audio(self): + """Terminate all test audio sources + """ + print 'TESTS:', self._running_tests_audio + for _ in range(len(self._running_tests_audio)): + self.terminate_index_audio(0) + + +class PreviewSinks(object): + + """A Controller for preview sinks to preview ports of gst-switch-srv + :param preview_port: The preview port to get the preview + """ + + def __init__(self, preview_port=3001): + super(PreviewSinks, self).__init__() + self._preview_port = None + self.preview = None + + self.preview_port = preview_port + + @property + def preview_port(self): + """Get the preview port""" + return self._preview_port + + @preview_port.setter + def preview_port(self, preview_port): + """Set the preview port + :raises RangeError: preview port must be in range 1 to 65535 + :raises TypeError: preview port must be a integer or convertable to + an integer + """ + if not preview_port: + raise ValueError("Preview_port: '{0}' cannot be blank" + .format(preview_port)) + else: + try: + i = int(preview_port) + if i < 1 or i > 65535: + raise RangeError( + 'Preview port must be in range 1 to 65535') + else: + self._preview_port = preview_port + except TypeError: + raise TypeError("Preview_port must be a string or number," + "not, '{0}'".format(type(preview_port))) + except ValueError: + raise TypeError("Port must be a valid number") + + def run(self): + """Run the Preview Sink""" + self.preview = testsource.Preview(self.preview_port) + self.preview.run() + print 'start preview' + + def terminate(self): + """End/Terminate the Preview Sink""" + try: + self.preview.end() + self.preview = None + print 'end preview' + except AttributeError: + raise AttributeError("No preview Sink to terminate") diff --git a/python-api/gstswitch/server.py b/python-api/gstswitch/server.py new file mode 100644 index 0000000..31f0deb --- /dev/null +++ b/python-api/gstswitch/server.py @@ -0,0 +1,337 @@ +""" +The server deals with all operations controlling gst-switch-srv +These include all OS related tasks +""" + +import os +import signal +import subprocess +from distutils import spawn + +from errno import ENOENT +from .exception import PathError, ServerProcessError +from time import sleep + + +__all__ = ["Server", ] + + +TOOLS_DIR = '/'.join(os.getcwd().split('/')[:-1]) + '/tools/' + + +class Server(object): + + """Control all server related operations + + :param path: Path where the executable gst-switch-srv + is located. Provide the full path. + By default looks in the current $PATH. + :param video_port: The video port number - default = 3000 + :param audio_port: The audio port number - default = 4000 + :param control_port: The control port number - default = 5000 + :param record_file: The record file format + :returns: nothing + """ + SLEEP_TIME = 0.5 + + def __init__( + self, + path=None, + video_port=3000, + audio_port=4000, + control_port=5000, + record_file=False): + + super(Server, self).__init__() + + self._path = None + self._video_port = None + self._audio_port = None + self._control_port = None + self._record_file = None + self.gst_option_string = '' + + self.path = path + self.video_port = video_port + self.audio_port = audio_port + self.control_port = control_port + self.record_file = record_file + + self.proc = None + self.pid = -1 + + @property + def path(self): + """Get the path""" + return self._path + + @path.setter + def path(self, path): + """Set path + :raises ValueError: Path cannot be left blank + """ + self._path = path + + @property + def video_port(self): + """Get the video port""" + return self._video_port + + @video_port.setter + def video_port(self, video_port): + """Set Video Port + :raises ValueError: Video Port cannot be left blank + :raises ValueError: Video Port must be in range 1 to 65535 + :raises TypeError: Video Port must be a string or a number + """ + if not video_port: + raise ValueError("Video Port '{0}' cannot be blank" + .format(video_port)) + else: + try: + i = int(video_port) + if i < 1 or i > 65535: + raise ValueError('Video Port must be in range 1 to 65535') + else: + self._video_port = video_port + except TypeError: + raise TypeError("Video Port must be a string or a number, " + "not '{0}'".format(type(video_port))) + + @property + def audio_port(self): + """Get the audio port""" + return self._audio_port + + @audio_port.setter + def audio_port(self, audio_port): + """Set Audio Port + :raises ValueError: Audio Port cannot be left blank + :raises ValueError: Audio Port must be in range 1 to 65535 + :raises TypeError: Audio Port must be a string or a number + """ + if not audio_port: + raise ValueError("Audio Port '{0}' cannot be blank" + .format(audio_port)) + else: + try: + i = int(audio_port) + if i < 1 or i > 65535: + raise ValueError('Audio Port must be in range 1 to 65535') + else: + self._audio_port = audio_port + except TypeError: + raise TypeError("Audio Port must be a string or a number," + " not '{0}'".format(type(audio_port))) + + @property + def control_port(self): + """Get the control port""" + return self._control_port + + @control_port.setter + def control_port(self, control_port): + """Set Control Port + :raises ValueError: Control Port cannot be left blank + :raises ValueError: Control Port must be in range 1 to 65535 + :raises TypeError: Control Port must be a string or a number + """ + if not control_port: + raise ValueError("Control Port '{0}' cannot be blank" + .format(control_port)) + else: + try: + i = int(control_port) + if i < 1 or i > 65535: + raise ValueError( + 'Control Port must be in range 1 to 65535') + else: + self._control_port = control_port + except TypeError: + raise TypeError("Control Port must be a string or a number," + " not '{0}'".format(type(control_port))) + + @property + def record_file(self): + """Get the record file""" + return self._record_file + + @record_file.setter + def record_file(self, record_file): + """Set Record File + :raises ValueError: Non-string file format + :raises ValueError: Record File cannot have forward slashes + """ + if record_file is False: + self._record_file = False + elif record_file is True: + self._record_file = True + else: + if not record_file: + raise ValueError("Record File: '{0}' " + "Non-string file format".format(record_file)) + else: + rec = str(record_file) + if rec.find('/') < 0: + self._record_file = rec + else: + raise ValueError("Record File: '{0}' " + "cannot have forward slashes".format(rec)) + + def run(self, gst_option=''): + """Launch the server process + + :param: None + :gst-option: Any gstreamer option. + Refer to http://www.linuxmanpages.com/man1/gst-launch-0.8.1.php#lbAF. + Multiple can be added separated by spaces + :returns: nothing + :raises IOError: Fail to open /dev/null (os.devnull) + :raises PathError: Unable to find gst-switch-srv at path specified + :raises ServerProcessError: Running gst-switch-srv + gives a OS based error. + """ + self.gst_option_string = gst_option + print "Starting server" + self.proc = self._run_process() + if self.proc: + self.pid = self.proc.pid + sleep(self.SLEEP_TIME) + + def _run_process(self): + """Non-public method: Runs the gst-switch-srv process + """ + cmd = [''] + if not self.path: + srv_location = spawn.find_executable('gst-switch-srv') + if srv_location: + cmd[0] = srv_location + else: + raise PathError("Cannot find gst-switch-srv in $PATH.\ + Please specify the path.") + else: + cmd[0] += os.path.join(self.path, 'gst-switch-srv') + if self.gst_option_string: + cmd += [self.gst_option_string] + cmd.append("--video-input-port={0}".format(self.video_port)) + cmd.append("--audio-input-port={0}".format(self.audio_port)) + cmd.append("--control-port={0}".format(self.control_port)) + if self.record_file is False: + pass + elif self.record_file is True: + cmd.append("-r") + else: + if self.record_file is not False: + cmd.append("--record={0}".format(self.record_file)) + + proc = self._start_process(cmd) + return proc + + def _start_process(self, cmd): + """Non-public method: Start a process + + :param cmd: The command which needs to be executed + :returns: process created + """ + print 'Creating process %s' % (cmd) + try: + with open('server.log', 'w') as tempf: + process = subprocess.Popen( + cmd, + stdout=tempf, + stderr=tempf, + bufsize=-1, + shell=False) + print cmd + return process + except OSError as error: + if error.errno == ENOENT: + raise PathError("Cannot find gst-switch-srv at path:" + " '{0}'".format(self.path)) + else: + raise ServerProcessError("Internal error " + "while launching process") + + @classmethod + def make_coverage(cls): + """Generate coverage + Calls 'make coverage' to generate coverage in .gcov files + """ + cmd = 'make -C {0} coverage'.format(TOOLS_DIR) + print TOOLS_DIR + with open(os.devnull, 'w'): + proc = subprocess.Popen( + cmd.split(), + bufsize=-1, + shell=False) + out, _ = proc.communicate() + print out + + def terminate(self, cov=False): + """Terminate the server. + self.proc is made None on success + + :param: None + :returns: True when success + :raises ServerProcessError: Process does not exist + :raises ServerProcessError: Cannot terminate process. Try killing it + """ + print 'Killing server' + proc = self.proc + if proc is None: + raise ServerProcessError('Server Process does not exist') + else: + try: + if cov: + self.gcov_flush() + self.make_coverage() + proc.terminate() + print 'Server Killed' + self.proc = None + return True + except OSError: + raise ServerProcessError("Cannot terminate server process. " + "Try killing it") + + def kill(self, cov=False): + """Kill the server process by sending signal.SIGKILL + self.proc is made None on success + + :param: None + :returns: True when success + :raises ServerProcessError: Process does not exist + :raises ServerProcessError: Cannot kill process + """ + if self.proc is None: + raise ServerProcessError('Server Process does not exist') + else: + try: + if cov: + self.gcov_flush() + self.make_coverage() + os.kill(self.pid, signal.SIGKILL) + self.proc = None + return True + except OSError: + raise ServerProcessError('Cannot kill process') + + def gcov_flush(self): + """Generate gcov coverage by sending the signal SIGUSR1 + The generated gcda files are dumped in tools directory. + Does not kill the process + + :param: None + :returns: True when success + :raises ServerProcessError: If Server is not running + :raises ServerProcessError: Unable to send signal + """ + + if self.proc is None: + raise ServerProcessError('Server process does not exist') + else: + try: + print "GCOV FLUSH" + os.kill(self.pid, signal.SIGUSR1) + return True + except OSError: + raise ServerProcessError('Unable to send signal') diff --git a/python-api/gstswitch/testsource.py b/python-api/gstswitch/testsource.py new file mode 100644 index 0000000..5f88d93 --- /dev/null +++ b/python-api/gstswitch/testsource.py @@ -0,0 +1,671 @@ +""" +The testsource contains all gstreamer pipelines +It provides the abse for all the other gstreamer +components are build upon. +""" + +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +GObject.threads_init() +Gst.init(None) + +from .exception import RangeError +import random + +# from pipeline import * +# IMPORTS + +__all__ = ["Preview", "VideoSrc", "AudioSrc"] + + +class BasePipeline(Gst.Pipeline): + + """A Basic pipeline + """ + + def __init__(self): + Gst.Pipeline.__init__(self) + self._playing = False + + def play(self): + """Set the pipeline as playing""" + self._playing = True + self.set_state(Gst.State.PLAYING) + + def pause(self): + """Pause the pipeline""" + self._playing = False + self.set_state(Gst.State.PAUSED) + + def disable(self): + """Disable the pipeline""" + self._playing = False + self.set_state(Gst.State.NULL) + + @classmethod + def make(cls, elem, description=''): + """Make a new gstreamer pipeline element + :param elem: The name of the element to be made + :param description: Description of the element to be made + returns: The element which is made + """ + element = Gst.ElementFactory.make(elem, description) + return element + + +class VideoPipeline(BasePipeline): + + """A Video Pipeline which can be used by a Video Test Source + :param port: The port of where the TCP stream will be sent + Should be same as video port of gst-switch-src + :param width: The width of the output video + :param height: The height of the output video + :param pattern: The videotestsrc pattern of the output video + :param timeoverlay: True to enable a running time over video + :param clockoverlay: True to enable current clock time over video + """ + + def __init__( + self, + port, + host='localhost', + width=300, + height=200, + pattern=None, + timeoverlay=False, + clockoverlay=False): + super(VideoPipeline, self).__init__() + + self.host = host + + src = self.make_videotestsrc(pattern) + self.add(src) + vfilter = self.make_capsfilter(width, height) + self.add(vfilter) + src.link(vfilter) + gdppay = self.make_gdppay() + self.add(gdppay) + if timeoverlay: + _timeoverlay = self.make_timeoverlay() + if clockoverlay: + _clockoverlay = self.make_clockoverlay() + if timeoverlay and clockoverlay: + self.add(_timeoverlay) + self.add(_clockoverlay) + vfilter.link(_timeoverlay) + _timeoverlay.link(_clockoverlay) + _clockoverlay.link(gdppay) + if timeoverlay: + self.add(_timeoverlay) + vfilter.link(_timeoverlay) + _timeoverlay.link(gdppay) + if clockoverlay: + self.add(_clockoverlay) + vfilter.link(_clockoverlay) + _clockoverlay.link(gdppay) + else: + vfilter.link(gdppay) + + sink = self.make_tcpclientsink(port) + self.add(sink) + gdppay.link(sink) + + def make_videotestsrc(self, pattern): + """Return a videotestsrc element + :param pattern: The video test source pattern (1-20) + :return: A video test source element + """ + element = self.make('videotestsrc', 'src') + element.set_property('pattern', int(pattern)) + return element + + def make_capsfilter(self, width, height): + """Return a caps filter + :param width: The width of the caps + :param height: The height of the caps + :returns: A caps filter element + """ + element = self.make("capsfilter", "vfilter") + width = str(width) + height = str(height) + capsstring = "video/x-raw, format=(string)I420, width={0},\ + height={1}".format(width, height) + print capsstring + caps = Gst.Caps.from_string(capsstring) + element.set_property('caps', caps) + return element + + def make_gdppay(self): + """Return a gdppay element + :returns: A gdppay element + """ + element = self.make('gdppay', 'gdppay') + return element + + def make_timeoverlay(self): + """Return a time overlay element (Verdana bold 50) + :returns: a time overlay element + """ + element = self.make('timeoverlay', 'timeoverlay') + element.set_property('font-desc', "Verdana bold 50") + return element + + def make_clockoverlay(self): + """Return a clock overlay element (Verdana bold 50) + :returns: a clock overlay element + """ + element = self.make('clockoverlay', 'clockoverlay') + element.set_property('font-desc', "Verdana bold 50") + return element + + def make_tcpclientsink(self, port): + """Return a TCP client sink element + :port: Port to sink + :returns: A TCP client sink element + """ + element = self.make('tcpclientsink', 'tcpclientsink') + element.set_property('host', self.host) + element.set_property('port', int(port)) + return element + + +class AudioPipeline(BasePipeline): + + """docstring for AudioPipeline""" + + def __init__( + self, + port, + host='localhost', + freq=110, + wave=None): + super(AudioPipeline, self).__init__() + + self.host = host + + src = self.make_audiotestsrc(freq, wave) + self.add(src) + gdppay = self.make_gdppay() + self.add(gdppay) + src.link(gdppay) + sink = self.make_tcpclientsink(port) + self.add(sink) + gdppay.link(sink) + + def make_audiotestsrc(self, freq, wave=None): + """Return a Audio Source Element + :freq: The Frequency + :wave: The wave pattern (valid between 1 and 12) + """ + element = self.make('audiotestsrc', 'src') + element.set_property('freq', int(freq)) + element.set_property('wave', int(wave)) + return element + + def make_gdppay(self): + """Return a gdppay element + :returns: A gdppay element + """ + element = self.make('gdppay', 'gdppay') + return element + + def make_tcpclientsink(self, port): + """Return a TCP client sink element + :port: Port to sink + :returns: A TCP client sink element + """ + element = self.make('tcpclientsink', 'tcpclientsink') + element.set_property('host', self.host) + element.set_property('port', int(port)) + return element + + +class PreviewPipeline(BasePipeline): + + """Pipeline for usage by a Preview + :param port: The preview port + """ + + def __init__(self, port): + super(PreviewPipeline, self).__init__() + self.host = 'localhost' + self.preview_port = port + src = self.make_tcpclientsrc() + self.add(src) + + gdpdepay = self.make_gdpdepay() + self.add(gdpdepay) + src.link(gdpdepay) + + conv1 = self.make_videoconvert("conv1") + self.add(conv1) + gdpdepay.link(conv1) + + cairo = self.make_cairooverlay() + self.add(cairo) + conv1.link(cairo) + + conv2 = self.make_videoconvert("conv2") + self.add(conv2) + cairo.link(conv2) + + sink = self.make_xvimagesink() + self.add(sink) + conv2.link(sink) + + def make_tcpclientsrc(self): + """Return a TCP Client Source element + :param port: The port of the server + :returns: A TCP Client Source element + """ + element = self.make('tcpclientsrc', 'tcpclientsrc') + element.set_property('host', self.host) + element.set_property('port', self.preview_port) + return element + + def make_gdpdepay(self): + """Return a gdpdepay element + :returns: A gdpdepay element + """ + element = self.make('gdpdepay', 'gdpdepay') + return element + + def make_videoconvert(self, desc): + """Return a videoconvert element + :returns: A videoconvert element + """ + element = self.make('videoconvert', desc) + return element + + def make_cairooverlay(self): + """Return a cairooverlay element + :returns: A cairooverlay element + """ + element = self.make('cairooverlay', 'cairooverlay') + return element + + def make_xvimagesink(self): + """Return a xvimagesink element + :returns: A xvimagesink element + """ + element = self.make('xvimagesink', 'xvimagesink') + element.set_property('sync', 'false') + return element + + +class VideoSrc(object): + + """A Test Video Source + :param width: The width of the output video + :param height: The height of the output video + :param pattern: The videotestsrc pattern of the output video + None for random + :param timeoverlay: True to enable a running time over video + :param clockoverlay: True to enable current clock time over video + """ + HOST = 'localhost' + + def __init__( + self, + port, + width=300, + height=200, + pattern=None, + timeoverlay=False, + clockoverlay=False): + super(VideoSrc, self).__init__() + self._port = None + self._width = None + self._height = None + self._pattern = None + self._timeoverlay = None + self._clockoverlay = None + + self.port = port + self.width = width + self.height = height + self.pattern = self.generate_pattern(pattern) + self.timeoverlay = timeoverlay + self.clockoverlay = clockoverlay + self.pipeline = VideoPipeline( + self.port, + self.HOST, + self.width, + self.height, + self.pattern, + self.timeoverlay, + self.clockoverlay) + + @property + def port(self): + """Get the Video Port""" + return self._port + + @port.setter + def port(self, port): + """Set the video Port + :raises RangeError: Port not in range 1 to 65535 + :raises TypeError: Port cannot be converted to integer + :raises ValueError: Port cannot be left blank + """ + if not port: + raise ValueError("Port: '{0}' cannot be blank" + .format(port)) + else: + try: + i = int(port) + if i < 1 or i > 65535: + raise RangeError('Port must be in range 1 to 65535') + else: + self._port = port + except TypeError: + raise TypeError("Port must be a string or number," + "not, '{0}'".format(type(port))) + except ValueError: + raise TypeError("Port must be a valid number") + + @property + def width(self): + """Get the width""" + return self._width + + @width.setter + def width(self, width): + """Set the Width + raises ValueError: Width must be a positive value + raises ValueError: Width must not be blank + raises TypeError: Width must be convertable into a float + """ + if not width: + raise ValueError("Width: '{0}' cannot be blank" + .format(width)) + try: + i = float(width) + if i <= 0: + raise ValueError("Width: '{0}' must be a valid positive value") + else: + self._width = width + except TypeError: + raise TypeError("Width must be a valid value not '{0}'" + .format(type(width))) + + @property + def height(self): + """Get the height""" + return self._height + + @height.setter + def height(self, height): + """Set the height + raises ValueError: Height must be a positive value + raises ValueError: Height must not be blank + raises TypeError: Height must be convertable into a float + """ + if not height: + raise ValueError("Height: '{0}' cannot be blank" + .format(height)) + try: + i = float(height) + if i <= 0: + raise ValueError( + "Height: '{0}' must be a valid positive value") + else: + self._height = height + except TypeError: + raise TypeError("Height must be a valid value not '{0}'" + .format(type(height))) + + @property + def pattern(self): + """Get the Pattern""" + return self._pattern + + @pattern.setter + def pattern(self, pattern): + """Set the Pattern + :raises RangeError: Pattern must be in range 0 to 19 + :raises TypeError: Pattern must be convertable to an integer + """ + try: + i = int(float(pattern)) + if i < 0 or i > 19: + raise RangeError('Pattern must be in range 0 and 19') + else: + self._pattern = pattern + except ValueError: + raise TypeError("Pattern must be a valid number") + + @property + def timeoverlay(self): + """Get the timeoverlay""" + return self._timeoverlay + + @timeoverlay.setter + def timeoverlay(self, timeoverlay): + """Set the timeoverlay + :raises ValueError: Timeoverlay must be True or False + """ + timeover = str(timeoverlay) + if timeover == 'True' or timeover == 'False': + self._timeoverlay = timeoverlay + else: + raise ValueError("Timeoverlay: '{0}' must be True of False" + .format(timeoverlay)) + + @property + def clockoverlay(self): + """Get the Clockoverlay""" + return self._clockoverlay + + @clockoverlay.setter + def clockoverlay(self, clockoverlay): + """Set the Clockoverlay + :raises ValueError: Clockoverlay must be True or False + """ + clockover = str(clockoverlay) + if clockover == 'True' or clockover == 'False': + self._clockoverlay = clockoverlay + else: + raise ValueError("Clockoverlay: '{0}' must be True of False" + .format(clockoverlay)) + + def run(self): + """Run the pipeline""" + self.pipeline.play() + + def pause(self): + """End the pipeline""" + self.pipeline.pause() + + def end(self): + """End/disable the pipeline""" + self.pipeline.disable() + + @classmethod + def generate_pattern(cls, pattern): + """Generate a random pattern if not specified + """ + if pattern is None: + pattern = random.randint(0, 19) + pattern = str(pattern) + return pattern + + +class AudioSrc(object): + + """docstring for AudioSrc""" + + HOST = 'localhost' + + def __init__( + self, + port, + freq=110, + wave=None): + super(AudioSrc, self).__init__() + self._port = None + self._freq = None + self._wave = None + + self.port = port + self.freq = freq + self.wave = self.generate_wave(wave) + self.pipeline = AudioPipeline( + self.port, + self.HOST, + self.freq, + self.wave) + + @property + def port(self): + """Get the Audio Port""" + return self._port + + @port.setter + def port(self, port): + """Set the Audio Port + :raises RangeError: Port not in range 1 to 65535 + :raises TypeError: Port cannot be converted to integer + :raises ValueError: Port cannot be left blank + """ + if not port: + raise ValueError("Port: '{0}' cannot be blank" + .format(port)) + else: + try: + i = int(port) + if i < 1 or i > 65535: + raise RangeError('Port must be in range 1 to 65535') + else: + self._port = port + except TypeError: + raise TypeError("Port must be a string or number," + "not, '{0}'".format(type(port))) + except ValueError: + raise TypeError("Port must be a valid number") + + @property + def freq(self): + """Get the Frequency""" + return self._freq + + @freq.setter + def freq(self, freq): + """Set the Frequency + raises ValueError: Width must be a positive value + raises ValueError: Width must not be blank + raises TypeError: Width must be convertable into a float + """ + if not freq: + raise ValueError("Frequency: '{0}' cannot be blank" + .format(freq)) + else: + try: + i = int(freq) + if i < 1: + raise RangeError("Frequency must be a positive value") + else: + self._freq = freq + except TypeError: + raise TypeError("Frequency must be a string or number," + "not, '{0}'".format(type(freq))) + except ValueError: + raise TypeError("Frequency must be a valid number") + + @property + def wave(self): + """Get the wave number""" + return self._wave + + @wave.setter + def wave(self, wave): + """Set the Wave number + :raises RangeError: Wave must be in range 0 to 12 + :raises TypeError: Wave must be convertable to integer + """ + try: + i = int(float(wave)) + if i < 0 or i > 12: + raise RangeError('Wave must be in range 0 and 12') + else: + self._wave = wave + except ValueError: + raise TypeError("Wave must be a valid number") + + def run(self): + """Run the pipeline""" + self.pipeline.play() + + def pause(self): + """End the pipeline""" + self.pipeline.pause() + + def end(self): + """End/disable the pipeline""" + self.pipeline.disable() + + @classmethod + def generate_wave(cls, wave): + """Generate a random wave if not specified + """ + if wave is None: + wave = random.randint(0, 12) + wave = str(wave) + return wave + + +class Preview(object): + + """A Preview Element + :param port: The preview port + """ + + def __init__(self, port): + super(Preview, self).__init__() + self._preview_port = None + + self.preview_port = port + self.pipeline = PreviewPipeline(self.preview_port) + + @property + def preview_port(self): + """Get the Preview Port""" + return self._preview_port + + @preview_port.setter + def preview_port(self, preview_port): + """Set the Preview Port + :raises RangeError: Port not in range 1 to 65535 + :raises TypeError: Port cannot be converted to integer + :raises ValueError: Port cannot be left blank + """ + if not preview_port: + raise ValueError("preview_port: '{0}' cannot be blank" + .format(preview_port)) + else: + try: + i = int(preview_port) + if i < 1 or i > 65535: + raise RangeError( + 'preview_port must be in range 1 to 65535') + else: + self._preview_port = preview_port + except TypeError: + raise TypeError("preview_port must be a string or number," + "not, '{0}'".format(type(preview_port))) + except ValueError: + raise TypeError("Port must be a valid number") + + def run(self): + """Run the pipeline""" + self.pipeline.play() + + def pause(self): + """Pause the pipeline""" + self.pipeline.pause() + + def end(self): + """End/disable the pipeline""" + self.pipeline.disable() diff --git a/python-api/scripts/__init__.py b/python-api/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-api/scripts/dbusConnect.py b/python-api/scripts/dbusConnect.py new file mode 100755 index 0000000..2684828 --- /dev/null +++ b/python-api/scripts/dbusConnect.py @@ -0,0 +1,19 @@ + #!usr/bin/env python +import dbus +import dbus.mainloop.glib +import os +import time +import gobject + +def main(): + loop = gobject.MainLoop() + bus = dbus.SessionBus() + obj = bus.get_object("info.duzy.gst_switch.SwitchControllerInterface","/info/duzy/gst_switch/SwitchController") + print obj.Introspect() + loop.run() + +if __name__=="__main__": + os.environ["DBUS_SESSION_BUS_ADDRESS"] = "unix:abstract=gstswitch" + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + main() + \ No newline at end of file diff --git a/python-api/scripts/dbusConnect.sh b/python-api/scripts/dbusConnect.sh new file mode 100755 index 0000000..539d6bd --- /dev/null +++ b/python-api/scripts/dbusConnect.sh @@ -0,0 +1,10 @@ +dbus-send \ + --address="unix:abstract=gstswitch" \ + --print-reply=literal \ + --dest="info.duzy.gst_switch.SwitchClientInterface" \ + /info/duzy/gst_switch/SwitchController \ + info.duzy.gst_switch.SwitchControllerInterface.get_compose_port + +gdbus introspect --address unix:abstract=gstswitch \ + --dest info.duzy.gst.switch.SwitchUIInterface \ + --object-path /info/duzy/gst/switch/SwitchUI diff --git a/python-api/scripts/dbusConnect2.py b/python-api/scripts/dbusConnect2.py new file mode 100755 index 0000000..b23ba29 --- /dev/null +++ b/python-api/scripts/dbusConnect2.py @@ -0,0 +1,32 @@ +from gi.repository import Gio, GLib +from time import sleep + +address = "unix:abstract=gstswitch" +name = None +object_path = "/info/duzy/gst/switch/SwitchController" + + +# a = connection.call_sync( +# name, object_path, 'org.freedesktop.DBus.Introspectable', 'Introspect', +# None, GLib.VariantType.new("(s)"), Gio.DBusCallFlags.NONE, -1, +# None) +#mode = 1 +#print type(mode), mode +mode=0 +while mode<=3: + print "mode:", mode + connection = Gio.DBusConnection.new_for_address_sync( + address, + Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT, + None, None) + args = GLib.Variant('(i)',(mode,)) + print "args:", args + b = connection.call_sync( + name, object_path, 'info.duzy.gst.switch.SwitchControllerInterface', 'set_composite_mode', + args, GLib.VariantType.new("(b)"), Gio.DBusCallFlags.NONE, -1, + None) + print b.unpack()[0],type(b.unpack()[0]) + mode += 1 + if mode==4: + mode=0 + sleep(0.5) \ No newline at end of file diff --git a/python-api/scripts/dbusConnection3.py b/python-api/scripts/dbusConnection3.py new file mode 100644 index 0000000..ba08601 --- /dev/null +++ b/python-api/scripts/dbusConnection3.py @@ -0,0 +1,33 @@ +from gi.repository import Gio, GLib +from time import sleep + +address = "unix:abstract=gstswitch" +name = None +object_path = "/info/duzy/gst/switch/SwitchController" + + +# a = connection.call_sync( +# name, object_path, 'org.freedesktop.DBus.Introspectable', 'Introspect', +# None, GLib.VariantType.new("(s)"), Gio.DBusCallFlags.NONE, -1, +# None) +#mode = 1 +#print type(mode), mode +# mode=0 +# while mode<=3: + # print "mode:", mode +connection = Gio.DBusConnection.new_for_address_sync( + address, + Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT, + None, None) +# args = GLib.Variant('(i)',(mode,)) +# print "args:", args +args = None +b = connection.call_sync( + name, object_path, 'info.duzy.gst.switch.SwitchControllerInterface', 'get_preview_ports', + args, GLib.VariantType.new("(s)"), Gio.DBusCallFlags.NONE, -1, + None) +print b, b.unpack()[0],type(b.unpack()[0]) +# mode += 1 +# if mode==4: + # mode=0 +sleep(0.5) \ No newline at end of file diff --git a/python-api/scripts/switch_infinite_test.py b/python-api/scripts/switch_infinite_test.py new file mode 100755 index 0000000..830f979 --- /dev/null +++ b/python-api/scripts/switch_infinite_test.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../.."))) +# should be in PYTHONPATH +from gstswitch.server import Server +from gstswitch.testsource import BasePipeline +from gstswitch.helpers import * +from gstswitch.controller import Controller +import gi +import subprocess +import time +import psutil +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst +GObject.threads_init() +Gst.init(None) + + + + + +PATH = '/usr/bin/' +INTERVAL = 1.0 + + + + +s = Server(path=PATH) +v_port = s.video_port +a_port = s.audio_port +try: + s.run() + + cmd_vid1 = PATH + cmd_vid2 = PATH + cmd_vid1 += """gst-launch-1.0 videotestsrc pattern=1 ! \ + video/x-raw, width=300, height=200 !\ + gdppay ! + tcpclientsink port={0} + """.format(v_port) + print cmd_vid1 + + cmd_vid2 += """gst-launch-1.0 videotestsrc ! \ + video/x-raw, width=300, height=200 !\ + gdppay ! + tcpclientsink port={0} + """.format(v_port) + print cmd_vid2 + + vid1 = subprocess.Popen( + cmd_vid1.split(), + bufsize=-1, + shell=False) + vid2 = subprocess.Popen( + cmd_vid2.split(), + bufsize=-1, + shell=False) + time.sleep(1) + sources = TestSources(audio_port=a_port) + sources.new_test_audio() + sources.new_test_audio() + + controller = Controller() + controller.establish_connection() + + cmd = PATH + cmd += """gst-switch-ui""" + cmd = " ".join(cmd.split()) + with open('ui.log', 'w') as tempf: + ui_process = subprocess.Popen( + cmd.split(), + stdout=tempf, + stderr=tempf, + bufsize=-1, + shell=False) + + params = [(65,3004), (65, 3003)] + i = 0 + p = psutil.Process(s.pid) + cpu_usage_file = open("cpu.log", "w") + while 1: + # print i + cpu = p.get_cpu_percent(interval=INTERVAL) + mem = p.get_memory_percent() + print cpu + print mem + cpu_usage_file.write(str(cpu) + '\n' + str(mem) + '\n') + controller.switch(params[i][0], params[i][1]) + # time.sleep(1) + i+=1 + i%=2 + + #end all + sources.terminate_audio() + vid1.terminate() + vid2.terminate() + s.terminate() +finally: + if s.proc: + s.kill() + diff --git a/python-api/scripts/test.py b/python-api/scripts/test.py new file mode 100755 index 0000000..311bfd6 --- /dev/null +++ b/python-api/scripts/test.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +# WARNING: +# THIS FILE IS ONLY FOR MY PERSONAL TESTING PURPOSES. +# IT IS NOT MADE TO AND SHOULD NOT BE TESTING THE ENTIRE API + +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +GObject.threads_init() +Gst.init(None) +from gstswitch.server import Server +from gstswitch.helpers import * +from gstswitch.controller import Controller +import time + +import gi +gi.require_version('Gst', '1.0') + +# all executables (gst-launch-1.0, gst-switch-srv, gst-switch-ui, gst-switch-cap) at this path +path = '/usr/bin/' +# path = '/usr/bin/' +s = Server(path) +try: + s.run() # launches the server default parameters + video_port = s.video_port + audio_port = s.audio_port + # connects a gstreamer module to view the output of the gst-switch-srv + output = PreviewSinks() + output.run() + # adding two test video sources + sources = TestSources(video_port=video_port, audio_port=audio_port) + + sources.new_test_audio() + sources.new_test_audio() + + sources.new_test_video() + sources.new_test_video() + sources.new_test_video(timeoverlay=True) + + controller = Controller() + controller.establish_connection() + res = [] + tests_get = [controller.get_compose_port, + controller.get_encode_port, + controller.get_audio_port, + controller.get_preview_ports, + ] + + test_set_composite_mode = controller.set_composite_mode + + # testing random 10 modes + for x in tests_get: + print x.__name__, x() + time.sleep(1) + modes = [0, 3, 2, 1, 0, 1, 2, 3, 0] + for mode in modes: + print 'composite mode=', mode + test_set_composite_mode(mode) + time.sleep(0.6) + output.terminate() + sources.terminate_video() + sources.terminate_audio() + s.terminate() + +finally: + if s.proc: + s.kill() diff --git a/python-api/tests/integrationtests/__init__.py b/python-api/tests/integrationtests/__init__.py new file mode 100644 index 0000000..3f89cd7 --- /dev/null +++ b/python-api/tests/integrationtests/__init__.py @@ -0,0 +1,3 @@ +""" +Integration tests for gst-switch and Python API +""" diff --git a/python-api/tests/integrationtests/compare.py b/python-api/tests/integrationtests/compare.py new file mode 100644 index 0000000..48d80d4 --- /dev/null +++ b/python-api/tests/integrationtests/compare.py @@ -0,0 +1,139 @@ +""" +Comparison between two videos +Generates reference frames and compares test frames with them +""" + +import os + +from scipy.misc import imread +from scipy.linalg import norm + +import tempfile + +import subprocess + +__all__ = [ + 'GenerateReferenceFrames', + 'CompareVideo', +] + + +CONV = None +if os.system('which ffmpeg > /dev/null') == 0: + CONV = 'ffmpeg' +elif os.system('which avconv > /dev/null') == 0: + CONV = 'avconv' +else: + raise SystemError("Need ffmpeg or avcon tools.") + + +class BaseCompareVideo(object): + + """Base class containing image operations""" + + TESTS = { + 'composite_mode_0': 0, + 'composite_mode_1': 1, + 'composite_mode_2': 2, + 'composite_mode_3': 3, + 'adjust_pip_4': 4 + } + REF_FRAME_DIR = os.getcwd() + '/tests/integrationtests/reference_frames' + + def __init__(self): + pass + + def comp_image(self, image1, image2): + """Compare two images""" + file1, file2 = (image1, image2) + # read images as 2D arrays (convert to grayscale for simplicity) + img1 = imread(file1).astype(float) + img2 = imread(file2).astype(float) + # compare + n_0 = self.compare_images(img1, img2) + return n_0 * 1.0 / img1.size + # print "Manhattan norm:", n_m, "/ per pixel:", n_m/img1.size + # print "Zero norm:", n_0, "/ per pixel:", n_0*1.0/img1.size + + def normalize(self, arr): + """Normalize an image""" + rng = arr.max() - arr.min() + 1e-10 + amin = arr.min() + return (arr - amin) * 255 / rng + + def compare_images(self, img1, img2): + """Calculate the zero norm between two images""" + # normalize to compensate for exposure difference, + # this may be unnecessary + # consider disabling it + # img1 = self.normalize(img1) + # img2 = self.normalize(img2) + # calculate the difference and its norms + diff = img1 - img2 # elementwise for scipy arrays + # m_norm = sum(abs(diff)) # Manhattan norm + z_norm = norm(diff.ravel(), 0) # Zero norm + return z_norm + + def generate_frames(self, directory=REF_FRAME_DIR): + """Generate reference frames in directory""" + if not os.path.exists(directory): + os.mkdir(directory) + + cmd1 = CONV + " -i {0} -ss 00:00:02.000 -f image2 -vframes 1 \ + {1}/out{2}_1.png".format(self.video, directory, self.TESTS[self.test]) + cmd2 = CONV + " -i {0} -ss 00:00:05.000 -f image2 -vframes 1 \ + {1}/out{2}_2.png".format(self.video, directory, self.TESTS[self.test]) + # print cmd + proc = subprocess.Popen( + cmd1.split(), + bufsize=-1, + shell=False) + proc.wait() + + proc = subprocess.Popen( + cmd2.split(), + bufsize=-1, + shell=False) + proc.wait() + + +class GenerateReferenceFrames(BaseCompareVideo): + + """Generate the original frames for storage in reference_frames/ + """ + + def __init__(self, test, video): + super(GenerateReferenceFrames, self).__init__() + self.test = test + self.video = video + + +class CompareVideo(BaseCompareVideo): + + """Compare a video with pre-stored frames + :parameter test: The test which is conducted. + self.TESTS stores these tests and maps them with frames + :parameter video: The video which has to be compared + """ + + def __init__(self, test, video): + super(CompareVideo, self).__init__() + self.test_frame_dir = tempfile.mkdtemp() + self.test = test + self.video = video + print self.test_frame_dir + + def compare(self): + """Compare videos""" + img1 = '/out{0}_1.png'.format(self.TESTS[self.test]) + img2 = '/out{0}_2.png'.format(self.TESTS[self.test]) + + self.generate_frames(self.test_frame_dir) + + res1 = self.comp_image( + self.REF_FRAME_DIR + img1, + self.test_frame_dir + img1) + res2 = self.comp_image( + self.REF_FRAME_DIR + img2, + self.test_frame_dir + img2) + return (res1, res2) diff --git a/python-api/tests/integrationtests/generate_reference_frames.py b/python-api/tests/integrationtests/generate_reference_frames.py new file mode 100644 index 0000000..a89fa82 --- /dev/null +++ b/python-api/tests/integrationtests/generate_reference_frames.py @@ -0,0 +1,50 @@ +""" +Used for generating reference frames +Usually should be run just once +""" + +from integrationtests.compare import GenerateReferenceFrames +from integrationtests.test_controller import TestSetCompositeMode +from integrationtests.test_controller import TestAdjustPIP + + +def set_composite_mode_ref_frames(mode): + """Set the composite mode for indexing the frames""" + test = 'composite_mode_{0}'.format(mode) + video = 'output-{0}.data'.format(mode) + gen = GenerateReferenceFrames(test, video) + gen.generate_frames() + + +def adjust_pip_ref_frames(index): + """Set the index of frames""" + test = 'adjust_pip_{0}'.format(index) + video = 'output-{0}.data'.format(index) + gen = GenerateReferenceFrames(test, video) + gen.generate_frames() + + +def main(): + """Generate reference frames""" + test1 = TestSetCompositeMode() + for i in range(4): + test1.set_composite_mode(i, True) + set_composite_mode_ref_frames(i) + + test2 = TestAdjustPIP() + dic = [ + [50, 75, 0, 0], + ] + for i in range(4, 5): + test2.adjust_pip( + dic[i - 4][0], + dic[i - 4][1], + dic[i - 4][2], + dic[i - 4][3], + i, + True) + adjust_pip_ref_frames(i) + + +if __name__ == '__main__': + main() diff --git a/python-api/tests/integrationtests/reference_frames/out0_1.png b/python-api/tests/integrationtests/reference_frames/out0_1.png new file mode 100644 index 0000000..6fd461a Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out0_1.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out0_2.png b/python-api/tests/integrationtests/reference_frames/out0_2.png new file mode 100644 index 0000000..1251935 Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out0_2.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out1_1.png b/python-api/tests/integrationtests/reference_frames/out1_1.png new file mode 100644 index 0000000..6fd461a Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out1_1.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out1_2.png b/python-api/tests/integrationtests/reference_frames/out1_2.png new file mode 100644 index 0000000..5b74b08 Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out1_2.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out2_1.png b/python-api/tests/integrationtests/reference_frames/out2_1.png new file mode 100644 index 0000000..6fd461a Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out2_1.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out2_2.png b/python-api/tests/integrationtests/reference_frames/out2_2.png new file mode 100644 index 0000000..e54510f Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out2_2.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out3_1.png b/python-api/tests/integrationtests/reference_frames/out3_1.png new file mode 100644 index 0000000..6fd461a Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out3_1.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out3_2.png b/python-api/tests/integrationtests/reference_frames/out3_2.png new file mode 100644 index 0000000..6fd461a Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out3_2.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out4_1.png b/python-api/tests/integrationtests/reference_frames/out4_1.png new file mode 100644 index 0000000..5b74b08 Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out4_1.png differ diff --git a/python-api/tests/integrationtests/reference_frames/out4_2.png b/python-api/tests/integrationtests/reference_frames/out4_2.png new file mode 100644 index 0000000..da44216 Binary files /dev/null and b/python-api/tests/integrationtests/reference_frames/out4_2.png differ diff --git a/python-api/tests/integrationtests/test_controller.py b/python-api/tests/integrationtests/test_controller.py new file mode 100644 index 0000000..0936748 --- /dev/null +++ b/python-api/tests/integrationtests/test_controller.py @@ -0,0 +1,722 @@ +""" +Integration Tests for the dbus Controller +""" + +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.server import Server +from gstswitch.helpers import TestSources, PreviewSinks +from gstswitch.controller import Controller +import time +import datetime + +from integrationtests.compare import CompareVideo + +import subprocess + +# PATH = os.getenv("HOME") + '/gst/stage/bin/' +PATH = '../tools/' + + +class TestEstablishConnection(object): + + """Test establish_connection method""" + NUM = 1 + # fails above 3 + + def establish_connection(self): + """Create Controller object and call establish_connection""" + controller = Controller() + controller.establish_connection() + # print controller.connection + assert controller.connection is not None + + def test_establish(self): + """Test for establish_connection""" + serv = Server(path=PATH) + try: + serv.run() + for i in range(self.NUM): + print i + self.establish_connection() + serv.terminate(1) + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + +class TestGetComposePort(object): + + """Test get_compose_port method""" + NUM = 1 + FACTOR = 1 + + def get_compose_port(self): + """Create Controller and call get_compose_port method""" + res = [] + controller = Controller() + controller.establish_connection() + for _ in range(self.NUM * self.FACTOR): + res.append(controller.get_compose_port()) + return res + + def test_compose_ports(self): + """Test get_compose_port""" + res = [] + expected_result = [] + for i in range(self.NUM): + video_port = (i + 7) * 1000 + expected_result.append([video_port + 1] * self.NUM * self.FACTOR) + serv = Server(path=PATH, video_port=video_port) + try: + serv.run() + sources = TestSources(video_port=video_port) + sources.new_test_video() + sources.new_test_video() + + res.append(self.get_compose_port()) + sources.terminate_video() + serv.terminate(1) + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + set_expected = [tuple(i) for i in expected_result] + set_res = [tuple(i) for i in res] + assert set(set_expected) == set(set_res) + + +class TestGetEncodePort(object): + + """Test get_encode_port method""" + NUM = 1 + FACTOR = 1 + + def get_encode_port(self): + """Create a Controller object and call get_encode_port method""" + res = [] + controller = Controller() + controller.establish_connection() + for _ in range(self.NUM * self.FACTOR): + res.append(controller.get_encode_port()) + return res + + def test_encode_ports(self): + """Test get_encode_port""" + res = [] + expected_result = [] + for i in range(self.NUM): + video_port = (i + 8) * 1000 + expected_result.append([video_port + 2] * self.NUM * self.FACTOR) + serv = Server(path=PATH, video_port=video_port) + try: + serv.run() + sources = TestSources(video_port=video_port) + sources.new_test_video() + sources.new_test_video() + + res.append(self.get_encode_port()) + sources.terminate_video() + serv.terminate(1) + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + set_expected = [tuple(i) for i in expected_result] + set_res = [tuple(i) for i in res] + assert set(set_expected) == set(set_res) + + +class TestGetAudioPort(object): + + """Test get_audio_port method""" + NUM = 1 + FACTOR = 1 + + def get_audio_port(self): + """Create Controller object and call get_audio_port method""" + res = [] + controller = Controller() + controller.establish_connection() + for _ in range(self.NUM * self.FACTOR): + res.append(controller.get_audio_port()) + return res + + def test_audio_ports(self): + """Test get_audio_port""" + res = [] + expected_result = [] + for i in range(1, self.NUM + 1): + audio_port = (i + 10) * 1000 + expected_result.append([3003] * self.NUM * self.FACTOR) + serv = Server(path=PATH, video_port=3000, audio_port=audio_port) + try: + serv.run() + sources = TestSources(video_port=3000, audio_port=audio_port) + sources.new_test_audio() + + res.append(self.get_audio_port()) + + sources.terminate_audio() + serv.terminate(1) + + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + # print res + # print expected_result + set_expected = [tuple(i) for i in expected_result] + set_res = [tuple(i) for i in res] + assert set(set_expected) == set(set_res) + + +class TestGetPreviewPorts(object): + + """Test get_preview_ports method""" + NUM = 1 + FACTOR = 1 + + def get_preview_ports(self): + """Create Controller object and call get_preview_ports method""" + res = [] + controller = Controller() + controller.establish_connection() + for _ in range(self.NUM * self.FACTOR): + res.append(controller.get_preview_ports()) + return res + + def test_get_preview_ports(self): + """Test get_preview_ports""" + + for _ in range(self.NUM): + serv = Server(path=PATH) + try: + serv.run() + sources = TestSources(video_port=3000, audio_port=4000) + for _ in range(self.NUM): + sources.new_test_audio() + sources.new_test_video() + expected_result = map( + tuple, + [[x for x in range(3003, 3004 + self.NUM)]] + * self.NUM * self.FACTOR) + res = map(tuple, self.get_preview_ports()) + print '\n', res, '\n' + print expected_result + assert set(expected_result) == set(res) + sources.terminate_video() + sources.terminate_audio() + serv.terminate(1) + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + +class VideoFileSink(object): + + """Sink the video to a file + """ + + def __init__(self, port, filename): + cmd = "gst-launch-1.0 tcpclientsrc port={0} ! gdpdepay ! jpegenc \ + ! avimux ! filesink location={1}".format(port, filename) + with open(os.devnull, 'w') as tempf: + self.proc = subprocess.Popen( + cmd.split(), + stdout=tempf, + stderr=tempf, + bufsize=-1, + shell=False) + + def terminate(self): + """Terminate sinking""" + self.proc.terminate() + + +class TestSetCompositeMode(object): + + """Test set_composite_mode method""" + NUM = 1 + FACTOR = 1 + + def set_composite_mode(self, mode, generate_frames=False): + """Create Controller object and call set_composite_mode method""" + for _ in range(self.NUM): + + serv = Server(path=PATH) + try: + serv.run() + + preview = PreviewSinks() + preview.run() + + out_file = 'output-{0}.data'.format(mode) + video_sink = VideoFileSink(serv.video_port + 1, out_file) + + sources = TestSources(video_port=3000) + sources.new_test_video(pattern=4) + sources.new_test_video(pattern=5) + + time.sleep(3) + # expected_result = [mode != 3] * self.FACTOR + # print mode, expected_result + controller = Controller() + res = controller.set_composite_mode(mode) + print res + time.sleep(3) + video_sink.terminate() + preview.terminate() + sources.terminate_video() + serv.terminate(1) + if not generate_frames: + if mode == 3: + assert res is False + else: + assert res is True + assert self.verify_output(mode, out_file) is True + # assert expected_result == res + + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + def verify_output(self, mode, video): + """Verify if the output is correct by comparing key frames""" + test = 'composite_mode_{0}'.format(mode) + cmpr = CompareVideo(test, video) + res1, res2 = cmpr.compare() + print "RESULTS", res1, res2 + folder = cmpr.test_frame_dir + cmd = "./imgurbash.sh {0}/*.*".format(folder) + print cmd + proc = subprocess.Popen( + cmd, + bufsize=-1, + shell=True) + print proc.wait() + # Experimental Value + if res1 <= 0.04 and res2 <= 0.04: + return True + return False + + def test_set_composite_mode(self): + """Test set_composite_mode""" + for i in range(4): + self.set_composite_mode(i) + + +class TestNewRecord(object): + + """Test new_record method""" + NUM = 1 + FACTOR = 1 + + def new_record(self): + """Create a Controller object and call new_record method""" + res = [] + controller = Controller() + for _ in range(self.NUM * self.FACTOR): + res.append(controller.new_record()) + return res + + def test_new_record(self): + """Test new_record""" + for _ in range(self.NUM): + serv = Server(path=PATH, record_file="test-%Y.data") + try: + serv.run() + + sources = TestSources(video_port=3000) + sources.new_test_video() + sources.new_test_video() + + curr_time = datetime.datetime.now() + time_str = curr_time.strftime('%Y') + test_filename = "test-{0}.data".format(time_str) + + res = self.new_record() + print res + sources.terminate_video() + serv.terminate(1) + assert os.path.exists(test_filename) is True + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + +class TestAdjustPIP(object): + + """Test adjust_pip method""" + NUM = 1 + FACTOR = 1 + + def adjust_pip(self, + xpos, + ypos, + width, + heigth, + index, + generate_frames=False): + """Create Controller object and call adjust_pip""" + for _ in range(self.NUM): + serv = Server(path=PATH) + try: + serv.run() + sources = TestSources(video_port=3000) + preview = PreviewSinks() + preview.run() + out_file = "output-{0}.data".format(index) + video_sink = VideoFileSink(3001, out_file) + sources.new_test_video(pattern=4) + sources.new_test_video(pattern=5) + controller = Controller() + controller.set_composite_mode(1) + time.sleep(3) + res = controller.adjust_pip(xpos, ypos, width, heigth) + time.sleep(3) + sources.terminate_video() + preview.terminate() + video_sink.terminate() + serv.terminate(1) + if not generate_frames: + assert res is not None + assert self.verify_output(index, out_file) is True + + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + def verify_output(self, index, video): + """Verify if the output is correct by comparing key frames""" + test = 'adjust_pip_{0}'.format(index) + cmpr = CompareVideo(test, video) + res1, res2 = cmpr.compare() + print "RESULTS", res1, res2 + # Experimental Value + if res1 <= 0.04 and res2 <= 0.04: + return True + return False + + def test_adjust_pip(self): + """Test adjust_pip""" + dic = [ + [50, 75, 0, 0], + ] + for i in range(4, 5): + self.adjust_pip( + dic[i - 4][0], + dic[i - 4][1], + dic[i - 4][2], + dic[i - 4][3], + i) + + +class TestSwitch(object): + + """Test switch method""" + + NUM = 1 + + def switch(self, channel, port, index): + """Create Controller object and call switch method""" + for _ in range(self.NUM): + serv = Server(path=PATH) + try: + serv.run() + + sources = TestSources(3000) + sources.new_test_video(pattern=4) + sources.new_test_video(pattern=5) + preview = PreviewSinks(3001) + preview.run() + out_file = "output-{0}.data".format(index) + video_sink = VideoFileSink(3001, out_file) + time.sleep(3) + controller = Controller() + res = controller.switch(channel, port) + print res + time.sleep(3) + video_sink.terminate() + sources.terminate_video() + preview.terminate() + serv.terminate(1) + + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + def test_switch(self): + """Test switch""" + dic = [ + [65, 3004] + ] + start = 5 + for i in range(start, 6): + self.switch(dic[i - start][0], dic[i - start][1], i) + + +class TestClickVideo(object): + + """Test click_video method""" + NUM = 1 + FACTOR = 1 + + def click_video(self, + xpos, + ypos, + width, + heigth, + index, + generate_frames=False): + """Create Controller object and call click_video method""" + for _ in range(self.NUM): + serv = Server(path=PATH) + try: + serv.run() + sources = TestSources(video_port=3000) + preview = PreviewSinks() + preview.run() + out_file = "output-{0}.data".format(index) + video_sink = VideoFileSink(3001, out_file) + sources.new_test_video(pattern=4) + sources.new_test_video(pattern=5) + controller = Controller() + time.sleep(1) + res = controller.click_video(xpos, ypos, width, heigth) + print res + time.sleep(1) + sources.terminate_video() + preview.terminate() + video_sink.terminate() + serv.terminate(1) + if not generate_frames: + assert res is not None + assert self.verify_output(index, out_file) is True + + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + def verify_output(self, index, video): + """Verify if the output is correct by comparing key frames""" + test = 'click_video_{0}'.format(index) + cmpr = CompareVideo(test, video) + res1, res2 = cmpr.compare() + print "RESULTS", res1, res2 + # Experimental Value + if res1 <= 0.04 and res2 <= 0.04: + return True + return False + + def test_click_video(self): + """Test click_video""" + dic = [ + [0, 0, 10, 10], + ] + start = 6 + for i in range(start, 7): + self.click_video( + dic[i - start][0], + dic[i - start][1], + dic[i - start][2], + dic[i - start][3], + i, + True) + + +class TestMarkFace(object): + + """Test mark_face method""" + NUM = 1 + + def mark_face(self, faces, index, generate_frames=False): + """Create the Controller object and call mark_face method""" + for _ in range(self.NUM): + serv = Server(path=PATH) + try: + serv.run() + sources = TestSources(video_port=3000) + preview = PreviewSinks() + preview.run() + out_file = "output-{0}.data".format(index) + video_sink = VideoFileSink(3001, out_file) + sources.new_test_video(pattern=4) + sources.new_test_video(pattern=5) + controller = Controller() + time.sleep(1) + res = controller.mark_face(faces) + print res + time.sleep(1) + sources.terminate_video() + preview.terminate() + video_sink.terminate() + serv.terminate(1) + if not generate_frames: + assert res is not None + assert self.verify_output(index, out_file) is True + + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + def verify_output(self, index, video): + """Verify if the output is correct by comparing key frames""" + test = 'mark_face_{0}'.format(index) + cmpr = CompareVideo(test, video) + res1, res2 = cmpr.compare() + print "RESULTS", res1, res2 + # Experimental Value + if res1 <= 0.04 and res2 <= 0.04: + return True + return False + + def test_mark_face(self): + """Test mark_face""" + dic = [ + [(1, 1, 1, 1), (10, 10, 1, 1)], + ] + start = 7 + for i in range(start, 8): + self.mark_face(dic[i - start], i, True) + + +class TestMarkTracking(object): + + """Test mark_tracking method""" + NUM = 1 + + def mark_tracking(self, faces, index, generate_frames=False): + """Create Controller object and call mark_tracking method""" + for _ in range(self.NUM): + serv = Server(path=PATH) + try: + serv.run() + sources = TestSources(video_port=3000) + preview = PreviewSinks() + preview.run() + out_file = "output-{0}.data".format(index) + video_sink = VideoFileSink(3001, out_file) + sources.new_test_video(pattern=4) + sources.new_test_video(pattern=5) + controller = Controller() + time.sleep(1) + res = controller.mark_tracking(faces) + print res + time.sleep(1) + sources.terminate_video() + preview.terminate() + video_sink.terminate() + serv.terminate(1) + if not generate_frames: + assert res is not None + assert self.verify_output(index, out_file) is True + + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + def verify_output(self, index, video): + """Verify if the output is correct by comparing key frames""" + test = 'mark_tracking_{0}'.format(index) + cmpr = CompareVideo(test, video) + res1, res2 = cmpr.compare() + print "RESULTS", res1, res2 + # Experimental Value + if res1 <= 0.04 and res2 <= 0.04: + return True + return False + + def test_mark_tracking(self): + """Test mark_tracking""" + dic = [ + [(1, 1, 1, 1), (10, 10, 1, 1)], + ] + start = 7 + for i in range(start, 8): + self.mark_tracking(dic[i - start], i, True) diff --git a/python-api/tests/integrationtests/test_helpers.py b/python-api/tests/integrationtests/test_helpers.py new file mode 100644 index 0000000..aa73378 --- /dev/null +++ b/python-api/tests/integrationtests/test_helpers.py @@ -0,0 +1,74 @@ +""" +Integartion tests for TestSources, PreviewSinks in helpers.py +""" +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.server import Server +from gstswitch.helpers import TestSources, PreviewSinks +import time + + +# PATH = os.getenv("HOME") + '/gst/stage/bin/' +PATH = '../tools/' + + +class TestTestSourcesPreviews(object): + + """Test for TestSources and PreviewSinks""" + NUM = 1 + + def add_video_sources(self, num, video_port): + """Add a video source""" + sources = TestSources(video_port=video_port) + for _ in range(num): + sources.new_test_video(pattern=6) + # print "done adding" + str(num) + time.sleep(2) + sources.terminate_video() + time.sleep(2) + + def test_video_sources(self): + """Test video sources""" + video_port = 3000 + serv = Server(PATH, video_port=video_port) + try: + serv.run() + preview = PreviewSinks() + preview.run() + for i in range(self.NUM): + self.add_video_sources(i * 10 + 1, video_port) + preview.terminate() + serv.terminate() + finally: + if serv.proc: + serv.kill() + + def add_audio_sources(self, num, audio_port): + """Add audio sources""" + sources = TestSources(audio_port=audio_port) + for _ in range(num): + sources.new_test_audio(wave=10) + # print "done adding" + str(num) + # print sources.get_test_audio() + time.sleep(2) + sources.terminate_audio() + time.sleep(2) + + def test_audio_sources(self): + """Test audio sources""" + audio_port = 4000 + serv = Server(PATH, audio_port=audio_port) + try: + serv.run() + preview = PreviewSinks() + preview.run() + for i in range(self.NUM): + self.add_audio_sources(i * 10 + 1, audio_port) + + preview.terminate() + serv.terminate() + finally: + if serv.proc: + serv.kill() diff --git a/python-api/tests/integrationtests/test_server.py b/python-api/tests/integrationtests/test_server.py new file mode 100644 index 0000000..5b5feb6 --- /dev/null +++ b/python-api/tests/integrationtests/test_server.py @@ -0,0 +1,44 @@ +""" +Integration tests for Server in server.py +""" +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.server import Server + +# PATH = os.getenv("HOME") + '/gst/stage/bin/' +PATH = '../tools/' + + +class TestServerStartStop(object): + + """Test Starting and Stopping the Server + Run Server and Stop multiple times + """ + NUM = 5 + + def startstop(self): + """Start and Stop the Server""" + serv = Server(path=PATH) + try: + serv.run() + pid = serv.pid + assert pid > 0 + serv.terminate(1) + assert serv.proc is None + finally: + if serv.proc: + poll = serv.proc.poll() + print self.__class__ + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(abs(poll)) + serv.terminate(1) + log = open('server.log') + print log.read() + + def test_start_stop(self): + """Test Start and Stop the Server""" + for _ in range(self.NUM): + self.startstop() diff --git a/python-api/tests/performancetests/performance_dbus.py b/python-api/tests/performancetests/performance_dbus.py new file mode 100644 index 0000000..24ceb24 --- /dev/null +++ b/python-api/tests/performancetests/performance_dbus.py @@ -0,0 +1,381 @@ +""" +Performance/Torture test for DBus methods +""" + + +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.server import Server +from gstswitch.helpers import TestSources, PreviewSinks +from gstswitch.controller import Controller +import time + +PATH = '../tools/' + + + +def get_compose_port(num): + """Test get_compose_port - num times""" + video_port = 8000 + serv = Server(path=PATH, video_port=video_port) + try: + serv.run() + sources = TestSources(video_port=video_port) + sources.new_test_video() + + expected_res = [video_port + 1] * num + + controller = Controller() + controller.establish_connection() + res = [] + for _ in range(num): + res.append(controller.get_compose_port()) + + assert expected_res == res + finally: + if serv.proc: + poll = serv.proc.poll() + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + +class TestGetComposePort(object): + """Performance test for get_compose_port method - 100 times""" + + def test_100(self): + """Performance test for get_compose_port method""" + number_test_runs = 100 + get_compose_port(number_test_runs) + + + def test_200(self): + """Performance test for get_compose_port method""" + number_test_runs = 200 + get_compose_port(number_test_runs) + + + def test_300(self): + """Performance test for get_compose_port method""" + number_test_runs = 300 + get_compose_port(number_test_runs) + + + def test_400(self): + """Performance test for get_compose_port method""" + number_test_runs = 400 + get_compose_port(number_test_runs) + + def test_500(self): + """Performance test for get_compose_port method""" + number_test_runs = 500 + get_compose_port(number_test_runs) + + +def get_encode_port(num): + """Test get_encode_port - num times""" + video_port = 3000 + serv = Server(path=PATH, video_port=video_port) + try: + serv.run() + sources = TestSources(video_port=video_port) + sources.new_test_video() + + expected_res = [video_port + 2] * num + + controller = Controller() + controller.establish_connection() + res = [] + for _ in range(num): + res.append(controller.get_encode_port()) + + assert expected_res == res + finally: + if serv.proc: + poll = serv.proc.poll() + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + +class TestGetEncodePort(object): + """Performance test for get_encode_port method""" + + def test_100(self): + """Performance test for get_encode_port method - 100 times""" + number_test_runs = 100 + get_encode_port(number_test_runs) + + def test_200(self): + """Performance test for get_encode_port method - 200 times""" + number_test_runs = 200 + get_encode_port(number_test_runs) + + def test_300(self): + """Performance test for get_encode_port method - 300 times""" + number_test_runs = 300 + get_encode_port(number_test_runs) + + def test_400(self): + """Performance test for get_encode_port method - 400 times""" + number_test_runs = 400 + get_encode_port(number_test_runs) + + def test_500(self): + """Performance test for get_encode_port method - 500 times""" + number_test_runs = 500 + get_encode_port(number_test_runs) + + + +def get_audio_port(num): + """Test get_audio_port""" + audio_port = 8000 + serv = Server(path=PATH, audio_port=audio_port) + try: + serv.run() + sources = TestSources(audio_port=audio_port) + sources.new_test_audio() + + expected_res = [3003] * num + + controller = Controller() + controller.establish_connection() + res = [] + for _ in range(num): + res.append(controller.get_audio_port()) + + assert expected_res == res + + finally: + if serv.proc: + poll = serv.proc.poll() + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + +class TestGetAudioPort(object): + + def test_100(self): + """Performance test for get_audio_port method - 100 times""" + number_test_runs = 100 + get_audio_port(number_test_runs) + + + def test_200(self): + """Performance test for get_audio_port method - 200 times""" + number_test_runs = 200 + get_audio_port(number_test_runs) + + + def test_300(self): + """Performance test for get_audio_port method - 300 times""" + number_test_runs = 300 + get_audio_port(number_test_runs) + + + def test_400(self): + """Performance test for get_audio_port method - 400 times""" + number_test_runs = 400 + get_audio_port(number_test_runs) + + + def test_500(self): + """Performance test for get_audio_port method - 500 times""" + number_test_runs = 500 + get_audio_port(number_test_runs) + + +def get_preview_ports(num): + """Get Preview Ports when num number of sources are added""" + video_port = 3000 + serv = Server(path=PATH, video_port=video_port) + try: + serv.run() + sources = TestSources(video_port=video_port) + + + expected_res = [ i for i in range(3003, 3003 + num)] + + controller = Controller() + + res = [] + for _ in range(num): + sources.new_test_video() + controller.establish_connection() + res = controller.get_preview_ports() + print res + + assert expected_res == res + finally: + if serv.proc: + poll = serv.proc.poll() + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + + +class _TestGetPreviewPorts(object): + + def test_50(self): + """Test when 50 sources are added""" + number_souces = 50 + get_preview_ports(number_souces) + + def test_100(self): + """Test when 100 sources are added""" + number_souces = 100 + get_preview_ports(number_souces) + + def test_200(self): + """Test when 200 sources are added""" + number_souces = 200 + get_preview_ports(number_souces) + + def test_500(self): + """Test when 500 sources are added""" + number_souces = 500 + get_preview_ports(number_souces) + + def test_1000(self): + """Test when 1000 sources are added""" + number_souces = 1000 + get_preview_ports(number_souces) + + +def permutate_composite_mode(num, delay): + """Change composite mode num number of times""" + import random + + video_port = 3000 + serv = Server(path=PATH, video_port=video_port) + try: + serv.run() + sources = TestSources(video_port=video_port) + sources.new_test_video(pattern=6) + sources.new_test_video(pattern=5) + preview = PreviewSinks() + preview.run() + controller = Controller() + res = controller.set_composite_mode(0) + assert res is True + + prev_mode = 0 + for _ in range(num-1): + + time.sleep(delay) + mode = random.randint(0,3) + expected_res = bool(prev_mode ^ mode) + prev_mode = mode + assert expected_res == controller.set_composite_mode(mode) + preview.terminate() + sources.terminate_video() + + finally: + if serv.proc: + poll = serv.proc.poll() + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + +class TestSetCompositeMode(object): + """Performance Tests for set_composite_mode""" + + def test_num_20_delay_1(self): + delay = 1 + num = 20 + permutate_composite_mode(num, delay) + + def test_num_20_delay_point_6(self): + delay = 0.6 + num = 20 + permutate_composite_mode(num, delay) + + def test_num_20_delay_point_5(self): + delay = 0.5 + num = 20 + permutate_composite_mode(num, delay) + + def test_num_20_delay_point_2(self): + delay = 0.2 + num = 20 + permutate_composite_mode(num, delay) + + +def permutate_adjust_pip(num, delay): + """Adjust_pip num number of times""" + import random + + video_port = 3000 + serv = Server(path=PATH, video_port=video_port) + try: + serv.run() + sources = TestSources(video_port=video_port) + sources.new_test_video(pattern=6) + sources.new_test_video(pattern=5) + preview = PreviewSinks() + preview.run() + controller = Controller() + + for _ in range(num): + + xpos = random.randrange(-20, 20) + ypos = random.randrange(-20, 20) + res = controller.adjust_pip(xpos, ypos, 0, 0) + time.sleep(delay) + assert res is not None + preview.terminate() + sources.terminate_video() + + finally: + if serv.proc: + poll = serv.proc.poll() + if poll == -11: + print "SEGMENTATION FAULT OCCURRED" + print "ERROR CODE - {0}".format(poll) + serv.terminate(1) + log = open('server.log') + print log.read() + +class TestAdjustPip(object): + """Performance Tests for set_adjust_pip""" + + def test_num_20_delay_1(self): + delay = 1 + num = 20 + permutate_adjust_pip(num, delay) + + def test_num_20_delay_point_6(self): + delay = 0.6 + num = 20 + permutate_adjust_pip(num, delay) + + def test_num_20_delay_point_5(self): + delay = 0.5 + num = 20 + permutate_adjust_pip(num, delay) + + def test_num_20_delay_point_2(self): + delay = 0.2 + num = 20 + permutate_adjust_pip(num, delay) + + diff --git a/python-api/tests/unittests/__init__.py b/python-api/tests/unittests/__init__.py new file mode 100644 index 0000000..13dd066 --- /dev/null +++ b/python-api/tests/unittests/__init__.py @@ -0,0 +1,3 @@ +""" +Unittests for gst-switch Python-API +""" diff --git a/python-api/tests/unittests/test_connection_unit.py b/python-api/tests/unittests/test_connection_unit.py new file mode 100644 index 0000000..9604ab2 --- /dev/null +++ b/python-api/tests/unittests/test_connection_unit.py @@ -0,0 +1,359 @@ +"""Unittests for Connection class in connection.py""" +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.connection import Connection +from gstswitch.exception import ConnectionError +import pytest +from gi.repository import Gio, GLib +from mock import Mock + + +class TestAddress(object): + + """Unittests for address parameter""" + + def test_address_null(self): + """Test if address is null""" + addresses = ['', None, [], {}] + for address in addresses: + with pytest.raises(ValueError): + Connection(address=address) + + def test_address_colon(self): + """Test if address has no colon""" + address = 'abcdefghijk' + with pytest.raises(ValueError): + Connection(address=address) + + def test_address_normal(self): + """"Test if address is valid""" + address = ['unix:abstract=gstswitch', 'unix:temp=/tmp/abcd/xyz'] + for addr in address: + conn = Connection(address=addr) + assert conn.address == addr + + +class TestBusName(object): + + """Unittests for bus_name parameter""" + + def test_normal(self): + """Test if bus_name is not null""" + names = ['', 'abcd', 12345] + for bus in names: + conn = Connection(bus_name=bus) + assert conn.bus_name == str(bus) + + def test_normal_none(self): + """Test if bus_name is null""" + name = None + conn = Connection(bus_name=name) + assert conn.bus_name == name + + +class TestObjectPath(object): + + """Unittests for object_path parameter""" + + def test_object_path_blank(self): + """Test if object_path is null""" + paths = [None, '', {}, []] + for object_path in paths: + with pytest.raises(ValueError): + Connection(object_path=object_path) + + def test_object_path_slash(self): + """Test when object_path doesn't have slash in start""" + object_path = 'a/////////' + with pytest.raises(ValueError): + Connection(object_path=object_path) + + def test_object_path_normal(self): + """Test of object_path is valid""" + object_path = "/info/duzy/gst/switch/SwitchController" + conn = Connection(object_path=object_path) + assert conn.object_path == object_path + + +class TestInterface(object): + + """Unittests for default_interface parameter""" + + def test_interface_none(self): + """Test if default_interface is null""" + default_interface = [None, '', [], {}] + for interface_name in default_interface: + with pytest.raises(ValueError): + Connection(default_interface=interface_name) + + def test_interface_dot(self): + """Test when the default_interface has <2 dots""" + default_interface = ['.', 'info.', 'info'] + for interface_name in default_interface: + with pytest.raises(ValueError): + Connection(default_interface=interface_name) + + def test_interface_normal(self): + """Test if default_interface is valid""" + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + assert default_interface == conn.default_interface + + +class TestConnectDBus(object): + + """Unittests for the connect_dbus method of Connection class""" + + def test_bad_address(self): + """Test if wrong address is given - 1""" + address = 'unix:path=gstswitch' + conn = Connection(address=address) + with pytest.raises(ConnectionError): + conn.connect_dbus() + + def test_bad_address2(self): + """Test if wrong address is given - 2""" + address = 'unix:temp=gstswitch' + conn = Connection(address=address) + with pytest.raises(ConnectionError): + conn.connect_dbus() + + def test_bad_address3(self): + """Test if wrong address is given - 3""" + address = 'unix:path' + conn = Connection(address=address) + with pytest.raises(ConnectionError): + conn.connect_dbus() + + def test_mock1(self, monkeypatch): + """Test GLib.GError exception""" + monkeypatch.setattr( + Gio.DBusConnection, + 'new_for_address_sync', + Mock(side_effect=GLib.GError)) + conn = Connection() + with pytest.raises(ConnectionError): + conn.connect_dbus() + + def test_mock2(self, monkeypatch): + """Test GLib.GError exception""" + monkeypatch.setattr( + Gio.DBusConnection, 'new_for_address_sync', + Mock(return_value=1)) + conn = Connection() + conn.connect_dbus() + assert conn.connection is not None + + +class MockConnection(object): + + """A class which mocks the Connection class""" + funs = { + 'get_compose_port': (3001,), + 'get_encode_port': (3002,), + 'get_audio_port': (4000,), + 'get_preview_ports': ('[(3002, 1, 7), (3003, 1, 8)]',), + 'set_composite_mode': (False,), + 'set_encode_mode': (False,), + 'new_record': (False,), + 'adjust_pip': (1,), + 'switch': (True,), + 'click_video': (True,), + 'mark_face': None, + 'mark_tracking': None + } + + def __init__(self, method): + self.method = method + self.return_result = self.funs[method] + + def call_sync( + self, + bus_name, + object_path, + interface_name, + method_name, + parameters, + reply_type, flags, + timeout_msec, + cancellable): + """Mock of call_sync method, + raises GLib.GError if interface_name invalid""" + if interface_name == "info.duzy.gst.switch.SwitchControllerInterface": + return self.return_result + else: + raise GLib.GError('{0}: Test Failed'.format(self.method)) + + +def test_get_compose_port(): + """Test the get_compose_port method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_compose_port') + with pytest.raises(ConnectionError): + conn.get_compose_port() + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_compose_port') + assert conn.get_compose_port() == (3001,) + + +def test_get_encode_port(): + """Test the get_encode_port method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_encode_port') + with pytest.raises(ConnectionError): + conn.get_encode_port() + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_encode_port') + assert conn.get_encode_port() == (3002,) + + +def test_get_audio_port(): + """Test the get_audio_port method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_audio_port') + with pytest.raises(ConnectionError): + conn.get_audio_port() + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_audio_port') + assert conn.get_audio_port() == (4000,) + + +def test_get_preview_ports(): + """Test the get_preview_ports method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_preview_ports') + with pytest.raises(ConnectionError): + conn.get_preview_ports() + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('get_preview_ports') + assert conn.get_preview_ports() == ('[(3002, 1, 7), (3003, 1, 8)]',) + + +def test_set_composite_mode(): + """Test the set_composite_mode method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('set_composite_mode') + with pytest.raises(ConnectionError): + conn.set_composite_mode(2) + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('set_composite_mode') + assert conn.set_composite_mode(2) == (False,) + + +def test_set_encode_mode(): + """Test the set_encode_mode method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('set_encode_mode') + with pytest.raises(ConnectionError): + conn.set_encode_mode(2) + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('set_encode_mode') + assert conn.set_encode_mode(2) == (False,) + + +def test_new_record(): + """Test the new_record method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('new_record') + with pytest.raises(ConnectionError): + conn.new_record() + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('new_record') + assert conn.new_record() == (False,) + + +def test_adjust_pip(): + """Test the adjust_pip method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('adjust_pip') + with pytest.raises(ConnectionError): + conn.adjust_pip(1, 2, 3, 4) + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('adjust_pip') + assert conn.adjust_pip(1, 2, 3, 4) == (1,) + + +def test_switch(): + """Test the switch method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('switch') + with pytest.raises(ConnectionError): + conn.switch(1, 2) + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('switch') + assert conn.switch(1, 2) == (True,) + + +def test_click_video(): + """Test the click_video method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('click_video') + with pytest.raises(ConnectionError): + conn.click_video(1, 2, 3, 4) + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('click_video') + assert conn.click_video(1, 2, 3, 4) == (True,) + + +def test_mark_face(): + """Test the mark_face method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('mark_face') + with pytest.raises(ConnectionError): + face = [(1, 1, 1, 1), (2, 2, 2, 2)] + conn.mark_face(face) + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('mark_face') + face = [(1, 1, 1, 1), (2, 2, 2, 2)] + assert conn.mark_face(face) is None + + +def test_mark_tracking(): + """Test the mark_tracking method""" + default_interface = "info.duzy.gst.switch" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('mark_tracking') + with pytest.raises(ConnectionError): + face = [(1, 1, 1, 1), (2, 2, 2, 2)] + conn.mark_tracking(face) + + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Connection(default_interface=default_interface) + conn.connection = MockConnection('mark_tracking') + face = [(1, 1, 1, 1), (2, 2, 2, 2)] + assert conn.mark_tracking(face) is None diff --git a/python-api/tests/unittests/test_controller_unit.py b/python-api/tests/unittests/test_controller_unit.py new file mode 100644 index 0000000..199beaa --- /dev/null +++ b/python-api/tests/unittests/test_controller_unit.py @@ -0,0 +1,445 @@ +"""Unittests for Controller class in controller.py""" +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.controller import Controller +from gstswitch.exception import ConnectionReturnError +import pytest +from mock import Mock +from gstswitch.connection import Connection +from gi.repository import GLib + + +class TestAddress(object): + + """Test the address parameter""" + + def test_address_null(self): + """Test if address is null""" + address = ['', None, [], {}] + for addr in address: + with pytest.raises(ValueError): + Controller(address=addr) + + def test_address_colon(self): + """Test if address has no colon""" + address = 'abcdefghijk' + with pytest.raises(ValueError): + Controller(address=address) + + def test_address_normal(self): + """Test if address is valid""" + address = ['unix:abstract=gstswitch', 'unix:temp=/tmp/abcd/xyz'] + for addr in address: + conn = Controller(address=addr) + assert conn.address == addr + + +class TestBusName(object): + + """Test bus_name parameter""" + + def test_normal(self): + """Test when bus_name is not null""" + names = ['', 'abcd', 12345] + for bus in names: + conn = Controller(bus_name=bus) + assert conn.bus_name == str(bus) + + def test_normal_none(self): + """Test when bus_name is null""" + name = None + conn = Controller(bus_name=name) + assert conn.bus_name == name + + +class TestObjectPath(object): + + """Test object_path parameter""" + + def test_object_path_blank(self): + """Test when the object_path is null""" + paths = [None, '', {}, []] + for object_path in paths: + with pytest.raises(ValueError): + Controller(object_path=object_path) + + def test_object_path_slash(self): + """Test when object_path doesn't have slash in start""" + object_path = 'a/////////' + with pytest.raises(ValueError): + Controller(object_path=object_path) + + def test_object_path_normal(self): + """Test when object_path is valid""" + object_path = "/info/duzy/gst/switch/SwitchController" + conn = Controller(object_path=object_path) + assert conn.object_path == object_path + + +class TestInterface(object): + + """Test the default_interface parameter""" + + def test_interface_none(self): + """Test when the default_interface is null""" + default_interface = [None, '', [], {}] + for interface in default_interface: + with pytest.raises(ValueError): + Controller(default_interface=interface) + + def test_interface_dot(self): + """Test when the default_interface has <2 dots""" + default_interface = ['.', 'info.', 'info'] + for interface in default_interface: + with pytest.raises(ValueError): + Controller(default_interface=interface) + + def test_interface_normal(self): + """Test when the interface is valid""" + default_interface = "info.duzy.gst.switch.SwitchControllerInterface" + conn = Controller(default_interface=default_interface) + assert default_interface == conn.default_interface + + +class TestEstablishConnection(object): + + """Test the establish_connection method""" + + def test_normal(self, monkeypatch): + """Test if the parameters are valid""" + monkeypatch.setattr(Connection, 'connect_dbus', Mock()) + controller = Controller(address='unix:abstract=abcd') + controller.establish_connection() + assert controller.connection is not None + + +class MockConnection(object): + + """A class which mocks the Connection class""" + + def __init__(self, mode): + self.mode = mode + + def get_compose_port(self): + """mock of get_compose_port""" + if self.mode is False: + return GLib.Variant('(i)', (3001,)) + else: + return (0,) + + def get_encode_port(self): + """mock of get_encode_port""" + if self.mode is False: + return GLib.Variant('(i)', (3002,)) + else: + return (0,) + + def get_audio_port(self): + """mock of get_audio_port""" + if self.mode is False: + return GLib.Variant('(i)', (4000,)) + else: + return (0,) + + def get_preview_ports(self): + """mock of get_preview_ports""" + if self.mode is False: + return GLib.Variant('(s)', ('[(3002, 1, 7), (3003, 1, 8)]',)) + else: + return (0,) + + def set_composite_mode(self, mode): + """mock of set_composite_mode""" + if self.mode is False: + return GLib.Variant('(b)', (True,)) + else: + return (False,) + + def set_encode_mode(self, mode): + """mock of get_set_encode_mode""" + if self.mode is False: + return GLib.Variant('(b)', (True,)) + else: + return (True,) + + def new_record(self): + """mock of new_record""" + if self.mode is False: + return GLib.Variant('(b)', (True,)) + else: + return (True,) + + def adjust_pip(self, xpos, ypos, width, height): + """mock of adjust_pip""" + if self.mode is False: + return GLib.Variant('(u)', (1,)) + else: + return (1,) + + def switch(self, channel, port): + """mock of switch""" + if self.mode is False: + return GLib.Variant('(b)', (True,)) + else: + return (True,) + + def click_video(self, xpos, ypos, width, height): + """mock of click_video""" + if self.mode is False: + return GLib.Variant('(b)', (True,)) + else: + return (True,) + + def mark_face(self, face): + """mock of mark_face""" + pass + + def mark_tracking(self, face): + """mock of mark_tracking""" + pass + + +class TestGetComposePort(object): + + """Test the get_compose_port method""" + + def test_unpack(self): + """Test when values cant unpack""" + controller = Controller(address='unix:abstract=abcdefghijk') + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.get_compose_port() + + def test_normal_unpack(self): + """Test when valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.connection = MockConnection(False) + assert controller.get_compose_port() == 3001 + + +class TestGetEncodePort(object): + + """Test the get_encode_port method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcdefghijk') + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.get_encode_port() + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.connection = MockConnection(False) + assert controller.get_encode_port() == 3002 + + +class TestGetAudioPort(object): + + """ Test the get_audio_port method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcdefghijk') + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.get_audio_port() + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.connection = MockConnection(False) + assert controller.get_audio_port() == 4000 + + +class TestGetPreviewPorts(object): + + """Test the get_preview_ports method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcdefghijk') + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.get_preview_ports() + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.connection = MockConnection(False) + controller.parse_preview_ports = Mock(return_value=[3001, 3002]) + assert controller.get_preview_ports() == [3001, 3002] + + +class TestSetCompositeMode(object): + + """Test the set_composite_mode method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcdefghijk') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.set_composite_mode(1) + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(False) + assert controller.set_composite_mode(1) is True + + +class TestSetEncodeMode(object): + + """Test the set_encode_mode method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcde') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.set_encode_mode(1) + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(False) + assert controller.set_encode_mode(1) is True + + +class TestNewRecord(object): + + """Test the new_record method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcde') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.new_record() + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(False) + assert controller.new_record() is True + + +class TestAdjustPIP(object): + + """Test the adjust_pip method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcde') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.adjust_pip(1, 2, 3, 4) + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(False) + assert controller.adjust_pip(1, 2, 3, 4) == 1 + + +class TestSwitch(object): + + """Test the switch method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcde') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.switch(1, 2) + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(False) + assert controller.switch(1, 2) is True + + +class TestClickVideo(object): + + """Test the click_video method""" + + def test_unpack(self): + """Test if unpack fails""" + controller = Controller(address='unix:abstract=abcde') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + with pytest.raises(ConnectionReturnError): + controller.click_video(1, 2, 3, 4) + + def test_normal_unpack(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcdef') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(False) + assert controller.click_video(1, 2, 3, 4) is True + + +class TestMarkFaces(object): + + """Tes the mark_face method""" + + def test_normal(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcde') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + face = [(1, 2, 3, 4), (1, 1, 1, 1)] + controller.mark_face(face) + + +class TestMarkTracking(object): + + """Test the mark_tracking method""" + + def test_normal(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcde') + controller.establish_connection = Mock(return_value=None) + controller.connection = MockConnection(True) + face = [(1, 2, 3, 4), (1, 1, 1, 1)] + controller.mark_tracking(face) + + +class TestParsePreviewPorts(object): + + """Test the parse_preview_ports class method""" + + def test_value_error(self): + """Test if invalid""" + controller = Controller(address='unix:abstract=abcde') + test = 1234 + with pytest.raises(ConnectionReturnError): + controller.parse_preview_ports(test) + + def test_syntax_error(self): + """Test if syntax error detected""" + controller = Controller(address='unix:abstract=abcde') + test = '{}[]' + with pytest.raises(ConnectionReturnError): + controller.parse_preview_ports(test) + + def test_normal(self): + """Test if valid""" + controller = Controller(address='unix:abstract=abcde') + test = '[(1, 2, 3), (2, 2, 2)]' + assert controller.parse_preview_ports(test) == [1, 2] diff --git a/python-api/tests/unittests/test_helpers_unit.py b/python-api/tests/unittests/test_helpers_unit.py new file mode 100644 index 0000000..0e736ce --- /dev/null +++ b/python-api/tests/unittests/test_helpers_unit.py @@ -0,0 +1,301 @@ +"""Unittests for helper.py""" +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.helpers import TestSources, PreviewSinks +from gstswitch.exception import RangeError, InvalidIndexError +import pytest +from gstswitch import testsource + + +class TestTestSourcesVideoPort(object): + + """Test for video_port parameter""" + + def test_range(self): + """Test when out of range""" + tests = [-100, 1e7, 65536] + for test in tests: + with pytest.raises(RangeError): + TestSources(video_port=test) + + def test_invalid(self): + """Test when not integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, '1e10'] + for test in tests: + with pytest.raises(TypeError): + TestSources(video_port=test) + + def test_normal(self): + """Test when valid""" + tests = [1, 65535, 1000] + for test in tests: + src = TestSources(video_port=test) + assert src.video_port == test + + +class TestTestSourcesAudioPort(object): + + """Test for audio_port parameter""" + + def test_range(self): + """Test when out of range""" + tests = [-100, 1e7, 65536] + for test in tests: + with pytest.raises(RangeError): + TestSources(audio_port=test) + + def test_invalid(self): + """Test when not integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, '1e10'] + for test in tests: + with pytest.raises(TypeError): + TestSources(audio_port=test) + + def test_normal(self): + """Test when valid""" + tests = [1, 65535, 1000] + for test in tests: + src = TestSources(audio_port=test) + assert src.audio_port == test + + +class TestTestSources(object): + + """Test for testsources""" + class MockVideoSrc(object): + + """A mock video source""" + + def __init__(self, + port, + width=300, + height=200, + pattern=None, + timeoverlay=False, + clockoverlay=False): + pass + + def run(self): + """Run the mock video source""" + pass + + def test_new_test_video(self, monkeypatch): + """Test for new_test_video""" + test = TestSources(video_port=3000) + monkeypatch.setattr(testsource, 'VideoSrc', self.MockVideoSrc) + test.new_test_video() + assert test.running_tests_video[0] is not None + assert len(test.running_tests_video) != 0 + + class MockTest(object): + + """A mock test""" + + def __init__(self, pattern): + self.pattern = pattern + + def end(self): + """End the test""" + pass + + def test_get_test_video(self): + """Test for get_test_video""" + test = TestSources(video_port=3000) + test.running_tests_video = [ + self.MockTest(1), + self.MockTest(2), + self.MockTest(3), + self.MockTest(19)] + test.get_test_video() + + def test_terminate_index_error_video(self): + """Test for terminate_video""" + testsrc = TestSources(video_port=3000) + testsrc.running_tests_video = [ + self.MockTest(1), + self.MockTest(2), + self.MockTest(3), + self.MockTest(19)] + tests = [-100, 20, 1e10, "hi", [1, 2, 3]] + for test in tests: + with pytest.raises(InvalidIndexError): + testsrc.terminate_index_video(test) + + def test_terminate_index_normal_video(self): + """Test terminate_index_video""" + test = TestSources(video_port=3000) + test.running_tests_video = [ + self.MockTest(1), + self.MockTest(2), + self.MockTest(3), + self.MockTest(19)] + test.terminate_index_video(0) + + def test_terminate1_video(self): + """Test terminate_video multiple""" + test = TestSources(video_port=3000) + test.running_tests_video = [ + self.MockTest(1), + self.MockTest(2), + self.MockTest(3), + self.MockTest(19)] + test.terminate_video() + + def test_terminate2_video(self): + """Test terminate_video none present""" + test = TestSources(video_port=3000) + test.running_tests_video = [] + test.terminate_video() + + class MockAudioSrc(object): + + """A Mock audio source""" + + def __init__(self, port, freq=110, wave=None): + pass + + def run(self): + """Run the source""" + pass + + def test_new_test_audio(self, monkeypatch): + """Test new_test_audio""" + test = TestSources(audio_port=3000) + monkeypatch.setattr(testsource, 'AudioSrc', self.MockAudioSrc) + test.new_test_audio() + assert test.running_tests_audio[0] is not None + assert len(test.running_tests_audio) != 0 + + class MockTest2(object): + + """A mock audio source""" + + def __init__(self, wave): + self.wave = wave + + def end(self): + """End the source""" + pass + + def test_get_test_audio(self): + """Test get_test_audio""" + test = TestSources(audio_port=4000) + test.running_tests_audio = [ + self.MockTest2(1), + self.MockTest2(2), + self.MockTest2(3), + self.MockTest2(10)] + test.get_test_audio() + + def test_terminate_index_error_audio(self): + """Test terminate_index_audio - invalid index""" + testsrc = TestSources(audio_port=4000) + testsrc.running_tests_audio = [ + self.MockTest2(1), + self.MockTest2(2), + self.MockTest2(3), + self.MockTest2(10)] + tests = [-100, 20, 1e10, "hi", [1, 2, 3]] + for test in tests: + with pytest.raises(InvalidIndexError): + testsrc.terminate_index_audio(test) + + def test_terminate_index_normal_audio(self): + """Test terminate_index_audio valid index""" + test = TestSources(audio_port=4000) + test.running_tests_audio = [ + self.MockTest2(1), + self.MockTest2(2), + self.MockTest2(3), + self.MockTest2(10)] + test.terminate_index_audio(0) + + def test_terminate1_audio(self): + """Test terminate_audio multiple""" + test = TestSources(audio_port=4000) + test.running_tests_audio = [ + self.MockTest2(1), + self.MockTest2(2), + self.MockTest2(3), + self.MockTest2(10)] + test.terminate_audio() + + def test_terminate2_audio(self): + """Test terminate_audio none present""" + test = TestSources(audio_port=4000) + test.running_tests_audio = [] + test.terminate_audio() + + +class TestPreviewSinksPreviewPort(object): + + """Test preview_port parameter""" + + def test_blank(self): + """Test when preview_port is blank""" + tests = ['', None, [], {}] + for test in tests: + with pytest.raises(ValueError): + PreviewSinks(preview_port=test) + + def test_range(self): + """Test when preview_port is out of range""" + tests = [-100, 1e7, 65536] + for test in tests: + with pytest.raises(RangeError): + PreviewSinks(preview_port=test) + + def test_invalid(self): + """Test when preview_port is not a integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, '1e10'] + for test in tests: + with pytest.raises(TypeError): + PreviewSinks(preview_port=test) + + def test_normal(self): + """Test when preview_port is valid""" + tests = [1, 65535, 1000] + for test in tests: + src = PreviewSinks(preview_port=test) + assert src.preview_port == test + + +class TestPreviewSinks(object): + + """Test Preview Sinks""" + class MockPreview(object): + + """A mock preview class""" + + def __init__(self, preview_port=3001): + pass + + def run(self): + """Run the preview""" + pass + + def end(self): + """End the preview""" + pass + + def test_run(self, monkeypatch): + """Test running preview""" + preview = PreviewSinks() + monkeypatch.setattr(testsource, 'Preview', self.MockPreview) + preview.run() + assert preview.preview is not None + + def test_terminate_fail(self): + """Test terminating a preview when none exists""" + preview = PreviewSinks() + with pytest.raises(AttributeError): + preview.terminate() + + def test_terminate_normal(self, monkeypatch): + """Test terminating a preview when valid exists""" + preview = PreviewSinks() + preview.preview = self.MockPreview() + preview.terminate() + assert preview.preview is None diff --git a/python-api/tests/unittests/test_server_unit.py b/python-api/tests/unittests/test_server_unit.py new file mode 100644 index 0000000..5e0a2b8 --- /dev/null +++ b/python-api/tests/unittests/test_server_unit.py @@ -0,0 +1,433 @@ +"""Unittests for Server class in server.py""" +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.server import Server +import pytest +from gstswitch.exception import ServerProcessError +import subprocess +from distutils import spawn +from mock import Mock + + +PATH = '/usr/bin/' + + +class TestPath(object): + + """Test the path parameter""" + # Path Tests + + def test_path_provided_slash(self): + """Test if a path is provided""" + def mock_method(arg): + """Mocking _start_process""" + return arg + path = '/usr/' + serv = Server(path=path) + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000".split() + + def test_path_provided_no_slash(self): + """Test if a path is provided""" + def mock_method(arg): + """Mocking _start_process""" + return arg + path = '/usr' + serv = Server(path=path) + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000".split() + + def test_path_empty(self, monkeypatch): + """Test if null path is given""" + + def mock_method(arg): + "Mocking _start_process" + return arg + + def mockreturn(path): + "Mocking distutils.spawn.find_executable" + return '/usr/gst-switch-srv' + monkeypatch.setattr(spawn, 'find_executable', mockreturn) + paths = [None, ''] + for path in paths: + serv = Server(path=path) + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000".split() + + +class TestVideoPort(object): + + """Test for video_port parameter""" + # Video Port Tests + + def test_invalid_video_port_null(self): + """Test when the video_port is null""" + video_ports = [None, '', [], {}] + for video_port in video_ports: + with pytest.raises(ValueError): + Server(path=PATH, video_port=video_port) + + def test_invalid_video_port_type(self): + """Test when the video port given is not a valid + integral value""" + video_ports = [[1, 2, 3], {1: 2, 2: 3}] + for video_port in video_ports: + with pytest.raises(TypeError): + Server(path=PATH, video_port=video_port) + + def test_invalid_video_port_range(self): + """Test when the video port is not in range""" + video_ports = [-99, -1, 1e6] + for video_port in video_ports: + with pytest.raises(ValueError): + Server(path=PATH, video_port=video_port) + + +class TestAudioPort(object): + + """Test for audio_port parameter""" + # Audio Port Tests + + def test_invalid_audio_port_null(self): + """Test when the audio_port is null""" + audio_ports = [None, '', [], {}] + for audio_port in audio_ports: + with pytest.raises(ValueError): + Server(path=PATH, audio_port=audio_port) + + def test_invalid_audio_port_type(self): + """Test when the audio port given is not a valid + integral value""" + audio_ports = [[1, 2, 3], {1: 2, 2: 3}] + for audio_port in audio_ports: + with pytest.raises(TypeError): + Server(path=PATH, audio_port=audio_port) + + def test_invalid_audio_port_range(self): + """Test when the audio port is not in range""" + audio_ports = [-99, -1, 1e6] + for audio_port in audio_ports: + with pytest.raises(ValueError): + Server(path=PATH, audio_port=audio_port) + + +class TestControlPort(object): + + """Test the control_port parameter""" + # Control Port Tests + + def test_invalid_control_port_null(self): + """Test when the control port is null""" + control_ports = [None, '', [], {}] + for control_port in control_ports: + with pytest.raises(ValueError): + Server(path=PATH, control_port=control_port) + + def test_invalid_control_port_type(self): + """Test when the control port is not a valid + integral value""" + control_ports = [[1, 2, 3], {1: 2, 2: 3}] + for control_port in control_ports: + with pytest.raises(TypeError): + Server(path=PATH, control_port=control_port) + + def test_invalid_control_port_range(self): + """Test when the control port is not in range""" + control_ports = [-99, -1, 1e6] + for control_port in control_ports: + with pytest.raises(ValueError): + Server(path=PATH, control_port=control_port) + + +class TestRecordFile(object): + + """Test the record_file parameter""" + # Record File + + def test_record_file_false(self): + """Test if record file is False""" + def mock_method(arg): + """Mocking _start_process""" + return arg + path = '/usr' + serv = Server(path=path, record_file=False) + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000".split() + + def test_record_file_true(self): + """Test if record file is True""" + def mock_method(arg): + """Mocking _start_process""" + return arg + path = '/usr' + serv = Server(path=path, record_file=True) + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000 -r".split() + + def test_record_file_valid(self): + """Test if record file is valid""" + def mock_method(arg): + """Mocking _start_process""" + return arg + path = '/usr' + serv = Server(path=path, record_file="record.data") + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000 --record=record.data".split() + + def test_record_file_valid_date(self): + """Test if record file is valid""" + def mock_method(arg): + """Mocking _start_process""" + return arg + path = '/usr' + serv = Server(path=path, record_file="record_%Y.data") + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000 \ +--record=record_%Y.data".split() + + def test_record_file_valid_space(self): + """Test if record file is valid and has a space""" + def mock_method(arg): + """Mocking _start_process""" + return arg + path = '/usr' + serv = Server(path=path, record_file='record 1.data') + serv._start_process = mock_method + assert serv._run_process() == "/usr/gst-switch-srv \ +--video-input-port=3000 --audio-input-port=4000 \ +--control-port=5000".split() + ["--record=record 1.data"] + + def test_record_file_invalid(self): + """Test when the record_file is invalid""" + files = ['', None, [], {}] + for record_file in files: + with pytest.raises(ValueError): + Server(path=PATH, record_file=record_file) + + def test_record_file_slashes(self): + """Test when the record_file has forward slashes""" + filename = 'abcd/xyz/' + with pytest.raises(ValueError): + + Server(path=PATH, record_file=filename) + + +class TestKillTerminate(object): + + """Test kill, terminate and gcov_flush methods""" + # OS Errors + + def test_terminate_fail(self): + """Test when terminate fails""" + class FakeProc(object): + + """A mock process""" + + def __init__(self): + pass + + def terminate(self): + """Terminate the mock process""" + raise OSError + + serv = Server(path=PATH) + serv.proc = FakeProc() + with pytest.raises(ServerProcessError): + serv.terminate() + + def test_kill_fail(self): + """Test when kill fails""" + serv = Server(path=PATH) + serv.proc = 1 + serv.pid = -300 + with pytest.raises(ServerProcessError): + serv.kill() + + def test_no_process_kill(self): + """Test when no process exists and kill is called""" + serv = Server(path=PATH) + with pytest.raises(ServerProcessError): + serv.kill() + + def test_no_process_terminate(self): + """Test when no process exists and terminate is called""" + serv = Server(path=PATH) + with pytest.raises(ServerProcessError): + serv.terminate() + + def test_no_process_gov_flush(self): + """Test when no process exists and gcov_flush is called""" + serv = Server(path=PATH) + with pytest.raises(ServerProcessError): + serv.gcov_flush() + + def test_gcov_flush_fail(self): + """Test when gcov_flush fails""" + serv = Server(path=PATH) + serv.proc = 1 + serv.pid = -300 + with pytest.raises(ServerProcessError): + serv.gcov_flush() + + +class TestRun(object): + + """Test running the server""" + + def test_run(self): + """Test the run method""" + serv = Server(path='abc') + serv._run_process = Mock(return_value=MockProcess()) + serv.run() + assert serv.pid == 1 + assert serv.proc is not None + + def test_run_process(self): + """Test _run_process method""" + serv = Server(path='abc') + serv._start_process = Mock(return_value=MockProcess()) + serv.gst_option_string = '' + ret = serv._run_process() + assert ret is not None + + def test_start_process_error(self, monkeypatch): + """Test _start_process method""" + serv = Server(path='abc') + monkeypatch.setattr(subprocess, 'Popen', Mock(side_effect=OSError)) + with pytest.raises(ServerProcessError): + serv._start_process('cmd') + + def test_start_process_normal(self, monkeypatch): + """Test _start_process normally""" + serv = Server(path='abc') + monkeypatch.setattr( + subprocess, + 'Popen', + Mock(return_value=MockProcess())) + serv._start_process('cmd') + + +class MockProcess(object): + + """A mock process""" + + def __init__(self, mode=True): + self.mode = mode + self.pid = 1 + + def terminate(self): + """Terminate the mock process""" + if self.mode is True: + pass + if self.mode is False: + raise OSError('Testing terminate') + + def kill(self): + """Kill the mock process""" + if self.mode is True: + pass + if self.mode is False: + raise OSError('Testing kill') + + def make_coverage(self): + """Dump coverage""" + pass + + +class MockPopen(object): + + """Mock Popen method""" + + def __init__(self, cmd, bufsize, shell): + pass + + def communicate(self): + """Mock communicate method of Popen""" + return 0, 0 + + +class TestNormal(object): + # Normal Functioning Tests + + """Test the functioning of tests under valid conditions""" + + def test_normal_terminate(self): + """Test terminal when normally called""" + serv = Server(path='abc') + serv.proc = MockProcess(True) + serv.terminate() + assert serv.proc is None + + def test_normal_kill(self, monkeypatch): + """Test kill when normally called""" + serv = Server(path='abc') + serv.proc = Mock() + monkeypatch.setattr(os, 'kill', Mock()) + res = serv.kill() + assert res is True + assert serv.proc is None + + def test_terminate(self): + """Test terminate ServerProcessError""" + serv = Server(path='abc') + serv.proc = MockProcess(False) + with pytest.raises(ServerProcessError): + serv.terminate() + + def test_terminate_cov(self): + """Test terminate and gcov_flush ServerProcessError""" + serv = Server(path='abc') + serv.proc = MockProcess(False) + serv.gcov_flush = Mock() + serv.make_coverage = Mock() + with pytest.raises(ServerProcessError): + serv.terminate(True) + + def test_kill(self, monkeypatch): + """Test kill ServerProcessError""" + serv = Server(path='abc') + serv.proc = Mock() + monkeypatch.setattr(os, 'kill', Mock(side_effect=OSError)) + with pytest.raises(ServerProcessError): + serv.kill() + + def test_kill_cov(self, monkeypatch): + """Test kill and gcov_flush ServerProcessError""" + serv = Server(path='abc') + serv.proc = MockProcess(False) + serv.gcov_flush = Mock() + serv.make_coverage = Mock() + monkeypatch.setattr(os, 'kill', Mock(side_effect=OSError)) + with pytest.raises(ServerProcessError): + serv.kill(True) + + def test_normal_gcov_flush(self, monkeypatch): + """Test gcov_flush""" + serv = Server(path='abc') + serv.proc = Mock() + monkeypatch.setattr(os, 'kill', Mock()) + res = serv.gcov_flush() + assert res is True + assert serv.proc is not None + + def test_make_coverage(self, monkeypatch): + """Test dumping coverage""" + serv = Server(path='abc') + monkeypatch.setattr(subprocess, 'Popen', MockPopen) + serv.make_coverage() diff --git a/python-api/tests/unittests/test_testsource_unit.py b/python-api/tests/unittests/test_testsource_unit.py new file mode 100644 index 0000000..6717540 --- /dev/null +++ b/python-api/tests/unittests/test_testsource_unit.py @@ -0,0 +1,468 @@ +"""Unitests for testsource.py""" +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../../"))) + +from gstswitch.exception import RangeError +from gstswitch.testsource import Preview, VideoSrc +from gstswitch.testsource import BasePipeline, VideoPipeline, AudioSrc +import pytest +from mock import Mock +from gi.repository import Gst + + +class TestVideoSrcPort(object): + + """Test the port parameter""" + + def test_blank(self): + """Test when the port is null""" + tests = ['', None, [], {}] + for test in tests: + with pytest.raises(ValueError): + VideoSrc(port=test) + + def test_range(self): + """Test when the port is not in range""" + tests = [-100, 1e7, 65536] + for test in tests: + with pytest.raises(RangeError): + VideoSrc(port=test) + + def test_invalid(self): + """Test when the port is not a valid integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, '1e10'] + for test in tests: + with pytest.raises(TypeError): + VideoSrc(port=test) + + def test_normal(self): + """Test when port is a valid value""" + tests = [1, 65535, 1000] + for test in tests: + src = VideoSrc(port=test) + assert src.port == test + + +class TestVideoSrcWidth(object): + + """Test the width parameter""" + + def test_blank(self): + """Test when the width is null""" + tests = ['', None, [], {}] + port = 1000 + for test in tests: + with pytest.raises(ValueError): + VideoSrc(port=port, width=test) + + def test_non_positive(self): + """Test when the width is non-positive""" + tests = ['-1.111', -1.111, 0, -1e10] + port = 1000 + for test in tests: + with pytest.raises(ValueError): + VideoSrc(port=port, width=test) + + def test_invalid(self): + """Test when the width is not a valid float value""" + tests = [[1, 2, 3], {1: 2, 2: 3}, (1, 2)] + port = 1000 + for test in tests: + with pytest.raises(TypeError): + VideoSrc(port=port, width=test) + + def test_normal(self): + """Test when the width is valid""" + tests = [1e6, 300, '200'] + port = 1000 + for test in tests: + src = VideoSrc(port=port, width=test) + assert src.width == test + + +class TestVideoSrcHeight(object): + + """Test for height parameter""" + + def test_blank(self): + """Test when the height is a null""" + tests = ['', None, [], {}] + port = 1000 + for test in tests: + with pytest.raises(ValueError): + VideoSrc(port=port, height=test) + + def test_non_positive(self): + """Test when heightis non-positive""" + tests = ['-1.111', -1.111, 0, -1e10] + port = 1000 + for test in tests: + with pytest.raises(ValueError): + VideoSrc(port=port, height=test) + + def test_invalid(self): + """Test when height is not a valid float value""" + tests = [[1, 2, 3], {1: 2, 2: 3}, (1, 2)] + port = 1000 + for test in tests: + with pytest.raises(TypeError): + VideoSrc(port=port, height=test) + + def test_normal(self): + """Test when height is valid""" + tests = [1e6, 300, '200'] + port = 1000 + for test in tests: + src = VideoSrc(port=port, height=test) + assert src.height == test + + +class TestVideoSrcPattern(object): + + """Test the pattern parameter""" + + def test_range(self): + """Test when pattern is not in range""" + tests = [-100, 1e7, 65536, -1, 20] + port = 1000 + for test in tests: + with pytest.raises(RangeError): + VideoSrc(port=port, pattern=test) + + def test_invalid(self): + """Test when pattern is not a valid integer""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}] + port = 1000 + for test in tests: + with pytest.raises(TypeError): + VideoSrc(port=port, pattern=test) + + def test_normal(self): + """Test when pattern is valid""" + tests = [1, 0, 19, 10] + port = 1000 + for test in tests: + src = VideoSrc(port=port, pattern=test) + assert src.pattern == str(test) + + +class TestVideoSrcTimeOverlay(object): + + """Test timeoverlay parameter""" + + def test_fail(self): + """Test when timeoverlay is not boolean/valid""" + tests = ['', 1234, 'hi', [1, 2], {1: 2}, None, 0, []] + port = 1000 + for test in tests: + with pytest.raises(ValueError): + VideoSrc(port=port, timeoverlay=test) + + def test_normal(self): + """Test when timeoverlay is valid""" + tests = [True, False] + port = 1000 + for test in tests: + src = VideoSrc(port=port, timeoverlay=test) + assert src.timeoverlay == test + + +class TestVideoSrcClockOverlay(object): + + """Test clockoverlay pattern""" + + def test_fail(self): + """Test when clockoverlay is not boolean/valid""" + tests = ['', 1234, 'hi', [1, 2], {1: 2}, None, 0, []] + port = 1000 + for test in tests: + with pytest.raises(ValueError): + VideoSrc(port=port, clockoverlay=test) + + def test_normal(self): + """Test when clockoverlay is valid""" + tests = [True, False] + port = 1000 + for test in tests: + src = VideoSrc(port=port, clockoverlay=test) + assert src.clockoverlay == test + + +class MockPipeline(object): + + """Mock Pipeline""" + + def play(self): + """Play the pipeline""" + pass + + def pause(self): + """Pause the pipeline""" + pass + + def disable(self): + """Disable the pipeline""" + pass + + +class TestVideoSrcPlay(object): + + """Test Video Source options - play, pause, disable""" + + def test_run(self): + """Test run method""" + src = VideoSrc(port=3000) + src.pipeline = MockPipeline() + src.run() + + def test_pause(self): + """Test pause method""" + src = VideoSrc(port=3000) + src.pipeline = MockPipeline() + src.pause() + + def test_end(self): + """Test end method""" + src = VideoSrc(port=3000) + src.pipeline = MockPipeline() + src.end() + + +class TestPreviewPort(object): + + """Test port parameter""" + + def test_blank(self): + """Test when port is null""" + tests = ['', None, [], {}] + for test in tests: + with pytest.raises(ValueError): + Preview(port=test) + + def test_range(self): + """Test when port is not in range""" + tests = [-100, 1e7, 65536] + for test in tests: + with pytest.raises(RangeError): + Preview(port=test) + + def test_invalid(self): + """Test when port is not a valid integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, '1e10'] + for test in tests: + with pytest.raises(TypeError): + Preview(port=test) + + def test_normal(self): + """Test when port is valid""" + tests = [1, 65535, 1000] + for test in tests: + src = Preview(port=test) + assert src.preview_port == test + + +class TestPreviewPlay(object): + + """Test preview options - play, pause, end""" + + def test_run(self): + """Test play method""" + src = Preview(port=3001) + src.pipeline = MockPipeline() + src.run() + + def test_pause(self): + """Test pause method""" + src = Preview(port=3001) + src.pipeline = MockPipeline() + src.pause() + + def test_end(self): + """Test end method""" + src = Preview(port=3001) + src.pipeline = MockPipeline() + src.end() + + +class TestBasePipeline(object): + + """Test Base Pipeline""" + + def test_play(self, monkeypatch): + """Test play method""" + monkeypatch.setattr(Gst.Pipeline, '__init__', Mock()) + pipeline = BasePipeline() + pipeline.set_state = Mock() + pipeline.play() + + def test_pause(self, monkeypatch): + """Test pause method""" + monkeypatch.setattr(Gst.Pipeline, '__init__', Mock()) + pipeline = BasePipeline() + pipeline.set_state = Mock() + pipeline.pause() + + def test_disable(self, monkeypatch): + """Test disable method""" + monkeypatch.setattr(Gst.Pipeline, '__init__', Mock()) + pipeline = BasePipeline() + pipeline.set_state = Mock() + pipeline.disable() + + +class TestVideoPipeline(object): + + """Test VideoPipeline""" + + def test_permuate_time_clock_1(self): + """Test when timeoverlay False and clockoverlay False""" + VideoPipeline( + port=3000, + pattern=10, + timeoverlay=False, + clockoverlay=False) + + def test_permuate_time_clock_2(self): + """test when timeoverlay False and clockoverlay True""" + VideoPipeline( + port=3000, + pattern=10, + timeoverlay=False, + clockoverlay=True) + + def test_permuate_time_clock_3(self): + """Test when timeoverlay True and clockoverlay False""" + VideoPipeline( + port=3000, + pattern=10, + timeoverlay=True, + clockoverlay=False) + + def test_permuate_time_clock_4(self): + """Test when timeoverlay True and clockoverlay True""" + VideoPipeline( + port=3000, + pattern=10, + timeoverlay=True, + clockoverlay=True) + + +class TestAudioSrcPort(object): + + """Test port parameter""" + + def test_blank(self): + """Test when port is null""" + tests = ['', None, [], {}] + for test in tests: + with pytest.raises(ValueError): + AudioSrc(port=test) + + def test_range(self): + """Test when port is not in range""" + tests = [-100, 1e7, 65536] + for test in tests: + with pytest.raises(RangeError): + AudioSrc(port=test) + + def test_invalid(self): + """Test when port is not a valid integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, '1e10'] + for test in tests: + with pytest.raises(TypeError): + AudioSrc(port=test) + + def test_normal(self): + """Test when port is valid""" + tests = [1, 65535, 1000] + for test in tests: + src = AudioSrc(port=test) + assert src.port == test + + +class TestAudioSrcFreq(object): + + """Test frequency parameter""" + + def test_blank(self): + """Test when frequency is null""" + tests = ['', None, [], {}, 0] + port = 4000 + for test in tests: + with pytest.raises(ValueError): + AudioSrc(port=port, freq=test) + + def test_range(self): + """Test when frequency is not in range (negative)""" + tests = [-100, -1] + port = 4000 + for test in tests: + with pytest.raises(RangeError): + AudioSrc(port=port, freq=test) + + def test_invalid(self): + """Test when frequency is not a valid integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, '1e10'] + port = 4000 + for test in tests: + with pytest.raises(TypeError): + AudioSrc(port=port, freq=test) + + def test_normal(self): + """Test when frequency is valid""" + tests = [1, 65535, 1000] + for test in tests: + src = AudioSrc(port=4000, freq=test) + assert src.freq == test + + +class TestAudioSrcWave(object): + + """Test wave parameter""" + + def test_range(self): + """Test when wave is not in range""" + tests = [-100, -1, 13, 1e2, '1e10'] + port = 4000 + for test in tests: + with pytest.raises(RangeError): + AudioSrc(port=port, wave=test) + + def test_invalid(self): + """Test when wave is not a valid integral value""" + tests = [[1, 2, 3, 4], {1: 2, 2: 3}, ] + port = 4000 + for test in tests: + with pytest.raises(TypeError): + AudioSrc(port=port, wave=test) + + def test_normal(self): + """Test when wave is valid""" + tests = [0, 10, 12] + for test in tests: + src = AudioSrc(port=4000, wave=test) + assert src.wave == str(test) + + +class TestAudioSrcPlay(object): + + """Test Audio Source options - play, pause, end""" + + def test_run(self): + """Run the audio source""" + src = AudioSrc(port=3000) + src.pipeline = MockPipeline() + src.run() + + def test_pause(self): + """Pause the audio source""" + src = AudioSrc(port=3000) + src.pipeline = MockPipeline() + src.pause() + + def test_end(self): + """End the audio source""" + src = AudioSrc(port=3000) + src.pipeline = MockPipeline() + src.end() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1cbd97f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +mock +pytest-cov +pytest-pep8 +pylint diff --git a/run-demo.sh b/run-demo.sh new file mode 100755 index 0000000..f2941f5 --- /dev/null +++ b/run-demo.sh @@ -0,0 +1,38 @@ +#!/bin/bash -ex + +# FIXME: Rewrite using the Python API so this is reliable. + +VIDEO_CAPS="video/x-raw, format=(string)I420, pixel-aspect-ratio=(fraction)1/1, width=(int)300, height=(int)200, framerate=(fraction)25/1" +AUDIO_CAPS="audio/x-raw, rate=48000, channels=2, format=S16LE, layout=interleaved" + +killall gst-switch-srv || true +sleep 2 +./tools/gst-switch-srv -f "$VIDEO_CAPS" -r & +sleep 2 +./tools/gst-switch-ui & +sleep 2 + +gst-launch-1.0 audiotestsrc is-live=true \ + ! audioconvert \ + ! $AUDIO_CAPS \ + ! gdppay \ + ! tcpclientsink port=4000 \ +& + +gst-launch-1.0 videotestsrc pattern=1 is-live=1 \ + ! timeoverlay \ + ! $VIDEO_CAPS \ + ! gdppay \ + ! tcpclientsink port=3000 \ +& + +gst-launch-1.0 videotestsrc pattern=18 is-live=1 \ + ! timeoverlay \ + ! $VIDEO_CAPS \ + ! gdppay \ + ! tcpclientsink port=3000 \ +& + +wait +killall gst-switch-srv || true +killall gst-switch-ui || true diff --git a/scripts/apps/app_stage.sh b/scripts/apps/app_stage.sh index 34ce577..5a8b340 100644 --- a/scripts/apps/app_stage.sh +++ b/scripts/apps/app_stage.sh @@ -12,7 +12,7 @@ function install-prerequisite() #local pkginfo=$(get-pkg-info $pkg) #if [[ "x$pkginfo" == "x" ]]; then printf "install $pkginfo..\n" - sudo apt-get install $pkg + sudo apt-get -y install $pkg #else #printf "package $pkg is ok\n" #fi @@ -32,7 +32,15 @@ function install-git-libvpx() --enable-shared --enable-vp8 make clean || true - make ${options[make-args]} && make ${options[make-args]} install + + make || { + printf "make of $project failed!!!\n" + exit -1 + } + make install || { + printf "make of $project failed!!!\n" + exit -1 + } cd $back @@ -114,7 +122,7 @@ function clone-project() function clone-duzy-project() { - clone-project https://github.com/duzy $1 .git + clone-project https://github.com/timvideos $1 .git } function clone-gst-project() @@ -151,7 +159,14 @@ function build-project() exit -1 } make clean || true - make ${options[make-args]} && make ${options[make-args]} install + make || { + printf "make of $project failed!!!\n" + exit -1 + } + make install || { + printf "make of $project failed!!!\n" + exit -1 + } cd $backdir } @@ -209,7 +224,6 @@ function main() gstreamer \ gst-plugins-base \ gst-plugins-good \ - gst-plugins-bad \ gst-plugins-ugly fi @@ -219,6 +233,8 @@ function main() [[ -s scripts/stage ]]; then cd $back && build-project . else + clone-duzy-project gst-plugins-bad + build-gst-project gst-plugins-bad clone-duzy-project gst-switch build-project gst-switch true diff --git a/scripts/hooks/c-pre-commit b/scripts/hooks/c-pre-commit new file mode 100755 index 0000000..2bb8cc7 --- /dev/null +++ b/scripts/hooks/c-pre-commit @@ -0,0 +1,50 @@ +#!/bin/bash +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# + +function find-indent () { + if [[ -d .git && -h scripts/indent ]]; then + echo $PWD/scripts/indent + else + cd .. && { + find-indent + cd - > /dev/null + } + fi +} + +# If there are whitespace errors, print the offending file names and +files=$(git diff-index --cached --name-only HEAD --diff-filter=ACMR | grep "\.c$") +indent=$(find-indent) + +for file in $files; do + nf=`git checkout-index --temp ${file} | cut -f 1` + newfile=`mktemp /tmp/${nf}.XXXXXX` || exit 1 + $indent $nf -o $newfile 2>> /dev/null + # FIXME: Call indent twice as it tends to do line-breaks + # different for every second call. + $indent $newfile 2>> /dev/null + diff -u -p "${nf}" "${newfile}" + r=$? + rm "${newfile}" + rm "${nf}" + if [ $r != 0 ] ; then + echo "=================================================================================================" + echo " Code style error in: $file " + echo " " + echo " Please fix before committing. Don't forget to run git add before trying to commit again. " + echo " If the whole file is to be committed, this should work (run from the top-level directory): " + echo " " + echo " $indent $file; git add $file; git commit " + echo " " + echo "=================================================================================================" + exit 1 + fi + echo "Code style okay: $file" +done + +echo "Code style okay!" diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit index 2bb8cc7..10aa931 100755 --- a/scripts/hooks/pre-commit +++ b/scripts/hooks/pre-commit @@ -1,50 +1,4 @@ #!/bin/bash -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -function find-indent () { - if [[ -d .git && -h scripts/indent ]]; then - echo $PWD/scripts/indent - else - cd .. && { - find-indent - cd - > /dev/null - } - fi -} - -# If there are whitespace errors, print the offending file names and -files=$(git diff-index --cached --name-only HEAD --diff-filter=ACMR | grep "\.c$") -indent=$(find-indent) - -for file in $files; do - nf=`git checkout-index --temp ${file} | cut -f 1` - newfile=`mktemp /tmp/${nf}.XXXXXX` || exit 1 - $indent $nf -o $newfile 2>> /dev/null - # FIXME: Call indent twice as it tends to do line-breaks - # different for every second call. - $indent $newfile 2>> /dev/null - diff -u -p "${nf}" "${newfile}" - r=$? - rm "${newfile}" - rm "${nf}" - if [ $r != 0 ] ; then - echo "=================================================================================================" - echo " Code style error in: $file " - echo " " - echo " Please fix before committing. Don't forget to run git add before trying to commit again. " - echo " If the whole file is to be committed, this should work (run from the top-level directory): " - echo " " - echo " $indent $file; git add $file; git commit " - echo " " - echo "=================================================================================================" - exit 1 - fi - echo "Code style okay: $file" -done - -echo "Code style okay!" +./scripts/hooks/c-pre-commit +./scripts/hooks/py-pre-commit \ No newline at end of file diff --git a/scripts/hooks/py-pre-commit b/scripts/hooks/py-pre-commit new file mode 100755 index 0000000..8a51c9c --- /dev/null +++ b/scripts/hooks/py-pre-commit @@ -0,0 +1,3 @@ +#!/bin/bash + +#make -C python-api lint pep8 diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..55370ec --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,146 @@ +#! /bin/bash -ex + +sudo apt-get -y install python-software-properties python-pip +sudo pip install mock +sudo pip install pytest-cov +sudo pip install pytest-pep8 +sudo pip install pylint +sudo apt-get -y install libglib2.0-dev gir1.2-glib-2.0 libgirepository1.0-dev libglib2.0-0 python-gi +sudo apt-get -y install python-scipy +sudo apt-get -y install ffmpeg || sudo apt-get -y install libav-tools +sudo apt-get -y install autoconf automake autopoint libbz2-dev libdv4-dev libfaac-dev libfaad-dev libgtk-3-dev libmjpegtools-dev libtag1-dev libasound2-dev libtool libvpx-dev libxv-dev libx11-dev libogg-dev libvorbis-dev libopencv-dev libcv-dev libhighgui-dev libv4l-dev pkg-config zlib1g-dev gtk-doc-tools yasm bison flex +export PKG_CONFIG_PATH=/usr/lib/pkgconfig/ +export LD_LIBRARY_PATH=/usr/lib/ +git clone http://git.chromium.org/webm/libvpx.git +cd libvpx +git checkout v1.2.0 +./configure --enable-shared --enable-vp8 --prefix=/usr +[[ -f Makefile ]] || { + printf "Configure libvpx failed, no Makefile generated!!!\n" + exit -1 + } +make +sudo make install || { + printf "make install of libvpx failed!!!\n" + exit -1 + } +cd .. +git clone git://anongit.freedesktop.org/gstreamer/gstreamer +cd gstreamer +git checkout 6c11da1 +./autogen.sh --prefix=/usr || { +printf "Failed to do autogen!!!\n" +exit -1 +} +[[ -f Makefile ]] || { + printf "Configure gstreamer failed, no Makefile generated!!!\n" + exit -1 + } +make clean +make || { + printf "make of $project failed!!!\n" + exit -1 +} +sudo make install || { + printf "make install of gstreamer failed!!!\n" + exit -1 + } +cd .. +git clone git://anongit.freedesktop.org/gstreamer/gst-plugins-base +cd gst-plugins-base +git checkout a8df760 +./autogen.sh --prefix=/usr || { +printf "Failed to do autogen!!!\n" +exit -1 +} +[[ -f Makefile ]] || { + printf "Configure gst-plugins-base failed, no Makefile generated!!!\n" + exit -1 + } +make clean +make || { + printf "make of gst-plugins-base failed!!!\n" + exit -1 +} +sudo make install || { + printf "make install of $project failed!!!\n" + exit -1 + } +cd .. +git clone git://anongit.freedesktop.org/gstreamer/gst-plugins-good +cd gst-plugins-good +git checkout d14d4c4 +./autogen.sh --enable-experimental --prefix=/usr || { +printf "Failed to do autogen!!!\n" +exit -1 +} +[[ -f Makefile ]] || { + printf "Configure gst-plugins-good failed, no Makefile generated!!!\n" + exit -1 + } +make clean +make || { + printf "make of gst-plugins-good failed!!!\n" + exit -1 +} +sudo make install || { + printf "make install of $project failed!!!\n" + exit -1 + } +cd .. +git clone git://anongit.freedesktop.org/gstreamer/gst-plugins-ugly +cd gst-plugins-ugly +git checkout 68985ba +./autogen.sh --prefix=/usr || { +printf "Failed to do autogen!!!\n" +exit -1 +} +[[ -f Makefile ]] || { + printf "Configure gst-plugins-ugly failed, no Makefile generated!!!\n" + exit -1 + } +make clean +make || { + printf "make of gst-plugins-ugly failed!!!\n" + exit -1 +} +sudo make install || { + printf "make install of gst-plugins-ugly failed!!!\n" + exit -1 + } +cd .. +git clone https://github.com/hyades/gst-plugins-bad.git +cd gst-plugins-bad +./autogen.sh --prefix=/usr || { +printf "Failed to do autogen!!!\n" +exit -1 +} +[[ -f Makefile ]] || { + printf "Configure gst-plugins-bad failed, no Makefile generated!!!\n" + exit -1 + } +make clean +make || { + printf "make of gst-plugins-bad failed!!!\n" + exit -1 +} +sudo make install || { + printf "make install of gst-plugins-bad failed!!!\n" + exit -1 + } +cd .. +git clone https://github.com/hyades/gst-switch.git +cd gst-switch +./autogen.sh --prefix=/usr || { +printf "Failed to do autogen!!!\n" +exit -1 +} +make clean +make || { + printf "make of gstswitch failed!!!\n" + exit -1 +} +sudo make install || { + printf "make install of gstswitch failed!!!\n" + exit -1 + } diff --git a/scripts/install2.sh b/scripts/install2.sh new file mode 100644 index 0000000..116293f --- /dev/null +++ b/scripts/install2.sh @@ -0,0 +1,8 @@ +#!/bin/bash -ex +mkdir build +cd build +sudo apt-get install git python-pip +git clone https://github.com/timvideos/gst-switch.git +cd gst-switch +chmod +x .travis-setup.sh +./.travis-setup.sh diff --git a/tests/.gitignore b/tests/.gitignore index 544b00e..84a202c 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -2,7 +2,7 @@ /Makefile.in /.deps /tools -/test-recording*.data +/test-recording*.data* /test-server-*.log /test-fd-leaks /tests diff --git a/tests/faac.supp b/tests/faac.supp deleted file mode 100644 index 9f5dd99..0000000 --- a/tests/faac.supp +++ /dev/null @@ -1,49 +0,0 @@ -{ - - Memcheck:Cond - obj:/usr/lib/libfaac.so.0.0.0 - fun:* -} -{ - - Memcheck:Value8 - obj:/usr/lib/libfaac.so.0.0.0 - fun:* -} -{ - - Memcheck:Cond - ... - fun:gst_faac_handle_frame - ... - fun:start_thread - fun:clone -} -{ - - Memcheck:Value8 - ... - fun:gst_faac_handle_frame - ... - fun:start_thread - fun:clone -} -{ - - Memcheck:Leak - ... - fun:gst_faac_set_format - ... - fun:start_thread - fun:clone -} -{ - - Memcheck:Leak - ... - fun:vp8e_init - ... - fun:start_thread - fun:clone -} - diff --git a/tests/test_switch_server.c b/tests/test_switch_server.c index 6100a5c..7170d4a 100644 --- a/tests/test_switch_server.c +++ b/tests/test_switch_server.c @@ -328,7 +328,7 @@ child_quit (GPid pid, gint status, gpointer data) INFO ("quit %d", pid); { g_mutex_lock (&children_lock); - children = g_list_remove (children, (gconstpointer) pid); + children = g_list_remove (children, (gconstpointer) GINT_TO_POINTER(pid)); g_mutex_unlock (&children_lock); } @@ -447,7 +447,7 @@ launch (const gchar * name, ...) if (ok) { g_mutex_lock (&children_lock); - children = g_list_append (children, (gpointer) pid); + children = g_list_append (children, (gpointer) GINT_TO_POINTER(pid)); g_mutex_unlock (&children_lock); } @@ -883,8 +883,8 @@ testclient_new_mode (testclient * client, gint mode) { //INFO ("set-encode-port: %d", port); client->new_mode_count += 1; - g_assert_cmpint (mode, >=, COMPOSE_MODE_0); - g_assert_cmpint (mode, <=, COMPOSE_MODE_3); + g_assert_cmpint (mode, >=, COMPOSE_MODE_NONE); + g_assert_cmpint (mode, <=, COMPOSE_MODE_DUAL_EQUAL); } static void @@ -2369,7 +2369,7 @@ kill_children (void) GList *child = NULL; g_mutex_lock (&children_lock); for (child = children; child; child = g_list_next (child)) { - GPid pid = (GPid) child->data; + GPid pid = (GPid) GPOINTER_TO_INT(child->data); //INFO ("kill %d", pid); kill (pid, SIGKILL); children = g_list_remove_link (children, child); diff --git a/tests/test_ui_lag1.sh b/tests/test_ui_lag1.sh new file mode 100755 index 0000000..06c1b71 --- /dev/null +++ b/tests/test_ui_lag1.sh @@ -0,0 +1,31 @@ +#!/bin/bash -ex + +# test_ui_lag1.sh +# demos issue #30: lag between thumb and canvas + +cd ../tools + +./gst-switch-srv & srvpid=$! +sleep 5 +./gst-switch-ui & uipid=$! + +# srcpid=() +for i in 1 2 3 4 5 6; do + sleep 2 + gst-launch-1.0 videotestsrc pattern=18 is-live=1 \ + ! textoverlay text="s ${i}" \ + halignment=2 valignment=2 font-desc="Sans 40" \ + ! timeoverlay font-desc="Sans 40" \ + ! clockoverlay time-format="%S" font-desc="Sans 240" \ + ! video/x-raw, width=300, height=200 \ + ! gdppay \ + ! tcpclientsink port=3000 \ + & srcpid[i]=$! +done + +read -p "press enter to kill all the processes:" + +kill $srvpid $uipid +for i in 1 2 3 4 5 6; do kill ${srcpid[i]}; done + + diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am new file mode 100644 index 0000000..cb794e5 --- /dev/null +++ b/tests/unit/Makefile.am @@ -0,0 +1,37 @@ +include $(top_srcdir)/build/glib-tap.mk + +LDADD = $(GTK_LIBS) $(GLIB_LIBS) $(GST_LIBS) + +test_gstswitchopts_SOURCES = test_gstswitchopts.c +test_gstswitchopts_CFLAGS = $(GST_CFLAGS) $(GST_BASE_CFLAGS) \ + $(GCOV_CFLAGS) $(GST_PLUGINS_BASE_CFLAGS) -DLOG_PREFIX="\"./tests\"" +test_gstrecorder_filename_SOURCES = test_gstrecorder_filename.c ../../tools/gstworker.c +test_gstrecorder_filename_CFLAGS = -fprofile-arcs $(GST_CFLAGS) $(GST_BASE_CFLAGS) \ + $(GCOV_CFLAGS) $(GST_PLUGINS_BASE_CFLAGS) -DLOG_PREFIX="\"./tests\"" +test_gstrecorder_filename_LDFLAGS = $(GCOV_LFLAGS) +test_gstcomposite_CFLAGS = $(GST_CFLAGS) $(GST_BASE_CFLAGS) \ + $(GCOV_CFLAGS) $(GST_PLUGINS_BASE_CFLAGS) -DLOG_PREFIX="\"./tests\"" +test_gstcomposite_LDFLAGS = $(GCOV_LFLAGS) + +test_gst_pipeline_string_SOURCES = test_gst_pipeline_string.c ../../tools/gstworker.c +test_gst_pipeline_string_CFLAGS = $(GST_CFLAGS) $(GST_BASE_CFLAGS) \ + $(GCOV_CFLAGS) $(GST_PLUGINS_BASE_CFLAGS) -DLOG_PREFIX="\"./tests\"" +test_gst_pipeline_string_LDFLAGS = $(GCOV_LFLAGS) + + +dist_test_data = \ + $(NULL) + +test_programs = \ + test_gstswitchopts \ + test_gstrecorder_filename \ + test_gstcomposite \ + test_gst_pipeline_string \ + $(NULL) + +if GCOV_ENABLED +coverage: + gcov *.o +endif + +include $(top_srcdir)/build/Makefile.am.gitignore diff --git a/tests/unit/gstswitchexample.c b/tests/unit/gstswitchexample.c new file mode 100644 index 0000000..e4afed5 --- /dev/null +++ b/tests/unit/gstswitchexample.c @@ -0,0 +1,23 @@ +#include + +static void +success (void) +{ + g_assert_cmpint (1234, ==, 1234); +} + +static void +fail (void) +{ + g_assert_cmpint (1234, ==, 1); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_set_nonfatal_assertions (); + g_test_add_func ("/gstswitch/server/options/success", success); + g_test_add_func ("/gstswitch/server/options/fail", fail); + return g_test_run (); +} diff --git a/tests/unit/test_gst_pipeline_string.c b/tests/unit/test_gst_pipeline_string.c new file mode 100644 index 0000000..b08afa4 --- /dev/null +++ b/tests/unit/test_gst_pipeline_string.c @@ -0,0 +1,240 @@ + +#include "tools/gstcase.c" +#include + +gboolean verbose = FALSE; + +static gchar *results[GST_CASE__LAST_TYPE + 1] = { + // GST_CASE_UNKNOWN + NULL, + // GST_CASE_COMPOSITE_VIDEO_A + NULL, + // GST_CASE_COMPOSITE_VIDEO_B + NULL, + // GST_CASE_COMPOSITE_AUDIO + NULL, + // GST_CASE_PREVIEW + NULL, + // GST_CASE_INPUT_AUDIO + NULL, + // GST_CASE_INPUT_VIDEO + NULL, + // GST_CASE_BRANCH_VIDEO_A + NULL, + // GST_CASE_BRANCH_VIDEO_B + NULL, + // GST_CASE_BRANCH_AUDIO + NULL, + // GST_CASE_BRANCH_PREVIEW + NULL +}; + +static gchar * +expected_string (int case_type, int serve_type) +{ + switch (case_type) { + case GST_CASE_PREVIEW: // special case + if (serve_type == GST_SERVE_VIDEO_STREAM) + return ""; + return ""; + case GST_CASE_COMPOSITE_VIDEO_A: + case GST_CASE_COMPOSITE_VIDEO_B: + case GST_CASE_COMPOSITE_AUDIO: + case GST_CASE_INPUT_AUDIO: + case GST_CASE_INPUT_VIDEO: + case GST_CASE_BRANCH_VIDEO_A: + case GST_CASE_BRANCH_VIDEO_B: + case GST_CASE_BRANCH_AUDIO: + case GST_CASE_BRANCH_PREVIEW: + return results[case_type]; + } + return NULL; +} + + +static GstCase * +new_case (int case_type, int serve_type) +{ + switch (serve_type) { + case GST_SERVE_AUDIO_STREAM: + return GST_CASE (g_object_new (GST_TYPE_CASE, + "name", "audio", "type", case_type, "serve", serve_type, + "port", 1234, NULL)); + case GST_SERVE_VIDEO_STREAM: + return GST_CASE (g_object_new (GST_TYPE_CASE, + "name", "video", "type", case_type, "serve", serve_type, + "width", 640, "height", 480, "port", 1234, NULL)); + } + return NULL; +} + +static void +test_get_pipeline_string_input_audio (void) +{ + GstCase *cas = new_case (GST_CASE_INPUT_AUDIO, GST_SERVE_AUDIO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_INPUT_AUDIO, GST_SERVE_AUDIO_STREAM), ==, desc->str); + printf ("\nGST_CASE_INPUT_AUDIO: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_input_video (void) +{ + GstCase *cas = new_case (GST_CASE_INPUT_VIDEO, GST_SERVE_VIDEO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_INPUT_VIDEO, GST_SERVE_VIDEO_STREAM), ==, desc->str); + printf ("\nGST_CASE_INPUT_VIDEO: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_preview_video (void) +{ + GstCase *cas = new_case (GST_CASE_PREVIEW, GST_SERVE_VIDEO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_PREVIEW, GST_SERVE_VIDEO_STREAM), ==, desc->str); + printf ("\nGST_CASE_PREVIEW/V: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_preview_audio (void) +{ + GstCase *cas = new_case (GST_CASE_PREVIEW, GST_SERVE_AUDIO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_PREVIEW, GST_SERVE_AUDIO_STREAM), ==, desc->str); + printf ("\nGST_CASE_PREVIEW/A: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_composite_audio (void) +{ + GstCase *cas = new_case (GST_CASE_COMPOSITE_AUDIO, GST_SERVE_AUDIO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_COMPOSITE_AUDIO, GST_SERVE_AUDIO_STREAM), ==, desc->str); + printf ("\nGST_CASE_COMPOSITE_AUDIO: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_composite_video_a (void) +{ + GstCase *cas = new_case (GST_CASE_COMPOSITE_VIDEO_A, GST_SERVE_VIDEO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_COMPOSITE_VIDEO_A, GST_SERVE_VIDEO_STREAM), ==, desc->str); + printf ("\nGST_CASE_COMPOSITE_VIDEO_A: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_composite_video_b (void) +{ + GstCase *cas = new_case (GST_CASE_COMPOSITE_VIDEO_B, GST_SERVE_VIDEO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_COMPOSITE_VIDEO_B, GST_SERVE_VIDEO_STREAM), ==, desc->str); + printf ("\nGST_CASE_COMPOSITE_VIDEO_B: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_branch_video_a (void) +{ + GstCase *cas = new_case (GST_CASE_BRANCH_VIDEO_A, GST_SERVE_VIDEO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_BRANCH_VIDEO_A, GST_SERVE_VIDEO_STREAM), ==, desc->str); + printf ("\nGST_CASE_BRANCH_VIDEO_A: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_branch_video_b (void) +{ + GstCase *cas = new_case (GST_CASE_BRANCH_VIDEO_B, GST_SERVE_VIDEO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_BRANCH_VIDEO_B, GST_SERVE_VIDEO_STREAM), ==, desc->str); + printf ("\nGST_CASE_BRANCH_VIDEO_B: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_branch_preview (void) +{ + GstCase *cas = new_case (GST_CASE_BRANCH_PREVIEW, GST_SERVE_VIDEO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_BRANCH_PREVIEW, GST_SERVE_VIDEO_STREAM), ==, desc->str); + printf ("\nGST_CASE_BRANCH_PREVIEW: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +static void +test_get_pipeline_string_branch_audio (void) +{ + GstCase *cas = new_case (GST_CASE_BRANCH_AUDIO, GST_SERVE_AUDIO_STREAM); + GString *desc = gst_case_get_pipeline_string (cas); + g_assert (desc != NULL && strlen (desc->str) > 0); +//g_assert_cmpstr(expected_string(GST_CASE_BRANCH_AUDIO, GST_SERVE_AUDIO_STREAM), ==, desc->str); + printf ("\nGST_CASE_BRANCH_AUDIO: %s\n", desc->str); + g_string_free (desc, TRUE); + g_object_unref (cas); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + gst_init (&argc, &argv); + g_test_set_nonfatal_assertions (); + g_test_add_func ("/gstswitch/server/gstcase/get_pipeline_string/INPUT/AUDIO", + test_get_pipeline_string_input_audio); + g_test_add_func ("/gstswitch/server/gstcase/get_pipeline_string/INPUT/VIDEO", + test_get_pipeline_string_input_video); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/BRANCH/PREVIEW/AUDIO", + test_get_pipeline_string_preview_audio); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/BRANCH/PREVIEW/VIDEO", + test_get_pipeline_string_preview_video); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/COMPOSITE/AUDIO", + test_get_pipeline_string_composite_audio); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/COMPOSITE/VIDEO_A", + test_get_pipeline_string_composite_video_a); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/COMPOSITE/VIDEO_B", + test_get_pipeline_string_composite_video_b); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/BRANCH/VIDEO_A", + test_get_pipeline_string_branch_video_a); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/BRANCH/VIDEO_B", + test_get_pipeline_string_branch_video_b); + g_test_add_func ("/gstswitch/server/gstcase/get_pipeline_string/BRANCH/AUDIO", + test_get_pipeline_string_branch_audio); + g_test_add_func + ("/gstswitch/server/gstcase/get_pipeline_string/BRANCH/PREVIEW", + test_get_pipeline_string_branch_preview); + return g_test_run (); +} diff --git a/tests/unit/test_gstcomposite.c b/tests/unit/test_gstcomposite.c new file mode 100644 index 0000000..72d7514 --- /dev/null +++ b/tests/unit/test_gstcomposite.c @@ -0,0 +1,20 @@ +#include + +#include "tools/gstcomposite.h" + +static void +mode_to_string (void) +{ + g_assert_cmpstr (gst_composite_mode_to_string (COMPOSE_MODE_NONE), ==, + "COMPOSE_MODE_NONE"); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_set_nonfatal_assertions (); + g_test_add_func ("/gstswitch/server/composite/mode_to_string", + mode_to_string); + return g_test_run (); +} diff --git a/tests/unit/test_gstrecorder_filename.c b/tests/unit/test_gstrecorder_filename.c new file mode 100644 index 0000000..ed625fc --- /dev/null +++ b/tests/unit/test_gstrecorder_filename.c @@ -0,0 +1,91 @@ + +#include "tools/gstrecorder.c" +#include + +#define BASEDIR "/tmp/unittests/" + +// fake options struct, not used in these tests +GstSwitchServerOpts opts; +gboolean verbose = FALSE; + + + +static char * +filepath (char *buf, size_t len, char const *fn) +{ + snprintf (buf, len, BASEDIR "%d/%s", getpid (), fn); + return buf; +} + + +static gboolean +exists (const char *filepath, gboolean dir) +{ + struct stat s; + + if (stat (filepath, &s) == 0) { + if (!dir || S_ISDIR (s.st_mode)) + return TRUE; + } + return FALSE; +} + +static gboolean +touch (const char *filepath) +{ + int fd = + open (filepath, O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd != -1) { + close (fd); + return TRUE; + } + return FALSE; +} + + +static int +create_new_directory (const char *name) +{ + char buf[256]; + + gst_recorder_mkdirs (filepath (buf, sizeof buf, name)); + return exists (buf, TRUE); +} + + +static gboolean +create_capture_file (const char *name) +{ + char buf[256]; + char const *capfilename = + gst_recorder_new_filename (filepath (buf, sizeof buf, name)); + return touch (capfilename); +} + +static void +test_mkdirs () +{ + g_assert_cmpint (create_new_directory ("testdir1"), !=, 0); + g_assert_cmpint (create_new_directory ("testdir2"), !=, 0); + g_assert_cmpint (create_new_directory ("testdir1/testdir3"), !=, 0); + g_assert_cmpint (create_new_directory ("testdir3/testdir1"), !=, 0); +} + +static void +test_new_filename () +{ + g_assert_true (create_capture_file ("%Y%m%d_%T")); + g_assert_true (create_capture_file ("%Y%m/%d_%T")); + g_assert_true (create_capture_file ("%Y/%m/%d/%T")); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + gst_init (&argc, &argv); + g_test_set_nonfatal_assertions (); + g_test_add_func ("/gstswitch/options/mkdirs", test_mkdirs); + g_test_add_func ("/gstswitch/options/filename", test_new_filename); + return g_test_run (); +} diff --git a/tests/unit/test_gstswitchopts.c b/tests/unit/test_gstswitchopts.c new file mode 100644 index 0000000..50ef565 --- /dev/null +++ b/tests/unit/test_gstswitchopts.c @@ -0,0 +1,44 @@ + +#include "tools/gstswitchopts.c" + +static void +test_strings_good (void) +{ + g_assert_cmpint (parse_format ("debug", NULL, NULL), ==, 0); + g_assert_cmpint (parse_format ("pal", NULL, NULL), ==, 0); + g_assert_cmpint (parse_format ("720p60", NULL, NULL), ==, 0); + g_assert_cmpint (parse_format ("1024x768@60", NULL, NULL), ==, 0); + g_assert_cmpint (parse_format ("VGA@60", NULL, NULL), ==, 0); + g_assert_cmpint (parse_format ("4k@60", NULL, NULL), ==, 0); + g_assert_cmpint (parse_format + ("video/x-raw,height=400,width=500,framerate=25/1", NULL, NULL), ==, 0); +} + +static void +test_strings_bad (void) +{ + g_assert_cmpint (parse_format + ("video/x-raw,height=[400,800],width=500,framerate=25/1", NULL, NULL), ==, + -1); + g_assert_cmpint (parse_format ("720p@75", NULL, NULL), ==, -1); + g_assert_cmpint (parse_format ("bad-format-string", NULL, NULL), ==, -1); + g_assert_cmpint (parse_format + ("video/x-raw,height=10,width=500,framerate=25/1", NULL, NULL), ==, -1); + g_assert_cmpint (parse_format + ("video/x-raw,height=400,width=10,framerate=25/1", NULL, NULL), ==, -1); + g_assert_cmpint (parse_format + ("video/x-raw,height=400,width=10,framerate=1001/1", NULL, NULL), ==, -1); +// @FIXME the following actually works but it shouldn't +// g_assert_cmpint (parse_format("pal@75", NULL, NULL), ==, -1); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + gst_init (&argc, &argv); + g_test_set_nonfatal_assertions (); + g_test_add_func ("/gstswitch/options/format/good", test_strings_good); + g_test_add_func ("/gstswitch/options/format/bad", test_strings_bad); + return g_test_run (); +} diff --git a/tools/Makefile.am b/tools/Makefile.am index 8802a5a..b8b10c9 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,4 +1,8 @@ -bin_PROGRAMS = gst-switch-srv gst-switch-ui gst-switch-cap gst-switch-ptz +if SPEAKERTRACK_ENABLED + bin_PROGRAMS = gst-switch-srv gst-switch-ui gst-switch-cap gst-switch-ptz +else + bin_PROGRAMS = gst-switch-srv gst-switch-ui gst-switch-cap +endif if GCOV_ENABLED # --coverage @@ -11,11 +15,17 @@ GCOV_CFLAGS = GCOV_LFLAGS = endif +if DEBUG +AM_CFLAGS = -g3 -O0 -gdwarf-4 -fvar-tracking-assignments -fvar-tracking +else +AM_CFLAGS = -O2 +endif + gst_switch_srv_SOURCES = gstworker.c gstswitchserver.c gstcase.c \ gstcomposite.c gstswitchcontroller.c gstrecorder.c \ - gio/gsocketinputstream.c + gio/gsocketinputstream.c gstswitchopts.c gst_switch_srv_CFLAGS = $(GST_CFLAGS) $(GST_BASE_CFLAGS) $(GCOV_CFLAGS) \ - $(GST_PLUGINS_BASE_CFLAGS) -DLOG_PREFIX="\"./tools\"" + $(GST_PLUGINS_BASE_CFLAGS) $(AM_CFLAGS) -DLOG_PREFIX="\"gst-switch-srv\"" gst_switch_srv_LDFLAGS = $(GCOV_LFLAGS) $(GST_LIBS) $(GST_BASE_LIBS) \ $(GST_PLUGINS_BASE_LIBS) $(GSTPB_BASE_LIBS) gst_switch_srv_LDADD = $(GIO_LIBS) $(LIBM) @@ -23,8 +33,8 @@ gst_switch_srv_LDADD = $(GIO_LIBS) $(LIBM) gst_switch_ui_SOURCES = gstworker.c gstswitchui.c gstvideodisp.c \ gstaudiovisual.c gstswitchclient.c gst_switch_ui_CFLAGS = $(GST_CFLAGS) $(GST_BASE_CFLAGS) $(GCOV_CFLAGS) \ - $(GST_PLUGINS_BASE_CFLAGS) $(X_CFLAGS) $(GTK_CFLAGS) \ - -DLOG_PREFIX="\"./tools\"" + $(GST_PLUGINS_BASE_CFLAGS) $(X_CFLAGS) $(GTK_CFLAGS) $(AM_CFLAGS) \ + -DLOG_PREFIX="\"gst-switch-ui\"" gst_switch_ui_LDFLAGS = $(GCOV_LFLAGS) $(GST_LIBS) $(GST_BASE_LIBS) \ $(GST_PLUGINS_BASE_LIBS) $(GSTPB_BASE_LIBS) -lm gst_switch_ui_LDADD = $(GST_LIBS) $(X_LIBS) $(LIBM) $(GTK_LIBS) $(GLIB_LIBS) @@ -32,7 +42,7 @@ gst_switch_ui_LDADD = $(GST_LIBS) $(X_LIBS) $(LIBM) $(GTK_LIBS) $(GLIB_LIBS) gst_switch_cap_SOURCES = gstworker.c gstswitchcapture.c gstswitchclient.c gst_switch_cap_CFLAGS = $(GST_CFLAGS) $(GST_BASE_CFLAGS) $(GCOV_CFLAGS) \ $(GST_PLUGINS_BASE_CFLAGS) $(X_CFLAGS) $(GTK_CFLAGS) \ - -DLOG_PREFIX="\"./tools\"" + -DLOG_PREFIX="\"gst-switch-cap\"" gst_switch_cap_LDFLAGS = $(GCOV_LFLAGS) $(GST_LIBS) $(GST_BASE_LIBS) \ $(GST_PLUGINS_BASE_LIBS) $(GSTPB_BASE_LIBS) -lm gst_switch_cap_LDADD = $(GST_LIBS) $(X_LIBS) $(LIBM) $(GTK_LIBS) $(GLIB_LIBS) @@ -40,12 +50,12 @@ gst_switch_cap_LDADD = $(GST_LIBS) $(X_LIBS) $(LIBM) $(GTK_LIBS) $(GLIB_LIBS) gst_switch_ptz_SOURCES = gstworker.c gstvideodisp.c gstswitchptz.c gst_switch_ptz_CFLAGS = -g -ggdb $(GST_CFLAGS) $(GST_BASE_CFLAGS) $(GCOV_CFLAGS) \ $(GST_PLUGINS_BASE_CFLAGS) $(X_CFLAGS) $(GTK_CFLAGS) \ - -DLOG_PREFIX="\".\"" + -DLOG_PREFIX="\"gst-switch-ptz\"" gst_switch_ptz_LDFLAGS = $(GCOV_LFLAGS) $(GST_LIBS) $(GST_BASE_LIBS) \ $(GST_PLUGINS_BASE_LIBS) $(GST_PLUGINS_BAD_LIBS) $(GSTPB_BASE_LIBS) gst_switch_ptz_LDADD = $(GST_LIBS) $(X_LIBS) $(LIBM) $(GTK_LIBS) \ $(GLIB_LIBS) -lm \ - $(HOME)/gst/stage/lib/libgstcamcontrol-1.0.so + $(prefix)/lib/libgstcamcontrol-1.0.so # -lgstcamcontrol diff --git a/tools/gio/gsocketinputstream.c b/tools/gio/gsocketinputstream.c index 6ec2bff..99946ae 100644 --- a/tools/gio/gsocketinputstream.c +++ b/tools/gio/gsocketinputstream.c @@ -85,6 +85,21 @@ g_socket_input_stream_set_property (GObject * object, } } +static gboolean +g_socket_input_stream_close (GInputStream * s, + GCancellable * cancellable, GError ** error) +{ + GSocketInputStreamX *stream = G_SOCKET_INPUT_STREAM (s); + gboolean ret = TRUE; + + if (stream->priv->socket) { + ret = g_socket_close (stream->priv->socket, error); + g_object_unref (stream->priv->socket); + stream->priv->socket = NULL; + } + return ret; +} + static void g_socket_input_stream_finalize (GObject * object) { @@ -92,15 +107,10 @@ g_socket_input_stream_finalize (GObject * object) if (stream->priv->socket) { GError *error = NULL; - /* - g_print ("%s:%d: %s, %d\n", __FILE__, __LINE__, __FUNCTION__, - g_socket_get_fd (stream->priv->socket)); - */ - g_socket_close (stream->priv->socket, &error); + g_socket_input_stream_close (&stream->parent_instance, NULL, &error); if (error) { //ERROR ("%s", error->message); } - g_object_unref (stream->priv->socket); } if (G_OBJECT_CLASS (g_socket_input_stream_parent_class)->finalize) @@ -117,6 +127,7 @@ g_socket_input_stream_read (GInputStream * stream, buffer, count, TRUE, cancellable, error); } + static void g_socket_input_stream_class_init (GSocketInputStreamXClass * klass) { @@ -130,6 +141,7 @@ g_socket_input_stream_class_init (GSocketInputStreamXClass * klass) gobject_class->set_property = g_socket_input_stream_set_property; ginputstream_class->read_fn = g_socket_input_stream_read; + ginputstream_class->close_fn = g_socket_input_stream_close; g_object_class_install_property (gobject_class, PROP_SOCKET, g_param_spec_object ("socket", diff --git a/tools/gio/gsocketinputstream.h b/tools/gio/gsocketinputstream.h index 9d319f4..98ed08b 100644 --- a/tools/gio/gsocketinputstream.h +++ b/tools/gio/gsocketinputstream.h @@ -62,4 +62,4 @@ GSocketInputStreamX * _g_socket_input_stream_new (GSocke G_END_DECLS #undef __GIO_GIO_H_INSIDE__ -#endif /* __G_SOCKET_INPUT_STREAM_H___ */ +#endif //__G_SOCKET_INPUT_STREAM_H__ diff --git a/tools/gstaudiovisual.c b/tools/gstaudiovisual.c index 8308fb8..c8553d1 100644 --- a/tools/gstaudiovisual.c +++ b/tools/gstaudiovisual.c @@ -238,22 +238,26 @@ gst_audio_visual_get_pipeline_string (GstAudioVisual * visual) desc = g_string_new (""); - g_string_append_printf (desc, "tcpclientsrc name=source " - "port=%d ", visual->port); - g_string_append_printf (desc, "! gdpdepay ! faad ! tee name=a "); + g_string_append_printf ( + desc, "tcpclientsrc name=source port=%d ", visual->port); + g_string_append_printf (desc, "! gdpdepay ! tee name=a\n"); if (visual->active) { - g_string_append_printf (desc, "a. ! queue2 ! audioconvert " - "! level name=level message=true " "! alsasink name=play "); + g_string_append_printf ( + desc, + "a. ! queue ! audioconvert ! level name=level message=true !" + " autoaudiosink name=play sync=false\n"); } - g_string_append_printf (desc, "a. ! queue2 ! audioconvert ! monoscope "); + g_string_append_printf (desc, "a. ! queue ! audioconvert ! monoscope "); if (visual->active) { g_string_append_printf (desc, "! textoverlay text=\"active\" " "font-desc=\"Sans 50\" " "shaded-background=true " "auto-resize=true "); } - g_string_append_printf (desc, "! autovideoconvert "); - g_string_append_printf (desc, "! xvimagesink name=visual "); + g_string_append_printf (desc, "! videoconvert "); + g_string_append_printf (desc, "! xvimagesink name=visual sync=false "); + + INFO("Audio preview: %s", desc->str); return desc; } @@ -341,12 +345,12 @@ gst_audio_visual_message (GstAudioVisual * visual, GstMessage * message) INFO (" channel: %d, RMS=%f dB, peak=%f dB, decay=%f dB, " "normalized-RMS=%f", i, rms_dB, peak_dB, decay_dB, rms); } - } else if (G_VALUE_HOLDS (list_rms, G_TYPE_VALUE_ARRAY)) { - GValueArray *va = (GValueArray *) g_value_get_boxed (list_rms); + } else if (G_VALUE_HOLDS (list_rms, G_TYPE_ARRAY)) { + GArray *va = (GArray *) g_value_get_boxed (list_rms); gdouble v = 0.0; - channels = va->n_values; + channels = va->len; for (i = 0; i < channels; ++i) { - value = g_value_array_get_nth (va, i); + value = g_array_index (va, GValue*, i); rms_dB = g_value_get_double (value); /* converting from dB to normal gives us a value between 0.0 and 1.0 diff --git a/tools/gstaudiovisual.h b/tools/gstaudiovisual.h index f782784..50ffef0 100644 --- a/tools/gstaudiovisual.h +++ b/tools/gstaudiovisual.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_AUDIO_VISUAL_H__by_Duzy_Chan__ -#define __GST_AUDIO_VISUAL_H__by_Duzy_Chan__ 1 +#ifndef __GST_AUDIO_VISUAL_H__ +#define __GST_AUDIO_VISUAL_H__ + #include "gstworker.h" #define GST_TYPE_AUDIO_VISUAL (gst_audio_visual_get_type ()) @@ -56,7 +57,7 @@ struct _GstAudioVisual gboolean renewing; /*!< Used by GstSwitchUI. */ GMutex endtime_lock; /*!< the lock for %endtime */ - GstClockTime endtime; /*!< the endtime of the last audo sample */ + GstClockTime endtime; /*!< the endtime of the last audio sample */ GMutex value_lock; /*!< the lock for %value */ gdouble value; /*!< the value of the last audio sample */ }; @@ -76,4 +77,4 @@ GType gst_audio_visual_get_type (void); GstClockTime gst_audio_visual_get_endtime (GstAudioVisual * visual); gdouble gst_audio_visual_get_value (GstAudioVisual * visual); -#endif //__GST_AUDIO_VISUAL_H__by_Duzy_Chan__ +#endif //__GST_AUDIO_VISUAL_H__ diff --git a/tools/gstcase.c b/tools/gstcase.c index 22e4940..c6db90b 100644 --- a/tools/gstcase.c +++ b/tools/gstcase.c @@ -90,24 +90,20 @@ gst_case_init (GstCase * cas) } /** - * @param cas The GstCase instance. + * @param cas the GstCase instance. * @memberof GstCase * - * Disposing from it's parent object. - * - * @see GObject + * Closes/releases resources used by the GstCase */ static void -gst_case_dispose (GstCase * cas) +gst_case_close (GstCase * cas) { if (cas->stream) { -#if 0 GError *error = NULL; g_input_stream_close (cas->stream, NULL, &error); if (error) { ERROR ("%s", error->message); } -#endif g_object_unref (cas->stream); cas->stream = NULL; } @@ -121,6 +117,20 @@ gst_case_dispose (GstCase * cas) g_object_unref (cas->branch); cas->branch = NULL; } +} + +/** + * @param cas The GstR (Case instance. + * @memberof GstCase + * + * Disposing from it's parent object. + * + * @see GObject + */ +static void +gst_case_dispose (GstCase * cas) +{ + gst_case_close (cas); //INFO ("dispose %p", cas); G_OBJECT_CLASS (parent_class)->dispose (G_OBJECT (cas)); } @@ -281,167 +291,83 @@ gst_case_set_property (GstCase * cas, guint property_id, static GString * gst_case_get_pipeline_string (GstCase * cas) { - GString *desc; - gchar *channel = NULL; - gchar *caps = NULL; - gchar *scale = NULL; - gchar *srctype = NULL; - gchar *sink = NULL; - - desc = g_string_new (""); + gboolean is_audiostream = cas->serve_type == GST_SERVE_AUDIO_STREAM; + GString *desc = g_string_new (""); + const gchar *caps = + is_audiostream ? + gst_switch_server_get_audio_caps_str () : + gst_switch_server_get_video_caps_str (); switch (cas->type) { - case GST_CASE_INPUT_a: - case GST_CASE_INPUT_v: - g_string_append_printf (desc, "giostreamsrc name=source "); + case GST_CASE_INPUT_AUDIO: + g_string_append_printf (desc, + "giostreamsrc name=source ! gdpdepay ! %s ! interaudiosink name=sink channel=input_%d", + caps, cas->sink_port); + break; + + case GST_CASE_INPUT_VIDEO: + g_string_append_printf (desc, + "giostreamsrc name=source ! gdpdepay ! %s ! intervideosink name=sink channel=input_%d", + caps, cas->sink_port); break; - case GST_CASE_COMPOSITE_A: - case GST_CASE_COMPOSITE_B: - case GST_CASE_COMPOSITE_a: + case GST_CASE_PREVIEW: - if (srctype == NULL) - srctype = "input"; - case GST_CASE_BRANCH_A: - case GST_CASE_BRANCH_B: - case GST_CASE_BRANCH_a: - case GST_CASE_BRANCH_p: - if (srctype == NULL) - srctype = "branch"; - if (cas->serve_type == GST_SERVE_AUDIO_STREAM) { - g_string_append_printf (desc, "interaudiosrc"); + if (is_audiostream) { + g_string_append_printf (desc, + "interaudiosrc name=source channel=input_%d ! audioparse raw-format=s16le rate=48000 ! interaudiosink name=sink channel=branch_%d", + cas->sink_port, cas->sink_port); } else { - g_string_append_printf (desc, "intervideosrc"); + g_string_append_printf (desc, + "intervideosrc name=source channel=input_%d ! %s ! intervideosink name=sink channel=branch_%d", + cas->sink_port, caps, cas->sink_port); } - g_string_append_printf (desc, " name=source channel=%s_%d ", - srctype, cas->sink_port); break; - default: - ERROR ("unknown case %d", cas->type); + + case GST_CASE_COMPOSITE_AUDIO: + g_string_append_printf (desc, + "interaudiosrc name=source channel=input_%d ! audioparse raw-format=s16le rate=48000 ! tee name=s " + "s. ! queue ! interaudiosink name=sink1 channel=branch_%d " + "s. ! queue ! interaudiosink name=sink2 channel=composite_audio", + cas->sink_port, cas->sink_port); break; - } - srctype = NULL; + case GST_CASE_COMPOSITE_VIDEO_A: + case GST_CASE_COMPOSITE_VIDEO_B: + { + gchar *channel = cas->type == GST_CASE_COMPOSITE_VIDEO_A ? "a" : "b"; + g_string_append_printf (desc, + "intervideosrc name=source channel=input_%d ! %s ! tee name=s " + "s. ! queue ! intervideosink name=sink1 channel=branch_%d " + "s. ! queue ! intervideosink name=sink2 channel=composite_%s", + cas->sink_port, caps, cas->sink_port, channel); + break; + } - switch (cas->type) { - case GST_CASE_COMPOSITE_A: - if (channel == NULL) - channel = "a"; - if (scale == NULL) - scale = - g_strdup_printf ("videoscale ! video/x-raw,width=%d,height=%d", - cas->a_width, cas->a_height); - case GST_CASE_COMPOSITE_B: - if (channel == NULL) - channel = "b"; - if (scale == NULL) - scale = - g_strdup_printf ("videoscale ! video/x-raw,width=%d,height=%d", - cas->b_width, cas->b_height); - caps = - g_strdup_printf ("video/x-raw,width=%d,height=%d", cas->width, - cas->height); - sink = "intervideosink"; - case GST_CASE_COMPOSITE_a: - if (channel == NULL) - channel = "audio"; - if (sink == NULL) - sink = "interaudiosink"; - g_string_append_printf (desc, "source. "); - /* - ASSESS ("assess-composite-%s-source-%d", channel, cas->sink_port); - */ - if (caps) - g_string_append_printf (desc, "! %s ", caps); - g_string_append_printf (desc, "! tee name=s "); - g_string_append_printf (desc, "s. ! queue2 "); - /* - ASSESS ("assess-composite-%s-branch-%d", channel, cas->sink_port); - */ - g_string_append_printf (desc, "! %s name=sink1 channel=branch_%d ", - sink, cas->sink_port); - g_string_append_printf (desc, "s. ! queue2 "); - if (scale) { - /* - g_string_append_printf (desc, "! %s", scale); - */ - } - ASSESS ("assess-composite-%s-compose-%d", channel, cas->sink_port); - g_string_append_printf (desc, "! %s name=sink2 channel=composite_%s ", - sink, channel); - if (scale) - g_free (scale), scale = NULL; - if (caps) - g_free (caps), caps = NULL; + case GST_CASE_BRANCH_AUDIO: + g_string_append_printf (desc, + "interaudiosrc name=source channel=branch_%d ! audioparse raw-format=s16le rate=48000 ! gdppay ! tcpserversink name=sink port=%d", + cas->sink_port, cas->sink_port); break; - case GST_CASE_PREVIEW: - if (srctype == NULL) - srctype = "branch"; - case GST_CASE_INPUT_a: - case GST_CASE_INPUT_v: - if (srctype == NULL) - srctype = "input"; - if (cas->serve_type == GST_SERVE_AUDIO_STREAM) { - g_string_append_printf (desc, "interaudiosink"); - } else { - g_string_append_printf (desc, "intervideosink"); - } - g_string_append_printf (desc, " name=sink channel=%s_%d ", - srctype, cas->sink_port); - - if (cas->serve_type == GST_SERVE_AUDIO_STREAM) { - g_string_append_printf (desc, "source. "); - /* - if (cas->type == GST_CASE_PREVIEW) - ASSESS ("assess-audio-preview-%d", cas->sink_port); - else - ASSESS ("assess-audio-input-%d", cas->sink_port); - */ - //g_string_append_printf (desc, "! gdpdepay "); - g_string_append_printf (desc, "! sink. "); - } else if (cas->type == GST_CASE_PREVIEW) { - g_string_append_printf (desc, "source. " "! video/x-raw,width=%d,height=%d ", cas->width, cas->height); //cas->a_width, cas->a_height); - /* - ASSESS ("assess-video-preview-%d", cas->sink_port); - */ - g_string_append_printf (desc, "! sink. "); - } else { - g_string_append_printf (desc, "source. "); - ASSESS ("assess-video-input-%d", cas->sink_port); - g_string_append_printf (desc, "! gdpdepay ! sink. "); - } + + case GST_CASE_BRANCH_VIDEO_A: + case GST_CASE_BRANCH_VIDEO_B: + g_string_append_printf (desc, + "intervideosrc name=source channel=branch_%d ! %s ! gdppay ! tcpserversink name=sink port=%d", + cas->sink_port, caps, cas->sink_port); break; - case GST_CASE_BRANCH_A: - case GST_CASE_BRANCH_B: - case GST_CASE_BRANCH_a: - case GST_CASE_BRANCH_p: - g_string_append_printf (desc, "tcpserversink name=sink port=%d ", - cas->sink_port); - g_string_append_printf (desc, "source. "); - if (cas->serve_type == GST_SERVE_AUDIO_STREAM) { - /* - ASSESS ("assess-branch-source-%d", cas->sink_port); - */ - g_string_append_printf (desc, "! faac "); - /* - ASSESS ("assess-branch-audio-encoded-%d", cas->sink_port); - */ - } else { - g_string_append_printf (desc, "! video/x-raw,width=%d,height=%d ", cas->width, cas->height); //cas->a_width, cas->a_height); - /* - ASSESS ("assess-branch-source-%d", cas->sink_port); - */ - } - g_string_append_printf (desc, "! gdppay "); - /* - ASSESS ("assess-branch-payed-%d", cas->sink_port); - */ - g_string_append_printf (desc, "! sink. "); + + case GST_CASE_BRANCH_PREVIEW: + g_string_append_printf (desc, + "intervideosrc name=source channel=branch_%d ! %s ! gdppay ! tcpserversink name=sink port=%d", + cas->sink_port, caps, cas->sink_port); break; - case GST_CASE_UNKNOWN: + + default: ERROR ("unknown case (%d)", cas->type); break; } + INFO ("pipeline(%p): %s\n", cas, desc->str); return desc; } @@ -493,8 +419,8 @@ gst_case_prepare (GstCase * cas) GstWorker *worker = GST_WORKER (cas); GstElement *source = NULL; switch (cas->type) { - case GST_CASE_INPUT_a: - case GST_CASE_INPUT_v: + case GST_CASE_INPUT_AUDIO: + case GST_CASE_INPUT_VIDEO: if (!cas->stream) { ERROR ("no stream for new case"); return FALSE; @@ -508,10 +434,10 @@ gst_case_prepare (GstCase * cas) gst_object_unref (source); break; - case GST_CASE_BRANCH_A: - case GST_CASE_BRANCH_B: - case GST_CASE_BRANCH_a: - case GST_CASE_BRANCH_p: + case GST_CASE_BRANCH_VIDEO_A: + case GST_CASE_BRANCH_VIDEO_B: + case GST_CASE_BRANCH_AUDIO: + case GST_CASE_BRANCH_PREVIEW: { GstElement *sink = gst_worker_get_element_unlocked (worker, "sink"); @@ -589,45 +515,46 @@ gst_case_class_init (GstCaseClass * klass) g_param_spec_uint ("width", "Width", "Output width", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_WIDTH, + gst_composite_default_width (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_HEIGHT, g_param_spec_uint ("height", "Height", "Output height", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT, + gst_composite_default_height (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_A_WIDTH, g_param_spec_uint ("awidth", "A Width", "Channel A width", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_WIDTH, + gst_composite_default_width (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_A_HEIGHT, g_param_spec_uint ("aheight", "A Height", "Channel A height", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT, + gst_composite_default_height (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_B_WIDTH, g_param_spec_uint ("bwidth", "B Width", "Channel B width", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_WIDTH, + gst_composite_default_width (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_B_HEIGHT, g_param_spec_uint ("bheight", "B Height", "Channel B height", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT, + gst_composite_default_height (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); worker_class->prepare = (GstWorkerPrepareFunc) gst_case_prepare; worker_class->get_pipeline_string = (GstWorkerGetPipelineStringFunc) gst_case_get_pipeline_string; + worker_class->close = (GstWorkerCloseFunc) gst_case_close; } diff --git a/tools/gstcase.h b/tools/gstcase.h index e2c7c91..a1619dc 100644 --- a/tools/gstcase.h +++ b/tools/gstcase.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_CASE_H__by_Duzy_Chan__ -#define __GST_CASE_H__by_Duzy_Chan__ 1 +#ifndef __GST_CASE_H__ +#define __GST_CASE_H__ + #include "gstworker.h" #include @@ -44,18 +45,18 @@ typedef struct _GstCaseClass GstCaseClass; */ typedef enum { - GST_CASE_UNKNOWN, /*!< unknown case */ - GST_CASE_COMPOSITE_A, /*!< special case for composite channel A */ - GST_CASE_COMPOSITE_B, /*!< special case for composite channel B */ - GST_CASE_COMPOSITE_a, /*!< special case for composite channel audio */ - GST_CASE_PREVIEW, /*!< special case for previews */ - GST_CASE_INPUT_a, /*!< special case for audio input */ - GST_CASE_INPUT_v, /*!< special case for video input */ - GST_CASE_BRANCH_A,/*!< special case for branching channel A to output */ - GST_CASE_BRANCH_B,/*!< special case for branching channel B to output */ - GST_CASE_BRANCH_a,/*!< special case for branching active audio to output */ - GST_CASE_BRANCH_p,/*!< special case for branching preview to output */ - GST_CASE__LAST_TYPE = GST_CASE_BRANCH_p + GST_CASE_UNKNOWN, /*!< unknown case */ + GST_CASE_COMPOSITE_VIDEO_A, /*!< special case for composite channel A */ + GST_CASE_COMPOSITE_VIDEO_B, /*!< special case for composite channel B */ + GST_CASE_COMPOSITE_AUDIO, /*!< special case for composite channel audio */ + GST_CASE_PREVIEW, /*!< special case for previews */ + GST_CASE_INPUT_AUDIO, /*!< Audio input from TCP socket */ + GST_CASE_INPUT_VIDEO, /*!< Video input from TCP socket */ + GST_CASE_BRANCH_VIDEO_A, /*!< special case for branching channel A to output */ + GST_CASE_BRANCH_VIDEO_B, /*!< special case for branching channel B to output */ + GST_CASE_BRANCH_AUDIO, /*!< special case for branching active audio to output */ + GST_CASE_BRANCH_PREVIEW, /*!< special case for branching preview to output */ + GST_CASE__LAST_TYPE = GST_CASE_BRANCH_PREVIEW } GstCaseType; /** @@ -116,4 +117,4 @@ typedef struct _GstCaseClass GType gst_case_get_type (void); -#endif //__GST_CASE_H__by_Duzy_Chan__ +#endif //__GST_CASE_H__ diff --git a/tools/gstcomposite.c b/tools/gstcomposite.c index 99791c4..f7be15a 100644 --- a/tools/gstcomposite.c +++ b/tools/gstcomposite.c @@ -156,11 +156,11 @@ gst_composite_set_mode (GstComposite * composite, GstCompositeMode mode) return; } - composite->width = GST_SWITCH_COMPOSITE_DEFAULT_WIDTH; - composite->height = GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT; + composite->width = gst_composite_default_width (); + composite->height = gst_composite_default_height (); switch ((composite->mode = mode)) { - case COMPOSE_MODE_0: + case COMPOSE_MODE_NONE: composite->a_x = 0; composite->a_y = 0; composite->a_width = composite->width; @@ -172,7 +172,7 @@ gst_composite_set_mode (GstComposite * composite, GstCompositeMode mode) composite->width = composite->a_width; composite->height = composite->a_height; break; - case COMPOSE_MODE_1: + case COMPOSE_MODE_PIP: composite->a_x = 0; composite->a_y = 0; composite->a_width = composite->width; @@ -184,7 +184,7 @@ gst_composite_set_mode (GstComposite * composite, GstCompositeMode mode) composite->width = composite->a_width; composite->height = composite->a_height; break; - case COMPOSE_MODE_2: + case COMPOSE_MODE_DUAL_PREVIEW: composite->a_x = 0; composite->a_y = 0; composite->a_width = (guint) ((double) composite->width * 0.7 + 0.5); @@ -196,7 +196,7 @@ gst_composite_set_mode (GstComposite * composite, GstCompositeMode mode) composite->b_height = composite->height - composite->a_y - composite->a_height; break; - case COMPOSE_MODE_3: + case COMPOSE_MODE_DUAL_EQUAL: composite->a_width = (guint) ((double) composite->width * 0.5 + 0.5); composite->a_height = (guint) ((double) composite->height * 0.5 + 0.5); composite->a_x = 0; @@ -236,7 +236,7 @@ gst_composite_ready_for_transition (GstComposite * composite) /** * gst_composite_start_transition: * - * Start the new transtition request, this will set the %transition flag into + * Start the new transition request, this will set the %transition flag into * TRUE. */ static void @@ -302,7 +302,7 @@ gst_composite_set_property (GstComposite * composite, guint property_id, case PROP_MODE: { guint mode = g_value_get_uint (value); - if (COMPOSE_MODE_0 <= mode && mode <= COMPOSE_MODE__LAST) { + if (COMPOSE_MODE_NONE <= mode && mode <= COMPOSE_MODE__LAST) { gst_composite_set_mode (composite, (GstCompositeMode) mode); } else { WARN ("invalid composite mode %d", mode); @@ -425,14 +425,14 @@ gst_composite_get_pipeline_string (GstComposite * composite) g_string_append_printf (desc, "intervideosrc name=source_a channel=composite_a_scaled "); - if (composite->mode == COMPOSE_MODE_0) { + if (composite->mode == COMPOSE_MODE_NONE) { g_string_append_printf (desc, "source_a. ! video/x-raw,width=%d,height=%d ", composite->a_width, composite->a_height); /* ASSESS ("assess-compose-a-source"); */ - g_string_append_printf (desc, "! queue2 "); + g_string_append_printf (desc, "! queue "); g_string_append_printf (desc, "! identity name=mix "); } else { g_string_append_printf (desc, @@ -452,7 +452,7 @@ gst_composite_get_pipeline_string (GstComposite * composite) "source_b. ! video/x-raw,width=%d,height=%d ", composite->b_width, composite->b_height); ASSESS ("assess-compose-b-source"); - g_string_append_printf (desc, "! queue2 "); + g_string_append_printf (desc, "! queue "); #if 0 if (composite->width != composite->b_width || composite->height != composite->b_height) { @@ -471,7 +471,7 @@ gst_composite_get_pipeline_string (GstComposite * composite) "source_a. ! video/x-raw,width=%d,height=%d ", composite->a_width, composite->a_height); ASSESS ("assess-compose-a-source"); - g_string_append_printf (desc, "! queue2 "); + g_string_append_printf (desc, "! queue "); #if 0 if (composite->width != composite->a_width || composite->height != composite->a_height) { @@ -491,7 +491,7 @@ gst_composite_get_pipeline_string (GstComposite * composite) ASSESS ("assess-compose-result"); g_string_append_printf (desc, "! tee name=result "); - g_string_append_printf (desc, "result. ! queue2 "); + g_string_append_printf (desc, "result. ! queue "); /* ASSESS ("assess-compose-to-output"); */ @@ -500,7 +500,7 @@ gst_composite_get_pipeline_string (GstComposite * composite) "intervideosink name=out channel=composite_out "); if (opts.record_filename) { - g_string_append_printf (desc, "result. ! queue2 "); + g_string_append_printf (desc, "result. ! queue "); /* ASSESS ("assess-compose-to-record"); */ @@ -536,7 +536,7 @@ gst_composite_get_scaler_string (GstWorker * worker, GstComposite * composite) g_string_append_printf (desc, "source_a. ! video/x-raw,width=%d,height=%d ", composite->width, composite->height); - g_string_append_printf (desc, "! queue2 "); + g_string_append_printf (desc, "! queue "); /* g_string_append_printf (desc, "! videoconvert ! facedetect2 ! speakertrack ! videoconvert "); @@ -545,7 +545,7 @@ gst_composite_get_scaler_string (GstWorker * worker, GstComposite * composite) "! videoscale ! video/x-raw,width=%d,height=%d ! sink_a. ", composite->a_width, composite->a_height); - if (composite->mode == COMPOSE_MODE_0) { + if (composite->mode == COMPOSE_MODE_NONE) { } else { g_string_append_printf (desc, "intervideosrc name=source_b channel=composite_b "); @@ -555,7 +555,7 @@ gst_composite_get_scaler_string (GstWorker * worker, GstComposite * composite) g_string_append_printf (desc, "source_b. ! video/x-raw,width=%d,height=%d ", composite->width, composite->height); - g_string_append_printf (desc, "! queue2 "); + g_string_append_printf (desc, "! queue "); /* g_string_append_printf (desc, "! videoconvert ! facedetect2 ! speakertrack ! videoconvert "); @@ -597,7 +597,7 @@ gst_composite_prepare (GstComposite * composite) /** * gst_composite_start: * - * This is invokved when the composite pipeline started. + * This is invoked when the composite pipeline started. */ static void gst_composite_start (GstComposite * composite) @@ -751,7 +751,7 @@ gst_composite_close_adjustment (GstComposite * composite) /** * gst_composite_alive: * - * Invoked when the compisite pipeline is online. + * Invoked when the composite pipeline is online. */ static void gst_composite_alive (GstComposite * composite) @@ -794,6 +794,89 @@ gst_composite_null (GstComposite * composite) return composite->deprecated ? GST_WORKER_NR_END : GST_WORKER_NR_REPLAY; } + +/* gst_caps_get_value: + * - the missing API + * iterate the gst_structure in the given caps to return a specified field vaue + */ +static gboolean +gst_caps_get_value (GstCaps * caps, const gchar * field, GValue * value) +{ + guint i; + GstStructure *structure; + for (i = 0; (structure = gst_caps_get_structure (caps, i)) != NULL; i++) { + const GValue *svalue = gst_structure_get_value (structure, field); + if (svalue != NULL) { + g_value_copy (svalue, value); + return TRUE; + } + } + return FALSE; +} + +static gint +gst_composite_get_caps_int_value (const gchar * fieldname) +{ + GstCaps *caps = gst_switch_server_getcaps (); + g_assert (caps != NULL && !gst_caps_is_empty (caps)); + GValue value = G_VALUE_INIT; + g_value_init (&value, G_TYPE_INT); + gst_caps_get_value (caps, fieldname, &value); + return g_value_get_int (&value); +} + +/* cache these so we don't need to do the above every time */ +static gint cached_default_width = -1; +static gint cached_default_height = -1; + +/* gst_composite_defaut_width: + * + * @return default width based on current video caps + */ +gint +gst_composite_default_width () +{ + if (cached_default_width == -1) + cached_default_width = gst_composite_get_caps_int_value ("width"); + return cached_default_width; +} + +/* gst_composite_defaut_height: + * + * @return default height based on current video caps + */ +gint +gst_composite_default_height () +{ + if (cached_default_height == -1) + cached_default_height = gst_composite_get_caps_int_value ("height"); + return cached_default_height; +} + +/* gst_composite_min_pip_width: + * + * @param pip_w desired width of PIP + * @return enforce minimum width of PIP + */ +gint +gst_check_composite_min_pip_width (gint pip_w) +{ + gint min_width = gst_composite_default_width () / 4; + return pip_w < min_width ? min_width : pip_w; +} + +/* gst_composite_min_pip_height: + * + * @param pip_w desired height of PIP + * @return enforce minimum height of PIP + */ +gint +gst_check_composite_min_pip_height (gint pip_h) +{ + gint min_height = gst_composite_default_height () / 3; + return pip_h < min_height ? min_height : pip_h; +} + /** * gst_composite_adjust_pip: * @param composite The GstComposite instance @@ -801,7 +884,7 @@ gst_composite_null (GstComposite * composite) * @param y the Y position of the PIP * @param w the width of the PIP * @param h the height of the PIP - * @return PIP has been changed succefully + * @return PIP has been changed successfully * * Change the PIP position and size. */ @@ -1026,7 +1109,7 @@ gst_composite_class_init (GstCompositeClass * klass) g_object_class_install_property (object_class, PROP_MODE, g_param_spec_uint ("mode", "Mode", "Composite Mode", - COMPOSE_MODE_0, + COMPOSE_MODE_NONE, COMPOSE_MODE__LAST, DEFAULT_COMPOSE_MODE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); @@ -1060,15 +1143,15 @@ gst_composite_class_init (GstCompositeClass * klass) g_param_spec_uint ("awidth", "A Width", "Channel A frame width", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_WIDTH, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + gst_composite_default_width + (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_A_HEIGHT, g_param_spec_uint ("aheight", "A Height", "Channel A frame height", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + gst_composite_default_height + (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_B_X, g_param_spec_uint ("bx", "B xpos", @@ -1084,31 +1167,31 @@ gst_composite_class_init (GstCompositeClass * klass) g_param_spec_uint ("bwidth", "B Width", "Channel B frame width", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_WIDTH, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + gst_composite_default_width + (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_B_HEIGHT, g_param_spec_uint ("bheight", "B Height", "Channel B frame height", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + gst_composite_default_height + (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_WIDTH, g_param_spec_uint ("width", "Composite Width", "Output frame width", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_WIDTH, - G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + gst_composite_default_width + (), G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_HEIGHT, g_param_spec_uint ("height", "Composite Height", "Output frame height", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT, - G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + gst_composite_default_height + (), G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); worker_class->alive = (GstWorkerAliveFunc) gst_composite_alive; worker_class->null = (GstWorkerNullFunc) gst_composite_null; diff --git a/tools/gstcomposite.h b/tools/gstcomposite.h index f6b8813..39ca565 100644 --- a/tools/gstcomposite.h +++ b/tools/gstcomposite.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_COMPOSITE_H__by_Duzy_Chan__ -#define __GST_COMPOSITE_H__by_Duzy_Chan__ 1 +#ifndef __GST_COMPOSITE_H__ +#define __GST_COMPOSITE_H__ + #include "gstworker.h" #define GST_TYPE_COMPOSITE (gst_composite_get_type ()) @@ -35,35 +36,50 @@ #define GST_IS_COMPOSITE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), GST_TYPE_COMPOSITE)) #define GST_IS_COMPOSITE_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE ((class), GST_TYPE_COMPOSITE)) -#if ENABLE_LOW_RESOLUTION -#define GST_SWITCH_COMPOSITE_DEFAULT_WIDTH LOW_RES_W /* 640 */ -#define GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT LOW_RES_H /* 480 */ -#define GST_SWITCH_COMPOSITE_MIN_PIP_W 13 -#define GST_SWITCH_COMPOSITE_MIN_PIP_H 7 -#else -#define GST_SWITCH_COMPOSITE_DEFAULT_WIDTH 1280 -#define GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT 720 -#define GST_SWITCH_COMPOSITE_MIN_PIP_W 320 -#define GST_SWITCH_COMPOSITE_MIN_PIP_H 240 -#endif +//#if ENABLE_LOW_RESOLUTION +//#define GST_SWITCH_COMPOSITE_DEFAULT_WIDTH LOW_RES_W /* 640 */ +//#define GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT LOW_RES_H /* 480 */ +//#define GST_SWITCH_COMPOSITE_MIN_PIP_W 13 +//#define GST_SWITCH_COMPOSITE_MIN_PIP_H 7 +//#else +//#define GST_SWITCH_COMPOSITE_DEFAULT_WIDTH 1280 +//#define GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT 720 +//#define GST_SWITCH_COMPOSITE_MIN_PIP_W 320 +//#define GST_SWITCH_COMPOSITE_MIN_PIP_H 240 +//#endif #define GST_SWITCH_FACEDETECT_FRAME_WIDTH 150 #define GST_SWITCH_FACEDETECT_FRAME_HEIGHT 100 -#define DEFAULT_COMPOSE_MODE COMPOSE_MODE_3 +#define DEFAULT_COMPOSE_MODE COMPOSE_MODE_DUAL_EQUAL /** * @enum GstCompositeMode: */ typedef enum { - COMPOSE_MODE_0, /*!< none */ - COMPOSE_MODE_1, /*!< picture-in-picture */ - COMPOSE_MODE_2, /*!< side-by-side (preview) */ - COMPOSE_MODE_3, /*!< side-by-side (equal) */ - COMPOSE_MODE__LAST = COMPOSE_MODE_3 + COMPOSE_MODE_NONE, /*!< none */ + COMPOSE_MODE_PIP, /*!< picture-in-picture */ + COMPOSE_MODE_DUAL_PREVIEW, /*!< side-by-side (preview) */ + COMPOSE_MODE_DUAL_EQUAL, /*!< side-by-side (equal) */ + COMPOSE_MODE__LAST = COMPOSE_MODE_DUAL_EQUAL } GstCompositeMode; +inline static const char* gst_composite_mode_to_string(GstCompositeMode mode) { + switch(mode) { + case COMPOSE_MODE_NONE: + return "COMPOSE_MODE_NONE"; + case COMPOSE_MODE_PIP: + return "COMPOSE_MODE_PIP"; + case COMPOSE_MODE_DUAL_PREVIEW: + return "COMPOSE_MODE_DUAL_PREVIEW"; + case COMPOSE_MODE_DUAL_EQUAL: + return "COMPOSE_MODE_DUAL_EQUAL"; + } + //ASSERT(false); + return "COMPOSE_INVALID_VALUE"; +} + typedef struct _GstComposite GstComposite; typedef struct _GstCompositeClass GstCompositeClass; @@ -89,7 +105,7 @@ typedef struct _GstCompositeClass GstCompositeClass; * @param adjusting the status of adjusting PIP * @param transition the status of transiting modes * @param deprecated (deprecated) - * @param scaler the scaller for A/B videos + * @param scaler the scaler for A/B videos */ struct _GstComposite { @@ -138,5 +154,10 @@ struct _GstCompositeClass GType gst_composite_get_type (void); gboolean gst_composite_adjust_pip (GstComposite * composite, gint x, gint y, gint w, gint h); +gint gst_composite_default_width (); +gint gst_composite_default_height (); +gint gst_check_composite_min_pip_width (gint pip_w); +gint gst_check_composite_min_pip_height (gint pip_h); + -#endif //__GST_COMPOSITE_H__by_Duzy_Chan__ +#endif //__GST_COMPOSITE_H__ diff --git a/tools/gstrecorder.c b/tools/gstrecorder.c index 7440778..025fe65 100644 --- a/tools/gstrecorder.c +++ b/tools/gstrecorder.c @@ -33,6 +33,7 @@ #include #include #include +#include #include "gstswitchserver.h" #include "gstcomposite.h" #include "gstrecorder.h" @@ -71,6 +72,9 @@ gst_recorder_init (GstRecorder * rec) rec->width = 0; rec->height = 0; + // Recording pipeline needs clean shut-down + // via EOS to close out each recording + GST_WORKER (rec)->send_eos_on_stop = TRUE; //INFO ("init %p", rec); } @@ -170,45 +174,102 @@ gst_recorder_set_property (GstRecorder * rec, guint property_id, } } +/* + * @param dir - directory to create + * @return nothing + * Create a directory and all intermediary directories + * if necessary. Note that errors are ignored here, if + * the resulting path is in fact unusable having early + * warning here is not necessary + */ +static void +gst_recorder_mkdirs (const char *dir) +{ + char tmp[256]; + strncpy (tmp, dir, sizeof (tmp)); + size_t len = strlen (tmp); + if (len > 0) { + if (tmp[len - 1] == '/') + tmp[len--] = 0; + if (len > 0) { + size_t at = 1; // skip leading slash + while (at < len) { + char *p = strchr (tmp + at, '/'); + if (p != NULL && *p == '/') { + *p = '\0'; + mkdir (tmp, S_IRWXU); + *p = '/'; + at = p - tmp + 1; + } else + at = len; + } + mkdir (tmp, S_IRWXU); + } + } +} + /** - * @param rec The GstRecorder instance. - * @memberof GstRecorder + * @param filepath - file file/path of a unix file + * @return length of the path potion of the file, excluding the separator + */ +static size_t +gst_recorder_pathlen (const char *filepath) +{ + if (filepath != NULL && strlen (filepath) > 0) { + char const *sep = strrchr (filepath + 1, '/'); + if (sep != NULL) + return sep - filepath; + } + return 0; +} + + +/** + * @param filename Template name of the file to save * @return the file name string, need to be freed after used * - * This is used to genearte a new recording file name for the recorder. + * This is used to generate a new recording file name for the recorder. */ static const gchar * -gst_recorder_new_filename (GstRecorder * rec) +gst_recorder_new_filename (const gchar * filename) { - time_t t; - struct tm *tm; - gchar stamp[128]; - const gchar *dot = NULL; - const gchar *filename = opts.record_filename; - if (!filename) { + if (!filename) return NULL; - } - - t = time (NULL); - tm = localtime (&t); - if (tm == NULL) { - static gint num = 0; - num += 1; - snprintf (stamp, sizeof (stamp), "%d", num); - } else { - strftime (stamp, sizeof (stamp), "%F %H%M%S", tm); + gchar fnbuf[256]; + time_t t = time (NULL); + struct tm *tm = localtime (&t); + // Note: reserve some space for collision suffix + strftime (fnbuf, sizeof (fnbuf) - 5, filename, tm); + // We now have a fully built name in our buffer + // If there is at least one directory present, make sure they exist + size_t pathlen = gst_recorder_pathlen (fnbuf); + if (pathlen > 0) { + fnbuf[pathlen] = '\0'; + gst_recorder_mkdirs (fnbuf); + fnbuf[pathlen] = '/'; } - - if ((dot = g_strrstr (filename, "."))) { - const gchar *s = g_strndup (filename, dot - filename); - filename = g_strdup_printf ("%s %s%s", s, stamp, dot); - g_free ((gpointer) s); - } else { - filename = g_strdup_printf ("%s %s.dat", filename, stamp); + pathlen = strlen (fnbuf); // reuse for length of file/path + + // handle name collisions by adding a suffix/extension + size_t suffix = 0; + while (1) { + struct stat s; + if (-1 == stat (fnbuf, &s)) { + if (ENOENT == errno) + break; + else { + perror (fnbuf); + return NULL; // can't record + } + } + snprintf (fnbuf + pathlen, 256 - pathlen, ".%03d", (int) suffix++); + // can't record if we've used up our additions + if (suffix > 999) + return NULL; } - return filename; + return g_strdup (fnbuf); } /** @@ -221,80 +282,43 @@ gst_recorder_new_filename (GstRecorder * rec) static GString * gst_recorder_get_pipeline_string (GstRecorder * rec) { - const gchar *filename = gst_recorder_new_filename (rec); + const gchar *filename = + gst_recorder_new_filename (gst_switch_server_get_record_filename ()); GString *desc; //INFO ("Recording to %s and port %d", filename, rec->sink_port); desc = g_string_new (""); - g_string_append_printf (desc, "intervideosrc name=source_video " - "channel=composite_video "); - g_string_append_printf (desc, "interaudiosrc name=source_audio " - "channel=composite_audio "); + // Encode the video with lossless jpeg + g_string_append_printf (desc, + "intervideosrc name=source_video channel=composite_video " + "! video/x-raw,width=%d,height=%d " + "! queue ! jpegenc quality=100 ! mux. \n", rec->width, rec->height); + + // Don't encode the audio + g_string_append_printf (desc, + "interaudiosrc name=source_audio channel=composite_audio ! queue ! mux. \n"); + // Output in streamable mkv format g_string_append_printf (desc, - "source_video. ! video/x-raw,width=%d,height=%d ", - rec->width, rec->height); - /* - ASSESS ("assess-record-video-source"); - */ - g_string_append_printf (desc, "! queue2 "); - /* - ASSESS ("assess-record-video-encode-queued"); - */ - g_string_append_printf (desc, "! vp8enc "); - /* - ASSESS ("assess-record-video-encoded"); - */ - g_string_append_printf (desc, "! mux. "); - - g_string_append_printf (desc, "source_audio. "); - /* - ASSESS ("assess-record-audio-source"); - */ - g_string_append_printf (desc, "! queue2 "); - /* - ASSESS ("assess-record-audio-queued"); - */ - g_string_append_printf (desc, "! faac "); - /* - ASSESS ("assess-record-audio-encoded"); - */ - g_string_append_printf (desc, "! mux. "); - - g_string_append_printf (desc, "avimux name=mux "); - /* - ASSESS ("assess-record-mux-result"); - */ + "matroskamux name=mux streamable=true " + " writing-app='gst-switch' min-index-interval=1000000 "); + g_string_append_printf (desc, "! tee name=result "); + g_string_append_printf (desc, "\n"); if (filename) { - g_string_append_printf (desc, "filesink name=disk_sink sync=false " - "location=\"%s\" ", filename); + g_string_append_printf (desc, "result. ! queue max-size-buffers=1 " + "! filesink name=disk_sink sync=false location=\"%s\" ", filename); g_free ((gpointer) filename); - g_string_append_printf (desc, "result. "); - /* - ASSESS ("assess-record-file-to-queue"); - */ - g_string_append_printf (desc, "! queue2 "); - /* - ASSESS ("assess-record-file-to-sink"); - */ - g_string_append_printf (desc, "! disk_sink. "); } - g_string_append_printf (desc, "tcpserversink name=tcp_sink sync=false " - "port=%d ", rec->sink_port); - g_string_append_printf (desc, "result. "); - /* - ASSESS ("assess-record-tcp-to-queue"); - */ - g_string_append_printf (desc, "! queue2 "); - /* - ASSESS ("assess-record-tcp-to-sink"); - */ - g_string_append_printf (desc, "! gdppay ! tcp_sink. "); + g_string_append_printf (desc, "result. ! queue max-size-buffers=1 ! gdppay " + "! tcpserversink name=tcp_sink sync=false port=%d ", rec->sink_port); + + INFO ("Recording pipeline\n----\n%s\n---", desc->str); + return desc; } @@ -384,9 +408,9 @@ gst_recorder_class_init (GstRecorderClass * klass) g_object_class_install_property (object_class, PROP_MODE, g_param_spec_uint ("mode", "Mode", "Composite Mode", - COMPOSE_MODE_0, + COMPOSE_MODE_NONE, COMPOSE_MODE__LAST, - COMPOSE_MODE_0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + COMPOSE_MODE_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_PORT, g_param_spec_uint ("port", "Port", @@ -400,7 +424,7 @@ gst_recorder_class_init (GstRecorderClass * klass) g_param_spec_uint ("width", "Input Width", "Input video frame width", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_WIDTH, + gst_composite_default_width (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_HEIGHT, @@ -408,7 +432,7 @@ gst_recorder_class_init (GstRecorderClass * klass) "Input Height", "Input video frame height", 1, G_MAXINT, - GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT, + gst_composite_default_height (), G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); worker_class->prepare = (GstWorkerPrepareFunc) gst_recorder_prepare; diff --git a/tools/gstrecorder.h b/tools/gstrecorder.h index f21a76b..fb95613 100644 --- a/tools/gstrecorder.h +++ b/tools/gstrecorder.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_RECORDER_H__by_Duzy_Chan__ -#define __GST_RECORDER_H__by_Duzy_Chan__ 1 +#ifndef __GST_RECORDER_H__ +#define __GST_RECORDER_H__ + #include "gstworker.h" #include "gstcomposite.h" #include @@ -71,4 +72,4 @@ struct _GstRecorderClass */ GType gst_recorder_get_type (void); -#endif //__GST_RECORDER_H__by_Duzy_Chan__ +#endif //__GST_RECORDER_H__ diff --git a/tools/gstswitchcapture.c b/tools/gstswitchcapture.c index 0f7ac50..1c73bc1 100644 --- a/tools/gstswitchcapture.c +++ b/tools/gstswitchcapture.c @@ -35,6 +35,15 @@ #include #include "../logutils.h" +/* @FIXME: moved from gstcomposite which now determines these dynamically */ +#if ENABLE_LOW_RESOLUTION +#define GST_SWITCH_COMPOSITE_DEFAULT_WIDTH LOW_RES_W /* 640 */ +#define GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT LOW_RES_H /* 480 */ +#else +#define GST_SWITCH_COMPOSITE_DEFAULT_WIDTH 1280 +#define GST_SWITCH_COMPOSITE_DEFAULT_HEIGHT 720 +#endif + #define GST_TYPE_SWITCH_CAPTURE_WORKER (gst_switch_capture_worker_get_type ()) #define GST_SWITCH_CAPTURE_WORKER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), GST_TYPE_SWITCH_CAPTURE_WORKER, GstSwitchCaptureWorker)) #define GST_SWITCH_CAPTURE_WORKER_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), GST_TYPE_SWITCH_CAPTURE_WORKER, GstSwitchCaptureWorkerClass)) @@ -132,7 +141,7 @@ gst_switch_capture_pipeline (GstWorker * worker, GstSwitchCapture * capture) g_string_append_printf (desc, "! tee name=video "); } - g_string_append_printf (desc, "video. ! queue2 "); + g_string_append_printf (desc, "video. ! queue "); g_string_append_printf (desc, "! videoscale ! video/x-raw,width=%d,height=%d ", GST_SWITCH_FACEDETECT_FRAME_WIDTH, GST_SWITCH_FACEDETECT_FRAME_HEIGHT); @@ -142,9 +151,9 @@ gst_switch_capture_pipeline (GstWorker * worker, GstSwitchCapture * capture) g_string_append_printf (desc, "! camcontrol name=camctl " "device=%s protocol=%s ", device, protocol); g_string_append_printf (desc, "! videoconvert "); - g_string_append_printf (desc, "! xvimagesink "); + g_string_append_printf (desc, "! xvimagesink sync=false "); - g_string_append_printf (desc, "video. ! queue2 "); + g_string_append_printf (desc, "video. ! queue "); g_string_append_printf (desc, "! videoconvert "); g_string_append_printf (desc, "! videoscale " "! video/x-raw,format=I420,width=%d,height=%d ", w, h); diff --git a/tools/gstswitchcapture.h b/tools/gstswitchcapture.h index 244fa97..55188f4 100644 --- a/tools/gstswitchcapture.h +++ b/tools/gstswitchcapture.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_SWITCH_CAPTURE_H__by_Duzy_Chan__ -#define __GST_SWITCH_CAPTURE_H__by_Duzy_Chan__ 1 +#ifndef __GST_SWITCH_CAPTURE_H__ +#define __GST_SWITCH_CAPTURE_H__ + #include "gstworker.h" #include "gstswitchclient.h" @@ -65,4 +66,4 @@ typedef struct _GstSwitchCaptureClass GType gst_switch_capture_get_type (void); -#endif//__GST_SWITCH_CAPTURE_H__by_Duzy_Chan__ +#endif //__GST_SWITCH_CAPTURE_H__ diff --git a/tools/gstswitchclient.c b/tools/gstswitchclient.c index 437ac7e..13de9e8 100644 --- a/tools/gstswitchclient.c +++ b/tools/gstswitchclient.c @@ -95,6 +95,7 @@ static const gchar introspection_xml[] = " " ""; extern gboolean verbose; +gint gst_switch_client_dbus_timeout = 5000; /** * @brief Initialize instances of GstSwitchClient. @@ -141,7 +142,7 @@ gst_switch_client_call_controller (GstSwitchClient * client, //INFO ("calling: %s/%s", SWITCH_CONTROLLER_OBJECT_NAME, method_name); value = g_dbus_connection_call_sync (client->controller, NULL, /* bus_name */ - SWITCH_CONTROLLER_OBJECT_PATH, SWITCH_CONTROLLER_OBJECT_NAME, method_name, parameters, reply_type, G_DBUS_CALL_FLAGS_NONE, 5000, /* timeout_msec */ + SWITCH_CONTROLLER_OBJECT_PATH, SWITCH_CONTROLLER_OBJECT_NAME, method_name, parameters, reply_type, G_DBUS_CALL_FLAGS_NONE, gst_switch_client_dbus_timeout, /* timeout_msec */ NULL /* TODO: cancellable */ , &error); @@ -169,7 +170,7 @@ gst_switch_client_call_controller (GstSwitchClient * client, } /** - * @brief The the compose port number. + * @brief The compose port number. * @memberof GstSwitchClient */ gint @@ -213,7 +214,7 @@ gst_switch_client_get_encode_port (GstSwitchClient * client) * @param client the GstSwitchClient instance * @return the audio port number * - * The the audio port number. + * The audio port number. * */ gint @@ -356,7 +357,8 @@ gst_switch_client_mark_tracking_remotely (GstSwitchClient * client, * */ gboolean -gst_switch_client_set_composite_mode (GstSwitchClient * client, gint mode) +gst_switch_client_set_composite_mode (GstSwitchClient * client, + GstCompositeMode mode) { gboolean result = FALSE; GVariant *value = NULL; @@ -370,7 +372,7 @@ gst_switch_client_set_composite_mode (GstSwitchClient * client, gint mode) if (!client->changing_composite_mode) { value = gst_switch_client_call_controller (client, "set_composite_mode", - g_variant_new ("(i)", mode), G_VARIANT_TYPE ("(b)")); + g_variant_new ("(i)", (gint) mode), G_VARIANT_TYPE ("(b)")); if (value) { g_variant_get (value, "(b)", &result); client->changing_composite_mode = result; @@ -413,8 +415,8 @@ gst_switch_client_new_record (GstSwitchClient * client) * @param dy y position to be adjusted * @param dw w position to be adjusted * @param dh h position to be adjusted - * @return a unsign integer indicating which components are changed of per - * bit. + * @return a unsigned integer indicating which components are changed of per + * bit. * * Adjust the PIP. * diff --git a/tools/gstswitchclient.h b/tools/gstswitchclient.h index 96f3dc2..4eecf19 100644 --- a/tools/gstswitchclient.h +++ b/tools/gstswitchclient.h @@ -25,11 +25,14 @@ /*! @file */ -#ifndef __GST_SWITCH_CLIENT_H__by_Duzy_Chan__ -#define __GST_SWITCH_CLIENT_H__by_Duzy_Chan__ 1 +#ifndef __GST_SWITCH_CLIENT_H__ +#define __GST_SWITCH_CLIENT_H__ + #include #include +#include "gstcomposite.h" + #define GST_TYPE_SWITCH_CLIENT (gst_switch_client_get_type ()) #define GST_SWITCH_CLIENT(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), GST_TYPE_SWITCH_CLIENT, GstSwitchClient)) #define GST_SWITCH_CLIENT_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), GST_TYPE_SWITCH_CLIENT, GstSwitchClientClass)) @@ -117,7 +120,7 @@ GVariant *gst_switch_client_get_preview_ports (GstSwitchClient * client); gboolean gst_switch_client_switch (GstSwitchClient * client, gint channel, gint port); gboolean gst_switch_client_set_composite_mode (GstSwitchClient * client, - gint mode); + GstCompositeMode mode); gboolean gst_switch_client_click_video (GstSwitchClient * client, gint x, gint y, gint fw, gint fh); void gst_switch_client_mark_face_remotely (GstSwitchClient * client, @@ -128,4 +131,6 @@ gboolean gst_switch_client_new_record (GstSwitchClient * client); guint gst_switch_client_adjust_pip (GstSwitchClient * client, gint dx, gint dy, gint dw, gint dh); -#endif //__GST_SWITCH_CLIENT_H__by_Duzy_Chan__ +extern gint gst_switch_client_dbus_timeout; + +#endif //__GST_SWITCH_CLIENT_H__ diff --git a/tools/gstswitchcontroller.c b/tools/gstswitchcontroller.c index 1e413c5..a3fb86c 100644 --- a/tools/gstswitchcontroller.c +++ b/tools/gstswitchcontroller.c @@ -115,6 +115,8 @@ static const gchar introspection_xml[] = " " */ +gint gst_switch_controller_dbus_timeout = 5000; + /** * @brief Helper function for matching remoting method names. * @memberof GstSwitchController @@ -298,6 +300,7 @@ gst_switch_controller_emit_ui_signal (GstSwitchController * controller, signame, parameters, &error); if (!res) { + g_assert (error != NULL); ERROR ("emit: (%d) %s", num, error->message); } else { ++num; @@ -325,16 +328,16 @@ gst_switch_controller_on_connection_closed (GDBusConnection * connection, if (error) { WARN ("close: %s", error->message); - g_error_free (error); } - g_object_unref (connection); GST_SWITCH_CONTROLLER_LOCK_UIS (controller); controller->uis = g_list_remove (controller->uis, connection); GST_SWITCH_CONTROLLER_UNLOCK_UIS (controller); INFO ("closed: %p, %d (%d uis)", connection, vanished, g_list_length (controller->uis)); + + g_object_unref (connection); } static GVariant *gst_switch_controller_call_client (GstSwitchController * @@ -354,20 +357,18 @@ gst_switch_controller_add_new_client (GstSwitchAddNewClientParams * params) GDBusConnection *connection = params->connection; GstSwitchController *controller = params->controller; gint role = CLIENT_ROLE_NONE; - { - gint tests[] = { CLIENT_ROLE_UI, CLIENT_ROLE_CAPTURE, }; - GVariant *parameters = NULL, *res = NULL; - int n = 0; - for (; n < sizeof (tests); ++n) { - role = CLIENT_ROLE_NONE; - parameters = g_variant_new ("()"); - res = gst_switch_controller_call_client (controller, - connection, tests[n], "role", parameters, G_VARIANT_TYPE ("(i)")); - if (res) { - g_variant_get (res, "(i)", &role); - g_variant_unref (res); - break; - } + gint tests[] = { CLIENT_ROLE_UI, CLIENT_ROLE_CAPTURE, }; + GVariant *parameters = NULL, *res = NULL; + int n = 0; + for (; n < sizeof (tests) / sizeof (gint); ++n) { + role = CLIENT_ROLE_NONE; + parameters = g_variant_new ("()"); + res = gst_switch_controller_call_client (controller, + connection, tests[n], "role", parameters, G_VARIANT_TYPE ("(i)")); + if (res) { + g_variant_get (res, "(i)", &role); + g_variant_unref (res); + break; } } @@ -413,17 +414,18 @@ gst_switch_controller_on_new_connection (GDBusServer * server, NULL, /* user_data_free_func */ &error); - if (register_id <= 0 || error != NULL) { + if (error != NULL) { ERROR ("failed to register controller: %s", error->message); - return TRUE; + g_error_free (error); + return FALSE; + } else if (register_id <= 0) { + ERROR ("register_id invalid (<= 0): %d", register_id); + return FALSE; + } else { + INFO ("registered: %d, %s, %s", register_id, + SWITCH_CONTROLLER_OBJECT_PATH, introspection_data->interfaces[0]->name); } - /* - INFO ("registered: %d, %s, %s", register_id, - SWITCH_CONTROLLER_OBJECT_PATH, - introspection_data->interfaces[0]->name); - */ - g_signal_connect (connection, "closed", G_CALLBACK (gst_switch_controller_on_connection_closed), controller); @@ -474,12 +476,13 @@ gst_switch_controller_init (GstSwitchController * controller) auth_observer = g_dbus_auth_observer_new (); controller->bus_server = g_dbus_server_new_sync (SWITCH_CONTROLLER_ADDRESS, flags, guid, auth_observer, NULL, /* GCancellable */ &error); + if (error != NULL) { + g_error ("failed to register controller: %s", error->message); + } + g_assert (controller->bus_server != NULL); g_free (guid); - if (controller->bus_server == NULL) - goto error_new_server; - INFO ("Controller is listening at: %s", g_dbus_server_get_client_address (controller->bus_server)); @@ -496,14 +499,6 @@ gst_switch_controller_init (GstSwitchController * controller) // TODO: singleton object return; - - /* Errors Handling */ -error_new_server: - { - if (error) - ERROR ("%s", error->message); - return; - } } /** @@ -575,25 +570,25 @@ gst_switch_controller_call_client (GstSwitchController * controller, break; } + if (G_IS_DBUS_CONNECTION (connection) == 0) { + return NULL; + } + value = g_dbus_connection_call_sync (connection, NULL, /* bus_name */ - path, name, method_name, parameters, reply_type, G_DBUS_CALL_FLAGS_NONE, 5000, /* timeout_msec */ + path, name, method_name, parameters, reply_type, G_DBUS_CALL_FLAGS_NONE, gst_switch_controller_dbus_timeout, /* timeout_msec */ NULL /* TODO: cancellable */ , &error); - if (error != NULL) - goto error_call_sync; - - return value; - - /* ERRORS */ -error_call_sync: - { + if (error != NULL) { ERROR ("%s (%s)", error->message, method_name); g_error_free (error); if (value) g_variant_unref (value); return NULL; } + g_assert (value != NULL); + + return value; } /** diff --git a/tools/gstswitchcontroller.h b/tools/gstswitchcontroller.h index b01b70e..87c3504 100644 --- a/tools/gstswitchcontroller.h +++ b/tools/gstswitchcontroller.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_SWITCH_CONTROLLER_H__by_Duzy_Chan__ -#define __GST_SWITCH_CONTROLLER_H__by_Duzy_Chan__ 1 +#ifndef __GST_SWITCH_CONTROLLER_H__ +#define __GST_SWITCH_CONTROLLER_H__ + #include #include "../logutils.h" #include "gstworker.h" @@ -105,4 +106,6 @@ void gst_switch_controller_show_face_marker (GstSwitchController *controller, void gst_switch_controller_show_track_marker (GstSwitchController *controller, GVariant *faces); -#endif //__GST_SWITCH_CONTROLLER_H__by_Duzy_Chan__ +extern gint gst_switch_controller_dbus_timeout; + +#endif //__GST_SWITCH_CONTROLLER_H__ diff --git a/tools/gstswitchopts.c b/tools/gstswitchopts.c new file mode 100644 index 0000000..a9b0d6d --- /dev/null +++ b/tools/gstswitchopts.c @@ -0,0 +1,247 @@ +#include + +#include +#include + +#include +#include + + +typedef struct +{ + const const gchar **shortcuts; + const gchar *format; +} FormatAlias; + +#define ADD_FORMAT_ALIASES(format, ...) \ + { (const gchar*[]){ __VA_ARGS__ , NULL }, format } + +static FormatAlias format_aliases[] = { + ADD_FORMAT_ALIASES ("300x200@25", "debug"), + + // Computer resolutions. These can be at pretty much any frame rate (common + // ones being 60Hz, 75Hz and 80Hz) so the frame rate must still be specified. + // For example VGA@75. + ADD_FORMAT_ALIASES ("640x480", "VGA"), + ADD_FORMAT_ALIASES ("800x600", "SVGA"), + ADD_FORMAT_ALIASES ("1024x768", "XGA"), + + // Older Analog TV formats from DV. These formats have only single frame + // rate but is interlaced. After deinterlacing, the frame rates are halved. + // Both PAL and NSTC also have non-square pixels, which need to be converted + // to square pixels. + // See http://help.adobe.com/en_US/PremierePro/4.0/WS03BF7479-8C7B-4522-8C75-210AD102524Ea.html#WS082E922A-A5D7-4de1-9A0E-1EFC401EA6D1 + + // DV 4:3 PAL, 50 *fields* per second, 720x576 frame size, pixel ratio 59/54 (1.09). + // Therefore 788x576@25 + ADD_FORMAT_ALIASES ("788x576@25", "pal", "pal-4:3", "pal-dv"), + // DV 16:9 PAL, 50 *fields* per second, 720x576 frame size, pixel ratio 118/81 (1.46). + // Therefore 1050x576@25 + ADD_FORMAT_ALIASES ("1050x576@25", "pal-16:9", "pal-dvd"), + + // Technically NTSC has a frame rate of 60/1.001 and *either* 486 or 480 + // horizontal lines, but the dvdec plugin in gstreamer only supports 60Hz and + // 480 horizontal lines. + + // DV 4:3 NTSC, 60 *fields* per second, 720x480 frame size, pixel ratio 10/11 (0.91). + // Therefore 720x534@30 + ADD_FORMAT_ALIASES ("720x534@25", "ntsc", "ntsc-4:3", "ntsc-dv"), + // DV 16:9 NTSC, 60 *fields* per second, 720x480 frame size, pixel ratio 40/33 (1.21). + // Therefore 864x480@30 + ADD_FORMAT_ALIASES ("864x480@25", "ntsc", "ntsc-16:9", "ntsc-dvd"), + + // None of the HD formats also don't have default frame rates, so the rate + // must be specified. + + // In the following resolutions the 'p' is used in place of the '@', IE + // 720p60. + // Digital TV resolutions. + ADD_FORMAT_ALIASES ("1280x720@", "720p"), + ADD_FORMAT_ALIASES ("1920x1080@", "1080p"), + // Super high resolutions. + ADD_FORMAT_ALIASES ("4096x2160@", "2160p"), // DCI 4k + ADD_FORMAT_ALIASES ("7680x4320@", "4320p"), + + // Other super high resolution aliases. + ADD_FORMAT_ALIASES ("2048x1080", "2k"), + ADD_FORMAT_ALIASES ("4096x2160", "4k"), + ADD_FORMAT_ALIASES ("7680x4320", "8k"), + + // Terminator + ADD_FORMAT_ALIASES (NULL, NULL), +}; + +// Requirements for video; +static const gchar *requirements = "video/x-raw," + // Required by intervideo(sink|src). + "format=(string)I420," + // Square pixels, required for sanity. + "pixel-aspect-ratio=(fraction)1/1," + // Smallest resolution is 300x200 (required for PIP to work). + // Largest is 8k (arbitrarily chosen). + "width=(int)[300,7680]," "height=(int)[200,4320]," + // Pretty much any frame rate is supported, restricting to 1000fps to catch + // user error. + "framerate=(fraction)[0/1,1000/1]"; + +#define TMP_BUF_SIZE 255 + +// Parse a format string into it's bits. +// [Resolution X]x[Resolution Y]@[Frame Rate] +// +// Both x and y resolution must be whole integers, frame rate can be floating +// value. +static gboolean +parse_short_format (const gchar * format, GstCaps * caps) +{ + g_assert (gst_caps_is_writable (caps)); + + gint format_width = 0; + gint format_height = 0; + gdouble format_rate = 0; + gint format_rate_num = 0; + gint format_rate_den = 1; + int r; + + gsize format_len = strlen (format); + gchar format_buf[TMP_BUF_SIZE]; + memset (format_buf, '\0', TMP_BUF_SIZE); + + // Convert an alias into a full format string; + // VGA@60 -> 640x480@60 + // PAL -> 720x576@25 + // 720p25 -> 1280x720@25 + gsize i; + for (i = 0; format_aliases[i].format != NULL; i++) { + const gchar *alias_format = format_aliases[i].format; + gsize alias_format_len = strlen (alias_format); + + const gchar **shortcuts = format_aliases[i].shortcuts; + gsize j; + for (j = 0; shortcuts[j] != NULL; j++) { + const gchar *shortcut = shortcuts[j]; + gsize shortcut_len = strlen (shortcut); + + if (g_ascii_strncasecmp (shortcuts[j], format, shortcut_len) == 0) { + //printf("Found alias %p '%s' ('%s') in format '%s'\n", shortcut, shortcut, alias_format, format); + strncpy (format_buf, alias_format, alias_format_len); + strncpy (format_buf + alias_format_len, format + shortcut_len, + format_len - shortcut_len); + goto parse_short_format_found_alias; + } + } + } + + // No alias found, just use the given value +//printf("No alias found!\n"); + strncpy (format_buf, format, format_len); + +parse_short_format_found_alias: +//printf("Current format string: '%s'\n", format_buf); + r = sscanf (format_buf, "%dx%d@%lf/%d", &format_width, &format_height, + &format_rate, &format_rate_den); + + switch (r) { + case 4: // all 4 args consumed + format_rate_num = gst_gdouble_to_guint64 (format_rate); + // Check the double was really an int + if (gst_guint64_to_gdouble (format_rate_num) != format_rate) + return FALSE; + break; + case 3: // denominator only + gst_util_double_to_fraction (format_rate, &format_rate_num, + &format_rate_den); + // @TODO: this will ignore additional @rate specifiers which + // may occur e.g. pal@75, should raise an error + break; + default: // Wasn't able to parse the format format. + return FALSE; + } + + gst_caps_set_simple (caps, + "framerate", GST_TYPE_FRACTION, format_rate_num, format_rate_den, + "width", G_TYPE_INT, format_width, + "height", G_TYPE_INT, format_height, NULL); + + return TRUE; +} + +// Smallest resolution is 300x200 (required for PIP to work) and largest is +// 8k (arbitrarily chosen). +int +parse_format (const gchar * format, GstCaps ** caps, GError ** error) +{ + const gchar *error_msg = "Invalid video format specified"; + GstCaps *require_caps = gst_caps_from_string (requirements); + GstCaps *incoming_caps = NULL; + GstCaps *final_caps = NULL; + + gsize format_end = strlen (format); + if (format_end >= TMP_BUF_SIZE) { + // Invalid + goto parse_format_error; + } + + incoming_caps = gst_caps_new_simple ("video/x-raw", + "format", G_TYPE_STRING, "I420", + "pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1, NULL); + g_assert (incoming_caps); + + // Is the format already in gstreamer caps format, or in the simple + // abbreviation format. + if (strstr (format, "video/x-raw") == NULL) { + if (!parse_short_format (format, incoming_caps)) { + goto parse_format_error; + } + } else { + GstCaps *parsed_caps = gst_caps_from_string (format); + if (parsed_caps == NULL) { +// printf("parse failed!?\n"); + goto parse_format_error; + } +// printf(" parsed-caps: %s\n", gst_caps_to_string(parsed_caps)); + + // Check the incoming caps are fully specified (IE only one format, don't + // have any ranges, etc). + if (!gst_caps_is_fixed (parsed_caps)) { +// printf("Caps were not fixed!\n"); + gst_caps_unref (parsed_caps); + goto parse_format_error; + } + + GstCaps *merged_caps = gst_caps_intersect (incoming_caps, parsed_caps); +// printf(" merged-caps: %s\n", gst_caps_to_string(merged_caps)); + incoming_caps = merged_caps; +// GstCaps *merged_caps = gst_caps_intersect(incoming_caps, parsed_caps); + gst_caps_unref (parsed_caps); + } + +//GST_LOG ("caps are %" GST_PTR_FORMAT, caps); +//printf(" caps-to-test: %s\n", gst_caps_to_string(incoming_caps)); +//printf("required-caps: %s\n", gst_caps_to_string(require_caps)); + +//parse_format_ok: + final_caps = gst_caps_intersect (require_caps, incoming_caps); +//printf(" final-caps: %s\n", gst_caps_to_string(final_caps)); + int r = 0; + if (gst_caps_is_empty (final_caps) || !gst_caps_is_fixed (final_caps)) { + + parse_format_error: + r = -1; + if (error != NULL) { + GError *err = g_error_new (G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "%s\n", error_msg); + g_propagate_error (error, err); + } + } + + if (require_caps != NULL) + gst_caps_unref (require_caps); + if (incoming_caps != NULL) + gst_caps_unref (incoming_caps); + + if (caps != NULL) + *caps = final_caps; + + return r; +} diff --git a/tools/gstswitchptz.c b/tools/gstswitchptz.c index 4d31c81..b6ffabf 100644 --- a/tools/gstswitchptz.c +++ b/tools/gstswitchptz.c @@ -899,7 +899,7 @@ gst_switch_ptz_get_pipeline (GstSwitchPTZ * ptz) g_string_append_printf (desc, "v4l2src device=%s ", ptz_video_name); //g_string_append_printf (desc, "! video/x-raw,width=320,height=240 "); g_string_append_printf (desc, "! videoconvert ! videoscale "); - g_string_append_printf (desc, "! ximagesink name=sink "); + g_string_append_printf (desc, "! ximagesink name=sink sync=false "); return desc; } diff --git a/tools/gstswitchptz.h b/tools/gstswitchptz.h index 87755b9..8d9d7af 100644 --- a/tools/gstswitchptz.h +++ b/tools/gstswitchptz.h @@ -23,8 +23,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ -#ifndef __GST_SWITCH_PTZ_H____by_Duzy_Chan__ -#define __GST_SWITCH_PTZ_H____by_Duzy_Chan__ 1 +#ifndef __GST_SWITCH_PTZ_H__ +#define __GST_SWITCH_PTZ_H__ + #include #include #include "gstworker.h" @@ -58,4 +59,4 @@ typedef struct _GstSwitchPTZClass GType gst_switch_ptz_get_type (void); -#endif//__GST_SWITCH_PTZ_H____by_Duzy_Chan__ +#endif //__GST_SWITCH_PTZ_H__ diff --git a/tools/gstswitchserver.c b/tools/gstswitchserver.c index 009391e..858b3df 100644 --- a/tools/gstswitchserver.c +++ b/tools/gstswitchserver.c @@ -38,12 +38,23 @@ #include "./gio/gsocketinputstream.h" #include "../logutils.h" -#define GST_SWITCH_SERVER_DEFAULT_HOST "localhost" +#include +#include +#include +#include +#include +#include + +#define GST_SWITCH_SERVER_DEFAULT_HOST "0.0.0.0" #define GST_SWITCH_SERVER_DEFAULT_VIDEO_ACCEPTOR_PORT 3000 #define GST_SWITCH_SERVER_DEFAULT_AUDIO_ACCEPTOR_PORT 4000 #define GST_SWITCH_SERVER_DEFAULT_CONTROLLER_PORT 5000 #define GST_SWITCH_SERVER_LISTEN_BACKLOG 8 /* client connection queue */ +#define GST_SWITCH_SERVER_HOST_SPEC "%q" +#define GST_SWITCH_SERVER_DEFAULT_RECORD_FILE "recording-%q-%Y%m%d-%H%M%S" +#define GST_SWITCH_SERVER_DEFAULT_RECORD_EXT ".mkv" + #define GST_SWITCH_SERVER_LOCK_MAIN_LOOP(srv) (g_mutex_lock (&(srv)->main_loop_lock)) #define GST_SWITCH_SERVER_UNLOCK_MAIN_LOOP(srv) (g_mutex_unlock (&(srv)->main_loop_lock)) #define GST_SWITCH_SERVER_LOCK_VIDEO_ACCEPTOR(srv) (g_mutex_lock (&(srv)->video_acceptor_lock)) @@ -71,18 +82,148 @@ GstSwitchServerOpts opts = { GST_SWITCH_SERVER_DEFAULT_VIDEO_ACCEPTOR_PORT, GST_SWITCH_SERVER_DEFAULT_AUDIO_ACCEPTOR_PORT, GST_SWITCH_SERVER_DEFAULT_CONTROLLER_PORT, +//FALSE, + FALSE, + NULL, NULL }; gboolean verbose = FALSE; +static gboolean +gparse_record_filename (gchar * name, gchar * value, gpointer data, + GError ** error) +{ + size_t maxpathlen = 256; + gchar fnbuf[maxpathlen + 1]; + size_t fnlen = 0; + + if (value == NULL || (fnlen = strlen (value)) == 0) { + // file not specified, use the default filename.ext + strncpy (fnbuf, GST_SWITCH_SERVER_DEFAULT_RECORD_FILE, sizeof (fnbuf) - 5); + strcat (fnbuf, GST_SWITCH_SERVER_DEFAULT_RECORD_EXT); + } else if (fnlen < maxpathlen) { + strncpy (fnbuf, value, sizeof (fnbuf)); + if ((fnlen < 5 || strchr (value + fnlen - 5, '.') == NULL) + && fnlen + 4 < maxpathlen) + strcat (fnbuf, GST_SWITCH_SERVER_DEFAULT_RECORD_EXT); + } else { + GError *err = g_error_new (G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "%s path/filename too long: %s\n", name, value); + g_propagate_error (error, err); + return FALSE; + } + + // Do manual substitution of %q => hostname up front since this is static + // Time and date tokens are substituted within the recorder as the recording + // is started or cut to accurately reflect the date/time of recording + char *h = strstr (fnbuf, GST_SWITCH_SERVER_HOST_SPEC); + if (h != NULL) { + int mnlen = maxpathlen - fnlen; // limit size to what is available + char hostname[mnlen + 1]; + if (gethostname (hostname, mnlen) != 0) + strcpy (hostname, GST_SWITCH_SERVER_DEFAULT_HOST); + else + hostname[mnlen] = '\0'; + int hnlen = strlen (hostname); + hostname[hnlen] = '\0'; // guarantee nul termination + fnlen = strlen (fnbuf); + do { + size_t over = fnlen - (h - fnbuf) + 1; + memmove (h + hnlen, h + 2, over); + memcpy (h, hostname, hnlen); + fnlen += (hnlen - 2); // adjust the result length + } while ((h = strstr (fnbuf, GST_SWITCH_SERVER_HOST_SPEC)) != NULL); + } + + opts.record_filename = g_strdup (fnbuf); + return TRUE; +} + + +extern int +parse_format (const gchar * format, GstCaps ** final_caps, GError ** err); + +static gboolean +gparse_video_format (gchar * name, gchar * value, gpointer data, + GError ** error) +{ + if (parse_format (value, &opts.video_caps, error) == -1) + return FALSE; + return TRUE; +} + +static gboolean caps_dumped = FALSE; + +/* gst_switch_server_getcaps: + * @return current video caps + */ +GstCaps * +gst_switch_server_getcaps (void) +{ + static const gchar *audio_caps_str = + "audio/x-raw, rate=48000, channels=2, format=S16LE, layout=interleaved"; + + if (opts.video_caps == NULL) { + // use a sane default we know will parse correctly + parse_format (opts.low_res ? "VGA@60" : "720p60", &opts.video_caps, NULL); + } + if (!caps_dumped) { + opts.video_caps_str = gst_caps_to_string (opts.video_caps); + g_print ("video-caps: %s\n", opts.video_caps_str); + + opts.audio_caps_str = g_strdup (audio_caps_str); + g_print ("audio-caps: %s\n", opts.audio_caps_str); + + caps_dumped = TRUE; + } + return opts.video_caps; +} + +/* gst_switch_server_get_video_caps_str + * return video caps as a string + */ +const gchar * +gst_switch_server_get_video_caps_str (void) +{ + (void) gst_switch_server_getcaps (); + return opts.video_caps_str; +} + +/* gst_switch_server_get_audio_caps_str + * return audio caps as a string + */ +const gchar * +gst_switch_server_get_audio_caps_str (void) +{ + (void) gst_switch_server_getcaps (); + return opts.audio_caps_str; +} + +/* gst_switch_server_get_record_filename: + * @return record file template name, if one was set + */ +const gchar * +gst_switch_server_get_record_filename (void) +{ + return opts.record_filename; +} + static GOptionEntry entries[] = { {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Prompt more messages", NULL}, {"test-switch", 't', 0, G_OPTION_ARG_STRING, &opts.test_switch, "Perform switch test", "OUTPUT"}, - {"record", 'r', 0, G_OPTION_ARG_STRING, &opts.record_filename, - "Enable recorder and record into the specified FILENAME", - "FILENAME"}, + {"dbus-timeout", 'd', 0, G_OPTION_ARG_INT, + &gst_switch_controller_dbus_timeout, + "DBus timeout in msec (default 5000)"}, + {"record", 'r', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, + (gpointer) gparse_record_filename, + "Enable recorder and record into the specified FILENAME"}, + {"low-resolution", 'l', 0, G_OPTION_ARG_NONE, &opts.low_res, + "Enable low resolution mode (-f overrides)"}, + {"video-format", 'f', 0, G_OPTION_ARG_CALLBACK, + (gpointer) gparse_video_format, + "Specify the video format to use (shortcuts supported)"}, {"video-input-port", 'p', 0, G_OPTION_ARG_INT, &opts.video_input_port, "Specify the video input listen port.", "NUM"}, {"audio-input-port", 'a', 0, G_OPTION_ARG_INT, &opts.audio_input_port, @@ -95,20 +236,24 @@ static GOptionEntry entries[] = { /** * gst_switch_server_parse_args: * - * Parsing commiand line parameters. + * Parsing command line parameters. */ static void -gst_switch_server_parse_args (int *argc, char **argv[]) +gst_switch_server_parse_args (int argc, char *argv[]) { GError *error = NULL; GOptionContext *context; + gst_init (NULL, NULL); context = g_option_context_new (""); g_option_context_add_main_entries (context, entries, "gst-switch"); g_option_context_add_group (context, gst_init_get_option_group ()); - if (!g_option_context_parse (context, argc, argv, &error)) { + if (!g_option_context_parse (context, &argc, &argv, &error)) { ERROR ("option parsing failed: %s", error->message); exit (1); + } else if (argc > 1) { + ERROR ("unknown option: %s", argv[1]); + exit (1); } g_option_context_free (context); @@ -321,8 +466,8 @@ gst_switch_server_end_case (GstCase * cas, GstSwitchServer * srv) caseport = cas->sink_port; g_object_unref (cas); break; - case GST_CASE_INPUT_a: - case GST_CASE_INPUT_v: + case GST_CASE_INPUT_AUDIO: + case GST_CASE_INPUT_VIDEO: srv->cases = g_list_remove (srv->cases, cas); INFO ("Removed %s %p (%d cases left)", GST_WORKER (cas)->name, cas, g_list_length (srv->cases)); @@ -360,10 +505,10 @@ gst_switch_server_start_case (GstCase * cas, GstSwitchServer * srv) { gboolean is_branch = FALSE; switch (cas->type) { - case GST_CASE_BRANCH_A: - case GST_CASE_BRANCH_B: - case GST_CASE_BRANCH_a: - case GST_CASE_BRANCH_p: + case GST_CASE_BRANCH_VIDEO_A: + case GST_CASE_BRANCH_VIDEO_B: + case GST_CASE_BRANCH_AUDIO: + case GST_CASE_BRANCH_PREVIEW: is_branch = TRUE; default: break; @@ -375,7 +520,7 @@ gst_switch_server_start_case (GstCase * cas, GstSwitchServer * srv) gst_switch_controller_tell_preview_port (srv->controller, cas->sink_port, cas->serve_type, cas->type); - if (cas->type == GST_CASE_BRANCH_a) { + if (cas->type == GST_CASE_BRANCH_AUDIO) { gst_switch_controller_tell_audio_port (srv->controller, cas->sink_port); } } @@ -393,9 +538,9 @@ gst_switch_server_suggest_case_type (GstSwitchServer * srv, GstSwitchServeStreamType serve_type) { GstCaseType type = GST_CASE_UNKNOWN; - gboolean has_composite_A = FALSE; - gboolean has_composite_B = FALSE; - gboolean has_composite_a = FALSE; + gboolean has_composite_video_A = FALSE; + gboolean has_composite_video_B = FALSE; + gboolean has_composite_audio = FALSE; GList *item = srv->cases; for (; item; item = g_list_next (item)) { @@ -405,11 +550,11 @@ gst_switch_server_suggest_case_type (GstSwitchServer * srv, case GST_SERVE_VIDEO_STREAM: { switch (cas->type) { - case GST_CASE_COMPOSITE_A: - has_composite_A = TRUE; + case GST_CASE_COMPOSITE_VIDEO_A: + has_composite_video_A = TRUE; break; - case GST_CASE_COMPOSITE_B: - has_composite_B = TRUE; + case GST_CASE_COMPOSITE_VIDEO_B: + has_composite_video_B = TRUE; break; default: break; @@ -418,8 +563,8 @@ gst_switch_server_suggest_case_type (GstSwitchServer * srv, break; case GST_SERVE_AUDIO_STREAM: { - if (cas->type == GST_CASE_COMPOSITE_a) - has_composite_a = TRUE; + if (cas->type == GST_CASE_COMPOSITE_AUDIO) + has_composite_audio = TRUE; } break; case GST_SERVE_NOTHING: @@ -427,14 +572,14 @@ gst_switch_server_suggest_case_type (GstSwitchServer * srv, } #else switch (cas->type) { - case GST_CASE_COMPOSITE_A: - has_composite_A = TRUE; + case GST_CASE_COMPOSITE_VIDEO_A: + has_composite_video_A = TRUE; break; - case GST_CASE_COMPOSITE_B: - has_composite_B = TRUE; + case GST_CASE_COMPOSITE_VIDEO_B: + has_composite_video_B = TRUE; break; - case GST_CASE_COMPOSITE_a: - has_composite_a = TRUE; + case GST_CASE_COMPOSITE_AUDIO: + has_composite_audio = TRUE; break; default: break; @@ -445,16 +590,16 @@ gst_switch_server_suggest_case_type (GstSwitchServer * srv, switch (serve_type) { case GST_SERVE_VIDEO_STREAM: - if (!has_composite_A) - type = GST_CASE_COMPOSITE_A; - else if (!has_composite_B) - type = GST_CASE_COMPOSITE_B; + if (!has_composite_video_A) + type = GST_CASE_COMPOSITE_VIDEO_A; + else if (!has_composite_video_B) + type = GST_CASE_COMPOSITE_VIDEO_B; else type = GST_CASE_PREVIEW; break; case GST_SERVE_AUDIO_STREAM: - if (!has_composite_a) - type = GST_CASE_COMPOSITE_a; + if (!has_composite_audio) + type = GST_CASE_COMPOSITE_AUDIO; else type = GST_CASE_PREVIEW; break; @@ -494,10 +639,10 @@ gst_switch_server_serve (GstSwitchServer * srv, GSocket * client, GST_SWITCH_SERVER_LOCK_CASES (srv); switch (serve_type) { case GST_SERVE_AUDIO_STREAM: - inputtype = GST_CASE_INPUT_a; + inputtype = GST_CASE_INPUT_AUDIO; break; case GST_SERVE_VIDEO_STREAM: - inputtype = GST_CASE_INPUT_v; + inputtype = GST_CASE_INPUT_VIDEO; break; default: goto error_unknown_serve_type; @@ -505,17 +650,17 @@ gst_switch_server_serve (GstSwitchServer * srv, GSocket * client, type = gst_switch_server_suggest_case_type (srv, serve_type); switch (type) { - case GST_CASE_COMPOSITE_A: - branchtype = GST_CASE_BRANCH_A; + case GST_CASE_COMPOSITE_VIDEO_A: + branchtype = GST_CASE_BRANCH_VIDEO_A; break; - case GST_CASE_COMPOSITE_B: - branchtype = GST_CASE_BRANCH_B; + case GST_CASE_COMPOSITE_VIDEO_B: + branchtype = GST_CASE_BRANCH_VIDEO_B; break; - case GST_CASE_COMPOSITE_a: - branchtype = GST_CASE_BRANCH_a; + case GST_CASE_COMPOSITE_AUDIO: + branchtype = GST_CASE_BRANCH_AUDIO; break; case GST_CASE_PREVIEW: - branchtype = GST_CASE_BRANCH_p; + branchtype = GST_CASE_BRANCH_PREVIEW; break; default: goto error_unknown_case_type; @@ -939,7 +1084,7 @@ gst_switch_server_get_audio_sink_port (GstSwitchServer * srv) GList *item; GST_SWITCH_SERVER_LOCK_CASES (srv); for (item = srv->cases; item; item = g_list_next (item)) { - if (GST_CASE (item->data)->type == GST_CASE_COMPOSITE_a) { + if (GST_CASE (item->data)->type == GST_CASE_COMPOSITE_AUDIO) { port = GST_CASE (item->data)->sink_port; break; } @@ -970,9 +1115,9 @@ gst_switch_server_get_preview_sink_ports (GstSwitchServer * srv, GST_SWITCH_SERVER_LOCK_CASES (srv); for (item = srv->cases; item; item = g_list_next (item)) { switch (GST_CASE (item->data)->type) { - case GST_CASE_BRANCH_A: - case GST_CASE_BRANCH_B: - case GST_CASE_BRANCH_a: + case GST_CASE_BRANCH_VIDEO_A: + case GST_CASE_BRANCH_VIDEO_B: + case GST_CASE_BRANCH_AUDIO: case GST_CASE_PREVIEW: a = g_array_append_val (a, GST_CASE (item->data)->sink_port); if (s) @@ -1067,7 +1212,7 @@ gst_switch_server_new_record (GstSwitchServer * srv) /** * gst_switch_server_adjust_pip: - * @return: a unsigned number of indicating which compononent (x,y,w,h) has + * @return: a unsigned number of indicating which component (x,y,w,h) has * been changed * * Adjust the PIP position and size. @@ -1089,10 +1234,8 @@ gst_switch_server_adjust_pip (GstSwitchServer * srv, srv->pip_x = 0; if (srv->pip_y < 0) srv->pip_y = 0; - if (srv->pip_w < GST_SWITCH_COMPOSITE_MIN_PIP_W) - srv->pip_w = GST_SWITCH_COMPOSITE_MIN_PIP_W; - if (srv->pip_h < GST_SWITCH_COMPOSITE_MIN_PIP_H) - srv->pip_h = GST_SWITCH_COMPOSITE_MIN_PIP_H; + srv->pip_w = gst_check_composite_min_pip_width (srv->pip_w); + srv->pip_h = gst_check_composite_min_pip_height (srv->pip_h); result = gst_composite_adjust_pip (srv->composite, srv->pip_x, srv->pip_y, srv->pip_w, srv->pip_h); @@ -1139,15 +1282,15 @@ gst_switch_server_switch (GstSwitchServer * srv, gint channel, gint port) GstCase *cas = GST_CASE (item->data); switch (channel) { case 'A': - if (cas->type == GST_CASE_COMPOSITE_A) + if (cas->type == GST_CASE_COMPOSITE_VIDEO_A) goto get_target_stream; break; case 'B': - if (cas->type == GST_CASE_COMPOSITE_B) + if (cas->type == GST_CASE_COMPOSITE_VIDEO_B) goto get_target_stream; break; case 'a': - if (cas->type == GST_CASE_COMPOSITE_a) + if (cas->type == GST_CASE_COMPOSITE_AUDIO) goto get_target_stream; break; default: @@ -1159,9 +1302,9 @@ gst_switch_server_switch (GstSwitchServer * srv, gint channel, gint port) } } switch (cas->type) { - case GST_CASE_COMPOSITE_A: - case GST_CASE_COMPOSITE_B: - case GST_CASE_COMPOSITE_a: + case GST_CASE_COMPOSITE_VIDEO_A: + case GST_CASE_COMPOSITE_VIDEO_B: + case GST_CASE_COMPOSITE_AUDIO: case GST_CASE_PREVIEW: if (cas->sink_port == port) { candidate_case = cas; @@ -1395,7 +1538,7 @@ gst_switch_server_worker_start (GstWorker * worker, GstSwitchServer * srv) /** * gst_switch_server_worker_null: * - * Invoked when a worker is trunning into NULL. + * Invoked when a worker is running into NULL. */ static void gst_switch_server_worker_null (GstWorker * worker, GstSwitchServer * srv) @@ -1468,7 +1611,7 @@ gst_switch_server_end_transition (GstWorker * worker, GstSwitchServer * srv) /** * gst_switch_server_output_client_socket_added: * - * Invoekd when a client socket is added. + * Invoked when a client socket is added. */ static void gst_switch_server_output_client_socket_added (GstElement * element, @@ -1740,12 +1883,35 @@ gst_switch_server_run (GstSwitchServer * srv) } } +static unsigned long long i = 0; + +void +my_handler (int signum) +{ + extern void __gcov_flush (); + printf ("received signal\n"); + printf ("%llu\n", i); + __gcov_flush (); /* dump coverage data on receiving SIGUSR1 */ +} + + int main (int argc, char *argv[]) { + + struct sigaction new_action, old_action; + + /* setup signal hander */ + new_action.sa_handler = my_handler; + sigemptyset (&new_action.sa_mask); + new_action.sa_flags = 0; + sigaction (SIGUSR1, NULL, &old_action); + if (old_action.sa_handler != SIG_IGN) + sigaction (SIGUSR1, &new_action, NULL); + gint exit_code = 0; GstSwitchServer *srv; - gst_switch_server_parse_args (&argc, &argv); + gst_switch_server_parse_args (argc, argv); srv = GST_SWITCH_SERVER (g_object_new (GST_TYPE_SWITCH_SERVER, NULL)); diff --git a/tools/gstswitchserver.h b/tools/gstswitchserver.h index 613ac9b..69c16cb 100644 --- a/tools/gstswitchserver.h +++ b/tools/gstswitchserver.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_SWITCH_SERVER_H__by_Duzy_Chan__ -#define __GST_SWITCH_SERVER_H__by_Duzy_Chan__ 1 +#ifndef __GST_SWITCH_SERVER_H__ +#define __GST_SWITCH_SERVER_H__ + #include #include "gstcomposite.h" #include "gstswitchcontroller.h" @@ -61,6 +62,12 @@ struct _GstSwitchServerOpts gint video_input_port; gint audio_input_port; gint control_port; +//should really be in here +//gboolean verbose; + gboolean low_res; + GstCaps *video_caps; + gchar *video_caps_str; + gchar *audio_caps_str; }; /** @@ -73,7 +80,7 @@ struct _GstSwitchServerOpts * @param cancellable * @param video_acceptor_lock the lock for the video acceptor * @param video_acceptor the video acceptor thread - * @param video_acceptor_socket the vidoe acceptor socekt + * @param video_acceptor_socket the video acceptor socket * @param video_acceptor_port the video acceptor port number * @param audio_acceptor_lock the lock for the audio acceptor * @param audio_acceptor the audio acceptor thread @@ -95,8 +102,8 @@ struct _GstSwitchServerOpts * @param recorder_lock the lock for the %recorder * @param recorder the recorder instance * @param pip_lock the lock for PIP - * @param pip_x the PIP x position - * @param pip_y the PIP y position + * @param pip_x the PIP X position + * @param pip_y the PIP Y position * @param pip_w the PIP width * @param pip_h the PIP height * @param clock_lock the lock for %clock @@ -153,7 +160,7 @@ struct _GstSwitchServer /** * GstSwitchServerClass: - * @param base_calss the parent class + * @param base_class the parent class */ struct _GstSwitchServerClass { @@ -178,6 +185,11 @@ guint gst_switch_server_adjust_pip (GstSwitchServer * srv, gint dx, gint dy, gint dw, gint dh); gboolean gst_switch_server_new_record (GstSwitchServer * srv); +GstCaps *gst_switch_server_getcaps (void); +const gchar *gst_switch_server_get_audio_caps_str (void); +const gchar *gst_switch_server_get_video_caps_str (void); +const gchar *gst_switch_server_get_record_filename (void); + extern GstSwitchServerOpts opts; -#endif //__GST_SWITCH_SERVER_H__by_Duzy_Chan__ +#endif //__GST_SWITCH_SERVER_H__ diff --git a/tools/gstswitchui.c b/tools/gstswitchui.c index 91544f0..91a5ccb 100644 --- a/tools/gstswitchui.c +++ b/tools/gstswitchui.c @@ -36,14 +36,6 @@ #include "gstaudiovisual.h" #include "gstcase.h" -#if 3 <= GTK_MAJOR_VERSION && GTK_MINOR_VERSION <= 4 -#define CUSTOM_FRAME_DRAWING 1 -#define gst_switch_ui_update(w) (gtk_widget_queue_draw (w)) -#else -#define CUSTOM_FRAME_DRAWING 0 -#define gst_switch_ui_update(w) ((void) FALSE) -#endif - #define GST_SWITCH_UI_LOCK_AUDIO(ui) (g_mutex_lock (&(ui)->audio_lock)) #define GST_SWITCH_UI_UNLOCK_AUDIO(ui) (g_mutex_unlock (&(ui)->audio_lock)) #define GST_SWITCH_UI_LOCK_COMPOSE(ui) (g_mutex_lock (&(ui)->compose_lock)) @@ -61,6 +53,8 @@ gboolean verbose; static GOptionEntry entries[] = { {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Be verbose", NULL}, + {"dbus-timeout", 'd', 0, G_OPTION_ARG_INT, &gst_switch_client_dbus_timeout, + "DBus timeout in ms (default 5000)", NULL}, {NULL} }; @@ -196,63 +190,6 @@ gst_switch_ui_compose_view_press (GtkWidget * widget, GdkEventButton * event, return FALSE; } -#if CUSTOM_FRAME_DRAWING -static gboolean -gst_switch_ui_draw_preview_frame (GtkWidget * widget, cairo_t * cr, - GstSwitchUI * ui) -{ - GtkStyleContext *style; - gpointer data = NULL; - double x = 0.5, y = 0.5, width, height, radius, degrees; - style = gtk_widget_get_style_context (widget); - width = (double) gtk_widget_get_allocated_width (widget) - 0.5; - height = (double) gtk_widget_get_allocated_height (widget) - 0.5; - degrees = 3.14159265359 / 180.0; - radius = 10.0; - - data = g_object_get_data (G_OBJECT (widget), "audio-visual"); - if (!data) - data = g_object_get_data (G_OBJECT (widget), "video-display"); - -#if 0 - gtk_style_context_save (style); - gtk_style_context_add_class (style, "red"); - gtk_render_frame (style, cr, 0, 0, width, height); - gtk_style_context_remove_class (style, "red"); - gtk_style_context_restore (style); -#else - cairo_new_sub_path (cr); - cairo_arc (cr, x + width - radius, y + radius, radius, -90 * degrees, - 0 * degrees); - cairo_arc (cr, x + width - radius, y + height - radius, radius, 0 * degrees, - 90 * degrees); - cairo_arc (cr, x + radius, y + height - radius, radius, 90 * degrees, - 180 * degrees); - cairo_arc (cr, x + radius, y + radius, radius, 180 * degrees, 270 * degrees); - cairo_close_path (cr); - if (gtk_widget_get_state_flags (widget) & GTK_STATE_FLAG_SELECTED) { - cairo_set_source_rgba (cr, 0.25, 0.25, 0.85, 0.85); - } else if (data && GST_IS_VIDEO_DISP (data)) { - if (gtk_style_context_has_class (style, "active_video_frame")) { - cairo_set_source_rgba (cr, 0.85, 0.25, 0.25, 0.85); - } else - goto default_color; - } else if (data && GST_IS_AUDIO_VISUAL (data)) { - if (gtk_style_context_has_class (style, "active_audio_frame")) { - cairo_set_source_rgba (cr, 0.85, 0.25, 0.25, 0.85); - } else - goto default_color; - } else { - default_color: - cairo_set_source_rgba (cr, 0.25, 0.25, 0.25, 0.25); - } - cairo_set_line_width (cr, 10.0); - cairo_stroke (cr); -#endif - return TRUE; -} -#endif - static gboolean gst_switch_ui_key_event (GtkWidget *, GdkEvent *, GstSwitchUI *); static gboolean gst_switch_ui_compose_key_event (GtkWidget *, GdkEvent *, @@ -347,8 +284,7 @@ gst_switch_ui_init (GstSwitchUI * ui) GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_widget_set_size_request (scrollwin, 130, -1); gtk_widget_set_vexpand (scrollwin, TRUE); - gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrollwin), - ui->preview_box); + gtk_container_add (GTK_CONTAINER (scrollwin), ui->preview_box); gtk_container_add (GTK_CONTAINER (overlay), ui->compose_view); gtk_overlay_add_overlay (GTK_OVERLAY (overlay), ui->compose_overlay); @@ -754,7 +690,6 @@ gst_switch_ui_set_audio_port (GstSwitchUI * ui, gint port) visual = gst_switch_ui_renew_audio_visual (ui, frame, visual); if (visual->active) { gtk_style_context_add_class (style, "active_audio_frame"); - gst_switch_ui_update (frame); ui->audio = visual; } } @@ -785,7 +720,6 @@ gst_switch_ui_mark_active_video (GstSwitchUI * ui, gint port, gint type) } else if (disp->type == type) { gtk_style_context_remove_class (style, "active_video_frame"); } - gst_switch_ui_update (frame); } } } @@ -844,11 +778,11 @@ gst_switch_ui_preview_click (GtkWidget * w, GdkEvent * event, GstSwitchUI * ui) switch (ev->button) { case 1: // left button - newvideotype = GST_CASE_BRANCH_A; + newvideotype = GST_CASE_BRANCH_VIDEO_A; break; case 3: // right button if (disp) - newvideotype = GST_CASE_BRANCH_B; + newvideotype = GST_CASE_BRANCH_VIDEO_B; break; } @@ -889,15 +823,14 @@ gst_switch_ui_preview_click (GtkWidget * w, GdkEvent * event, GstSwitchUI * ui) if (prevframe && prevdisp) { GtkStyleContext *style = gtk_widget_get_style_context (prevframe); switch (t) { - case GST_CASE_BRANCH_A: - case GST_CASE_BRANCH_B: + case GST_CASE_BRANCH_VIDEO_A: + case GST_CASE_BRANCH_VIDEO_B: gtk_style_context_add_class (style, "active_video_frame"); break; - case GST_CASE_BRANCH_p: + case GST_CASE_BRANCH_PREVIEW: gtk_style_context_remove_class (style, "active_video_frame"); break; } - gst_switch_ui_update (prevframe); prevdisp->type = t; } } @@ -940,18 +873,14 @@ gst_switch_ui_add_preview_port (GstSwitchUI * ui, gint port, gint serve, gtk_widget_show_all (frame); -#if CUSTOM_FRAME_DRAWING - g_signal_connect (frame, "draw", - G_CALLBACK (gst_switch_ui_draw_preview_frame), ui); -#endif g_signal_connect (preview, "button-press-event", G_CALLBACK (gst_switch_ui_preview_click), ui); /* - GST_CASE_BRANCH_A, - GST_CASE_BRANCH_B, - GST_CASE_BRANCH_a, - GST_CASE_BRANCH_p, + GST_CASE_BRANCH_VIDEO_A, + GST_CASE_BRANCH_VIDEO_B, + GST_CASE_BRANCH_AUDIO, + GST_CASE_BRANCH_PREVIEW, */ switch (serve) { @@ -962,14 +891,13 @@ gst_switch_ui_add_preview_port (GstSwitchUI * ui, gint port, gint serve, g_signal_connect (G_OBJECT (disp), "end-worker", G_CALLBACK (gst_switch_ui_end_video_disp), ui); switch (type) { - case GST_CASE_BRANCH_A: - case GST_CASE_BRANCH_B: + case GST_CASE_BRANCH_VIDEO_A: + case GST_CASE_BRANCH_VIDEO_B: style = gtk_widget_get_style_context (frame); gtk_style_context_add_class (style, "active_video_frame"); gst_switch_ui_mark_active_video (ui, port, type); break; } - gst_switch_ui_update (frame); break; case GST_SERVE_AUDIO_STREAM: visual = gst_switch_ui_new_audio_visual (ui, preview, port); @@ -980,7 +908,6 @@ gst_switch_ui_add_preview_port (GstSwitchUI * ui, gint port, gint serve, style = gtk_widget_get_style_context (frame); gtk_style_context_add_class (style, "active_audio_frame"); } - gst_switch_ui_update (frame); break; default: gtk_widget_destroy (preview); @@ -1091,13 +1018,13 @@ gst_switch_ui_switch_unsafe (GstSwitchUI * ui, gint key) switch (key) { case GDK_KEY_A: case GDK_KEY_a: - type = GST_CASE_BRANCH_A; + type = GST_CASE_BRANCH_VIDEO_A; ok = gst_switch_client_switch (GST_SWITCH_CLIENT (ui), 'A', port); INFO ("switch-a: %d, %d", port, ok); break; case GDK_KEY_B: case GDK_KEY_b: - type = GST_CASE_BRANCH_B; + type = GST_CASE_BRANCH_VIDEO_B; ok = gst_switch_client_switch (GST_SWITCH_CLIENT (ui), 'B', port); INFO ("switch-b: %d, %d", port, ok); break; @@ -1150,15 +1077,17 @@ gst_switch_ui_switch (GstSwitchUI * ui, gint key) * @memberof GstSwitchUI */ static void -gst_switch_ui_next_compose_mode (GstSwitchUI * ui) +gst_switch_ui_next_compose (GstSwitchUI * ui, GstCompositeMode mode) { - gboolean ok = FALSE; - if (3 < ui->compose_mode) - ui->compose_mode = 0; - ok = gst_switch_client_set_composite_mode (GST_SWITCH_CLIENT (ui), - ui->compose_mode); - INFO ("set composite mode: %d (%d)", ui->compose_mode, ok); - ui->compose_mode += 1; + gboolean ok = + gst_switch_client_set_composite_mode (GST_SWITCH_CLIENT (ui), mode); + + INFO ("set composite mode: new %s (%d), previous %s", + gst_composite_mode_to_string (mode), + ok, gst_composite_mode_to_string (ui->compose_mode)); + + if (ok) + ui->compose_mode = mode; } /** @@ -1167,13 +1096,13 @@ gst_switch_ui_next_compose_mode (GstSwitchUI * ui) * @memberof GstSwitchUI */ static void -gst_switch_ui_next_compose_off (GstSwitchUI * ui) +gst_switch_ui_next_compose_mode (GstSwitchUI * ui) { - gboolean ok = gst_switch_client_set_composite_mode (GST_SWITCH_CLIENT (ui), - 0); - INFO ("set composite off (%d)", ok); - if (ok && ui->compose_mode == 0) - ui->compose_mode = 1; + GstCompositeMode next_mode = ui->compose_mode + 1; + if (next_mode > COMPOSE_MODE__LAST) + next_mode = 0; + + gst_switch_ui_next_compose (ui, next_mode); } /** @@ -1278,21 +1207,49 @@ gst_switch_ui_key_event (GtkWidget * w, GdkEvent * event, GstSwitchUI * ui) case GDK_KEY_b: gst_switch_ui_switch (ui, ke->keyval); break; + case GDK_KEY_R: + case GDK_KEY_r: + gst_switch_ui_new_record (ui); + break; + + // Keys to change the compose mode + // --------------------------------------------------------------- + // None + case GDK_KEY_Escape: + gst_switch_ui_next_compose (ui, COMPOSE_MODE_NONE); + break; + + // Picture-in-Picture + case GDK_KEY_F1: + case GDK_KEY_P: + case GDK_KEY_p: + gst_switch_ui_next_compose (ui, COMPOSE_MODE_PIP); + break; + + // Side-by-side (preview) + case GDK_KEY_F2: + case GDK_KEY_D: + case GDK_KEY_d: + gst_switch_ui_next_compose (ui, COMPOSE_MODE_DUAL_PREVIEW); + break; + + // Side-by-side (equal) + case GDK_KEY_F3: + case GDK_KEY_S: + case GDK_KEY_s: + gst_switch_ui_next_compose (ui, COMPOSE_MODE_DUAL_EQUAL); + break; + + // Cycle through the modes case GDK_KEY_Tab: { + // Cycle through them slowly.... if (200 <= ke->time - ui->tabtime) { ui->tabtime = ke->time; gst_switch_ui_next_compose_mode (ui); } } break; - case GDK_KEY_Escape: - gst_switch_ui_next_compose_off (ui); - break; - case GDK_KEY_R: - case GDK_KEY_r: - gst_switch_ui_new_record (ui); - break; } } break; diff --git a/tools/gstswitchui.h b/tools/gstswitchui.h index 6b8b350..d51464d 100644 --- a/tools/gstswitchui.h +++ b/tools/gstswitchui.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_SWITCH_UI_H__by_Duzy_Chan__ -#define __GST_SWITCH_UI_H__by_Duzy_Chan__ 1 +#ifndef __GST_SWITCH_UI_H__ +#define __GST_SWITCH_UI_H__ + #include #include #include "gstswitchclient.h" @@ -34,6 +35,9 @@ #include "gstvideodisp.h" #include "gstaudiovisual.h" +// Only allowed to use enum and to_string methods from this file. +#include "gstcomposite.h" + #define GST_TYPE_SWITCH_UI (gst_switch_ui_get_type ()) #define GST_SWITCH_UI(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), GST_TYPE_SWITCH_UI, GstSwitchUI)) #define GST_SWITCH_UI_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), GST_TYPE_SWITCH_UI, GstSwitchUIClass)) @@ -74,7 +78,7 @@ struct _GstSwitchUI GstVideoDisp *compose; guint32 tabtime; - gint compose_mode; + GstCompositeMode compose_mode; gint timer; GMutex faces_lock, tracking_lock; @@ -94,4 +98,4 @@ struct _GstSwitchUIClass GType gst_switch_ui_get_type (void); -#endif //__GST_SWITCH_UI_H__by_Duzy_Chan__ +#endif //__GST_SWITCH_UI_H__ diff --git a/tools/gstvideodisp.c b/tools/gstvideodisp.c index 7dbf0ee..f1bc4b5 100644 --- a/tools/gstvideodisp.c +++ b/tools/gstvideodisp.c @@ -151,7 +151,7 @@ gst_video_disp_get_pipeline_string (GstVideoDisp * disp) g_string_append_printf (desc, "! videoconvert "); g_string_append_printf (desc, "! cairooverlay name=overlay "); g_string_append_printf (desc, "! videoconvert "); - g_string_append_printf (desc, "! xvimagesink name=sink "); + g_string_append_printf (desc, "! xvimagesink name=sink sync=false "); return desc; } diff --git a/tools/gstvideodisp.h b/tools/gstvideodisp.h index 0aa0c3c..d930318 100644 --- a/tools/gstvideodisp.h +++ b/tools/gstvideodisp.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_VIDEO_DISP_H__by_Duzy_Chan__ -#define __GST_VIDEO_DISP_H__by_Duzy_Chan__ 1 +#ifndef __GST_VIDEO_DISP_H__ +#define __GST_VIDEO_DISP_H__ + #include "gstworker.h" #define GST_TYPE_VIDEO_DISP (gst_video_disp_get_type ()) @@ -63,4 +64,4 @@ struct _GstVideoDispClass GType gst_video_disp_get_type (void); -#endif //__GST_VIDEO_DISP_H__by_Duzy_Chan__ +#endif //__GST_VIDEO_DISP_H__ diff --git a/tools/gstworker.c b/tools/gstworker.c index a800c61..3a71a83 100644 --- a/tools/gstworker.c +++ b/tools/gstworker.c @@ -32,6 +32,8 @@ #include "gstworker.h" #include "gstswitchserver.h" +#include + #define GST_WORKER_LOCK_PIPELINE(srv) (g_mutex_lock (&(srv)->pipeline_lock)) #define GST_WORKER_UNLOCK_PIPELINE(srv) (g_mutex_unlock (&(srv)->pipeline_lock)) @@ -84,6 +86,7 @@ gst_worker_init (GstWorker * worker) worker->watch = 0; g_mutex_init (&worker->pipeline_lock); + g_cond_init (&worker->shutdown_cond); //INFO ("gst_worker init %p", worker); } @@ -145,6 +148,7 @@ gst_worker_finalize (GstWorker * worker) INFO ("gst_worker finalize %p", worker); g_mutex_clear (&worker->pipeline_lock); + g_cond_clear (&worker->shutdown_cond); g_free (worker->name); worker->name = NULL; @@ -378,29 +382,33 @@ gboolean gst_worker_stop_force (GstWorker * worker, gboolean force) { GstStateChangeReturn ret = GST_STATE_CHANGE_FAILURE; + gboolean no_eos; g_return_val_if_fail (GST_IS_WORKER (worker), FALSE); GST_WORKER_LOCK_PIPELINE (worker); if (worker->pipeline) { -#if 1 GstState state; ret = gst_element_get_state (worker->pipeline, &state, NULL, GST_CLOCK_TIME_NONE); + no_eos = (state == GST_STATE_PLAYING) && !worker->send_eos_on_stop; - if (state == GST_STATE_PLAYING || force) { + if (force || no_eos) { ret = gst_element_set_state (worker->pipeline, GST_STATE_NULL); gst_bus_set_flushing (worker->bus, TRUE); g_timeout_add (5, (GSourceFunc) gst_worker_state_ready_to_null_proxy, worker); + } else if (state == GST_STATE_PLAYING) { + /* Send an EOS to cleanly shutdown */ + gst_element_send_event (worker->pipeline, gst_event_new_eos ()); + + /* Go to sleep until the EOS handler calls stop_force (worker, TRUE); */ + g_cond_wait (&worker->shutdown_cond, &worker->pipeline_lock); } -#else - ret = gst_element_set_state (worker->pipeline, GST_STATE_NULL); -#endif } GST_WORKER_UNLOCK_PIPELINE (worker); @@ -449,7 +457,8 @@ gst_worker_missing_plugin (GstWorker *worker, GstStructure *structure) static void gst_worker_handle_eos (GstWorker * worker) { - gst_worker_stop (worker); + gst_worker_stop_force (worker, TRUE); + GST_WORKER_CLASS (G_OBJECT_GET_CLASS (worker))->close (worker); } static void @@ -480,26 +489,28 @@ gst_worker_handle_error (GstWorker * worker, GError * error, const char *debug) if (error->domain == GST_STREAM_ERROR) { ERROR ("%s: (STREAM: %d) %s", worker->name, error->code, error->message); } -#if 0 ERROR ("DEBUG INFO:\n%s\n", debug); -#endif -#if 0 gst_worker_stop (worker); -#endif + GstWorkerClass *worker_class = GST_WORKER_CLASS (G_OBJECT_GET_CLASS (worker)); + worker_class->close (worker); } static void gst_worker_handle_warning (GstWorker * worker, GError * error, const char *debug) { - WARN ("%s: %s", worker->name, error->message); + // kludge: some gstreamer "warnings" are apparently non-recoverable errors + if (strstr (error->message, "error:") != NULL) + gst_worker_handle_error (worker, error, debug); + else + WARN ("%s: %s (%s)", worker->name, error->message, debug); } static void gst_worker_handle_info (GstWorker * worker, GError * error, const char *debug) { - INFO ("%s: %s", worker->name, error->message); + INFO ("%s: %s (%s)", worker->name, error->message, debug); } static void @@ -603,6 +614,21 @@ gst_worker_pipeline_state_changed (GstWorker * worker, return TRUE; } +static GstBusSyncReply +gst_worker_message_sync (GstBus * bus, GstMessage * message, GstWorker * worker) +{ + switch (GST_MESSAGE_TYPE (message)) { + case GST_MESSAGE_EOS: + /* When we see EOS, wake up and gst_worker_stop that might be waiting */ + g_cond_signal (&worker->shutdown_cond); + break; + default: + break; + } + + return GST_BUS_PASS; +} + static gboolean gst_worker_message (GstBus * bus, GstMessage * message, GstWorker * worker) { @@ -762,6 +788,9 @@ gst_worker_prepare_unsafe (GstWorker * worker) if (!worker->watch) goto error_add_watch; + gst_bus_set_sync_handler (worker->bus, + (GstBusSyncHandler) (gst_worker_message_sync), worker, NULL); + if (workerclass->prepare && !workerclass->prepare (worker)) goto error_prepare; @@ -867,6 +896,12 @@ gst_worker_reset (GstWorker * worker) return ok; } +static void +gst_worker_close (GstWorker * worker) +{ + +} + /** * @brief Initialize GstWorkerClass. * @param klass The instance of GstWorkerClass. @@ -912,4 +947,5 @@ gst_worker_class_init (GstWorkerClass * klass) klass->create_pipeline = gst_worker_create_pipeline; klass->null = gst_worker_null; klass->reset = gst_worker_reset; + klass->close = gst_worker_close; } diff --git a/tools/gstworker.h b/tools/gstworker.h index 62cca74..3b70fcb 100644 --- a/tools/gstworker.h +++ b/tools/gstworker.h @@ -25,8 +25,9 @@ /*! @file */ -#ifndef __GST_WORKER_H__by_Duzy_Chan__ -#define __GST_WORKER_H__by_Duzy_Chan__ 1 +#ifndef __GST_WORKER_H__ +#define __GST_WORKER_H__ + #include #include "../logutils.h" @@ -90,6 +91,13 @@ typedef GstWorkerNullReturn (*GstWorkerNullFunc) (GstWorker * worker); */ typedef void (*GstWorkerAliveFunc) (GstWorker * worker); +/** + * @brief worker virtual close function + * @param worker The GstWorker instance. + */ + +typedef void (*GstWorkerCloseFunc) (GstWorker * worker); + /** * @class GstWorker * @struct _GstWorker @@ -104,6 +112,7 @@ struct _GstWorker //GstSwitchServer *server; /*!< */ GMutex pipeline_lock; /*!< Mutex for %pipeline */ + GCond shutdown_cond; /*!< Cond for shutting down pipelines cleanly */ GstElement *pipeline; /*!< The pipeline. */ GstBus *bus; /*!< The pipeline bus. */ @@ -114,6 +123,11 @@ struct _GstWorker gboolean auto_replay; /*!< The worker should replay if it's TRUE */ gboolean paused_for_buffering; /*!< Mark for buffering pause. */ guint watch; /*!< The watch number of the pipeline bus. */ + + /*!< TRUE if the recording pipeline needs clean shut-down + * via an EOS event to finish up before stopping + */ + gboolean send_eos_on_stop; }; /** @@ -200,6 +214,13 @@ struct _GstWorkerClass * @param worker The GstWorker instance. */ gboolean (*reset) (GstWorker * worker); + + /* + * @brief Close the worker, deallocating and closing resources + * @param worker The GstWorker instance. + */ + + void (*close) (GstWorker * worker); }; /** @@ -269,4 +290,4 @@ GstElement *gst_worker_get_element_unlocked (GstWorker *worker, const gchar *nam */ GstElement *gst_worker_get_element (GstWorker *worker, const gchar * name); -#endif //__GST_WORKER_H__by_Duzy_Chan__ +#endif //__GST_WORKER_H__