#!/usr/bin/python3

import atexit
import glob
import os
import platform
import select
import shutil
import signal
import re
import sys
import termios
import uuid
import subprocess

from datetime import datetime
from eventlib import api
from itertools import chain
from optparse import OptionParser
from threading import Thread
from time import sleep
from lxml import html

from application import log
from application.notification import NotificationCenter, NotificationData
from application.python import Null
from application.process import process
from application.python.queue import EventQueue
from application.system import makedirs

from gnutls.errors import GNUTLSError
from gnutls.crypto import X509Certificate, X509PrivateKey

from twisted.internet import reactor

from pathlib import Path
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.audio import WavePlayer
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import Engine, SIPCoreError, SIPURI, ToHeader, Header, CORE_REVISION, PJ_VERSION, PJ_SVN_REVISION
from sipsimple import __version__ as version
from sipsimple.lookup import DNSLookup
from sipsimple.session import Session, IllegalStateError
from sipsimple.streams import MediaStreamRegistry
from sipsimple.storage import FileStorage

from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension, BonjourAccountExtension
from sipclient.configuration.datatypes import ResourcePath
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger
from sipclient.system import IPAddressMonitor, copy_default_certificates


class BonjourNeighbour(object):
    def __init__(self, neighbour, uri, display_name, host):
        self.display_name = display_name
        self.host = host
        self.neighbour = neighbour
        self.uri = uri


class InputThread(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.setDaemon(True)
        self._old_terminal_settings = None

    def start(self):
        atexit.register(self._termios_restore)
        Thread.start(self)

    def run(self):
        notification_center = NotificationCenter()
        while True:
            chars = list(self._getchars())
            while chars:
                char = chars.pop(0)
                if char == '\x1b': # escape
                    if len(chars) >= 2 and chars[0] == '[' and chars[1] in ('A', 'B', 'C', 'D'): # one of the arrow keys
                        char = char + chars.pop(0) + chars.pop(0)
                notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char))

    def stop(self):
        self._termios_restore()

    def _termios_restore(self):
        if self._old_terminal_settings is not None:
            termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)

    def _getchars(self):
        fd = sys.stdin.fileno()
        if os.isatty(fd):
            self._old_terminal_settings = termios.tcgetattr(fd)
            new = termios.tcgetattr(fd)
            new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
            new[6][termios.VMIN] = b'\000'
            try:
                termios.tcsetattr(fd, termios.TCSADRAIN, new)
                if select.select([fd], [], [], None)[0]:
                    return sys.stdin.read(4192)
            finally:
                self._termios_restore()
        else:
            return os.read(fd, 4192)


class RTPStatisticsThread(Thread):
    def __init__(self, application):
        Thread.__init__(self)
        self.setDaemon(True)
        self.application = application
        self.stopped = False

    def run(self):
        notification_center = NotificationCenter()
        last_active_session = None
        while not self.stopped:
            if self.application.active_session is not None and self.application.active_session.streams:
                if last_active_session != self.application.active_session:
                    last_rx_packets = 0
                    lost_rtp_count = 0
                last_active_session = self.application.active_session
                audio_stream = self.application.active_session.streams[0]
                stats = audio_stream.statistics
                if stats is not None:
                    rx_packets = stats['rx']['packets'] - last_rx_packets
                    last_rx_packets = stats['rx']['packets']
                    if rx_packets == 0:
                        lost_rtp_count = lost_rtp_count + 1
                    else:
                        lost_rtp_count = 0

                    if lost_rtp_count:
                        notification_center.post_notification('RTPWasLost', sender=self.application.active_session, data=NotificationData(count=lost_rtp_count))

                    msg = '%s RTP audio statistics: RTT=%d ms, packet loss=%.1f%%, jitter RX/TX=%d/%d ms\n' % (datetime.now().replace(microsecond=0), stats['rtt']['avg'] / 1000, 100.0 * stats['rx']['packets_lost'] / stats['rx']['packets'] if stats['rx']['packets'] else 0, stats['rx']['jitter']['avg'] / 1000, stats['tx']['jitter']['avg'] / 1000)
                    notification_center.post_notification('RTPStatisticsLog', sender=self.application.active_session, data=NotificationData(line=msg))
                
                try:
                    video_stream = self.application.active_session.streams[1]
                except IndexError:
                    pass
                else:
                    stats = video_stream.statistics
                    if stats is not None:
                        msg = '%s RTP video statistics: RTT=%d ms, packet loss=%.1f%%, jitter RX/TX=%d/%d ms\n' % (datetime.now().replace(microsecond=0), stats['rtt']['avg'] / 1000, 100.0 * stats['rx']['packets_lost'] / stats['rx']['packets'] if stats['rx']['packets'] else 0, stats['rx']['jitter']['avg'] / 1000, stats['tx']['jitter']['avg'] / 1000)
                        notification_center.post_notification('RTPStatisticsLog', sender=self.application.active_session, data=NotificationData(line=msg))
                sleep(5)

    def stop(self):
        self.stopped = True


class CancelThread(Thread):
    def __init__(self, application):
        Thread.__init__(self)
        self.setDaemon(True)
        self.application = application
        self.stopped = False

    def run(self):
        while not self.stopped:
            self.application.end_session_if_needed()
            self.application.stop_if_needed()
            sleep(1)

    def stop(self):
        self.stopped = True


class SIPAudioApplication(SIPApplication):

    def __init__(self):
        self.account = None
        self.options = None
        self.target = None
        
        self.active_session = None
        self.answer_timers = {}
        self.hangup_timers = {}
        self.started_sessions = []
        self.incoming_sessions = []
        self.outgoing_session = None
        self.neighbours = {}
        self.registration_succeeded = False
        self.success = False
        
        self.input =  None
        self.output = None
        self.ip_address_monitor = IPAddressMonitor()
        self.logger = None
        self.rtp_statistics = None

        self.alert_tone_generator = None
        self.voice_tone_generator = None
        self.wave_inbound_ringtone = None
        self.wave_outbound_ringtone = None
        self.tone_ringtone = None
        self.hold_tone = None

        self.ignore_local_hold = False
        self.ignore_local_unhold = False

        self.batch_mode = False
        self.stop_call_thread = None
        self.session_spool_dir = None
        self.play_file = None
        self.playback_wave_player = None
        self.disable_ringtone = False
        self.disable_hanguptone = False

        self.enable_playback = False
        self.play_failure_code = False
        self.rtp_lost = False
        self.show_rtp_statistics = False
        self.transfer_session = False

    def _write(self, message):
        sys.stdout.write(message)
        sys.stdout.flush()

    def start(self, target, options):
        notification_center = NotificationCenter()

        if options.daemonize:
            process.daemonize()
        
        self.options = options
        self.target = target
        self.auto_reconnect = options.auto_reconnect
        self.batch_mode = options.batch_mode
        self.enable_video = options.enable_video
        self.log_register = options.log_register
        self.play_failure_code = options.play_failure_code
        self.input = InputThread() if not self.batch_mode else None
        self.output = EventQueue(self._write)
        self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications)
        
        notification_center.add_observer(self, sender=self)
        notification_center.add_observer(self, sender=self.input)
        notification_center.add_observer(self, name='SIPSessionNewIncoming')
        notification_center.add_observer(self, name='SIPSessionNewOutgoing')
        notification_center.add_observer(self, name='RTPStreamDidChangeRTPParameters')
        notification_center.add_observer(self, name='RTPStreamICENegotiationDidSucceed')
        notification_center.add_observer(self, name='RTPStreamICENegotiationDidFail')
        notification_center.add_observer(self, name='RTPStreamICENegotiationStateDidChange')
        notification_center.add_observer(self, name='SIPSessionGotConferenceInfo')
        notification_center.add_observer(self, name='TLSTransportHasChanged')
        notification_center.add_observer(self, name='MediaStreamDidStart')
        notification_center.add_observer(self, name='SessionMustReconnect')
        notification_center.add_observer(self, name='SIPSessionTransferGotProgress')
        notification_center.add_observer(self, name='SIPSessionTransferDidFail')
        notification_center.add_observer(self, name='SIPSessionTransferNewIncoming')
        notification_center.add_observer(self, name='CFGSettingsObjectDidChange')

        if self.input:
            self.input.start()
        self.output.start()

        log.level.current = log.level.WARNING # get rid of twisted messages

        Account.register_extension(AccountExtension)
        BonjourAccount.register_extension(BonjourAccountExtension)
        SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
        
        self.config_directory = options.config_directory or config_directory
        self.disable_ringtone = options.disable_ringtone
        self.disable_hanguptone = options.disable_hanguptone
        self.auto_record = options.auto_record
        
        try:
            self.output.put("Using config directory: %s\n" % self.config_directory)
            SIPApplication.start(self, FileStorage(self.config_directory))
        except ConfigurationError as e:
            self.output.put("Failed to load sipclient's configuration: %s\n" % str(e))
            self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n")
            self.output.stop()
        else:
            self.output.put("SDK version %s, core version %s, PJSIP version %s (%s)\n" % (version, CORE_REVISION, PJ_VERSION.decode(), PJ_SVN_REVISION))  

        if options.spool_dir:
            self.spool_dir = options.spool_dir
        else:
            self.spool_dir = "%s/spool/sesssions" % self.config_directory
        
        try:
            makedirs(self.spool_dir)
        except Exception as e:
            log.error('Failed to create spool directory at {directory}: {exception!s}'.format(directory=self.spool_dir, exception=e))
        else:
            self.output.put("Using spool directory %s\n" % self.spool_dir)

        stop_app_file = "%s/stop" % self.spool_dir
        if os.path.exists(stop_app_file):
            os.remove(stop_app_file)
            
        self.output.put("To stop the app: touch %s\n" % stop_app_file)

        self.scripts_dir = "%s/scripts" % self.config_directory
        try:
            makedirs(self.scripts_dir)
        except Exception as e:
            log.error('Failed to create scripts  directory at {directory}: {exception!s}'.format(directory=dir, exception=e))

        self.enable_playback = options.enable_playback
        if options.playback_dir:
            self.playback_dir = options.playback_dir
        else:
            self.playback_dir = "%s/spool/playback" % self.config_directory
            makedirs(self.playback_dir)

        self._remove_lock()

        if self.enable_playback:
            self.playback_queue = EventQueue(self._handle_outgoing_playback)

            active_playback_dir = self.playback_dir + '/active'
            makedirs(active_playback_dir)

            scripts_playback_dir = self.playback_dir + '/scripts'
            makedirs(scripts_playback_dir)

    def poll_playback_directory(self):
        if self.outgoing_session:
            reactor.callLater(0.2, self.poll_playback_directory)
            return

        files = list(filter(os.path.isfile, glob.glob(self.playback_dir + "/*.wav")))
        files.sort(key=lambda x: os.path.getmtime(x))
        for file in files:
            if len(file.split('@')) == 2:
                active_playback_dir = self.playback_dir + '/active'
                basename = os.path.basename(file)
                self.output.put("Audio recording detected: %s\n" % file)
                filename = '%s/%s-%s' % (active_playback_dir, datetime.now().strftime("%Y%m%d-%H%M%S"), basename)
                os.replace(file, filename)
                play_object = {'target': os.path.splitext(basename)[0], 
                               'filename': filename}
                self.playback_queue.put(play_object)

        reactor.callLater(0.2, self.poll_playback_directory)

    def _handle_outgoing_playback(self, play_object):
        self.play_file = play_object['filename']
        self.start_outgoing_call(play_object['target'])
        
    def print_help(self):
        message  = 'Available control keys:\n'
        message += '  s: toggle SIP trace on the console\n'
        message += '  j: toggle PJSIP trace on the console\n'
        message += '  n: toggle notifications trace on the console\n'
        message += '  p: toggle printing RTP statistics on the console\n'
        message += '  h: hang-up the active session\n'
        message += '  r: toggle audio recording\n'
        message += '  m: mute the microphone\n'
        message += '  i: change audio input device\n'
        message += '  o: change audio output device\n'
        message += '  a: change audio alert device\n'
        message += '  SPACE: hold/unhold\n'
        message += '  Ctrl-d: quit the program\n'
        message += '  ?: display this help message\n'
        self.output.put('\n'+message+'\n')

    def _NH_SIPApplicationWillStart(self, notification):
        account_manager = AccountManager()
        notification_center = NotificationCenter()
        settings = SIPSimpleSettings()

        #if 'armv7' in platform.platform() and settings.audio.echo_canceller.enabled:
        #    self.output.put("Disable echo canceller on ARM architecture\n")
        #    settings.audio.echo_canceller.enabled = False
        #    settings.save()

        for account in account_manager.iter_accounts():
            if isinstance(account, Account):
                account.sip.register = False
                account.presence.enabled = False
                account.xcap.enabled = False
                account.message_summary.enabled = False

        if self.options.account is None:
            self.account = account_manager.default_account
        else:
            possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled]
            if len(possible_accounts) > 1:
                self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts))))
                self.output.stop()
                self.stop()
                self.end_cancel_thread()
                return
            elif len(possible_accounts) == 0:
                self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
                self.output.stop()
                self.stop()
                self.end_cancel_thread()
                return
            else:
                self.account = possible_accounts[0]
        notification_center.add_observer(self, sender=self.account)

        if isinstance(self.account, Account) and self.target is None:
            self.account.sip.register = True
            self.account.presence.enabled = False
            self.account.xcap.enabled = False
            self.account.message_summary.enabled = False
        self.output.put('Using account %s\n' % self.account.id)
        
        self.logger.start()
        if settings.logs.trace_sip and self.logger._siptrace_filename is not None:
            self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename)
        if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None:
            self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename)
        if settings.logs.trace_notifications and self.logger._notifications_filename is not None:
            self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename)

        if self.options.disable_sound:
            settings.audio.input_device = None
            settings.audio.output_device = None
            settings.audio.alert_device = None

        if self.options.enable_default_devices:
            settings.audio.input_device = 'system_default'
            settings.audio.output_device = 'system_default'
            settings.audio.alert_device = 'system_default'

    def handle_notification(self, notification):
        alive_file = os.path.join(self.config_directory, 'last_notification')
        Path(alive_file).touch()

        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)
        
    def _NH_SIPApplicationDidStart(self, notification):
        engine = Engine()
        settings = SIPSimpleSettings()

        self.rtp_statistics = RTPStatisticsThread(self)
        self.rtp_statistics.start()

        engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
        engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
        
        self.ip_address_monitor.start()

        if self.enable_playback:
            self.output.put("Polling %s for wav files\n" % self.playback_dir)
            self.playback_queue.start()
            reactor.callLater(3, self.poll_playback_directory)
            
        self.output.put('Available audio input devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(engine.input_devices)))
        self.output.put('Available audio output devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(engine.output_devices)))
        self.output.put('Available audio codecs: %s\n' % ', '.join([codec.decode() for codec in engine._ua.available_codecs]))
        self.output.put('Configured audio codecs: %s\n' % ', '.join([codec for codec in settings.rtp.audio_codec_list]))
        self.output.put('Available video codecs: %s\n' % ', '.join([codec.decode() for codec in engine._ua.available_video_codecs]))
        self.output.put('Configured video codecs: %s\n' % ', '.join([codec for codec in settings.rtp.video_codec_list]))

        if engine.video_devices:
            self.output.put('Available cameras: %s\n' % ', '.join(sorted(engine.video_devices)))

        if self.enable_video:
            self.output.put('No camera present, video is disabled')
            self.enable_video = False

        if self.voice_audio_mixer.input_device == 'system_default':
            self.output.put('Using audio input device: %s (system default device)\n' % self.voice_audio_mixer.real_input_device)
        else:
            self.output.put('Using audio input device: %s\n' % self.voice_audio_mixer.input_device)
        if self.voice_audio_mixer.output_device == 'system_default':
            self.output.put('Using audio output device: %s (system default device)\n' % self.voice_audio_mixer.real_output_device)
        else:
            self.output.put('Using audio output device: %s\n' % self.voice_audio_mixer.output_device)
        if self.alert_audio_mixer.output_device == 'system_default':
            self.output.put('Using audio alert device: %s (system default device)\n' % self.alert_audio_mixer.real_output_device)
        else:
            self.output.put('Using audio alert device: %s\n' % self.alert_audio_mixer.output_device)

        if not self.batch_mode and not self.disable_ringtone:
            self.print_help()

        inbound_ringtone = self.account.sounds.audio_inbound.sound_file if self.account.sounds.audio_inbound is not None else None
        outbound_ringtone = settings.sounds.audio_outbound
        if inbound_ringtone:
            self.wave_inbound_ringtone = WavePlayer(self.alert_audio_mixer, inbound_ringtone.path.normalized, volume=inbound_ringtone.volume, loop_count=0, pause_time=2)
            self.alert_audio_bridge.add(self.wave_inbound_ringtone)
    
        if not self.disable_ringtone:
            if outbound_ringtone:
                self.wave_outbound_ringtone = WavePlayer(self.voice_audio_mixer, outbound_ringtone.path.normalized, volume=outbound_ringtone.volume, loop_count=0, pause_time=2)
                self.voice_audio_bridge.add(self.wave_outbound_ringtone)
            self.tone_ringtone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/ring_tone.wav').normalized, loop_count=0, pause_time=6)
            self.voice_audio_bridge.add(self.tone_ringtone)
            self.hold_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hold_tone.wav').normalized, loop_count=0, pause_time=30, volume=50)
            self.voice_audio_bridge.add(self.hold_tone)

        if settings.tls.ca_list is None:
            copy_default_certificates()
            self.output.put('Initializing default TLS certificates and settings')
            settings.tls.ca_list = os.path.join(config_directory, 'tls/ca.crt')
            settings.tls.certificate = os.path.join(config_directory, 'tls/default.crt')
            settings.tls.verify_server = True
            settings.save()

        if self.options.mute:
            self.output.put('Mute microphone at start\n')
            self.voice_audio_mixer.muted = True
            
        if self.target is not None:
            self.start_outgoing_call(target)

    def _NH_SessionMustReconnect(self, notification):
        self.output.put('Reconnecting session to %s\n' % notification.data.target)
        self.start_outgoing_call(notification.data.target)

    def start_outgoing_call(self, target):
        self.target = target

        if isinstance(self.account, BonjourAccount) and '@' not in self.target:
            self.output.put('Bonjour mode requires a host in the destination address\n')
            if not self.enable_playback:
                self.stop()
                self.end_cancel_thread()
            return

        if self.play_file:
            lock_file = "%s/playback.lock" % self.playback_dir
            Path(lock_file).touch()
            #self.output.put("Lock file %s created\n" % lock_file)
            
        if '@' not in self.target:
            self.target = '%s@%s' % (self.target, self.account.id.domain)
        if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
            self.target = 'sip:' + self.target
        try:
            self.target = SIPURI.parse(self.target)
        except SIPCoreError:
            self.output.put('Illegal SIP URI: %s\n' % self.target)
            if not self.enable_playback:
                self.stop()
        else:
            if '.' not in self.target.host.decode() and not isinstance(self.account, BonjourAccount):
                self.target.host = ('%s.%s' % (self.target.host.decode(), self.account.id.domain)).encode()
            lookup = DNSLookup()
            notification_center = NotificationCenter()
            settings = SIPSimpleSettings()
            notification_center.add_observer(self, sender=lookup)
            
            self.session_spool_dir = self.spool_dir + "/" + (self.options.external_id or str(uuid.uuid1()))

            if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None:
                uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
                tls_name = self.account.sip.tls_name or self.account.sip.outbound_proxy.host
            elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy:
                uri = SIPURI(host=self.account.id.domain)
                tls_name = self.account.sip.tls_name or self.account.id.domain
            else:
                uri = self.target
                tls_name = uri.host
                if self.account is not BonjourAccount():
                    if self.account.id.domain == uri.host.decode():
                        tls_name = self.account.sip.tls_name or self.account.id.domain
                    elif "isfocus" in str(uri) and uri.host.decode().endswith(self.account.id.domain):
                        tls_name = self.account.conference.tls_name or self.account.sip.tls_name or self.account.id.domain
                else:
                    is_ip_address = re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host.decode()) or ":" in uri.host.decode()
                    if "isfocus" in str(uri) and self.account.conference.tls_name:
                        tls_name = self.account.conference.tls_name
                    elif  is_ip_address and  self.account.sip.tls_name:
                        tls_name = self.account.sip.tls_name
                
            #self.output.put('DNS lookup for %s\n' % uri)

            lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=tls_name)

            try:
                makedirs(self.session_spool_dir)
            except Exception as e:
                log.error('Failed to create session spool directory at {directory}: {exception!s}'.format(directory=self.session_spool_dir, exception=e))
            else:    
                if self.stop_call_thread is None:
                    self.stop_call_thread = CancelThread(self)
                    self.stop_call_thread.start()
                self.output.put("To stop the call: touch %s/stop\n" % self.session_spool_dir)

    def _NH_SIPApplicationWillEnd(self, notification):
        if isinstance(self.account, Account):
            self.account.sip.register = False
        self.ip_address_monitor.stop()

    def _NH_SIPApplicationDidEnd(self, notification):
        if self.input:
            self.input.stop()
        self.output.stop()
        self.output.join()

    def _NH_SIPEngineDetectedNATType(self, notification):
        SIPApplication._NH_SIPEngineDetectedNATType(self, notification)
        if notification.data.succeeded:
            self.output.put('Detected NAT type: %s\n' % notification.data.nat_type)

    def _NH_SIPApplicationGotInput(self, notification):
        engine = Engine()
        notification_center = NotificationCenter()
        settings = SIPSimpleSettings()
        if notification.data.input == '\x04':
            if self.active_session is not None:
                self.output.put('Ending audio session...\n')
                self.active_session.end()
            elif self.outgoing_session is not None:
                self.output.put('Cancelling audio session...\n')
                self.outgoing_session.end()
            else:
                self.stop()
                self.end_cancel_thread()
        elif notification.data.input == '?':
            self.print_help()
        elif notification.data.input in ('y', 'n') and self.incoming_sessions:
            accepted_types = ['video', 'audio', 'chat'] if self.enable_video else ['audio', 'chat']
            session = self.incoming_sessions.pop(0)
            if notification.data.input == 'y':
                session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types])
            else:
                session.reject()
        elif notification.data.input in ('y', 'n') and self.transfer_session:
            session = self.transfer_session
            try:
                if notification.data.input == 'y':
                    session.accept_transfer()
                else:
                    session.reject_transfer()
            except IllegalStateError:
                pass
            self.transfer_session = False
        elif notification.data.input == 'm':
            self.voice_audio_mixer.muted = not self.voice_audio_mixer.muted
            self.output.put('The microphone is now %s\n' % ('muted' if self.voice_audio_mixer.muted else 'unmuted'))
        elif notification.data.input == 'i':
            input_devices = [None, 'system_default'] + sorted(engine.input_devices)
            if self.voice_audio_mixer.input_device in input_devices:
                old_input_device = self.voice_audio_mixer.input_device
            else:
                old_input_device = None
            tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0
            idx = input_devices.index(old_input_device)
            for new_input_device in input_devices[idx+1:] + input_devices[:idx+1]:
                try:
                    self.voice_audio_mixer.set_sound_devices(new_input_device, self.voice_audio_mixer.output_device, tail_length)
                except SIPCoreError as e:
                    self.output.put('Skipping input device %s: %s\n' % (new_input_device, str(e)))
                    continue
                if self.voice_audio_mixer.input_device != new_input_device:
                    self.output.put('Skipping input device %s: device failed to start\n' % new_input_device)
                    continue
                if new_input_device == 'system_default':
                    self.output.put('Audio input device changed to %s (system default device)\n' % self.voice_audio_mixer.real_input_device)
                else:
                    self.output.put('Audio input device changed to %s\n' % new_input_device)
                break
        elif notification.data.input == 'o':
            output_devices = [None, 'system_default'] + sorted(engine.output_devices)
            if self.voice_audio_mixer.output_device in output_devices:
                old_output_device = self.voice_audio_mixer.output_device
            else:
                old_output_device = None
            tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0
            idx = output_devices.index(old_output_device)
            for new_output_device in output_devices[idx+1:] + output_devices[:idx+1]:
                try:
                    self.voice_audio_mixer.set_sound_devices(self.voice_audio_mixer.input_device, new_output_device, tail_length)
                except SIPCoreError as e:
                    self.output.put('Skipping output device %s: %s\n' % (new_output_device, str(e)))
                    continue
                if self.voice_audio_mixer.output_device != new_output_device:
                    self.output.put('Skipping output device %s: device failed to start\n' % new_output_device)
                    continue
                if new_output_device == 'system_default':
                    self.output.put('Audio output device changed to %s (system default device)\n' % self.voice_audio_mixer.real_output_device)
                else:
                    self.output.put('Audio output device changed to %s\n' % new_output_device)
                break
        elif notification.data.input == 'a':
            output_devices = [None, 'system_default'] + sorted(engine.output_devices)
            if self.alert_audio_mixer.output_device in output_devices:
                old_output_device = self.alert_audio_mixer.output_device
            else:
                old_output_device = None
            tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0
            idx = output_devices.index(old_output_device)
            for new_output_device in output_devices[idx+1:] + output_devices[:idx+1]:
                try:
                    self.alert_audio_mixer.set_sound_devices(self.alert_audio_mixer.input_device, new_output_device, tail_length)
                except SIPCoreError as e:
                    self.output.put('Skipping alert device %s: %s\n' % (new_output_device, str(e)))
                    continue
                if self.alert_audio_mixer.output_device != new_output_device:
                    self.output.put('Skipping alert device %s: device failed to start\n' % new_output_device)
                    continue
                if new_output_device == 'system_default':
                    self.output.put('Audio alert device changed to %s (system default device)\n' % self.alert_audio_mixer.real_output_device)
                else:
                    self.output.put('Audio alert device changed to %s\n' % new_output_device)
                break
        elif notification.data.input == 'h':
            if self.active_session is not None:
                self.output.put('Ending audio session...\n')
                self.active_session.end()
            elif self.outgoing_session is not None:
                self.output.put('Cancelling audio session...\n')
                self.outgoing_session.end()
        elif notification.data.input == ' ':
            if self.active_session is not None:
                if self.active_session.on_hold:
                    self.active_session.unhold()
                else:
                    self.active_session.hold()
        elif notification.data.input in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D'):
            if self.active_session is not None:
                try:
                    audio_stream = self.active_session.streams[0]
                except IndexError:
                    pass
                else:
                    digit = notification.data.input
                    filename = 'sounds/dtmf_%s_tone.wav' % {'*': 'star', '#': 'pound'}.get(digit, digit)
                    wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(filename).normalized)
                    notification_center.add_observer(self, sender=wave_player)
                    audio_stream.send_dtmf(digit)
                    if self.active_session.account.rtp.inband_dtmf:
                        audio_stream.bridge.add(wave_player)
                    self.voice_audio_bridge.add(wave_player)
                    wave_player.start()
        elif notification.data.input in ('\x1b[A', '\x1b[D') and len(self.started_sessions) > 0: # UP and LEFT
            if self.active_session is None:
                self.active_session = self.started_sessions[0]
                self.active_session.unhold()
                self.ignore_local_unhold = True
            elif len(self.started_sessions) > 1:
                self.active_session.hold()
                self.active_session = self.started_sessions[self.started_sessions.index(self.active_session)-1]
                self.active_session.unhold()
                self.ignore_local_unhold = True
            else:
                return
            identity = str(self.active_session.remote_identity.uri)
            if self.active_session.remote_identity.display_name:
                identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity)
            self.output.put('Active audio session: "%s" (%d/%d)\n' % (identity, self.started_sessions.index(self.active_session)+1, len(self.started_sessions)))
        elif notification.data.input in ('\x1b[B', '\x1b[C') and len(self.started_sessions) > 0: # DOWN and RIGHT
            if self.active_session is None:
                self.active_session = self.started_sessions[0]
                self.active_session.unhold()
                self.ignore_local_unhold = True
            elif len(self.started_sessions) > 1:
                self.active_session.hold()
                self.active_session = self.started_sessions[(self.started_sessions.index(self.active_session)+1) % len(self.started_sessions)]
                self.active_session.unhold()
                self.ignore_local_unhold = True
            else:
                return
            identity = str(self.active_session.remote_identity.uri)
            if self.active_session.remote_identity.display_name:
                identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity)
            self.output.put('Active audio session: "%s" (%d/%d)\n' % (identity, self.started_sessions.index(self.active_session)+1, len(self.started_sessions)))
        elif notification.data.input == 'r':
            if self.active_session is None or not self.active_session.streams:
                return
            session = self.active_session
            audio_stream = self.active_session.streams[0]
            if audio_stream.recorder is not None:
                audio_stream.stop_recording()
            else:
                direction = session.direction
                remote = "%s@%s" % (session.remote_identity.uri.user, session.remote_identity.uri.host)
                filename = "%s-%s-%s.wav" % (datetime.now().strftime("%Y%m%d-%H%M%S"), remote, direction)
                path = os.path.join(settings.audio.directory.normalized, session.account.id, datetime.now().strftime("%Y%m%d"))
                makedirs(path)
                audio_stream.start_recording(os.path.join(path, filename))
        elif notification.data.input == 'p':
            self.show_rtp_statistics = not self.show_rtp_statistics
            if self.show_rtp_statistics:
                self.output.put('Output of RTP statistics on console is now activated\n')
            else:
                self.output.put('Output of RTP statistics on console is now dectivated\n')
        elif notification.data.input == 'j':
            self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
            engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
            self.output.put('PJSIP tracing to console is now %s\n' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
        elif notification.data.input == 'n':
            self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
            self.output.put('Notification tracing to console is now %s.\n' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
        elif notification.data.input == 's':
            self.logger.sip_to_stdout = not self.logger.sip_to_stdout
            engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
            self.output.put('SIP tracing to console is now %s\n' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))

    def _NH_SIPEngineGotException(self, notification):
        self.output.put('An exception occured within the SIP core:\n%s\n' % notification.data.traceback)

    def _NH_SIPAccountRegistrationDidSucceed(self, notification):
        if self.registration_succeeded:
            return
        contact_header = notification.data.contact_header
        contact_header_list = notification.data.contact_header_list
        expires = notification.data.expires
        registrar = notification.data.registrar
        if self.log_register:
            message = '%s Registered contact "%s" for sip:%s at %s:%d;transport=%s (expires in %d seconds).\n' % (datetime.now().replace(microsecond=0), contact_header.uri, self.account.id, registrar.address, registrar.port, registrar.transport, expires)
            if len(contact_header_list) > 1:
                message += 'Other registered contacts:\n%s\n' % '\n'.join(['  %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri])
            self.output.put(message)
        
        self.registration_succeeded = True

    def _NH_SIPAccountRegistrationDidFail(self, notification):
        if self.log_register:
            self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after))
        self.registration_succeeded = False

    def _NH_SIPAccountRegistrationDidEnd(self, notification):
        if self.log_register:
            self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0))

    def _NH_BonjourAccountRegistrationDidSucceed(self, notification):
        if self.log_register:
            self.output.put('%s Registered Bonjour contact %s\n' % (datetime.now().replace(microsecond=0), notification.data.name))

    def _NH_BonjourAccountRegistrationDidFail(self, notification):
        pass
        #self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason))

    def _NH_BonjourAccountRegistrationDidEnd(self, notification):
        if self.log_register:
            self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0))

    def _NH_BonjourAccountDidAddNeighbour(self, notification):
        neighbour, record = notification.data.neighbour, notification.data.record
        now = datetime.now().replace(microsecond=0)
        self.output.put('%s Bonjour neighbour joined: %s (%s) <%s>\n' % (now, record.name, record.host, record.uri))
        self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host)

    def _NH_BonjourAccountDidUpdateNeighbour(self, notification):
        neighbour, record = notification.data.neighbour, notification.data.record
        now = datetime.now().replace(microsecond=0)
        try:
            bonjour_neighbour = self.neighbours[neighbour]
        except KeyError:
            self.output.put('%s Bonjour neighbour joined: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri))
            self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host)
        else:
            self.output.put('%s Bonjour neighbour updated: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri))
            bonjour_neighbour.display_name = record.name
            bonjour_neighbour.host = record.host
            bonjour_neighbour.uri = record.uri

    def _NH_SIPSessionTransferGotProgress(self, notification):
        self.output.put('Session transfer progress: %s (%s)\n' % (notification.data.reason, notification.data.code))

    def _NH_SIPSessionTransferDidFail(self, notification):
        self.output.put('Session transfer failed: %s (%s)\n' % (notification.data.reason, notification.data.code))
        
    def _NH_CFGSettingsObjectDidChange(self, notification):
        settings = SIPSimpleSettings()
        if 'audio.input_device' in notification.data.modified:
            self.output.put('Audio input device is now %s\n' % settings.audio.input_device)
        if 'audio.output_device' in notification.data.modified:
            self.output.put('Audio output device is now %s\n' % settings.audio.output_device)
        
        #self.output.put('Modified settings: %s' % notification.data.modified)
            
    def _NH_SIPSessionTransferNewIncoming(self, notification):
        target = "%s@%s" % (notification.data.transfer_destination.user.decode(), notification.data.transfer_destination.host.decode())
        self.output.put("Call transfer request to %s, do you accept? (y/n)\n" % target)
        self.transfer_session = notification.sender

    def _NH_BonjourAccountDidRemoveNeighbour(self, notification):
        neighbour = notification.data.neighbour
        now = datetime.now().replace(microsecond=0)
        try:
            bonjour_neighbour = self.neighbours.pop(neighbour)
        except KeyError:
            pass
        else:
            self.output.put('%s Bonjour neighbour left: "%s (%s)" <%s>\n' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri))

    def _NH_DNSLookupDidSucceed(self, notification):
        notification_center = NotificationCenter()
        
        results = ('%s:%s (%s)' % (result.address, result.port, result.transport.upper()) for result in notification.data.result)
        result_text = ', '.join(results)

        self.output.put("\nDNS lookup for %s succeeded: %s\n" % (self.target.host.decode(), result_text))

        if self.end_session_if_needed():
            self.stop()
            self.end_cancel_thread()
        
        self.outgoing_session = session = Session(self.account)
        notification_center.add_observer(self, sender=session)
        streams = [MediaStreamRegistry.AudioStream(), MediaStreamRegistry.VideoStream()] if self.enable_video else [MediaStreamRegistry.AudioStream()]
        extra_headers = [Header('X-No-MediaProxy', 'yes')] if self.options.no_mp else []
        session.connect(ToHeader(self.target), routes=notification.data.result, streams=streams, extra_headers=extra_headers)

    def _NH_DNSLookupDidFail(self, notification):
        self.output.put('DNS lookup failed: %s\n' % notification.data.error)

        self._playback_end(failed_reason='outgoing-failed-DNS')

        if not self.enable_playback and not self.options.auto_reconnect:
            self.stop()
            self.end_cancel_thread()

        self.reconnect(10)

    def _NH_RTPStatisticsLog(self, notification):
        if not self.show_rtp_statistics:
            return

        self.output.put(notification.data.line)

    def _NH_RTPWasLost(self, notification):
        count = notification.data.count
        if self.account.rtp.hangup_on_timeout and count >= 4:
            self.output.put('RTP was lost\n')
            self.rtp_lost = True
            notification.sender.end()

    def reconnect(self, after=5):
        if not self.auto_reconnect:
           return

        api.sleep(after)
        notification_center = NotificationCenter()
        notification_center.post_notification('SessionMustReconnect', data=NotificationData(target=str(self.target)))

    def auto_answer_allowed(self, uri):
       if not self.options.auto_answer_uris:
           self.output.put('Auto answer allowed for %s\n' % uri)
           return True

       uri = uri.split(":")[1]
       auto_answer_uris = self.options.auto_answer_uris.split(",")

       if uri in auto_answer_uris:
           self.output.put('Auto answer allowed for %s\n' % uri)
           return True

       self.output.put('Auto answer denied for %s\n' % uri)
       return False

    def _NH_SIPSessionNewIncoming(self, notification):
        session = notification.sender
        for stream in notification.data.streams:
            if stream.type in ('audio', 'chat'):
                break
        else:
            remote_identity = str(session.remote_identity.uri)
            if session.remote_identity.display_name:
                remote_identity = '"%s" <%s>' % (session.remote_identity.display_name, remote_identity)

            self.output.put('Session from %s rejected due to incompatible media\n' % remote_identity)
            session.reject(415)
            return

        self.session_spool_dir = self.spool_dir + "/" + str(uuid.uuid1())

        try:
            makedirs(self.session_spool_dir)
        except Exception as e:
            log.error('Failed to create session spool directory at {directory}: {exception!s}'.format(directory=self.session_spool_dir, exception=e))
        else:    
            if self.stop_call_thread is None:
                self.stop_call_thread = CancelThread(self)
                self.stop_call_thread.start()
            self.output.put("To stop the call: touch %s/stop\n" % self.session_spool_dir)

        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=session)
        accepted_types = ['video', 'audio', 'chat'] if self.enable_video else ['audio', 'chat']

        if self.options.auto_answer_interval is not None and self.auto_answer_allowed(str(session.remote_identity.uri)):
            if self.options.auto_answer_interval == 0:
                if len(self.incoming_sessions) == 0:
                    session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types])
                else:
                    session.reject()
                return
            else:
                def auto_answer():
                    self.incoming_sessions.remove(session)
                    if len(self.incoming_sessions) == 0:
                        session.accept([stream for stream in session.proposed_streams if stream.type in accepted_types])
                    else:
                        session.reject()
                timer = reactor.callLater(self.options.auto_answer_interval, auto_answer)
                self.answer_timers[id(session)] = timer
        session.send_ring_indication()
        self.incoming_sessions.append(session)
        if len(self.incoming_sessions) == 1:
            self._print_new_session()
            if not self.disable_ringtone:
                if not self.started_sessions:
                    if self.wave_inbound_ringtone:
                        self.wave_inbound_ringtone.start()
                else:
                    self.tone_ringtone.start()

    def _NH_SIPSessionNewOutgoing(self, notification):
        session = notification.sender
        local_identity = str(session.local_identity.uri).split(":")[1]
        if session.local_identity.display_name:
            local_identity = '"%s" <%s>' % (session.local_identity.display_name, local_identity)
        remote_identity = str(session.remote_identity.uri).split(":")[1]
        if session.remote_identity.display_name:
            remote_identity = '"%s" <%s>' % (session.remote_identity.display_name, remote_identity)
        self.output.put("Starting %s session from %s to %s via %s...\n" % ('video' if self.enable_video else 'audio', local_identity, remote_identity, session.route))
        self.outgoing_session = session

    def _NH_SIPSessionGotRingIndication(self, notification):
        if self.wave_outbound_ringtone and not self.disable_ringtone:
            self.wave_outbound_ringtone.start()

    def _NH_SIPSessionDidFail(self, notification):
        code = notification.data.code

        self._playback_end(failed_reason='outgoing-failed-' + str(code))
        
        session = notification.sender
        script = "%s/%s-%s-fail" % (self.scripts_dir, str(session.remote_identity.uri).split(":")[1], session.direction)
        self.output.put("Check for script %s\n" % script)
        if os.path.exists(script):
            self.output.put("Running script %s\n" % script)
            p = subprocess.Popen(script)

        if code and self.play_failure_code:
            if code == 486:
                text = 'User is busy'
            elif code == 404:
                text = 'User not found'
            elif code == 480:
                text = 'User not online'
            elif code == 408:
                text = 'Connection failed'
            else:
                text = str(notification.data.code)
            try:
                p = subprocess.Popen(['festival', '--tts', '--pipe'], stdin=subprocess.PIPE)
            except FileNotFoundError:
                pass
            else:
                p.communicate(text.encode())
        
        if not self.disable_ringtone:
            if session.direction == 'incoming' and session in self.incoming_sessions:
                if self.wave_inbound_ringtone:
                    self.wave_inbound_ringtone.stop()
                self.tone_ringtone.stop()
            elif session.direction == 'outgoing':
                if self.wave_outbound_ringtone:
                    self.wave_outbound_ringtone.stop()

        if notification.data.failure_reason == 'Call completed elsewhere' or notification.data.code == 487:
            self.output.put('Session cancelled\n')
            if session is self.outgoing_session:
                if self.auto_reconnect:
                    self.reconnect(10)
                else:
                    if not self.enable_playback:
                        self.stop()
                        self.end_cancel_thread()
            if session in self.incoming_sessions:
                self.incoming_sessions.remove(session)
        elif notification.data.failure_reason == 'user request':
            self.output.put('Session rejected by user (%d %s)\n' % (notification.data.code, notification.data.reason))
            if notification.sender is self.outgoing_session and not self.enable_playback:
                self.stop()
                self.end_cancel_thread()
        else:
            self.output.put('Session failed: %s %s\n' % (notification.data.code, notification.data.failure_reason))
            if session is self.outgoing_session and not self.enable_playback and not self.auto_reconnect:
               self.stop()
               self.end_cancel_thread()

            self.reconnect(10)
        
        if id(session) in self.answer_timers:
            timer = self.answer_timers[id(session)]
            if timer.active():
                timer.cancel()
            del self.answer_timers[id(session)]
        if self.incoming_sessions:
            self._print_new_session()
        elif session.direction == 'incoming':
            if not self.disable_ringtone:
                if self.wave_inbound_ringtone:
                    self.wave_inbound_ringtone.stop()
                self.tone_ringtone.stop()

        self.success = False
        self._remove_lock()

    def _NH_SIPSessionWillStart(self, notification):
        session = notification.sender
        if not self.disable_ringtone:
            if session.direction == 'incoming':
                if self.wave_inbound_ringtone:
                    self.wave_inbound_ringtone.stop()
                if not self.incoming_sessions:
                    self.tone_ringtone.stop()
            else:
                if self.wave_outbound_ringtone:
                    self.wave_outbound_ringtone.stop()
        
        if id(session) in self.answer_timers:
            timer = self.answer_timers[id(session)]
            if timer.active():
                timer.cancel()
            del self.answer_timers[id(session)]

        script = "%s/%s-%s-start" % (self.scripts_dir, str(session.remote_identity.uri).split(":")[1], session.direction)
        if os.path.exists(script):
            self.output.put("Running script %s\n" % script)
            p = subprocess.Popen(script)

    def _NH_SIPSessionDidStart(self, notification):
        notification_center = NotificationCenter()
        session = notification.sender
        self.output.put('Session started with %d streams\n' % len(notification.data.streams))
        if session.remote_user_agent is not None:
            self.output.put('Remote SIP User Agent is "%s"\n' % session.remote_user_agent)

        for stream in notification.data.streams:
            if stream.type in ('audio', 'video'):
                self.output.put('%s stream established using %s codec at %sHz\n' % (stream.type.title(), stream.codec.capitalize(), stream.sample_rate))
                
                if stream.ice_active:
                    self.output.put('%s RTP endpoints %s:%d (ICE type %s) <-> %s:%d (ICE type %s)\n' % (stream.type.title(), stream.local_rtp_address,
                                                                                                        stream.local_rtp_port,
                                                                                                        stream.local_rtp_candidate.type.lower(),
                                                                                                        stream.remote_rtp_address,
                                                                                                        stream.remote_rtp_port,
                                                                                                        stream.remote_rtp_candidate.type.lower()))
                else:
                    self.output.put('%s RTP endpoints %s:%d <-> %s:%d\n' % (stream.type.title(), 
                                                                               stream.local_rtp_address, 
                                                                               stream.local_rtp_port, 
                                                                               stream.remote_rtp_address, 
                                                                               stream.remote_rtp_port))
                if stream.encryption.active:
                    self.output.put('%s stream is encrypted using %s (%s)\n' % (stream.type.title(), stream.encryption.type, (lambda c: c.decode() if isinstance(c, bytes) else c)(stream.encryption.cipher)))

            
        self.started_sessions.append(session)
        if self.active_session is not None:
            self.active_session.hold()
        self.active_session = session
        if len(self.started_sessions) > 1:
            message = 'Connected sessions:\n'
            for session in self.started_sessions:
                identity = str(session.remote_identity.uri)
                if session.remote_identity.display_name:
                    identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
                message += '  Session %s (%d/%d) - %s\n' % (identity, self.started_sessions.index(session)+1, len(self.started_sessions), 'active' if session is self.active_session else 'on hold')
            message += 'Press arrow keys to switch the active session\n'
            self.output.put(message)

        if self.incoming_sessions:
            if not self.disable_ringtone:
                self.tone_ringtone.start()
            self._print_new_session()

        for stream in notification.data.streams:
            notification_center.add_observer(self, sender=stream)

        if self.options.auto_hangup_interval is not None:
            if self.options.auto_hangup_interval == 0:
                session.end()
            else:
                timer = reactor.callLater(self.options.auto_hangup_interval, session.end)
                self.hangup_timers[id(session)] = timer

        if self.auto_record:
            settings = SIPSimpleSettings()
            audio_stream = self.active_session.streams[0]
            direction = session.direction
            remote = "%s@%s" % (session.remote_identity.uri.user, session.remote_identity.uri.host)
            filename = "%s-%s-%s.wav" % (datetime.now().strftime("%Y%m%d-%H%M%S"), remote, direction)
            path = os.path.join(settings.audio.directory.normalized, session.account.id, datetime.now().strftime("%Y%m%d"))
            makedirs(path)
            audio_stream.start_recording(os.path.join(path, filename))
                
    def _NH_SIPSessionWillEnd(self, notification):
        notification_center = NotificationCenter()

        session = notification.sender
        if id(session) in self.hangup_timers:
            timer = self.hangup_timers[id(session)]
            if timer.active():
                timer.cancel()
            del self.hangup_timers[id(session)]

        if not self.disable_hanguptone:
            hangup_tone = WavePlayer(self.voice_audio_mixer, ResourcePath('sounds/hangup_tone.wav').normalized)
            notification_center.add_observer(self, sender=hangup_tone)
            self.voice_audio_bridge.add(hangup_tone)
            hangup_tone.start()

    def end_session_if_needed(self):
        if not self.session_spool_dir:
            return False

        stop_call_file = self.session_spool_dir + "/stop"

        if stop_call_file and os.path.exists(stop_call_file):
            if self.active_session is not None:
                self.output.put('Ending audio session...\n')
                self.active_session.end()
            elif self.outgoing_session is not None:
                self.outgoing_session.end()
            elif self.incoming_sessions:
                session = self.incoming_sessions.pop(0)
                session.reject()
            return True

        return False

    def stop_if_needed(self):
        stop_file = self.spool_dir + "/stop"

        if stop_file and os.path.exists(stop_file):
            if self.active_session is not None:
                self.output.put('Ending audio session...\n')
                self.active_session.end()
            elif self.outgoing_session is not None:
                self.outgoing_session.end()
            elif self.incoming_sessions:
                session = self.incoming_sessions.pop(0)
                session.reject()
            self.stop()
            self.end_cancel_thread()

    def end_cancel_thread(self):
        if self.stop_call_thread is not None:
            self.stop_call_thread.stop()
            self.stop_call_thread = None

        if self.session_spool_dir:
            try:
                shutil.rmtree(self.session_spool_dir)
            except OSError:
                pass

        self.session_spool_dir = None

    def _NH_TLSTransportHasChanged(self, notification):
        self.output.put('TLS verify server: %s\n' % notification.data.verify_server)
        self.output.put('TLS certificate: %s\n' % notification.data.certificate)
        if notification.data.certificate:
            try:
                contents = open(os.path.expanduser(notification.data.certificate), 'rb').read()
                certificate = X509Certificate(contents) # validate the certificate
            except (GNUTLSError, OSError) as e:
                self.output.put('Cannot read TLS certificate: %s\n' % str(e))
                pass
            else:
                self.output.put('TLS Subject: %s\n' % certificate.subject)
        
    def _NH_SIPSessionDidEnd(self, notification):
        self.end_cancel_thread()
        reactor.callLater(0.1, self._remove_lock)
        
        session = notification.sender
        if session is not self.active_session:
            identity = str(session.remote_identity.uri)
            if session.remote_identity.display_name:
                identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
        else:
            identity = '\b'
        if notification.data.end_reason == 'user request':
            self.output.put('Session %s ended by %s party\n' % (identity, notification.data.originator))
        else:
            self.output.put('Session %s ended due to error: %s\n' % (identity, notification.data.end_reason))

        if session.end_time and session.start_time:
            duration = session.end_time - session.start_time
            seconds = duration.seconds if duration.microseconds < 500000 else duration.seconds+1
            minutes, seconds = seconds / 60, seconds % 60
            hours, minutes = minutes / 60, minutes % 60
            hours += duration.days*24
            if not minutes and not hours:
                duration_text = '%d seconds' % seconds
            elif not hours:
                duration_text = '%02d:%02d' % (minutes, seconds)
            else:
                duration_text = '%02d:%02d:%02d' % (hours, minutes, seconds)
            self.output.put('Session duration was %s\n' % duration_text)
        
        try: 
            self.started_sessions.remove(session)
        except ValueError:
            self.output.put('Session ended without starting')
            return
        
        if session is self.active_session:
            if self.started_sessions:
                self.active_session = self.started_sessions[0]
                self.active_session.unhold()
                self.ignore_local_unhold = True
                identity = str(self.active_session.remote_identity.uri)
                if self.active_session.remote_identity.display_name:
                    identity = '"%s" <%s>' % (self.active_session.remote_identity.display_name, identity)
                self.output.put('Active audio session: "%s" (%d/%d)\n' % (identity, self.started_sessions.index(self.active_session)+1, len(self.started_sessions)))
            else:
                self.active_session = None

        if session is self.outgoing_session:
            rtp_lost = self.rtp_lost
            self.rtp_lost = False

            if notification.data.originator == 'remote' and self.auto_reconnect:
                self.reconnect(5)
            else:
                if rtp_lost and self.auto_reconnect:
                    self.reconnect(5)
                    self.rtp_lost = False
                else:
                    if not self.enable_playback:
                        self.stop()
                        self.end_cancel_thread()

        script = "%s/%s-%s-end" % (self.scripts_dir, str(session.remote_identity.uri).split(":")[1], session.direction)
        if os.path.exists(script):
            self.output.put("Running script %s\n" % script)
            p = subprocess.Popen(script)

        try:
            on_hold_streams = [stream for stream in chain(*(session.streams for session in self.started_sessions)) if stream.on_hold]
        except TypeError:
            self.hold_tone.stop()
        else:
            if not on_hold_streams and self.hold_tone and self.hold_tone.is_active:
                self.hold_tone.stop()

        self.success = True

    def _NH_SIPSessionDidChangeHoldState(self, notification):
        session = notification.sender
        if notification.data.on_hold:
            if notification.data.originator == 'remote':
                if session is self.active_session:
                    self.output.put('Remote party has put the audio session on hold\n')
                else:
                    identity = str(session.remote_identity.uri)
                    if session.remote_identity.display_name:
                        identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
                    self.output.put('%s has put the audio session on hold\n' % identity)
            elif not self.ignore_local_hold:
                if session is self.active_session:
                    self.output.put('Session is put on hold\n')
                else:
                    identity = str(session.remote_identity.uri)
                    if session.remote_identity.display_name:
                        identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
                    self.output.put('Session %s is put on hold\n' % identity)
            else:
                self.ignore_local_hold = False
        else:
            if notification.data.originator == 'remote':
                if session is self.active_session:
                    self.output.put('Remote party has taken the audio session out of hold\n')
                else:
                    identity = str(session.remote_identity.uri)
                    if session.remote_identity.display_name:
                        identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
                    self.output.put('%s has taken the audio session out of hold\n' % identity)
            elif not self.ignore_local_unhold:
                if session is self.active_session:
                    self.output.put('Session is taken out of hold\n')
                else:
                    identity = str(session.remote_identity.uri)
                    if session.remote_identity.display_name:
                        identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)
                    self.output.put('Session %s is taken out of hold\n' % identity)
            else:
                self.ignore_local_unhold = False

    def _NH_SIPSessionNewProposal(self, notification):
        if notification.data.originator == 'remote':
            session = notification.sender
            accepted_types = ['video', 'audio', 'chat'] if self.enable_video else ['audio', 'chat']
            streams = [stream for stream in notification.data.proposed_streams if stream.type in accepted_types]
            if audio_streams:
                session.accept_proposal(streams)
            else:
                session.reject_proposal(488)

    def _NH_AudioStreamGotDTMF(self, notification):
        notification_center = NotificationCenter()
        digit = notification.data.digit
        filename = 'sounds/dtmf_%s_tone.wav' % {'*': 'star', '#': 'pound'}.get(digit, digit)
        self.output.put('DTMF code received: %s\n' % digit)
        wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(filename).normalized)
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=wave_player)
        self.voice_audio_bridge.add(wave_player)
        wave_player.start()

    def _NH_SIPSessionGotConferenceInfo(self, notification):
        info = notification.data.conference_info
        self.output.put('Conference users: %s\n' % ", ".join(user.entity.split(":")[1] for user in info.users))
        if info.conference_description.resources:
            for file in info.conference_description.resources.files:
                self.output.put('Conference shared file: %s (%s bytes)\n' % (file.name, file.size))

    def _NH_RTPStreamDidTimeout(self, notification):
        self.output.put('RTP timeout')
        stream = notification.sender

        if self.account.rtp.hangup_on_timeout:
            try:
                session = next(session for session in self.started_sessions if stream in session.streams)
            except StopIteration:
                self.output.put('Session not found')
            else:
                session.end()

    def _NH_RTPStreamICENegotiationStateDidChange(self, notification):
        data = notification.data
        if data.state == 'GATHERING':
            self.output.put("Gathering ICE Candidates...\n")
        elif data.state == 'NEGOTIATION_START':
            self.output.put("Connecting...\n")
        elif data.state == 'NEGOTIATING':
            self.output.put("Negotiating ICE...\n")
        elif data.state == 'GATHERING_COMPLETE':
            self.output.put("Gathering Complete\n")
        elif data.state == 'RUNNING':
            self.output.put("ICE Negotiation Succeeded\n")
        elif data.state == 'FAILED':
            self.output.put("ICE Negotiation Failed\n", True)

    def _NH_RTPStreamICENegotiationDidFail(self, notification):
        stream = notification.sender
        self.output.put('%s ICE negotiation failed: %s' % (notification.sender.type, notification.data.reason))

    def _NH_MediaStreamDidStart(self, notification):
        stream = notification.sender
        if stream.type == 'audio' and self.play_file:
            script = "%s/scripts/%s-playback-start" % (self.playback_dir, self.target)
            if os.path.exists(script):
                self.output.put("Running script %s\n" % script)
                p = subprocess.Popen(script)
            else:
                self.output.put("Script does not exist %s\n" % script)
    
            self.playback_wave_player = WavePlayer(self.voice_audio_mixer, ResourcePath(self.play_file).normalized)
            notification_center = NotificationCenter()
            notification_center.add_observer(self, sender=self.playback_wave_player)
            self.output.put("Playback file %s\n" % self.play_file)
            stream.bridge.add(self.playback_wave_player)
            self.playback_wave_player.start()

    def _NH_RTPStreamICENegotiationDidSucceed(self, notification):
        stream = notification.sender
        self.output.put('%s ICE negotiation succeeded\n' % stream.type)
        self.output.put('%s RTP endpoints: %s:%d (%s) <-> %s:%d (%s)\n' % (stream.type, stream.local_rtp_address,
                                                                        stream.local_rtp_port,
                                                                        self.stream.local_rtp_candidate.type.lower(),
                                                                        stream.remote_rtp_address,
                                                                        stream.remote_rtp_port,
                                                                        stream.remote_rtp_candidate.type.lower()))

        if stream.local_rtp_candidate.type.lower() != 'relay' and stream.remote_rtp_candidate.type.lower() != 'relay':
            self.output.put('%s stream is peer to peer\n' % stream.type)
        else:
            self.output.put('%s stream is relayed by server\n' % stream.type)
            
    def _NH_RTPStreamDidChangeHoldState(self, notification):
        if notification.data.on_hold:
            if self.hold_tone and not self.hold_tone.is_active:
                self.hold_tone.start()
        else:
            try:
                on_hold_streams = [stream for stream in chain(*(session.streams for session in self.started_sessions)) if stream is not notification.sender and stream.on_hold]
            except TypeError:
                self.hold_tone.stop()
            else:
                if self.hold_tone and not on_hold_streams and self.hold_tone.is_active:
                    self.hold_tone.stop()

    def _NH_RTPStreamDidChangeRTPParameters(self, notification):
        stream = notification.sender
        self.output.put('Audio RTP parameters changed:\n')
        self.output.put('Audio stream using "%s" codec at %sHz\n' % (stream.codec, stream.sample_rate))
        self.output.put('Audio RTP endpoints %s:%d <-> %s:%d\n' % (stream.local_rtp_address, stream.local_rtp_port, stream.remote_rtp_address, stream.remote_rtp_port))
        if stream.encryption.active:
            self.output.put('RTP audio stream is encrypted using %s (%s)\n' % (stream.encryption.type, (lambda c: c.decode() if isinstance(c, bytes) else c)(stream.encryption.cipher)))

    def _NH_RTPStreamZRTPLog(self, notification):
        if 'Dropping packet' in notification.data.message:
            return
        #self.output.put('ZRTP message: %s\n' % notification.data.message)

    def _NH_RTPStreamDidEnableEncryption(self, notification):
        stream = notification.sender
        self.output.put("%s encryption activated using %s (%s)\n" % (stream.type.title(), stream.encryption.type, (lambda c: c.decode() if isinstance(c, bytes) else c)(stream.encryption.cipher)))

        if stream.encryption.type == 'ZRTP':
            peer_name = stream.encryption.zrtp.peer_name if stream.encryption.zrtp.peer_name else None
            sas = stream.encryption.zrtp.sas.decode() if isinstance(stream.encryption.zrtp.sas, bytes) else stream.encryption.zrtp.sas
            self.output.put("ZRTP secret is %s\n" % sas)
            self.output.put("ZRTP peer name is %s\n" % (peer_name or 'not set'))
            self.output.put("ZRTP peer is %s\n" % ('verified' if stream.encryption.zrtp.verified else 'not verified'))

    def _NH_ChatStreamGotMessage(self, notification):
        if not notification.data.message.content_type.startswith("text/"):
            return
        self.output.put(notification.data.message.content)

    def _NH_AudioStreamDidStartRecording(self, notification):
        self.output.put('Recording audio to %s\n' % notification.data.filename)

    def _NH_AudioStreamDidStopRecording(self, notification):
        self.output.put('Stopped recording audio to %s\n' % notification.data.filename)

    def _NH_WavePlayerDidFail(self, notification):
        notification_center = NotificationCenter()
        notification_center.remove_observer(self, sender=notification.sender)
        self.output.put('Failed to play %s: %s\n' % (notification.sender.filename, notification.data.error))
        if notification.sender == self.playback_wave_player or self.play_file:
            if self.active_session:
                self.active_session.end()
        
        self._playback_end(failed_reason='outgoing-failed-playback')

    def _NH_WavePlayerDidEnd(self, notification):
        notification_center = NotificationCenter()
        notification_center.remove_observer(self, sender=notification.sender)

        self._playback_end() 
            
        if notification.sender == self.playback_wave_player:
            self.active_session.end()

    def _playback_end(self, failed_reason=None):
        if not self.play_file:
            return

        settings = SIPSimpleSettings()

        if failed_reason or not self.auto_record:
            save_path = os.path.join(settings.audio.directory.normalized, self.account.id, datetime.now().strftime("%Y%m%d"))
            makedirs(save_path)
            base = Path(self.play_file).stem
            ext = Path(self.play_file).suffix
            save_path = save_path + '/' + base + '-' + (failed_reason or 'outgoing') + ext
            self.output.put("Saved playback file to %s\n" % save_path)
            os.rename(self.play_file, save_path)

        if not failed_reason:
            rogertone = settings.sounds.roger_beep
            roger_tone = WavePlayer(SIPApplication.voice_audio_mixer, rogertone.path.normalized)
            SIPApplication.voice_audio_bridge.add(roger_tone)
            roger_tone.start()
        else:     
            hangup_tone = WavePlayer(SIPApplication.voice_audio_mixer, ResourcePath('sounds/hangup_tone.wav').normalized)
            SIPApplication.voice_audio_bridge.add(hangup_tone)
            hangup_tone.start()
            
        script = "%s/scripts/%s-playback-end" % (self.playback_dir, self.target)

        if os.path.exists(script):
            self.output.put("Running script %s\n" % script)
            p = subprocess.Popen(script)

        if os.path.exists(self.play_file):
            try:
                os.remove(self.play_file)
            except OSError:
                pass
                
        self.play_file = None

    def _remove_lock(self):
        # used by external audio recorder to know if we are in call
        self.outgoing_session = None
        lock_file = "%s/playback.lock" % self.playback_dir
        if os.path.exists(lock_file):
            self.output.put("Lock file %s removed\n" % lock_file)
            os.remove(lock_file)

    def _NH_DefaultAudioDeviceDidChange(self, notification):
        SIPApplication._NH_DefaultAudioDeviceDidChange(self, notification)
        if notification.data.changed_input and self.voice_audio_mixer.input_device=='system_default':
            self.output.put('Switched default input device to: %s\n' % self.voice_audio_mixer.real_input_device)
        if notification.data.changed_output and self.voice_audio_mixer.output_device=='system_default':
            self.output.put('Switched default output device to: %s\n' % self.voice_audio_mixer.real_output_device)
        if notification.data.changed_output and self.alert_audio_mixer.output_device=='system_default':
            self.output.put('Switched alert device to: %s\n' % self.alert_audio_mixer.real_output_device)

    def _NH_AudioDevicesDidChange(self, notification):
        engine = Engine()
        settings = SIPSimpleSettings()

        old_devices = set(notification.data.old_devices)
        new_devices = set(notification.data.new_devices)
        added_devices = new_devices - old_devices
        removed_devices = old_devices - new_devices
        changed_input_device = self.voice_audio_mixer.real_input_device in removed_devices
        changed_output_device = self.voice_audio_mixer.real_output_device in removed_devices
        changed_alert_device = self.alert_audio_mixer.real_output_device in removed_devices

        SIPApplication._NH_AudioDevicesDidChange(self, notification)

        if added_devices:
            self.output.put('Added audio device(s): %s\n' % ', '.join(sorted(added_devices)))
        if removed_devices:
            self.output.put('Removed audio device(s): %s\n' % ', '.join(sorted(removed_devices)))
        if changed_input_device:
            self.output.put('Audio input device has been switched to: %s\n' % self.voice_audio_mixer.real_input_device)
        if changed_output_device:
            self.output.put('Audio output device has been switched to: %s\n' % self.voice_audio_mixer.real_output_device)
        if changed_alert_device:
            self.output.put('Audio alert device has been switched to: %s\n' % self.alert_audio_mixer.real_output_device)

        self.output.put('Available audio input devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(self.engine.input_devices)))
        self.output.put('Available audio output devices: %s\n' % ', '.join(['None', 'system_default'] + sorted(self.engine.output_devices)))
        
        try:
            device = str(list(added_devices)[0])
        except IndexError:
            pass
        else:
            in_devices = list(set(engine.input_devices))
            out_devices = list(set(engine.output_devices))

            new_input_device = self.voice_audio_mixer.input_device
            new_output_device = self.voice_audio_mixer.output_device
            if device in in_devices:
                settings.audio.input_device = str(device)
                new_input_device = device
                        
            if device in out_devices:
                settings.audio.output_device = str(device)
                new_output_device = device

            settings.save()
            
            tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0

            try:
                self.voice_audio_mixer.set_sound_devices(new_input_device, new_output_device, tail_length)
            except SIPCoreError as e:
                pass
            
    def _NH_RTPStreamICENegotiationDidSucceed(self, notification):
        self.output.put("ICE negotiation succeeded in %s\n" % notification.data.duration)

    def _NH_RTPStreamICENegotiationDidFail(self, notification):
        self.output.put("ICE negotiation failed: %s\n" % notification.data.reason.decode())

    def _print_new_session(self):
        session = self.incoming_sessions[0]
        identity = str(session.remote_identity.uri)
        if session.remote_identity.display_name:
            identity = '"%s" <%s>' % (session.remote_identity.display_name, identity)

        video_streams = [stream for stream in session.proposed_streams if stream.type in ['video']]
        media_type = 'video' if video_streams else 'audio'
        self.output.put("Incoming %s session from '%s', do you accept? (y/n)\n" % (media_type, identity))

def parse_handle_call_option(option, opt_str, value, parser, name):
    try:
        value = parser.rargs[0]
    except IndexError:
        value = 0
    else:
        if value == '' or value[0] == '-':
            value = 0
        else:
            try:
                value = int(value)
            except ValueError:
                value = 0
            else:
                del parser.rargs[0]
    setattr(parser.values, name, value)

if __name__ == '__main__':
    description = 'This script can sit idle waiting for an incoming audio session, or initiate an outgoing audio session to a SIP address. The program will close the session and quit when Ctrl+D is pressed.'
    usage = '%prog [options] [user@domain]'
    parser = OptionParser(usage=usage, description=description)
    parser.print_usage = parser.print_help
    parser.add_option('-a', '--account', type='string', dest='account', help='The account name to use for any outgoing traffic. If not supplied, the default account will be used.', metavar='NAME')
    parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.')
    parser.add_option('-d', '--playback-dir', type='string', dest='playback_dir', help='Directory with wav files to be played after calling. The destination SIP address is taken from the filename. After playback the call is hangup')
    parser.add_option('-p', '--enable_playback', action='store_true', dest='enable_playback', default=False, help='Enable polling playback directory for new wavs.')
    parser.add_option('-l', '--log-register', action='store_true', dest='log_register', default=False, help='Log result of registrations.')
    parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.')
    parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.')
    parser.add_option('-r', '--auto-record', action='store_true', dest='auto_record', default=False, help='Automatic recording of voice calls.')
    parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).')
    parser.add_option('--disable-ringtone', action='store_true', dest='disable_ringtone', default=False, help='Disable ringtone.')
    parser.add_option('-g', '--disable-hanguptone', action='store_true', dest='disable_hanguptone', default=False, help='Disable hangup tone.')
    parser.add_option('-S', '--disable-sound', action='store_true', dest='disable_sound', default=False, help='Disables initializing the sound card.')
    parser.set_default('auto_answer_interval', None)
    parser.add_option('--auto-answer', action='callback', callback=parse_handle_call_option, callback_args=('auto_answer_interval',), help='Interval after which to answer an incoming session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (accept the session as soon as it starts ringing).', metavar='[INTERVAL]')
    parser.add_option('-u', '--auto-answer-uris', type='string', dest='auto_answer_uris', default="", help='Optional list of SIP URIs for which auto-answer is allowed')
    parser.add_option('-i', '--external-id', type='string', dest='external_id', help='id used for call control from external application')
    parser.add_option('-v', '--spool-dir', type='string', dest='spool_dir', default=None, help='Spool dir for call control from external applications, default is /var/spool/sipclients/sessions')
    parser.add_option('-t', '--enable-default-devices',  action='store_true', dest='enable_default_devices', help='Use default audio devices')
    parser.add_option('-m', '--mute',  action='store_true', dest='mute', help='Mute microphone')
    parser.add_option('-V', '--enable-video',  action='store_true', dest='enable_video', default=False, help='Enable video if camera is available')
    parser.set_default('auto_hangup_interval', None)
    parser.add_option('--auto-hangup', action='callback', callback=parse_handle_call_option, callback_args=('auto_hangup_interval',), help='Interval after which to hang up an established session (disabled by default). If the option is specified but the interval is not, it defaults to 0 (hangup the session as soon as it connects).', metavar='[INTERVAL]')
    parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading input from the console is disabled and the option --auto-answer is implied. This is particularly useful when running this script in a non-interactive environment.')
    parser.add_option('-f', '--play-failure-code', action='store_true', dest='play_failure_code', default=False, help='Play failure code using festival.')
    parser.add_option('-D', '--daemonize', action='store_true', dest='daemonize', default=False, help='Enable running this program as a deamon.')
    parser.add_option('--no-mp', action='store_true', dest='no_mp', default=False, help='Add X-No-MediaProxy header to outgoing INVITE to bypass MediaProxy.')
    parser.add_option('-R', '--auto-reconnect', action='store_true', dest='auto_reconnect', default=False, help='Auto reconnect call if disconnected by remote.')
    options, args = parser.parse_args()

    target = args[0] if args and not options.auto_answer_uris else None

    application = SIPAudioApplication()
    application.start(target, options)
    
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    application.output.join()
    sleep(0.1)

    sys.exit(0 if application.success else 1)

