diff --git a/playback.ini.sample b/playback.ini.sample index e018377..54f48af 100644 --- a/playback.ini.sample +++ b/playback.ini.sample @@ -1,9 +1,16 @@ ; Playback application configuration [Playback] ; files_dir = /usr/share/sylkserver/sounds/playback +; Toggle video support (SylkServer will send a color bar) +: enable_video = False + +; Delay for answering sessions +; answer_delay = 1 + ; [test@conference.example.com] ; file = test.wav +; enable_video and answer_delay can be overridden here diff --git a/sylk/applications/playback/__init__.py b/sylk/applications/playback/__init__.py index fe49125..59d13c0 100644 --- a/sylk/applications/playback/__init__.py +++ b/sylk/applications/playback/__init__.py @@ -1,125 +1,129 @@ # 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 +from sylk.applications.playback.configuration import get_config 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 to %s' % (session.call_id, session.remote_identity.uri, session.local_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) + ruri = session._invitation.request_uri + config = get_config('%s@%s' % (ruri.user, ruri.host)) + if config is None: + log.msg(u'Session %s rejected: no configuration found for %s' % (session.call_id, ruri)) + session.reject(488) + return + stream_types = {'audio'} + if config.enable_video: + stream_types.add('video') + streams = [stream for stream in session.proposed_streams if stream.type in stream_types] + if not streams: + log.msg(u'Session %s rejected: invalid media, only RTP audio and video 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): + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=session) + session.send_ring_indication() + reactor.callLater(config.answer_delay, self._accept_session, session, streams) + + def _accept_session(self, session, streams): if session.state == 'incoming': - session.accept([audio_stream]) + session.accept(streams) def incoming_subscription(self, request, data): request.reject(405) def incoming_referral(self, request, data): request.reject(405) def incoming_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.call_id) handler = PlaybackHandler(session) handler.run() def _NH_SIPSessionDidFail(self, notification): session = notification.sender log.msg('Session %s failed' % session.call_id) NotificationCenter().remove_observer(self, sender=session) def _NH_SIPSessionDidEnd(self, notification): session = notification.sender log.msg('Session %s ended' % session.call_id) NotificationCenter().remove_observer(self, sender=session) def _NH_SIPSessionNewProposal(self, notification): if notification.data.originator == 'remote': session = notification.sender session.reject_proposal() -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)) + config = get_config('%s@%s' % (ruri.user, ruri.host)) audio_stream = self.session.streams[0] - player = WavePlayer(audio_stream.mixer, file) + player = WavePlayer(audio_stream.mixer, config.file) audio_stream.bridge.add(player) - log.msg(u"Playing file %s for session %s" % (file, self.session.call_id)) + log.msg(u"Playing file %s for session %s" % (config.file, self.session.call_id)) try: player.play().wait() except (ValueError, WavePlayerError), e: - log.warning(u"Error playing file %s: %s" % (file, e)) - except InterruptPlayback: + log.warning(u"Error playing file %s: %s" % (config.file, e)) + except proc.ProcExit: pass finally: player.stop() 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) + self.proc.kill() diff --git a/sylk/applications/playback/configuration.py b/sylk/applications/playback/configuration.py index 0b4da89..20fdbf1 100644 --- a/sylk/applications/playback/configuration.py +++ b/sylk/applications/playback/configuration.py @@ -1,38 +1,46 @@ # Copyright (C) 2010-2011 AG Projects. See LICENSE for details. # -__all__ = ['get_file_for_uri'] +__all__ = ['get_config'] 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) + enable_video = False + answer_delay = 1 class PlaybackConfig(ConfigSection): __cfgfile__ = 'playback.ini' file = ConfigSetting(type=Path, value=None) + enable_video = GeneralConfig.enable_video + answer_delay = GeneralConfig.answer_delay -def get_file_for_uri(uri): +class Configuration(object): + def __init__(self, data): + self.__dict__.update(data) + + +def get_config(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.file = os.path.join(GeneralConfig.files_dir, PlaybackConfig.file) + config = Configuration(dict(PlaybackConfig)) PlaybackConfig.reset() - return f + return config return None diff --git a/sylk/server.py b/sylk/server.py index c465ebb..06ac65c 100644 --- a/sylk/server.py +++ b/sylk/server.py @@ -1,234 +1,235 @@ # Copyright (C) 2010-2011 AG Projects. See LICENSE for details. # import sys from threading import Event from uuid import uuid4 from application import log from application.notification import NotificationCenter from application.python import Null from eventlib import proc from sipsimple.account import Account, BonjourAccount, AccountManager from sipsimple.application import SIPApplication from sipsimple.audio import AudioDevice, RootAudioBridge from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import AudioMixer from sipsimple.lookup import DNSManager from sipsimple.storage import MemoryStorage from sipsimple.threading import ThreadManager from sipsimple.threading.green import run_in_green_thread from sipsimple.video import VideoDevice from twisted.internet import reactor # Load stream extensions needed for integration with SIP SIMPLE SDK import sylk.streams del sylk.streams from sylk.accounts import DefaultAccount from sylk.applications import IncomingRequestHandler from sylk.configuration import ServerConfig, SIPConfig, ThorNodeConfig from sylk.configuration.settings import AccountExtension, BonjourAccountExtension, SylkServerSettingsExtension from sylk.log import Logger from sylk.session import SessionManager class SylkServer(SIPApplication): def __init__(self): self.request_handler = Null self.thor_interface = Null self.logger = Logger() self.stopping_event = Event() self.stop_event = Event() def start(self, options): self.options = options if self.options.enable_bonjour: ServerConfig.enable_bonjour = True notification_center = NotificationCenter() notification_center.add_observer(self, sender=self) notification_center.add_observer(self, name='ThorNetworkGotFatalError') Account.register_extension(AccountExtension) BonjourAccount.register_extension(BonjourAccountExtension) SIPSimpleSettings.register_extension(SylkServerSettingsExtension) try: super(SylkServer, self).start(MemoryStorage()) except Exception, e: log.fatal("Error starting SIP Application: %s" % e) sys.exit(1) def _initialize_core(self): # SylkServer needs to listen for extra events and request types notification_center = NotificationCenter() settings = SIPSimpleSettings() # initialize core options = dict(# general ip_address=SIPConfig.local_ip, user_agent=settings.user_agent, # SIP detect_sip_loops=True, udp_port=settings.sip.udp_port if 'udp' in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if 'tcp' in settings.sip.transport_list else None, tls_port=None, # TLS tls_verify_server=False, tls_ca_file=None, tls_cert_file=None, tls_privkey_file=None, # rtp rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), # audio codecs=list(settings.rtp.audio_codec_list), # video video_codecs=list(settings.rtp.video_codec_list), + enable_colorbar_device=True, # logging log_level=settings.logs.pjsip_level if settings.logs.trace_pjsip else 0, trace_sip=settings.logs.trace_sip, # events and requests to handle events={'conference': ['application/conference-info+xml'], 'presence': ['application/pidf+xml'], 'refer': ['message/sipfrag;version=2.0']}, incoming_events=set(['conference', 'presence']), incoming_requests=set(['MESSAGE'])) notification_center.add_observer(self, sender=self.engine) self.engine.start(**options) @run_in_green_thread def _initialize_subsystems(self): account_manager = AccountManager() dns_manager = DNSManager() notification_center = NotificationCenter() session_manager = SessionManager() settings = SIPSimpleSettings() notification_center.post_notification('SIPApplicationWillStart', sender=self) if self.state == 'stopping': reactor.stop() return # Initialize default account default_account = DefaultAccount() account_manager.default_account = default_account # initialize TLS self._initialize_tls() # initialize PJSIP internal resolver self.engine.set_nameservers(dns_manager.nameservers) # initialize audio objects voice_mixer = AudioMixer(None, None, settings.audio.sample_rate, 0, 9999) self.voice_audio_device = AudioDevice(voice_mixer) self.voice_audio_bridge = RootAudioBridge(voice_mixer) self.voice_audio_bridge.add(self.voice_audio_device) # initialize video objects - self.video_device = VideoDevice(None, settings.video.resolution, settings.video.framerate) + self.video_device = VideoDevice(u'Colorbar generator', settings.video.resolution, settings.video.framerate) # initialize instance id settings.instance_id = uuid4().urn settings.save() # initialize middleware components dns_manager.start() account_manager.start() session_manager.start() notification_center.add_observer(self, name='CFGSettingsObjectDidChange') self.state = 'started' notification_center.post_notification('SIPApplicationDidStart', sender=self) # start SylkServer components if ThorNodeConfig.enabled: from sylk.interfaces.sipthor import ConferenceNode self.thor_interface = ConferenceNode() self.request_handler = IncomingRequestHandler() self.request_handler.start() @run_in_green_thread def _shutdown_subsystems(self): # shutdown SylkServer components procs = [proc.spawn(self.request_handler.stop), proc.spawn(self.thor_interface.stop)] proc.waitall(procs) # shutdown middleware components dns_manager = DNSManager() account_manager = AccountManager() session_manager = SessionManager() procs = [proc.spawn(dns_manager.stop), proc.spawn(account_manager.stop), proc.spawn(session_manager.stop)] proc.waitall(procs) # shutdown engine self.engine.stop() self.engine.join() # stop threads thread_manager = ThreadManager() thread_manager.stop() # stop the reactor reactor.stop() def _NH_AudioDevicesDidChange(self, notification): pass def _NH_DefaultAudioDeviceDidChange(self, notification): pass def _NH_SIPApplicationFailedToStartTLS(self, notification): log.fatal("Couldn't set TLS options: %s" % notification.data.error) def _NH_SIPApplicationWillStart(self, notification): self.logger.start() settings = SIPSimpleSettings() if settings.logs.trace_sip and self.logger._siptrace_filename is not None: log.msg('Logging SIP trace to file "%s"' % self.logger._siptrace_filename) if settings.logs.trace_msrp and self.logger._msrptrace_filename is not None: log.msg('Logging MSRP trace to file "%s"' % self.logger._msrptrace_filename) if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: log.msg('Logging PJSIP trace to file "%s"' % self.logger._pjsiptrace_filename) if settings.logs.trace_notifications and self.logger._notifications_filename is not None: log.msg('Logging notifications trace to file "%s"' % self.logger._notifications_filename) def _NH_SIPApplicationDidStart(self, notification): settings = SIPSimpleSettings() local_ip = SIPConfig.local_ip log.msg("SylkServer started, listening on:") for transport in settings.sip.transport_list: try: log.msg("%s:%d (%s)" % (local_ip, getattr(self.engine, '%s_port' % transport), transport.upper())) except TypeError: pass def _NH_SIPApplicationWillEnd(self, notification): log.msg('SIP application will end: %s' % self.end_reason) self.stopping_event.set() def _NH_SIPApplicationDidEnd(self, notification): log.msg('SIP application ended') self.logger.stop() if not self.stopping_event.is_set(): log.warning('SIP application ended without shutting down all subsystems') self.stopping_event.set() self.stop_event.set() def _NH_SIPEngineGotException(self, notification): log.error('An exception occured within the SIP core:\n%s\n' % notification.data.traceback) def _NH_SIPEngineDidFail(self, notification): log.error('SIP engine failed') super(SylkServer, self)._NH_SIPEngineDidFail(notification) def _NH_ThorNetworkGotFatalError(self, notification): log.error("All Thor Event Servers have unrecoverable errors.")