From 3c6c72fe551fc79917436587434fbfa6142252e9 Mon Sep 17 00:00:00 2001 From: Mediacore Developer Date: Mon, 24 Sep 2018 10:42:16 +0000 Subject: [PATCH] Initial commit of current code base --- .gitignore | 2 + BUILDING.txt | 36 +++ VERSION.py | 7 + debian/postinst | 40 +++ debian/prerm | 39 +++ etc/dbus-1/system.d/nl.miqra.MediaServer.conf | 17 ++ etc/init.d/mediaserver | 175 ++++++++++++ mediacore-mediaserver | 4 + mediaserver/PKG_CONFIG.py | 51 ++++ mediaserver/__init__.py | 15 + mediaserver/audioplayer.py | 68 +++++ mediaserver/basicplayer.py | 227 +++++++++++++++ mediaserver/event.py | 66 +++++ mediaserver/mediaserver.py | 248 ++++++++++++++++ mediaserver/quickplayer.py | 66 +++++ sbin/mediaserver | 4 + setup.cfg | 2 + setup.py | 53 ++++ stdeb.cfg | 9 + test_basicplayer.py | 43 +++ usr-share-mediaserver/3d-loudspeaker.svg | 28 ++ usr-share-mediaserver/background-image.png | Bin 0 -> 59537 bytes usr-share-mediaserver/background-image.svg | 266 ++++++++++++++++++ 23 files changed, 1466 insertions(+) create mode 100644 .gitignore create mode 100644 BUILDING.txt create mode 100644 VERSION.py create mode 100755 debian/postinst create mode 100644 debian/prerm create mode 100644 etc/dbus-1/system.d/nl.miqra.MediaServer.conf create mode 100755 etc/init.d/mediaserver create mode 100755 mediacore-mediaserver create mode 100644 mediaserver/PKG_CONFIG.py create mode 100644 mediaserver/__init__.py create mode 100644 mediaserver/audioplayer.py create mode 100644 mediaserver/basicplayer.py create mode 100644 mediaserver/event.py create mode 100644 mediaserver/mediaserver.py create mode 100644 mediaserver/quickplayer.py create mode 100644 sbin/mediaserver create mode 100644 setup.cfg create mode 100755 setup.py create mode 100755 stdeb.cfg create mode 100644 test_basicplayer.py create mode 100644 usr-share-mediaserver/3d-loudspeaker.svg create mode 100755 usr-share-mediaserver/background-image.png create mode 100755 usr-share-mediaserver/background-image.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dc31af --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +deb_dist/ +*.pyc diff --git a/BUILDING.txt b/BUILDING.txt new file mode 100644 index 0000000..896bfb5 --- /dev/null +++ b/BUILDING.txt @@ -0,0 +1,36 @@ +Package: khmedia + +PACKAGE BUILDING: + +For building the debian package, the following prerequisities are needed: + python-stdeb + +To start the package building process, execute: + ./setup.py bdist_deb + +after succesful excecution, the package files can be found in the directory + ./deb_dist/ + +INSTALLING LOCALLY: + +For testing purposes the package can be installed by executing: + ./setup.py install + +However, because packages installed like that can not be easily removed, +it is recommended for non-dedicated development stations to build the debian package, +and install that package. It can subsequently be easily removed by doing + apt-get remove + +TESTING AND DEVELOPING LOCALLY: +If the required packages are installed, this package can be tested for development without installing. +The scripts in the root dir of the package are intended to start the applications locally without installing. +The package in the subfolder will be used in that case. + +*NOTE 1: +Dependent packages to need to be installed either though a debian package (recommended), or by issuing + ./setup.py install +in their source folder. + +*NOTE 2: +If you install manually, you will also need to update the gtk icon cache with the following command + sudo gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor \ No newline at end of file diff --git a/VERSION.py b/VERSION.py new file mode 100644 index 0000000..5ecbbdc --- /dev/null +++ b/VERSION.py @@ -0,0 +1,7 @@ + +major=1 # Major version +minor=0 # Minor version +micro=2 # Bugfix version + +# code to generate version string from above data +version_string = "{0}.{1}.{2}".format(major,minor,micro) diff --git a/debian/postinst b/debian/postinst new file mode 100755 index 0000000..1f861a5 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,40 @@ +#!/bin/sh +# postinst script for piio-server +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + update-rc.d mediaserver defaults + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/prerm b/debian/prerm new file mode 100644 index 0000000..73f3269 --- /dev/null +++ b/debian/prerm @@ -0,0 +1,39 @@ +#!/bin/sh +# prerm script for piio-server +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `upgrade' +# * `failed-upgrade' +# * `remove' `in-favour' +# * `deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + remove|upgrade|deconfigure) + update-rc.d -f mediaserver remove + ;; + + failed-upgrade) + ;; + + *) + echo "prerm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/etc/dbus-1/system.d/nl.miqra.MediaServer.conf b/etc/dbus-1/system.d/nl.miqra.MediaServer.conf new file mode 100644 index 0000000..3518add --- /dev/null +++ b/etc/dbus-1/system.d/nl.miqra.MediaServer.conf @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/etc/init.d/mediaserver b/etc/init.d/mediaserver new file mode 100755 index 0000000..ea6968e --- /dev/null +++ b/etc/init.d/mediaserver @@ -0,0 +1,175 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: mediacore-mediaserver +# Required-Start: $syslog $dbus +# Required-Stop: $syslog $dbus +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Example initscript +# Description: This file should be used to construct scripts to be +# placed in /etc/init.d. +### END INIT INFO + +# Author: Miqra Engineering <@miqra.nl> +# +# Please remove the "Author" lines above and replace them +# with your own name if you copy and modify this script. + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/bin:/usr/sbin:/usr/bin +DESC="Mediacore media player server" +NAME=mediaserver +DAEMON=/usr/sbin/$NAME +DAEMON_ARGS="" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/usr/local/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +VERBOSE=yes + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # start background image + fbi -a -noverbose -t 1 -T 1 -d /dev/fb0 /usr/share/mediaserver/background-image.png >/dev/null 2> /dev/null & + + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + + start-stop-daemon --start --quiet --background -m --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --background -m --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # kill background image (fbi) + killall fbi + + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --signal TERM --pidfile $PIDFILE + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --exec $DAEMON + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + + status_of_proc "$DAEMON" "$NAME" >/dev/null + if [ $? != 0 ]; then + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + else + [ "$VERBOSE" != no ] && log_end_msg 1 + [ "$VERBOSE" != no ] && echo "$NAME already running" + fi + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/mediacore-mediaserver b/mediacore-mediaserver new file mode 100755 index 0000000..188a82e --- /dev/null +++ b/mediacore-mediaserver @@ -0,0 +1,4 @@ +#!/usr/bin/python +import mediaserver.mediaserver + +mediaserver.mediaserver.Run() \ No newline at end of file diff --git a/mediaserver/PKG_CONFIG.py b/mediaserver/PKG_CONFIG.py new file mode 100644 index 0000000..188b90e --- /dev/null +++ b/mediaserver/PKG_CONFIG.py @@ -0,0 +1,51 @@ +# shared package configuration settings for applications in KHMedia +##### Common ##### + +# folder to store user specific configuration +user_cfg_folder = "~/.khmedia" + +# khmedia config file +config_file = "khmedia_config.xml" + +# khmedia example config resource +example_config_resource = "khmedia_config.example.xml" + +# location of the khsystem_need_config file +khsystem_need_config = "~/.khsystem_config_needed" + +##### Player ##### + +#default folders to look for song files (last in line is the fallback folder that will be created if none are existing) +default_song_folders = ["/usr/share/khmedia/songs","/usr/local/share/khmedia/songs","~/Songs"] + +#default folders to look for background music files (last in line is the fallback folder that will be created if none are existing) +default_music_folders = ["/usr/share/khmedia/music","/usr/local/share/khmedia/music","~/Music"] + +# files that are considered playable music files +music_extensions = [".mp3",".wav",".ogg",".m4b",".m4a"] + +# default volume for song playback (Linear 0.0 <= value <= 1.0 ) +default_song_volume = 0.5 + +# default volume for music playback (Linear 0.0 <= value <= 1.0 ) +default_music_volume = 0.12 + +##### Recorder ##### + +#default folder to store recordings +# You can use the following replacements +# {XDG_DESKTOP_DIR} +# {XDG_DOWNLOAD_DIR} +# {XDG_TEMPLATES_DIR} +# {XDG_PUBLICSHARE_DIR} +# {XDG_DOCUMENTS_DIR} +# {XDG_MUSIC_DIR} +# {XDG_PICTURES_DIR} +# {XDG_VIDEOS_DIR} + +# lowercase text between brackets is replaced by the corresponding text in the +# currently loaded translation table +# e.g. {something} can be replaced by "Something" or "Iets" if that is defined in the +# translation tabled +default_recordings_folder = "{XDG_DESKTOP_DIR}/{dir_recordings}" + diff --git a/mediaserver/__init__.py b/mediaserver/__init__.py new file mode 100644 index 0000000..aee403e --- /dev/null +++ b/mediaserver/__init__.py @@ -0,0 +1,15 @@ +# perform gstreamer imports (python3 style) +import gi +gi.require_version('Gst','1.0') + +from gi.repository import GObject +from gi.repository import Gst + +# now perform once-per-module initializations +GObject.threads_init() +Gst.init(None) + +""" +from mediaserver.basicplayer import BasicPlayer +player = BasicPlayer() +""" \ No newline at end of file diff --git a/mediaserver/audioplayer.py b/mediaserver/audioplayer.py new file mode 100644 index 0000000..ab165a9 --- /dev/null +++ b/mediaserver/audioplayer.py @@ -0,0 +1,68 @@ +# perform gstreamer imports (python3 style) +import gi +gi.require_version('Gst','1.0') + +from gi.repository import GObject + +from threading import Thread +import time + +# import from this module +from basicplayer import BasicPlayer + +class MonitorThread(Thread): + def __init__(self,player): + self.player = player + Thread.__init__(self) + self.daemon = True # as in: don't wait on this thread to quit + self._running = False + + def stop(self): + if self._running: + self._running = False + #self.join(0.5) + + def run(self): + self._running = True + while self._running: + if self.player.player_state == "PLAYING" and self.player.playtime > 0: + position = self.player.position() + if (position - self.player.startpos) >= self.player.playtime: + self.player._finished() + if self._running and time is not None: + time.sleep(0.2) + pass + +class AudioPlayer(BasicPlayer): + + def __init__(self): + BasicPlayer.__init__(self) + # setup monitor thread + self.monitor = MonitorThread(self) + self.monitor.start() + self.playtime = -1 + + def __del__(self): + self.monitor.stop() + + def playfor(self,duration): + if duration > 0: # not much point in wasting system resources otherwise + self.startpos = self.position() + self.playtime = duration +# self.monitor.start() + BasicPlayer.play(self) + elif duration < 0: # same as normal play + self.play() + + def play(self): + if self.player_state != "PAUSED": + self.playtime = -1; + BasicPlayer.play(self) + + def stop(self): + self.playtime = -1; + BasicPlayer.stop(self) + #self.monitor.stop() + +GObject.type_register(AudioPlayer) + \ No newline at end of file diff --git a/mediaserver/basicplayer.py b/mediaserver/basicplayer.py new file mode 100644 index 0000000..8b53eaf --- /dev/null +++ b/mediaserver/basicplayer.py @@ -0,0 +1,227 @@ +# License for this source file +# +# Copyright (c) 2014 Miqra Engineering +# + +import sys, os +# perform gstreamer imports (python3 style) +import gi +gi.require_version('Gst','1.0') + +from gi.repository import GObject +from gi.repository import Gst + + +class BasicPlayer(GObject.GObject): + __gsignals__ = { + 'playback-ready' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: source_file tag_dict + (GObject.TYPE_STRING, GObject.TYPE_PYOBJECT)), + 'playback-playing' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: None + ()), + 'playback-stopped' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: None + ()), + 'playback-paused' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: None + ()), + 'playback-finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: None + ()), + 'playback-error' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: player_state error debug + (GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING,)), + 'volume-changed' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: volume + (GObject.TYPE_FLOAT,)), + 'playback-buffering' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, # parameters: volume + (GObject.TYPE_INT,)), + } + + __gproperties__ = { + + 'volume' : (GObject.TYPE_FLOAT, + 'Volume', + 'Volume', + 0, + 1, + 1, + GObject.PARAM_READWRITE), + } + + _running = False + tags = {} + + source = None + + def __init__(self): + GObject.GObject.__init__(self) + # setup gstreamer + self.pipeline_state = Gst.State.NULL; + self.prepare_gstreamer() + + self.buffering = False + + def __del__(self): + self.stop_gstreamer() + + def do_get_property(self, property): + if property.name == 'volume': + return self.pipeline.get_property('volume') + else: + raise AttributeError, 'unknown property %s' % property.name + + def do_set_property(self, property, value): + if property.name == 'volume': + volume = clamp(value,0.0,1.0) + self.pipeline.set_property('volume', volume) + #print "Set volume to {0}, got {1}".format(volume,self.pipeline.get_property('volume')) + self.emit('volume-changed',volume) + else: + raise AttributeError, 'unknown property %s' % property.name + + def play(self): + # start playback ;) + if self.player_state in ["READY","PAUSED",]: + self.player_state = "PLAYING" + self.pipeline.set_state(Gst.State.PLAYING) + self.emit('playback-playing') + + def load(self,file): + self.source = "file://" + file + #print "Attempting to load: '{0}'".format(file) + self.pipeline.set_state(Gst.State.NULL); + self.pipeline.set_property("uri", self.source) + self.pipeline.set_state(Gst.State.PAUSED); + + self.player_state = "LOADING" + self.tags.clear() + + def load_uri(self,uri): + self.source = uri + #print "Attempting to load: '{0}'".format(file) + self.pipeline.set_state(Gst.State.NULL); + self.pipeline.set_property("uri", self.source) + self.pipeline.set_state(Gst.State.PAUSED); + + self.player_state = "LOADING" + self.tags.clear() + + + def stop(self): + if self.player_state in ["PLAYING","PAUSED",]: + self._running = False + self.pipeline.set_state(Gst.State.READY) + self.player_state = "READY" + self.emit('playback-stopped') + + def pause(self): + # cannot pause/unpause if we're waiting for buffer + if self.player_state in ["PLAYING",] and not self.buffering: + self.player_state = "PAUSED" + self.pipeline.set_state(Gst.State.PAUSED) + self.emit('playback-paused') + + def _finished(self): + self._running = False + self.pipeline.set_state(Gst.State.READY) + self.seek(0) + self.player_state = "READY" + self.emit('playback-finished') + + def prepare_gstreamer(self): + # bin containing the recorder stuff + + self.pipeline = Gst.ElementFactory.make("playbin", None) + videosink = Gst.ElementFactory.make("eglglessink", None) + alsasink = Gst.ElementFactory.make("alsasink", None) + + # set output device + #devicename = self.config["Devices.Output"].getStr('name') + #if common.check_alsadev(devicename): + # alsasink.set_property('device', devicename) + + self.pipeline.set_property("video-sink", videosink) + self.pipeline.set_property("audio-sink", alsasink) + + # connect the bus listener to the message function + self.bus = self.pipeline.get_bus() + self.bus.add_signal_watch() + self.bus.connect("message", self.on_message) + + self.player_state = "NONE" + + def stop_gstreamer(self): + try: + self.pipeline.get_bus().disconnect(self.busconnection) + self.pipeline.get_bus().remove_signal_watch() + self.pipeline.set_state(Gst.State.NULL) + except GObject.GError, e: + self.set_sensitive(True) + + def seek(self,seconds): + self.pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seconds* Gst.SECOND) + + def duration(self): + result = self.pipeline.query_duration(Gst.Format.TIME) + if result is not None and result[0]: + return float(result[1]) / Gst.SECOND + else: + return -1; + + def position(self): + result = self.pipeline.query_position(Gst.Format.TIME) + if result is not None and result[0]: + return float(result[1]) / Gst.SECOND + else: + return -1; + + def on_message(self, bus, message): + t = message.type + + # detect end of stream, and + if t == Gst.MessageType.EOS: + self._finished() + elif t == Gst.MessageType.ERROR: + self.pipeline.set_state(Gst.State.NULL) + err, debug = message.parse_error() + #sys.stderr.write("Error: {0}\nDebug: {1}".format(err,debug)) + self.pipeline.set_state(Gst.State.NULL) + self._running = False + self.emit('playback-finished') + self.emit('playback-error', self.player_state, err, debug) + self.player_state = "NONE" + elif t == Gst.MessageType.TAG: + tags = message.parse_tag(); + for i in range(0,tags.n_tags()): + key = tags.nth_tag_name(i) + val = tags.get_value_index(key,0) + self.tags[key] = val; + elif t == Gst.MessageType.ASYNC_DONE: + if message.src == self.pipeline: + pass + elif t == Gst.MessageType.STREAM_STATUS: + (status,owner) = message.parse_stream_status() +# print "Stream status: {0} (by {1})\n".format(status,owner) + pass + elif t == Gst.MessageType.BUFFERING: + pct = message.parse_buffering() + print "Buffering: {0}%".format(pct) + self.emit('playback-buffering',pct) + if pct != 100: + if self.pipeline_state == Gst.State.PLAYING: + self.pipeline.set_state(Gst.State.PAUSED) + self.buffering = True + elif pct == 100: + if self.player_state == "PLAYING": + self.pipeline.set_state(Gst.State.PLAYING) + self.buffering = False + elif t == Gst.MessageType.DURATION_CHANGED: +# print "Stream duration changed: {0}\n".format(float(self.pipeline.query_duration(Gst.Format.TIME)[1])/Gst.SECOND) + pass + elif t == Gst.MessageType.STATE_CHANGED: + if message.src == self.pipeline: + (old,new,pending) = message.parse_state_changed() + self.pipeline_state = new +# print "State changed from '{0}' to '{1}' pending '{2}'\n".format(old,new,pending) + if old == Gst.State.READY and new == Gst.State.PAUSED and self.player_state == "LOADING": + self.pipeline.set_state(Gst.State.PAUSED) + self.player_state = "READY" + self.emit('playback-ready',self.source,dict(self.tags)) + + +GObject.type_register(BasicPlayer) diff --git a/mediaserver/event.py b/mediaserver/event.py new file mode 100644 index 0000000..3a4391d --- /dev/null +++ b/mediaserver/event.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +# event.py +# +# (C) 2011 Bram Kuijvenhoven + +''' +Provides generic event object that allows callback registration to events. + +''' + +class Event(object): + ''' + Generic event object providing callback registration. + + Event listeners are callables. + The semantics of the argument list to the callbacks is not hardcoded in this class; + instead is it implied by the context where this Event object lives. + Registration and unregistration are done by the += and -= operators. + A callable can only be registered once and only be unregistered once. + The event is fired by calling the event object itself with the desired parameters. + + Example: + + >>> def listener(message): + ... print message; + >>> event = Event(); + >>> event("x"); + >>> event += listener; + >>> event("x"); + x + >>> event -= listener; + >>> event("x"); + + ''' + def __init__(self): + self.listeners = []; + def __iadd__(self, listener): + """ + Add new event listener. + @param listener: callable; will be called whenever the event fires. + @return: self. + @raise ValueError: if the listener has already been registered for this event. + """ + if listener in self.listeners: + raise ValueError("Listener already registered to event"); + self.listeners.append(listener); + return self; + def __isub__(self, listener): + """ + Remove previously registered event listener. + @param listener: previously registered event listener. + @return: self. + @raise ValueError: if the listener is not registered for this event. + """ + if listener not in self.listeners: + raise ValueError("Listener not registered to event"); + self.listeners.remove(listener); + return self; + def __call__(self, *args, **kwargs): + """ + Fire event, passing the specified arguments to all listeners. + Each listener will be called with listener(*args, **kwargs). + """ + for listener in list(self.listeners): + listener(*args, **kwargs); diff --git a/mediaserver/mediaserver.py b/mediaserver/mediaserver.py new file mode 100644 index 0000000..2930752 --- /dev/null +++ b/mediaserver/mediaserver.py @@ -0,0 +1,248 @@ +# License for this source file +# +# Copyright (c) 2014 Miqra Engineering +# + +import sys, os +import signal + +from optparse import OptionParser +from collections import OrderedDict + +# perform gstreamer imports (python3 style) +import gi +gi.require_version('Gst','1.0') + +from gi.repository import GObject +from gi.repository import Gst +Gst.init(None) + +# perform dbus imports +import dbus +import dbus.service +from dbus.mainloop.glib import DBusGMainLoop + +# set dbus to use GObject Mainloop +DBusGMainLoop(set_as_default=True) + +# imports from module +from audioplayer import AudioPlayer +from quickplayer import QuickPlayer + + +class MediaService(dbus.service.Object): + def __init__(self): + + bus_name = dbus.service.BusName('nl.miqra.MediaCore.Media', bus=dbus.SystemBus()) + dbus.service.Object.__init__(self, bus_name, '/nl/miqra/MediaCore/Media') + + self.player = AudioPlayer() + self.quickplayer = QuickPlayer() + + self.player.connect('playback-ready',self.onPlayerReady) # parameters: source_file tag_dict + self.player.connect('playback-playing',self.onPlayerPlaying) # parameters: None + self.player.connect('playback-stopped',self.onPlayerStopped) # parameters: None + self.player.connect('playback-paused',self.onPlayerPaused) # parameters: None + self.player.connect('playback-finished',self.onPlayerFinished) # parameters: None + self.player.connect('playback-error',self.onPlayerError) # parameters: error debug + self.player.connect('volume-changed',self.onPlayerVolumeChanged) # parameters: volume + self.player.connect('playback-buffering',self.onPlayerBuffering) # parameters: volume + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='s', out_signature='') + def QuickPlay(self, file,): + """ Directly play back a local file + """ + self.quickplayer.play(file) + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='sd', out_signature='') + def QuickPlayFor(self, file, duration): + """ Directly play back a local file + """ + self.quickplayer.playfor(file,duration) + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='s', out_signature='') + def QuickPlayUrl(self, url,): + """ Directly play back a url + """ + self.quickplayer.playurl(url) + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='sd', out_signature='') + def QuickPlayUrlFor(self, url, duration): + """ Directly play back a url + """ + self.quickplayer.playurlfor(url,duration) + + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='s', out_signature='') + def LoadFile(self, file): + """ Load a local file for playback + """ + print "Loading file {0}".format(file) + self.player.load(file) + self.OnLoading("file://{0}".format(file)) + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='s', out_signature='') + def LoadUrl(self, uri): + """ Load an url for playback + """ + print "Loading url {0}".format(file) + self.player.load_uri(uri) + self.OnLoading(uri) + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='d', out_signature='') + def PlayFor(self, duration): + """ Starts playback for [duration] seconds + """ + print "Starting playback for {0} seconds".format(duration) + self.player.playfor(duration) + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='', out_signature='') + def Play(self): + """ Starts/resumes playback + """ + pos = self.player.position() + print "Starting playback at timestamp {0}".format(pos) + self.player.play() + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='', out_signature='') + def Pause(self): + """ Pauses playback + """ + print "Pausing" + self.player.pause() + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='', out_signature='') + def Stop(self): + """ Stops playback + """ + print "Stopping" + self.player.stop() + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='', out_signature='d') + def Length(self): + """ returns length of currently loaded source + """ + return self.player.duration() + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='', out_signature='d') + def Position(self): + """ returns current position in the source + """ + return self.player.position() + + @dbus.service.method(dbus_interface='nl.miqra.MediaCore.Media', in_signature='d', out_signature='b') + def Seek(self,position): + """ returns current position in the source + """ + self.player.seek(position) + + ### Callback functions + def onPlayerReady(self,player,filename,tags): + print "Loaded {0}".format(filename) + print "Tags:" + taglist = {} + for tag in tags: + try: + taglist[tag] = str(tags[tag]) + print " {0}: {1}".format(tag,taglist[tag]) + except Exception as x: + print "Error converting value for '{0}':\n {1}".format(tag,x) + self.OnReady(filename,taglist) + + def onPlayerPlaying(self,player): + self.OnPlaying() + + def onPlayerStopped(self,player): + self.OnStopped() + + def onPlayerPaused(self,player): + self.OnPaused() + + def onPlayerFinished(self,player): + self.OnFinished() + + def onPlayerError(self,player,state,error,debug): + print "Player state: {0}".format(state) + if state == "LOADING": + print "Failure during LOAD: {0}".format(error) + self.OnLoadFail(error) + else: + print "Failure during RUN: {0}".format(error) + self.OnRunFail(error) + + def onPlayerVolumeChanged(self,player,volume): + pass + + def onPlayerBuffering(self,player,pct): + self.OnBuffering(pct) + pass + + ## Signalling functions (DBus) + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='s') + def OnLoading(self,url): + """ gets called when a source has been requested for playback + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='sa{ss}') + def OnReady(self,filename,tags): + """ gets called when a source is ready for playback + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='') + def OnPlaying(self): + """ gets called when a playback has started + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='') + def OnPaused(self): + """ gets called when a playback is paused + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='q') + def OnBuffering(self,percent): + """ gets called when a playback is paused + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='') + def OnStopped(self): + """ gets called when playback has stopped + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='') + def OnFinished(self): + """ gets called when playback has completed + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='s') + def OnLoadFail(self,reason): + """ gets called when loading a source for playback fails + """ + pass + + @dbus.service.signal(dbus_interface='nl.miqra.MediaCore.Media', signature='s') + def OnRunFail(self,reason): + """ gets called when playback fails + """ + pass + +def Run(): + mediaservice = MediaService() + loop = GObject.MainLoop() + loopcontext = loop.get_context() + + def onsigint(signal,frame): + print "Quitting" + loop.quit() + + signal.signal(signal.SIGINT, onsigint) + + print "Starting..." + loop.run() diff --git a/mediaserver/quickplayer.py b/mediaserver/quickplayer.py new file mode 100644 index 0000000..b91df3a --- /dev/null +++ b/mediaserver/quickplayer.py @@ -0,0 +1,66 @@ +# perform gstreamer imports (python3 style) +import gi +gi.require_version('Gst','1.0') + +from gi.repository import GObject + +# import from this module +from audioplayer import AudioPlayer + +class QuickPlayer(GObject.GObject): + + def __init__(self): + GObject.GObject.__init__(self) + self.player = AudioPlayer() + self._duration = -1; + self.player.connect("playback-ready",self.onReady) + self.player.connect("playback-error",self.onError) + + + # Calling functions for file + def playfor(self,file,duration): + if duration > 0: # not much point in wasting system resources otherwise + self._duration = duration + self.player.load(file) + elif duration < 0: # same as normal play + self.play(file) + + def play(self,file): + self._duration = -1; + self.player.load(file) + + # Calling functions for urls + def playurlfor(self,url,duration): + if duration > 0: # not much point in wasting system resources otherwise + self._duration = duration + self.player.load_uri(url) + elif duration < 0: # same as normal play + self.playurl(url) + + def playurl(self,url): + self._duration = -1; + self.player.load_uri(url) + + # stop function + def stop(self): + self._duration = -1 + self.player.stop() + + def onReady(self,player,file,tags): + print "Quickplay loaded: {0}".format(file) + for tag in tags: + print " {0}: {1}".format(tag,tags[tag]) + if self._duration > 0: + self.player.playfor(self._duration) + elif self._duration < 0: + self.player.play() + + def onError(self,player,player_state,error,debug): + print "Quickplay error during " + player_state + ":" + print " " + error + print " " + print " " + debug + + +GObject.type_register(QuickPlayer) + \ No newline at end of file diff --git a/sbin/mediaserver b/sbin/mediaserver new file mode 100644 index 0000000..188a82e --- /dev/null +++ b/sbin/mediaserver @@ -0,0 +1,4 @@ +#!/usr/bin/python +import mediaserver.mediaserver + +mediaserver.mediaserver.Run() \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..467c90c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[global] +command-packages=stdeb.command \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..ef304da --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +'''KHMedia applications. +Includes scheduled recorder, music player, sound level meter and a webcam viewer +''' + +from distutils.core import setup +from distutils.extension import Extension +from glob import glob +import VERSION + +# patch distutils if it's too old to cope with the "classifiers" or +# "download_url" keywords +from sys import version +if version < '2.2.3': + from distutils.dist import DistributionMetadata + DistributionMetadata.classifiers = None + DistributionMetadata.download_url = None + +if __name__ == '__main__': + setup( + name = 'mediaserver', + version = VERSION.version_string, + description = 'Mediacore mediaserver', + long_description = __doc__, + author = 'Miqra Engineering', + author_email='packaging@miqra.nl', + maintainer = 'Miqra Engineering Packaging', + maintainer_email = 'packaging@miqra.nl', + license='', + platforms=['posix'], + url='', + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: End Users/Desktop', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 2', + 'Topic :: Multimedia :: Sound/Audio', + 'Topic :: Multimedia :: Sound/Audio :: Players', + ], + packages=['mediaserver'], + package_data={'mediaserver': ['image/*', ]}, + data_files = [ + # note that some files are forced to /usr/share/... instead of just share/.. + # this is because the system does not look in /usr/local/share/... for those files, but only in /usr/share/... + ('/usr/share/mediaserver', glob('usr-share-mediaserver/*')), + ('/etc/dbus-1/system.d', glob('etc/dbus-1/system.d/*')), + ('/etc/init.d', glob('etc/init.d/*')), + ('bin', glob('bin/*')), + ('sbin', glob('sbin/*')), + + ], + ) diff --git a/stdeb.cfg b/stdeb.cfg new file mode 100755 index 0000000..efa8e71 --- /dev/null +++ b/stdeb.cfg @@ -0,0 +1,9 @@ + +[DEFAULT] +Depends: python-dbus, python-gi, fbi, gir1.2-gstreamer-1.0, gir1.2-gst-plugins-base-1.0, gstreamer1.0-plugins-good, gstreamer1.0-plugins-ugly, gstreamer1.0-plugins-bad, gstreamer1.0-alsa, gstreamer1.0-omx, gstreamer1.0-libav +XS-Python-Version: >= 2.6 +Section: sound +Package: mediaserver +Suite: stable +# Do reset the debian version below to 1 for each public version update +Debian-Version: 2 diff --git a/test_basicplayer.py b/test_basicplayer.py new file mode 100644 index 0000000..085cbf5 --- /dev/null +++ b/test_basicplayer.py @@ -0,0 +1,43 @@ +#!/usr/bin/python + +from mediaserver.audioplayer import AudioPlayer +from mediaserver.quickplayer import QuickPlayer +from gi.repository import GObject + + +def onReady(player, file, tags): + print "Starting {0} ...\n".format(file) + print " Tags:\n" + for tag in tags: + print " {0} : '{1}'\n".format(tag,tags[tag]) + + duration = player.duration() + pos = player.position() + print "Song duration is {0} seconds".format(duration) + print "Current position is {0} seconds".format(pos) + + player.playfor(20) + +def onPlaying(player): + + print "Playing ..." + + #print "Jumping to 40 seconds" + #player.seek(40) + +def onStop(player): + print "Quitting...." + loop.quit() + +player = AudioPlayer() +player.connect("playback-ready",onReady) +player.connect("playback-playing",onPlaying) +player.connect("playback-finished",onStop) +player.connect("playback-stopped",onStop) + +player.load("/opt/mediacore/mediaserver2/snnw_E_138.mp3") + + + +loop = GObject.MainLoop() +loop.run() diff --git a/usr-share-mediaserver/3d-loudspeaker.svg b/usr-share-mediaserver/3d-loudspeaker.svg new file mode 100644 index 0000000..039f1db --- /dev/null +++ b/usr-share-mediaserver/3d-loudspeaker.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + \ No newline at end of file diff --git a/usr-share-mediaserver/background-image.png b/usr-share-mediaserver/background-image.png new file mode 100755 index 0000000000000000000000000000000000000000..96b4ca404b216e997c22860a382f3cece257bb6a GIT binary patch literal 59537 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%0DJRSaL*(?L6Gf3nAEQE5`4S~9Ee~M%vcXhA%wf71 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 KE7Tt+AN&h;yQfG1 literal 0 HcmV?d00001 diff --git a/usr-share-mediaserver/background-image.svg b/usr-share-mediaserver/background-image.svg new file mode 100755 index 0000000..c723152 --- /dev/null +++ b/usr-share-mediaserver/background-image.svg @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + Mediacore + + +