diff --git a/playback.ini.sample b/playback.ini.sample new file mode 100644 index 0000000..e018377 --- /dev/null +++ b/playback.ini.sample @@ -0,0 +1,9 @@ +; Playback application configuration + +[Playback] +; files_dir = /usr/share/sylkserver/sounds/playback + + +; [test@conference.example.com] +; file = test.wav + diff --git a/sylk/applications/playback/__init__.py b/sylk/applications/playback/__init__.py new file mode 100644 index 0000000..ba100eb --- /dev/null +++ b/sylk/applications/playback/__init__.py @@ -0,0 +1,119 @@ +# Copyright (C) 2013 AG Projects. See LICENSE for details +# + +from application.python import Null +from application.notification import IObserver, NotificationCenter +from eventlib import proc +from sipsimple.audio import WavePlayer, WavePlayerError +from sipsimple.threading.green import run_in_green_thread +from twisted.internet import reactor +from zope.interface import implements + +from sylk.applications import SylkApplication, ApplicationLogger +from sylk.applications.playback.configuration import get_file_for_uri + + +log = ApplicationLogger.for_package(__package__) + + +class PlaybackApplication(SylkApplication): + implements(IObserver) + + def start(self): + pass + + def stop(self): + pass + + def incoming_session(self, session): + log.msg('Incoming session %s from %s' % (session._invitation.call_id, session.remote_identity.uri)) + try: + audio_stream = next(stream for stream in session.proposed_streams if stream.type=='audio') + except StopIteration: + log.msg(u'Session %s rejected: invalid media, only RTP audio is supported' % session.call_id) + session.reject(488) + return + else: + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=session) + session.send_ring_indication() + # TODO: configurable answer delay + reactor.callLater(1, self._accept_session, session, audio_stream) + + def _accept_session(self, session, audio_stream): + if session.state == 'incoming': + session.accept([audio_stream]) + + def incoming_subscription(self, request, data): + request.reject(405) + + def incoming_referral(self, request, data): + request.reject(405) + + def incoming_sip_message(self, request, data): + request.reject(405) + + def handle_notification(self, notification): + handler = getattr(self, '_NH_%s' % notification.name, Null) + handler(notification) + + @run_in_green_thread + def _NH_SIPSessionDidStart(self, notification): + session = notification.sender + log.msg('Session %s started' % session._invitation.call_id) + handler = PlaybackHandler(session) + handler.run() + + def _NH_SIPSessionDidFail(self, notification): + session = notification.sender + log.msg('Session %s failed' % session._invitation.call_id) + NotificationCenter().remove_observer(self, sender=session) + + def _NH_SIPSessionDidEnd(self, notification): + session = notification.sender + log.msg('Session %s ended' % session._invitation.call_id) + NotificationCenter().remove_observer(self, sender=session) + + +class InterruptPlayback(Exception): pass + +class PlaybackHandler(object): + implements(IObserver) + + def __init__(self, session): + self.session = session + self.proc = None + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=session) + + def run(self): + self.proc = proc.spawn(self._play) + + def _play(self): + ruri = self.session._invitation.request_uri + file = get_file_for_uri('%s@%s' % (ruri.user, ruri.host)) + audio_stream = self.session.streams[0] + player = WavePlayer(audio_stream.mixer, file) + audio_stream.bridge.add(player) + log.msg(u"Playing file %s" % file) + try: + player.play().wait() + except (ValueError, WavePlayerError), e: + log.warning(u"Error playing file %s: %s" % (file, e)) + except InterruptPlayback: + pass + finally: + self.proc = None + audio_stream.bridge.remove(player) + self.session.end() + self.session = None + + def handle_notification(self, notification): + handler = getattr(self, '_NH_%s' % notification.name, Null) + handler(notification) + + def _NH_SIPSessionWillEnd(self, notification): + notification.center.remove_observer(self, sender=notification.sender) + if self.proc: + self.proc.kill(InterruptPlayback) + diff --git a/sylk/applications/playback/configuration.py b/sylk/applications/playback/configuration.py new file mode 100644 index 0000000..0b4da89 --- /dev/null +++ b/sylk/applications/playback/configuration.py @@ -0,0 +1,38 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +__all__ = ['get_file_for_uri'] + +import os + +from application.configuration import ConfigFile, ConfigSection, ConfigSetting + +from sylk.configuration.datatypes import Path, ResourcePath + + +class GeneralConfig(ConfigSection): + __cfgfile__ = 'playback.ini' + __section__ = 'Playback' + + files_dir = ConfigSetting(type=Path, value=ResourcePath('sounds/playback').normalized) + + +class PlaybackConfig(ConfigSection): + __cfgfile__ = 'playback.ini' + + file = ConfigSetting(type=Path, value=None) + + +def get_file_for_uri(uri): + config_file = ConfigFile(PlaybackConfig.__cfgfile__) + section = config_file.get_section(uri) + if section is not None: + PlaybackConfig.read(section=uri) + if not os.path.isabs(PlaybackConfig.file): + f = os.path.join(GeneralConfig.files_dir, PlaybackConfig.file) + else: + f = PlaybackConfig.file + PlaybackConfig.reset() + return f + return None +