#
# Object oriented PJSUA wrapper.
#
# Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
#

"""Multimedia communication client library based on SIP protocol.

This implements a fully featured multimedia communication client 
library based on PJSIP stack (http://www.pjsip.org)


1. FEATURES

  - Session Initiation Protocol (SIP) features:
     - Basic registration and call
     - Multiple accounts
     - Call hold, attended and unattended call transfer
     - Presence
     - Instant messaging
     - Multiple SIP accounts
  - Media features:
     - Audio
     - Conferencing
     - Narrowband and wideband
     - Codecs: PCMA, PCMU, GSM, iLBC, Speex, G.722, L16
     - RTP/RTCP
     - Secure RTP (SRTP)
     - WAV playback, recording, and playlist
  - NAT traversal features
     - Symmetric RTP
     - STUN
     - TURN
     - ICE
 

2. USING

See http://www.pjsip.org/trac/wiki/Python_SIP_Tutorial for a more thorough
tutorial. The paragraphs below explain basic tasks on using this module.


"""
try:
    import _thread as thread
except ImportError:
    import thread
import _pjsua
import threading
import weakref
import time

class Error:
    """Error exception class.
    
    Member documentation:

    op_name  -- name of the operation that generated this error.
    obj      -- the object that generated this error.
    err_code -- the error code.

    """
    op_name = ""
    obj = None
    err_code = -1
    _err_msg = ""

    def __init__(self, op_name, obj, err_code, err_msg=""):
        self.op_name = op_name
        self.obj = obj
        self.err_code = err_code
        self._err_msg = err_msg

    def err_msg(self):
        "Retrieve the description of the error."
        if self._err_msg != "":
            return self._err_msg
        self._err_msg = Lib.strerror(self.err_code)
        return self._err_msg

    def __str__(self):
        return "Object: " + str(self.obj) + ", operation=" + self.op_name + \
               ", error=" + self.err_msg()

# 
# Constants
#

class TransportType:
    """SIP transport type constants.
    
    Member documentation:
    UNSPECIFIED -- transport type is unknown or unspecified
    UDP         -- UDP transport
    TCP         -- TCP transport
    TLS         -- TLS transport
    IPV6        -- this is not a transport type but rather a flag
                   to select the IPv6 version of a transport
    UDP_IPV6    -- IPv6 UDP transport
    TCP_IPV6    -- IPv6 TCP transport
    """
    UNSPECIFIED = 0
    UDP = 1
    TCP = 2
    TLS = 3
    IPV6 = 128
    UDP_IPV6 = UDP + IPV6
    TCP_IPV6 = TCP + IPV6

class TransportFlag:
    """Transport flags to indicate the characteristics of the transport.
    
    Member documentation:
    
    RELIABLE    -- transport is reliable.
    SECURE      -- transport is secure.
    DATAGRAM    -- transport is datagram based.
    
    """
    RELIABLE = 1
    SECURE = 2
    DATAGRAM = 4

class CallRole:
    """Call role constants.
    
    Member documentation:

    CALLER  -- role is caller
    CALLEE  -- role is callee

    """
    CALLER = 0
    CALLEE = 1

class CallState:
    """Call state constants.
    
    Member documentation:

    NULL            -- call is not initialized.
    CALLING         -- initial INVITE is sent.
    INCOMING        -- initial INVITE is received.
    EARLY           -- provisional response has been sent or received.
    CONNECTING      -- 200/OK response has been sent or received.
    CONFIRMED       -- ACK has been sent or received.
    DISCONNECTED    -- call is disconnected.
    """
    NULL = 0
    CALLING = 1
    INCOMING = 2
    EARLY = 3
    CONNECTING = 4
    CONFIRMED = 5
    DISCONNECTED = 6


class MediaState:
    """Call media state constants.
    
    Member documentation:

    NULL        -- media is not available.
    ACTIVE      -- media is active.
    LOCAL_HOLD  -- media is put on-hold by local party.
    REMOTE_HOLD -- media is put on-hold by remote party.
    ERROR       -- media error (e.g. ICE negotiation failure).
    """
    NULL = 0
    ACTIVE = 1
    LOCAL_HOLD = 2
    REMOTE_HOLD = 3
    ERROR = 4


class MediaDir:
    """Media direction constants.
    
    Member documentation:

    NULL              -- media is not active
    ENCODING          -- media is active in transmit/encoding direction only.
    DECODING          -- media is active in receive/decoding direction only
    ENCODING_DECODING -- media is active in both directions.
    """
    NULL = 0
    ENCODING = 1
    DECODING = 2
    ENCODING_DECODING = 3


class PresenceActivity:
    """Presence activities constants.
    
    Member documentation:

    UNKNOWN -- the person activity is unknown
    AWAY    -- the person is currently away
    BUSY    -- the person is currently engaging in other activity
    """
    UNKNOWN = 0
    AWAY = 1
    BUSY = 2


class SubscriptionState:
    """Presence subscription state constants.

    """
    NULL = 0
    SENT = 1
    ACCEPTED = 2
    PENDING = 3
    ACTIVE = 4
    TERMINATED = 5
    UNKNOWN = 6


class TURNConnType:
    """These constants specifies the connection type to TURN server.
    
    Member documentation:
    UDP     -- use UDP transport.
    TCP     -- use TCP transport.
    TLS     -- use TLS transport.
    """
    UDP = 17
    TCP = 6
    TLS = 255


class UAConfig:
    """User agent configuration to be specified in Lib.init().
    
    Member documentation:

    max_calls   -- maximum number of calls to be supported.
    nameserver  -- list of nameserver hostnames or IP addresses. Nameserver
                   must be configured if DNS SRV resolution is desired.
    stun_domain -- if nameserver is configured, this can be used to query
                   the STUN server with DNS SRV.
    stun_host   -- the hostname or IP address of the STUN server. This will
                   also be used if DNS SRV resolution for stun_domain fails.
    user_agent  -- Optionally specify the user agent name.
    """
    max_calls = 4
    nameserver = []
    stun_domain = ""
    stun_host = ""
    user_agent = "pjsip python"
    
    def _cvt_from_pjsua(self, cfg):
        self.max_calls = cfg.max_calls
        self.thread_cnt = cfg.thread_cnt
        self.nameserver = cfg.nameserver
        self.stun_domain = cfg.stun_domain
        self.stun_host = cfg.stun_host
        self.user_agent = cfg.user_agent

    def _cvt_to_pjsua(self):
        cfg = _pjsua.config_default()
        cfg.max_calls = self.max_calls
        cfg.thread_cnt = 0
        cfg.nameserver = self.nameserver
        cfg.stun_domain = self.stun_domain
        cfg.stun_host = self.stun_host
        cfg.user_agent = self.user_agent
        return cfg


class LogConfig:
    """Logging configuration to be specified in Lib.init().
    
    Member documentation:

    msg_logging   -- specify if SIP messages should be logged. Set to
                     True.
    level         -- specify the input verbosity level.
    console_level -- specify the output verbosity level.
    decor         -- specify log decoration.
    filename      -- specify the log filename.
    callback      -- specify callback to be called to write the logging
                     messages. Sample function:

                     def log_cb(level, str, len):
                        print str,

    """
    msg_logging = True
    level = 5
    console_level = 5
    decor = 0
    filename = ""
    callback = None
    
    def __init__(self, level=-1, filename="", callback=None,
                 console_level=-1):
        self._cvt_from_pjsua(_pjsua.logging_config_default())
        if level != -1:
            self.level = level
        if filename != "":
            self.filename = filename
        if callback != None:
            self.callback = callback
        if console_level != -1:
            self.console_level = console_level

    def _cvt_from_pjsua(self, cfg):
        self.msg_logging = cfg.msg_logging
        self.level = cfg.level
        self.console_level = cfg.console_level
        self.decor = cfg.decor
        self.filename = cfg.log_filename
        self.callback = cfg.cb

    def _cvt_to_pjsua(self):
        cfg = _pjsua.logging_config_default()
        cfg.msg_logging = self.msg_logging
        cfg.level = self.level
        cfg.console_level = self.console_level
        cfg.decor = self.decor
        cfg.log_filename = self.filename
        cfg.cb = self.callback
        return cfg


class MediaConfig:
    """Media configuration to be specified in Lib.init().
    
    Member documentation:
    
    clock_rate          -- specify the core clock rate of the audio,
                           most notably the conference bridge.
    snd_clock_rate      -- optionally specify different clock rate for
                           the sound device.
    snd_auto_close_time -- specify the duration in seconds when the
                           sound device should be closed after inactivity
                           period.
    channel_count       -- specify the number of channels to open the sound
                           device and the conference bridge.
    audio_frame_ptime   -- specify the length of audio frames in millisecond.
    max_media_ports     -- specify maximum number of audio ports to be
                           supported by the conference bridge.
    quality             -- specify the audio quality setting (1-10)
    ptime               -- specify the audio packet length of transmitted
                           RTP packet.
    no_vad              -- disable Voice Activity Detector (VAD) or Silence
                           Detector (SD)
    ilbc_mode           -- specify iLBC codec mode (must be 30 for now)
    tx_drop_pct         -- randomly drop transmitted RTP packets (for
                           simulation). Number is in percent.
    rx_drop_pct         -- randomly drop received RTP packets (for
                           simulation). Number is in percent.
    ec_options          -- Echo Canceller option (specify zero).
    ec_tail_len         -- specify Echo Canceller tail length in milliseconds.
                           Value zero will disable the echo canceller.
    jb_min              -- specify the minimum jitter buffer size in
                           milliseconds. Put -1 for default.
    jb_max              -- specify the maximum jitter buffer size in
                           milliseconds. Put -1 for default.
    enable_ice          -- enable Interactive Connectivity Establishment (ICE)
    enable_turn         -- enable TURN relay. TURN server settings must also
                           be configured.
    turn_server         -- specify the domain or hostname or IP address of
                           the TURN server, in "host[:port]" format.
    turn_conn_type      -- specify connection type to the TURN server, from
                           the TURNConnType constant.
    turn_cred           -- specify AuthCred for the TURN credential.
    """
    clock_rate = 16000
    snd_clock_rate = 0
    snd_auto_close_time = 5
    channel_count = 1
    audio_frame_ptime = 20
    max_media_ports = 32
    quality = 6
    ptime = 0
    no_vad = False
    ilbc_mode = 30
    tx_drop_pct = 0
    rx_drop_pct = 0
    ec_options = 0
    ec_tail_len = 256
    jb_min = -1
    jb_max = -1
    enable_ice = True
    enable_turn = False
    turn_server = ""
    turn_conn_type = TURNConnType.UDP
    turn_cred = None
     
    def __init__(self):
        default = _pjsua.media_config_default()
        self._cvt_from_pjsua(default)

    def _cvt_from_pjsua(self, cfg):
        self.clock_rate = cfg.clock_rate
        self.snd_clock_rate = cfg.snd_clock_rate
        self.snd_auto_close_time = cfg.snd_auto_close_time
        self.channel_count = cfg.channel_count
        self.audio_frame_ptime = cfg.audio_frame_ptime
        self.max_media_ports = cfg.max_media_ports
        self.quality = cfg.quality
        self.ptime = cfg.ptime
        self.no_vad = cfg.no_vad
        self.ilbc_mode = cfg.ilbc_mode
        self.tx_drop_pct = cfg.tx_drop_pct
        self.rx_drop_pct = cfg.rx_drop_pct
        self.ec_options = cfg.ec_options
        self.ec_tail_len = cfg.ec_tail_len
        self.jb_min = cfg.jb_min
        self.jb_max = cfg.jb_max
        self.enable_ice = cfg.enable_ice
        self.enable_turn = cfg.enable_turn
        self.turn_server = cfg.turn_server
        self.turn_conn_type = cfg.turn_conn_type
        if cfg.turn_username:
            self.turn_cred = AuthCred(cfg.turn_realm, cfg.turn_username,
                                      cfg.turn_passwd, cfg.turn_passwd_type)
        else:
            self.turn_cred = None

    def _cvt_to_pjsua(self):
        cfg = _pjsua.media_config_default()
        cfg.clock_rate = self.clock_rate
        cfg.snd_clock_rate = self.snd_clock_rate
        cfg.snd_auto_close_time = self.snd_auto_close_time
        cfg.channel_count = self.channel_count
        cfg.audio_frame_ptime = self.audio_frame_ptime
        cfg.max_media_ports = self.max_media_ports
        cfg.quality = self.quality
        cfg.ptime = self.ptime
        cfg.no_vad = self.no_vad
        cfg.ilbc_mode = self.ilbc_mode
        cfg.tx_drop_pct = self.tx_drop_pct
        cfg.rx_drop_pct = self.rx_drop_pct
        cfg.ec_options = self.ec_options
        cfg.ec_tail_len = self.ec_tail_len
        cfg.jb_min = self.jb_min
        cfg.jb_max = self.jb_max
        cfg.enable_ice = self.enable_ice
        cfg.enable_turn = self.enable_turn
        cfg.turn_server = self.turn_server
        cfg.turn_conn_type = self.turn_conn_type
        if self.turn_cred:
            cfg.turn_realm = self.turn_cred.realm
            cfg.turn_username = self.turn_cred.username
            cfg.turn_passwd_type = self.turn_cred.passwd_type
            cfg.turn_passwd = self.turn_cred.passwd
        return cfg


class TransportConfig:
    """SIP transport configuration class.
    
    Member configuration:

    port        -- port number.
    bound_addr  -- optionally specify the address to bind the socket to.
                   Default is empty to bind to INADDR_ANY.
    public_addr -- optionally override the published address for this
                   transport. If empty, the default behavior is to get
                   the public address from STUN or from the selected
                   local interface. Format is "host:port".
    qos_type    -- High level traffic classification.
                   Enumerator:
                     0: PJ_QOS_TYPE_BEST_EFFORT
                          Best effort traffic (default value). Any QoS function calls with 
                          specifying this value are effectively no-op
                     1: PJ_QOS_TYPE_BACKGROUND
                          Background traffic.
                     2: PJ_QOS_TYPE_VIDEO
                          Video traffic.
                     3: PJ_QOS_TYPE_VOICE
                          Voice traffic.
                     4: PJ_QOS_TYPE_CONTROL
                          Control traffic.
                     5: PJ_QOS_TYPE_SIGNALLING
                          Signalling traffic.
    qos_params_flags    -- Determines which values to set, bitmask of pj_qos_flag.
                             PJ_QOS_PARAM_HAS_DSCP = 1
                             PJ_QOS_PARAM_HAS_SO_PRIO = 2
                             PJ_QOS_PARAM_HAS_WMM = 4
    qos_params_dscp_val -- The 6 bits DSCP value to set.
    qos_params_so_prio  -- Socket SO_PRIORITY value.
    qos_params_wmm_prio -- Standard WMM priorities.
                            Enumerator:
                              0: PJ_QOS_WMM_PRIO_BULK_EFFORT: Bulk effort priority
                              1: PJ_QOS_WMM_PRIO_BULK: Bulk priority.
                              2: PJ_QOS_WMM_PRIO_VIDEO: Video priority
                              3: PJ_QOS_WMM_PRIO_VOICE: Voice priority.
    """
    port = 0
    bound_addr = ""
    public_addr = ""
    
    qos_type = 0
    qos_params_flags = 0
    qos_params_dscp_val = 0
    qos_params_so_prio = 0
    qos_params_wmm_prio = 0
    
    

    def __init__(self, port=0, 
                 bound_addr="", public_addr=""):
        self.port = port
        self.bound_addr = bound_addr
        self.public_addr = public_addr

    def _cvt_from_pjsua(self, cfg):
        self.port = cfg.port
        self.bound_addr = cfg.bound_addr
        self.public_addr = cfg.public_addr
        self.qos_type = cfg.qos_type
        self.qos_params_flags = cfg.qos_params_flags
        self.qos_params_dscp_val = cfg.qos_params_dscp_val
        self.qos_params_so_prio = cfg.qos_params_so_prio
        self.qos_params_wmm_prio = cfg.qos_params_wmm_prio

    def _cvt_to_pjsua(self):
        cfg = _pjsua.transport_config_default()
        cfg.port = self.port
        cfg.bound_addr = self.bound_addr
        cfg.public_addr = self.public_addr
        cfg.qos_type = self.qos_type
        cfg.qos_params_flags = self.qos_params_flags
        cfg.qos_params_dscp_val = self.qos_params_dscp_val
        cfg.qos_params_so_prio = self.qos_params_so_prio
        cfg.qos_params_wmm_prio = self.qos_params_wmm_prio

        return cfg


class TransportInfo:
    """SIP transport info.
    
    Member documentation:

    type        -- transport type, from TransportType constants.
    description -- longer description for this transport.
    is_reliable -- True if transport is reliable.
    is_secure   -- True if transport is secure.
    is_datagram -- True if transport is datagram based.
    host        -- the IP address of this transport.
    port        -- the port number.
    ref_cnt     -- number of objects referencing this transport.
    """
    type = ""
    description = ""
    is_reliable = False
    is_secure = False
    is_datagram = False
    host = ""
    port = 0
    ref_cnt = 0
    
    def __init__(self, ti):
        self.type = ti.type_name
        self.description = ti.info
        self.is_reliable = (ti.flag & TransportFlag.RELIABLE)
        self.is_secure = (ti.flag & TransportFlag.SECURE)
        self.is_datagram = (ti.flag & TransportFlag.DATAGRAM)
        self.host = ti.addr
        self.port = ti.port
        self.ref_cnt = ti.usage_count
    
    
class Transport:
    "SIP transport class."
    _id = -1
    _lib = None
    _obj_name = ""

    def __init__(self, lib, id):
        self._lib = weakref.proxy(lib)
        self._id = id
        self._obj_name = "{Transport " + self.info().description + "}"
        _Trace((self, 'created'))

    def __del__(self):
        _Trace((self, 'destroyed'))
        
    def __str__(self):
        return self._obj_name

    def info(self):
        """Get TransportInfo.
        """
        lck = self._lib.auto_lock()
        ti = _pjsua.transport_get_info(self._id)
        if not ti:
            self._lib._err_check("info()", self, -1, "Invalid transport")
        return TransportInfo(ti)

    # NOTE: enable() and disable() methods are commented out because the underlying
    # pjsua_transport_set_enable function has been deprecated and is no longer available.
    # Please use PJSUA2 SWIG binding for modern transport management functionality.
    """
    def enable(self):
        \"\"\"Enable this transport.\"\"\"
        lck = self._lib.auto_lock()
        err = _pjsua.transport_set_enable(self._id, True)
        self._lib._err_check("enable()", self, err)

    def disable(self):
        \"\"\"Disable this transport.\"\"\"
        lck = self._lib.auto_lock()
        err = _pjsua.transport_set_enable(self._id, 0)
        self._lib._err_check("disable()", self, err)
    """

    def close(self, force=False):
        """Close and destroy this transport.

        Keyword argument:
        force   -- force deletion of this transport (not recommended).
        """
        lck = self._lib.auto_lock()
        err = _pjsua.transport_close(self._id, force)
        self._lib._err_check("close()", self, err)


class SIPUri:
    """Helper class to parse the most important components of SIP URI.

    Member documentation:

    scheme    -- URI scheme ("sip" or "sips")
    user      -- user part of the URI (may be empty)
    host      -- host name part
    port      -- optional port number (zero if port is not specified).
    transport -- transport parameter, or empty if transport is not
                 specified.

    """
    scheme = ""
    user = ""
    host = ""
    port = 0
    transport = ""

    def __init__(self, uri=None):
        if uri:
            self.decode(uri)

    def decode(self, uri):
        """Parse SIP URL.

        Keyword argument:
        uri -- the URI string.

        """
        self.scheme, self.user, self.host, self.port, self.transport = \
            _pjsua.parse_simple_uri(uri)

    def encode(self):
        """Encode this object into SIP URI string.

        Return:
            URI string.

        """
        output = self.scheme + ":"
        if self.user and len(self.user):
            output = output + self.user + "@"
        output = output + self.host
        if self.port:
            output = output + ":" + output(self.port)
        if self.transport:
            output = output + ";transport=" + self.transport
        return output


class AuthCred:
    """Authentication credential for SIP or TURN account.
    
    Member documentation:

    scheme      -- authentication scheme (default is "Digest")
    realm       -- realm
    username    -- username
    passwd_type -- password encoding (zero for plain-text)
    passwd      -- the password
    """
    scheme = "Digest"
    realm = "*"
    username = ""
    passwd_type = 0
    passwd = ""

    def __init__(self, realm, username, passwd, scheme="Digest", passwd_type=0):
        self.scheme = scheme
        self.realm = realm
        self.username = username
        self.passwd_type = passwd_type
        self.passwd = passwd


class AccountConfig:
    """ This describes account configuration to create an account.

    Member documentation:

    priority                -- account priority for matching incoming
                               messages.
    id                      -- SIP URI of this account. This setting is
                               mandatory.
    force_contact           -- force to use this URI as Contact URI. Setting
                               this value is generally not recommended.
    reg_uri                 -- specify the registrar URI. Mandatory if
                               registration is required.
    reg_timeout             -- specify the SIP registration refresh interval
                               in seconds.
    require_100rel          -- specify if reliable provisional response is
                               to be enforced (with Require header).
    publish_enabled         -- specify if PUBLISH should be used. When
                               enabled, the PUBLISH will be sent to the
                               registrar.
    pidf_tuple_id           -- optionally specify the tuple ID in outgoing
                               PIDF document.
    proxy                   -- list of proxy URI.
    auth_cred               -- list of AuthCred containing credentials to
                               authenticate against the registrars and
                               the proxies.
    auth_initial_send       -- specify if empty Authorization header should be
                               sent. May be needed for IMS.
    auth_initial_algorithm  -- when auth_initial_send is enabled, optionally
                               specify the authentication algorithm to use.
                               Valid values are "md5", "akav1-md5", or
                               "akav2-md5". 
    transport_id            -- optionally specify the transport ID to be used
                               by this account. Shouldn't be needed unless
                               for specific requirements (e.g. in multi-homed
                               scenario).
    allow_contact_rewrite   -- specify whether the account should learn its
                               Contact address from REGISTER response and 
                               update the registration accordingly. Default is
                               True.
    ka_interval             -- specify the interval to send NAT keep-alive 
                               packet.
    ka_data                 -- specify the NAT keep-alive packet contents.
    use_srtp                -- specify the SRTP usage policy. Valid values
                               are: 0=disable, 1=optional, 2=mandatory.
                               Default is 0.
    srtp_secure_signaling   -- specify the signaling security level required
                               by SRTP. Valid values are: 0=no secure 
                               transport is required, 1=hop-by-hop secure
                               transport such as TLS is required, 2=end-to-
                               end secure transport is required (i.e. "sips").
    rtp_transport_cfg       -- the rtp-transport-configuration that is usede, when
                               a rtp-connection is being established.
    """
    priority = 0
    id = ""
    force_contact = ""
    reg_uri = ""
    reg_timeout = 0
    require_100rel = False
    publish_enabled = False
    pidf_tuple_id = ""
    proxy = []
    auth_cred = []
    auth_initial_send = False
    auth_initial_algorithm = ""
    transport_id = -1
    allow_contact_rewrite = True
    ka_interval = 15
    ka_data = "\r\n"
    use_srtp = 0
    srtp_secure_signaling = 1
    rtp_transport_cfg = None
    mwi_enabled = False

    def __init__(self, domain="", username="", password="", 
                 display="", registrar="", proxy=""):
        """
        Construct account config. If domain argument is specified, 
        a typical configuration will be built.

        Keyword arguments:
        domain    -- domain name of the server.
        username  -- user name.
        password  -- plain-text password.
        display   -- optional display name for the user name.
        registrar -- the registrar URI. If domain name is specified
                     and this argument is empty, the registrar URI
                     will be constructed from the domain name.
        proxy     -- the proxy URI. If domain name is specified
                     and this argument is empty, the proxy URI
                     will be constructed from the domain name.

        """
        default = _pjsua.acc_config_default()
        self._cvt_from_pjsua(default)
        if domain!="":
            self.build_config(domain, username, password,
                              display, registrar, proxy)
        self.rtp_transport_cfg = TransportConfig()

    def build_config(self, domain, username, password, display="",
                     registrar="", proxy="", rtp_transport_cfg = None):
        """
        Construct account config. If domain argument is specified, 
        a typical configuration will be built.

        Keyword arguments:
        domain    -- domain name of the server.
        username  -- user name.
        password  -- plain-text password.
        display   -- optional display name for the user name.
        registrar -- the registrar URI. If domain name is specified
                     and this argument is empty, the registrar URI
                     will be constructed from the domain name.
        proxy     -- the proxy URI. If domain name is specified
                     and this argument is empty, the proxy URI
                     will be constructed from the domain name.

        """
        if display != "":
            display = display + " "
        userpart = username
        if userpart != "":
            userpart = userpart + "@"
        self.id = display + "<sip:" + userpart + domain + ">"
        self.reg_uri = registrar
        if self.reg_uri == "":
            self.reg_uri = "sip:" + domain
        if proxy == "":
            proxy = "sip:" + domain + ";lr"
        if proxy.find(";lr") == -1:
            proxy = proxy + ";lr"
        self.proxy.append(proxy)
        if username != "":
            self.auth_cred.append(AuthCred("*", username, password))
        
        if (rtp_transport_cfg is not None):
            self.rtp_transport_cfg = rtp_transport_cfg
        else:
            self.rtp_transport_cfg = TransportConfig()
    
    def _cvt_from_pjsua(self, cfg):
        self.priority = cfg.priority
        self.id = cfg.id
        self.force_contact = cfg.force_contact
        self.reg_uri = cfg.reg_uri
        self.reg_timeout = cfg.reg_timeout
        self.require_100rel = cfg.require_100rel
        self.publish_enabled = cfg.publish_enabled
        self.pidf_tuple_id = cfg.pidf_tuple_id
        self.proxy = cfg.proxy
        for cred in cfg.cred_info:
            self.auth_cred.append(AuthCred(cred.realm, cred.username, 
                                           cred.data, cred.scheme,
                                           cred.data_type))
        self.auth_initial_send = cfg.auth_initial_send
        self.auth_initial_algorithm = cfg.auth_initial_algorithm
        self.transport_id = cfg.transport_id
        self.allow_contact_rewrite = cfg.allow_contact_rewrite
        self.ka_interval = cfg.ka_interval
        self.ka_data = cfg.ka_data
        self.use_srtp = cfg.use_srtp
        self.srtp_secure_signaling = cfg.srtp_secure_signaling
        self.mwi_enabled = cfg.mwi_enabled
        if (self.rtp_transport_cfg is not None):
            self.rtp_transport_cfg._cvt_from_pjsua(cfg.rtp_transport_cfg)

    def _cvt_to_pjsua(self):
        cfg = _pjsua.acc_config_default()
        cfg.priority = self.priority
        cfg.id = self.id
        cfg.force_contact = self.force_contact
        cfg.reg_uri = self.reg_uri
        cfg.reg_timeout = self.reg_timeout
        cfg.require_100rel = self.require_100rel
        cfg.publish_enabled = self.publish_enabled
        cfg.pidf_tuple_id = self.pidf_tuple_id
        cfg.proxy = self.proxy
        for cred in self.auth_cred:
            c = _pjsua.Pjsip_Cred_Info()
            c.realm = cred.realm
            c.scheme = cred.scheme
            c.username = cred.username
            c.data_type = cred.passwd_type
            c.data = cred.passwd
            cfg.cred_info.append(c)
            
        cfg.auth_initial_send = self.auth_initial_send
        cfg.auth_initial_algorithm = self.auth_initial_algorithm
        cfg.transport_id = self.transport_id
        cfg.allow_contact_rewrite = self.allow_contact_rewrite
        cfg.ka_interval = self.ka_interval
        cfg.ka_data = self.ka_data
        cfg.use_srtp = self.use_srtp
        cfg.srtp_secure_signaling = self.srtp_secure_signaling
        cfg.mwi_enabled = self.mwi_enabled

        if (self.rtp_transport_cfg is not None):
            cfg.rtp_transport_cfg = self.rtp_transport_cfg._cvt_to_pjsua()
        
        return cfg
 
 
# Account information
class AccountInfo:
    """This describes Account info. Application retrives account info
    with Account.info().

    Member documentation:

    is_default      -- True if this is the default account.
    uri             -- the account URI.
    reg_active      -- True if registration is active for this account.
    reg_expires     -- contains the current registration expiration value,
                       in seconds.
    reg_status      -- the registration status. If the value is less than
                       700, it specifies SIP status code. Value greater than
                       this specifies the error code.
    reg_reason      -- contains the registration status text (e.g. the
                       error message).
    online_status   -- the account's presence online status, True if it's 
                       publishing itself as online.
    online_text     -- the account's presence status text.

    """
    is_default = False
    uri = ""
    reg_active = False
    reg_expires = -1
    reg_status = 0
    reg_reason = ""
    online_status = False
    online_text = ""

    def __init__(self, ai):
        self.is_default = ai.is_default
        self.uri = ai.acc_uri
        self.reg_active = ai.has_registration
        self.reg_expires = ai.expires
        self.reg_status = ai.status
        self.reg_reason = ai.status_text
        self.online_status = ai.online_status
        self.online_text = ai.online_status_text

# Account callback
class AccountCallback:
    """Class to receive notifications on account's events.

    Derive a class from this class and register it to the Account object
    using Account.set_callback() to start receiving events from the Account
    object.

    Member documentation:

    account     -- the Account object.

    """
    account = None

    def __init__(self, account=None):
        self._set_account(account)

    def __del__(self):
        pass

    def _set_account(self, account):
        if account:
            self.account = weakref.proxy(account)
        else:
            self.account = None

    def on_reg_state(self):
        """Notification that the registration status has changed.
        """
        pass

    def on_incoming_call(self, call):
        """Notification about incoming call.

        Application should implement one of on_incoming_call() or
        on_incoming_call2(), otherwise, the default behavior is to
        reject the call with default status code. Note that if both are
        implemented, only on_incoming_call2() will be called.

        Keyword arguments:
        call    -- the new incoming call
        """
        call.hangup()

    def on_incoming_call2(self, call, rdata):
        """Notification about incoming call, with received SIP message info.

        Application should implement one of on_incoming_call() or
        on_incoming_call2(), otherwise, the default behavior is to
        reject the call with default status code. Note that if both are
        implemented, only on_incoming_call2() will be called.

        Keyword arguments:
        call    -- the new incoming call
        rdata   -- the received message
        """
        call.hangup()

    def on_incoming_subscribe(self, buddy, from_uri, contact_uri, pres_obj):
        """Notification when incoming SUBSCRIBE request is received. 
        
        Application may use this callback to authorize the incoming 
        subscribe request (e.g. ask user permission if the request 
        should be granted)

        Keyword arguments:
        buddy       -- The buddy object, if buddy is found. Otherwise
                       the value is None.
        from_uri    -- The URI string of the sender.
        pres_obj    -- Opaque presence subscription object, which is
                       needed by Account.pres_notify()

        Return:
            Tuple (code, reason), where:
             code:      The status code. If code is >= 300, the
                        request is rejected. If code is 200, the
                        request is accepted and NOTIFY will be sent
                        automatically. If code is 202, application
                        must accept or reject the request later with
                        Account.press_notify().
             reason:    Optional reason phrase, or None to use the
                        default reasoh phrase for the status code.
        """
        return (200, None)

    def on_pager(self, from_uri, contact, mime_type, body):
        """
        Notification that incoming instant message is received on
        this account.

        Keyword arguments:
        from_uri   -- sender's URI
        contact    -- sender's Contact URI
        mime_type  -- MIME type of the instant message body
        body       -- the instant message body

        """
        pass

    def on_pager_status(self, to_uri, body, im_id, code, reason):
        """
        Notification about the delivery status of previously sent
        instant message.

        Keyword arguments:
        to_uri  -- the destination URI of the message
        body    -- the message body
        im_id   -- message ID
        code    -- SIP status code
        reason  -- SIP reason phrase

        """
        pass

    def on_typing(self, from_uri, contact, is_typing):
        """
        Notification that remote is typing or stop typing.

        Keyword arguments:
        buddy     -- Buddy object for the sender, if found. Otherwise
                     this will be None
        from_uri  -- sender's URI of the indication
        contact   -- sender's contact URI
        is_typing -- boolean to indicate whether remote is currently
                     typing an instant message.

        """
        pass

    def on_mwi_info(self, body):
        """
        Notification about change in Message Summary / Message Waiting
        Indication (RFC 3842) status. MWI subscription must be enabled
        in the account config to receive this notification.

        Keyword arguments:
        body      -- String containing message body as received in the
                     NOTIFY request.

        """
        pass



class Account:
    """This describes SIP account class.

    PJSUA accounts provide identity (or identities) of the user who is 
    currently using the application. In SIP terms, the identity is used 
    as the From header in outgoing requests.

    Account may or may not have client registration associated with it. 
    An account is also associated with route set and some authentication 
    credentials, which are used when sending SIP request messages using 
    the account. An account also has presence's online status, which 
    will be reported to remote peer when they subscribe to the account's 
    presence, or which is published to a presence server if presence 
    publication is enabled for the account.

    Account is created with Lib.create_account(). At least one account 
    MUST be created. If no user association is required, application can 
    create a userless account by calling Lib.create_account_for_transport().
    A userless account identifies local endpoint instead of a particular 
    user, and it correspond with a particular transport instance.

    Also one account must be set as the default account, which is used as 
    the account to use when PJSUA fails to match a request with any other
    accounts.

    """
    _id = -1        
    _lib = None
    _cb = AccountCallback(None)
    _obj_name = ""

    def __init__(self, lib, id, cb=None):
        """Construct this class. This is normally called by Lib class and
        not by application.

        Keyword arguments:
        lib -- the Lib instance.
        id  -- the pjsua account ID.
        cb  -- AccountCallback instance to receive events from this Account.
               If callback is not specified here, it must be set later
               using set_callback().
        """
        self._id = id
        self._lib = weakref.ref(lib)
        self._obj_name = "{Account " + self.info().uri + "}"
        self.set_callback(cb)
        _pjsua.acc_set_user_data(self._id, self)
        _Trace((self, 'created'))

    def __del__(self):
        if self._id != -1:
            _pjsua.acc_set_user_data(self._id, 0)
        _Trace((self, 'destroyed'))

    def __str__(self):
        return self._obj_name

    def info(self):
        """Retrieve AccountInfo for this account.
        """
        lck = self._lib().auto_lock()
        ai = _pjsua.acc_get_info(self._id)
        if ai==None:
            self._lib()._err_check("info()", self, -1, "Invalid account")
        return AccountInfo(ai)

    def is_valid(self):
        """
        Check if this account is still valid.

        """
        lck = self._lib().auto_lock()
        return _pjsua.acc_is_valid(self._id)

    def set_callback(self, cb):
        """Register callback to receive notifications from this object.

        Keyword argument:
        cb  -- AccountCallback instance.

        """
        if cb:
            self._cb = cb
        else:
            self._cb = AccountCallback(self)
        self._cb._set_account(self)

    def set_default(self):
        """ Set this account as default account to send outgoing requests
        and as the account to receive incoming requests when more exact
        matching criteria fails.
        """
        lck = self._lib().auto_lock()
        err = _pjsua.acc_set_default(self._id)
        self._lib()._err_check("set_default()", self, err)

    def is_default(self):
        """ Check if this account is the default account.

        """
        lck = self._lib().auto_lock()
        def_id = _pjsua.acc_get_default()
        return self.is_valid() and def_id==self._id

    def delete(self):
        """ Delete this account.
        
        """
        lck = self._lib().auto_lock()
        err = _pjsua.acc_set_user_data(self._id, 0)
        self._lib()._err_check("delete()", self, err)
        err = _pjsua.acc_del(self._id)
        self._lib()._err_check("delete()", self, err)
        self._id = -1

    def set_basic_status(self, is_online):
        """ Set basic presence status of this account.

        Keyword argument:
        is_online   -- boolean to indicate basic presence availability.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.acc_set_online_status(self._id, is_online)
        self._lib()._err_check("set_basic_status()", self, err)

    def set_presence_status(self, is_online, 
                            activity=PresenceActivity.UNKNOWN, 
                            pres_text="", rpid_id=""):
        """ Set presence status of this account. 
        
        Keyword arguments:
        is_online   -- boolean to indicate basic presence availability
        activity    -- value from PresenceActivity
        pres_text   -- optional string to convey additional information about
                       the activity (such as "On the phone")
        rpid_id     -- optional string to be placed as RPID ID. 

        """
        lck = self._lib().auto_lock()
        err = _pjsua.acc_set_online_status2(self._id, is_online, activity,
                                            pres_text, rpid_id)
        self._lib()._err_check("set_presence_status()", self, err)

    def set_registration(self, renew):
        """Manually renew registration or unregister from the server.

        Keyword argument:
        renew   -- boolean to indicate whether registration is renewed.
                   Setting this value for False will trigger unregistration.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.acc_set_registration(self._id, renew)
        self._lib()._err_check("set_registration()", self, err)

    def set_transport(self, transport):
        """Set this account to only use the specified transport to send
        outgoing requests.

        Keyword argument:
        transport   -- Transport object.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.acc_set_transport(self._id, transport._id)
        self._lib()._err_check("set_transport()", self, err)

    def make_call(self, dst_uri, cb=None, hdr_list=None):
        """Make outgoing call to the specified URI.

        Keyword arguments:
        dst_uri  -- Destination SIP URI.
        cb       -- CallCallback instance to be installed to the newly
                    created Call object. If this CallCallback is not
                    specified (i.e. None is given), it must be installed
                    later using call.set_callback().
        hdr_list -- Optional list of headers to be sent with outgoing
                    INVITE

        Return:
            Call instance.
        """
        lck = self._lib().auto_lock()
        call = Call(self._lib(), -1, cb)
        err, cid = _pjsua.call_make_call(self._id, dst_uri, 0, 
                                         call, Lib._create_msg_data(hdr_list))
        self._lib()._err_check("make_call()", self, err)
        call.attach_to_id(cid)
        return call

    def add_buddy(self, uri, cb=None):
        """Add new buddy.

        Keyword argument:
        uri     -- SIP URI of the buddy
        cb      -- BuddyCallback instance to be installed to the newly
                   created Buddy object. If this callback is not specified
                   (i.e. None is given), it must be installed later using
                   buddy.set_callback().

        Return:
            Buddy object
        """
        lck = self._lib().auto_lock()
        buddy_cfg = _pjsua.buddy_config_default()
        buddy_cfg.uri = uri
        buddy_cfg.subscribe = False
        err, buddy_id = _pjsua.buddy_add(buddy_cfg)
        self._lib()._err_check("add_buddy()", self, err)
        buddy = Buddy(self._lib(), buddy_id, self, cb)
        return buddy

    def pres_notify(self, pres_obj, state, reason="", hdr_list=None):
        """Send NOTIFY to inform account presence status or to terminate
        server side presence subscription.
        
        Keyword arguments:
        pres_obj    -- The subscription object from on_incoming_subscribe()
                       callback
        state       -- Subscription state, from SubscriptionState
        reason      -- Optional reason phrase.
        hdr_list    -- Optional header list.
        """
        lck = self._lib().auto_lock()
        _pjsua.acc_pres_notify(self._id, pres_obj, state, reason, 
                               Lib._create_msg_data(hdr_list))
    
    def send_pager(self, uri, text, im_id=0, content_type="text/plain", \
                   hdr_list=None):
        """Send instant message to arbitrary URI.

        Keyword arguments:
        text         -- Instant message to be sent
        uri          -- URI to send the Instant Message to.
        im_id        -- Optional instant message ID to identify this
                        instant message when delivery status callback
                        is called.
        content_type -- MIME type identifying the instant message
        hdr_list     -- Optional list of headers to be sent with the
                        request.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.im_send(self._id, uri, \
                             content_type, text, \
                             Lib._create_msg_data(hdr_list), \
                             im_id)
        self._lib()._err_check("send_pager()", self, err)

class CallCallback:
    """Class to receive event notification from Call objects. 

    Use Call.set_callback() method to install instance of this callback 
    class to receive event notifications from the call object.

    Member documentation:

    call    -- the Call object.

    """
    call = None

    def __init__(self, call=None):
        self._set_call(call)

    def __del__(self):
        pass

    def _set_call(self, call):
        if call:
            self.call = weakref.proxy(call)
        else:
            self.call = None

    def on_state(self):
        """Notification that the call's state has changed.

        """
        pass

    def on_media_state(self):
        """Notification that the call's media state has changed.

        """
        pass

    def on_dtmf_digit(self, digits):
        """Notification on incoming DTMF digits.

        Keyword argument:
        digits  -- string containing the received digits.

        """
        pass

    def on_transfer_request(self, dst, code):
        """Notification that call is being transferred by remote party. 

        Application can decide to accept/reject transfer request by returning
        code greater than or equal to 500. The default behavior is to accept 
        the transfer by returning 202.

        Keyword arguments:
        dst     -- string containing the destination URI
        code    -- the suggested status code to return to accept the request.

        Return:
        the callback should return 202 to accept the request, or 300-699 to
        reject the request.

        """
        return code

    def on_transfer_status(self, code, reason, final, cont):
        """
        Notification about the status of previous call transfer request. 

        Keyword arguments:
        code    -- SIP status code to indicate completion status.
        text    -- SIP status reason phrase.
        final   -- if True then this is a final status and no further
                   notifications will be sent for this call transfer
                   status.
        cont    -- suggested return value.

        Return:
        If the callback returns false then no further notification will
        be sent for the transfer request for this call.

        """
        return cont

    def on_replace_request(self, code, reason):
        """Notification when incoming INVITE with Replaces header is received. 

        Application may reject the request by returning value greather than
        or equal to 500. The default behavior is to accept the request.

        Keyword arguments:
        code    -- default status code to return
        reason  -- default reason phrase to return

        Return:
        The callback should return (code, reason) tuple.

        """
        return code, reason

    def on_replaced(self, new_call):
        """
        Notification that this call will be replaced with new_call. 
        After this callback is called, this call will be disconnected.

        Keyword arguments:
        new_call    -- the new call that will replace this call.
        """
        pass

    def on_pager(self, mime_type, body):
        """
        Notification that incoming instant message is received on
        this call.

        Keyword arguments:
        mime_type  -- MIME type of the instant message body.
        body       -- the instant message body.

        """
        pass

    def on_pager_status(self, body, im_id, code, reason):
        """
        Notification about the delivery status of previously sent
        instant message.

        Keyword arguments:
        body    -- message body
        im_id   -- message ID
        code    -- SIP status code
        reason  -- SIP reason phrase

        """
        pass

    def on_typing(self, is_typing):
        """
        Notification that remote is typing or stop typing.

        Keyword arguments:
        is_typing -- boolean to indicate whether remote is currently
                     typing an instant message.

        """
        pass


class CallInfo:
    """This structure contains various information about Call.

    Application may retrieve this information with Call.info().

    Member documentation:

    role            -- CallRole
    account         -- Account object.
    uri             -- SIP URI of local account.
    contact         -- local Contact URI.
    remote_uri      -- remote SIP URI.
    remote_contact  -- remote Contact URI
    sip_call_id     -- call's Call-ID identification
    state           -- CallState
    state_text      -- state text.
    last_code       -- last SIP status code
    last_reason     -- text phrase for last_code
    media_state     -- MediaState
    media_dir       -- MediaDir
    conf_slot       -- conference slot number for this call.
    call_time       -- call's connected duration in seconds.
    total_time      -- total call duration in seconds.
    """
    role = CallRole.CALLER
    account = None
    uri = ""
    contact = ""
    remote_uri = ""
    remote_contact = ""
    sip_call_id = ""
    state = CallState.NULL
    state_text = ""
    last_code = 0
    last_reason = ""
    media_state = MediaState.NULL
    media_dir = MediaDir.NULL
    conf_slot = -1
    call_time = 0
    total_time = 0

    def __init__(self, lib=None, ci=None):
        if lib and ci:
            self._cvt_from_pjsua(lib, ci)

    def _cvt_from_pjsua(self, lib, ci):
        self.role = ci.role
        self.account = lib._lookup_account(ci.acc_id)
        self.uri = ci.local_info
        self.contact = ci.local_contact
        self.remote_uri = ci.remote_info
        self.remote_contact = ci.remote_contact
        self.sip_call_id = ci.call_id
        self.state = ci.state
        self.state_text = ci.state_text
        self.last_code = ci.last_status
        self.last_reason = ci.last_status_text
        self.media_state = ci.media_status
        self.media_dir = ci.media_dir
        self.conf_slot = ci.conf_slot
        self.call_time = ci.connect_duration / 1000
        self.total_time = ci.total_duration / 1000


class Call:
    """This class represents SIP call.

    Application initiates outgoing call with Account.make_call(), and
    incoming calls are reported in AccountCallback.on_incoming_call().
    """
    _id = -1
    _cb = None
    _lib = None
    _obj_name = ""

    def __init__(self, lib, call_id, cb=None):
        self._lib = weakref.ref(lib)
        self.set_callback(cb)
        self.attach_to_id(call_id)
        _Trace((self, 'created'))

    def __del__(self):
        if self._id != -1:
            _pjsua.call_set_user_data(self._id, 0)
        _Trace((self, 'destroyed'))

    def __str__(self):
        return self._obj_name

    def attach_to_id(self, call_id):
        lck = self._lib().auto_lock()
        if self._id != -1:
            _pjsua.call_set_user_data(self._id, 0)
        self._id = call_id
        if self._id != -1:
            _pjsua.call_set_user_data(self._id, self)
            self._obj_name = "{Call " + self.info().remote_uri + "}"
        else:
            self._obj_name = "{Call object}"

    def set_callback(self, cb):
        """
        Set callback object to retrieve event notifications from this call.

        Keyword arguments:
        cb  -- CallCallback instance.
        """
        if cb:
            self._cb = cb
        else:
            self._cb = CallCallback(self)
        self._cb._set_call(self)

    def info(self):
        """
        Get the CallInfo.
        """
        lck = self._lib().auto_lock()
        ci = _pjsua.call_get_info(self._id)
        if not ci:
            self._lib()._err_check("info", self, -1, "Invalid call")
        call_info = CallInfo(self._lib(), ci)
        return call_info

    def is_valid(self):
        """
        Check if this call is still valid.
        """
        lck = self._lib().auto_lock()
        return _pjsua.call_is_active(self._id)

    def dump_status(self, with_media=True, indent="", max_len=1024):
        """
        Dump the call status.
        """
        lck = self._lib().auto_lock()
        return _pjsua.call_dump(self._id, with_media, max_len, indent)

    def answer(self, code=200, reason="", hdr_list=None):
        """
        Send provisional or final response to incoming call.

        Keyword arguments:
        code     -- SIP status code.
        reason   -- Reason phrase. Put empty to send default reason
                    phrase for the status code.
        hdr_list -- Optional list of headers to be sent with the
                    INVITE response.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_answer(self._id, code, reason, 
                                   Lib._create_msg_data(hdr_list))
        self._lib()._err_check("answer()", self, err)

    def hangup(self, code=603, reason="", hdr_list=None):
        """
        Terminate the call.

        Keyword arguments:
        code     -- SIP status code.
        reason   -- Reason phrase. Put empty to send default reason
                    phrase for the status code.
        hdr_list -- Optional list of headers to be sent with the
                    message.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_hangup(self._id, code, reason, 
                                   Lib._create_msg_data(hdr_list))
        self._lib()._err_check("hangup()", self, err)

    def hold(self, hdr_list=None):
        """
        Put the call on hold.

        Keyword arguments:
        hdr_list -- Optional list of headers to be sent with the
                    message.
        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_set_hold(self._id, Lib._create_msg_data(hdr_list))
        self._lib()._err_check("hold()", self, err)

    def unhold(self, hdr_list=None):
        """
        Release the call from hold.

        Keyword arguments:
        hdr_list -- Optional list of headers to be sent with the
                    message.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_reinvite(self._id, True, 
                                     Lib._create_msg_data(hdr_list))
        self._lib()._err_check("unhold()", self, err)

    def reinvite(self, hdr_list=None):
        """
        Send re-INVITE and optionally offer new codecs to use.

        Keyword arguments:
        hdr_list   -- Optional list of headers to be sent with the
                      message.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_reinvite(self._id, True, 
                                     Lib._create_msg_data(hdr_list))
        self._lib()._err_check("reinvite()", self, err)

    def update(self, hdr_list=None, options=0):
        """
        Send UPDATE and optionally offer new codecs to use.

        Keyword arguments:
        hdr_list   -- Optional list of headers to be sent with the
                      message.
        options    -- Must be zero for now.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_update(self._id, options, 
                                   Lib._create_msg_data(hdr_list))
        self._lib()._err_check("update()", self, err)

    def transfer(self, dest_uri, hdr_list=None):
        """
        Transfer the call to new destination.

        Keyword arguments:
        dest_uri -- Specify the SIP URI to transfer the call to.
        hdr_list -- Optional list of headers to be sent with the
                    message.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_xfer(self._id, dest_uri, 
                                 Lib._create_msg_data(hdr_list))
        self._lib()._err_check("transfer()", self, err)

    def transfer_to_call(self, call, hdr_list=None, options=0):
        """
        Attended call transfer.

        Keyword arguments:
        call     -- The Call object to transfer call to.
        hdr_list -- Optional list of headers to be sent with the
                    message.
        options  -- Must be zero for now.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_xfer_replaces(self._id, call._id, options,
                                          Lib._create_msg_data(hdr_list))
        self._lib()._err_check("transfer_to_call()", self, err)

    def dial_dtmf(self, digits):
        """
        Send DTMF digits with RTP event package.

        Keyword arguments:
        digits  -- DTMF digit string.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_dial_dtmf(self._id, digits)
        self._lib()._err_check("dial_dtmf()", self, err)

    def send_request(self, method, hdr_list=None, content_type=None,
                     body=None):
        """
        Send arbitrary request to remote call. 
        
        This is useful for example to send INFO request. Note that this 
        function should not be used to send request that will change the 
        call state such as CANCEL or BYE.

        Keyword arguments:
        method       -- SIP method name.
        hdr_list     -- Optional header list to be sent with the request.
        content_type -- Content type to describe the body, if the body
                        is present
        body         -- Optional SIP message body.

        """
        lck = self._lib().auto_lock()
        if hdr_list or body:
            msg_data = _pjsua.Msg_Data()
            if hdr_list:
                msg_data.hdr_list = hdr_list
            if content_type:
                msg_data.content_type = content_type
            if body:
                msg_data.msg_body = body
        else:
            msg_data = None
                
        err = _pjsua.call_send_request(self._id, method, msg_data)
        self._lib()._err_check("send_request()", self, err)

    def send_pager(self, text, im_id=0, content_type="text/plain", 
                   hdr_list=None):
        """Send instant message inside a call.

        Keyword arguments:
        text         -- Instant message to be sent
        im_id        -- Optional instant message ID to identify this
                        instant message when delivery status callback
                        is called.
        content_type -- MIME type identifying the instant message
        hdr_list     -- Optional list of headers to be sent with the
                        request.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.call_send_im(self._id, \
                             content_type, text, \
                             Lib._create_msg_data(hdr_list), \
                             im_id)
        self._lib()._err_check("send_pager()", self, err)

  
class BuddyInfo:
    """This class contains information about Buddy. Application may 
    retrieve this information by calling Buddy.info().

    Member documentation:

    uri             -- the Buddy URI.
    contact         -- the Buddy Contact URI, if available.
    online_status   -- the presence online status.
    online_text     -- the presence online status text.
    activity        -- the PresenceActivity
    subscribed      -- specify whether buddy's presence status is currently
                       being subscribed.
    sub_state       -- SubscriptionState
    sub_term_reason -- The termination reason string of the last presence
                       subscription to this buddy, if any.
    """
    uri = ""
    contact = ""
    online_status = 0
    online_text = ""
    activity = PresenceActivity.UNKNOWN
    subscribed = False
    sub_state = SubscriptionState.NULL
    sub_term_reason = ""

    def __init__(self, pjsua_bi=None):
        if pjsua_bi:
            self._cvt_from_pjsua(pjsua_bi)

    def _cvt_from_pjsua(self, inf):
        self.uri = inf.uri
        self.contact = inf.contact
        self.online_status = inf.status
        self.online_text = inf.status_text
        self.activity = inf.activity
        self.subscribed = inf.monitor_pres
        self.sub_state = inf.sub_state
        self.sub_term_reason = inf.sub_term_reason


class BuddyCallback:
    """This class can be used to receive notifications about Buddy's
    presence status change. Application needs to derive a class from
    this class, and register the instance with Buddy.set_callback().

    Member documentation:

    buddy   -- the Buddy object.
    """
    buddy = None

    def __init__(self, buddy=None):
        self._set_buddy(buddy)

    def _set_buddy(self, buddy):
        if buddy:
            self.buddy = weakref.proxy(buddy)
        else:
            self.buddy = None

    def on_state(self):
        """
        Notification that buddy's presence state has changed. Application
        may then retrieve the new status with Buddy.info() function.
        """
        pass
   
    def on_pager(self, mime_type, body):
        """Notification that incoming instant message is received from
        this buddy.

        Keyword arguments:
        mime_type  -- MIME type of the instant message body
        body       -- the instant message body

        """
        pass

    def on_pager_status(self, body, im_id, code, reason):
        """Notification about the delivery status of previously sent
        instant message.

        Keyword arguments:
        body    -- the message body
        im_id   -- message ID
        code    -- SIP status code
        reason  -- SIP reason phrase

        """
        pass

    def on_typing(self, is_typing):
        """Notification that remote is typing or stop typing.

        Keyword arguments:
        is_typing -- boolean to indicate whether remote is currently
                     typing an instant message.

        """
        pass


class Buddy:
    """A Buddy represents person or remote agent.

    This class provides functions to subscribe to buddy's presence and
    to send or receive instant messages from the buddy.
    """
    _id = -1
    _lib = None
    _cb = None
    _obj_name = ""
    _acc = None

    def __init__(self, lib, id, account, cb):
        self._id = id
        self._lib = weakref.ref(lib)
        self._acc = weakref.ref(account)
        self._obj_name = "{Buddy " + self.info().uri + "}"
        self.set_callback(cb)
        _pjsua.buddy_set_user_data(self._id, self)
        _Trace((self, 'created'))

    def __del__(self):
        if self._id != -1:
            _pjsua.buddy_set_user_data(self._id, 0)
        _Trace((self, 'destroyed'))

    def __str__(self):
        return self._obj_name

    def info(self):
        """
        Get buddy info as BuddyInfo.
        """
        lck = self._lib().auto_lock()
        return BuddyInfo(_pjsua.buddy_get_info(self._id))

    def set_callback(self, cb):
        """Install callback to receive notifications from this object.

        Keyword argument:
        cb  -- BuddyCallback instance.
        """
        if cb:
            self._cb = cb
        else:
            self._cb = BuddyCallback(self)
        self._cb._set_buddy(self)

    def subscribe(self):
        """
        Subscribe to buddy's presence status notification.
        """
        lck = self._lib().auto_lock()
        err = _pjsua.buddy_subscribe_pres(self._id, True)
        self._lib()._err_check("subscribe()", self, err)

    def unsubscribe(self):
        """
        Unsubscribe from buddy's presence status notification.
        """
        lck = self._lib().auto_lock()
        err = _pjsua.buddy_subscribe_pres(self._id, False)
        self._lib()._err_check("unsubscribe()", self, err)

    def delete(self):
        """
        Remove this buddy from the buddy list.
        """
        lck = self._lib().auto_lock()
        if self._id != -1:
            _pjsua.buddy_set_user_data(self._id, 0)
        err = _pjsua.buddy_del(self._id)
        self._lib()._err_check("delete()", self, err)

    def send_pager(self, text, im_id=0, content_type="text/plain", \
                   hdr_list=None):
        """Send instant message to remote buddy.

        Keyword arguments:
        text         -- Instant message to be sent
        im_id        -- Optional instant message ID to identify this
                        instant message when delivery status callback
                        is called.
        content_type -- MIME type identifying the instant message
        hdr_list     -- Optional list of headers to be sent with the
                        request.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.im_send(self._acc()._id, self.info().uri, \
                             content_type, text, \
                             Lib._create_msg_data(hdr_list), \
                             im_id)
        self._lib()._err_check("send_pager()", self, err)

    def send_typing_ind(self, is_typing=True, hdr_list=None):
        """Send typing indication to remote buddy.

        Keyword argument:
        is_typing -- boolean to indicate wheter user is typing.
        hdr_list  -- Optional list of headers to be sent with the
                     request.

        """
        lck = self._lib().auto_lock()
        err = _pjsua.im_typing(self._acc()._id, self.info().uri, \
                               is_typing, Lib._create_msg_data(hdr_list))
        self._lib()._err_check("send_typing_ind()", self, err)



# Sound device info
class SoundDeviceInfo:
    """This described the sound device info.

    Member documentation:
    name                -- device name.
    input_channels      -- number of capture channels supported.
    output_channels     -- number of playback channels supported.
    default_clock_rate  -- default sampling rate.
    """
    name = ""
    input_channels = 0
    output_channels = 0
    default_clock_rate = 0

    def __init__(self, sdi):
        self.name = sdi.name
        self.input_channels = sdi.input_count
        self.output_channels = sdi.output_count
        self.default_clock_rate = sdi.default_samples_per_sec


# Codec info
class CodecInfo:
    """This describes codec info.

    Member documentation:
    name            -- codec name
    priority        -- codec priority (0-255)
    clock_rate      -- clock rate
    channel_count   -- number of channels
    avg_bps         -- average bandwidth in bits per second
    frm_ptime       -- base frame length in milliseconds
    ptime           -- RTP frame length in milliseconds.
    pt              -- payload type.
    vad_enabled     -- specify if Voice Activity Detection is currently
                       enabled.
    plc_enabled     -- specify if Packet Lost Concealment is currently
                       enabled.
    """
    name = ""
    priority = 0
    clock_rate = 0
    channel_count = 0
    avg_bps = 0
    frm_ptime = 0
    ptime = 0
    pt = 0
    vad_enabled = False
    plc_enabled = False

    def __init__(self, codec_info, codec_param):
        self.name = codec_info.codec_id
        self.priority = codec_info.priority
        self.clock_rate = codec_param.info.clock_rate
        self.channel_count = codec_param.info.channel_cnt
        self.avg_bps = codec_param.info.avg_bps
        self.frm_ptime = codec_param.info.frm_ptime
        self.ptime = codec_param.info.frm_ptime * \
                        codec_param.setting.frm_per_pkt
        self.ptime = codec_param.info.pt
        self.vad_enabled = codec_param.setting.vad
        self.plc_enabled = codec_param.setting.plc

    def _cvt_to_pjsua(self):
        ci = _pjsua.Codec_Info()
        ci.codec_id = self.name
        ci.priority = self.priority
        return ci


# Codec parameter
class CodecParameter:
    """This specifies various parameters that can be configured for codec.

    Member documentation:

    ptime       -- specify the outgoing RTP packet length in milliseconds.
    vad_enabled -- specify if VAD should be enabled.
    plc_enabled -- specify if PLC should be enabled.
    """
    ptime = 0
    vad_enabled = False
    plc_enabled = False
    _codec_param = None
    
    def __init__(self, codec_param):
        self.ptime = codec_param.info.frm_ptime * \
                        codec_param.setting.frm_per_pkt
        self.vad_enabled = codec_param.setting.vad
        self.plc_enabled = codec_param.setting.plc
        self._codec_param = codec_param

    def _cvt_to_pjsua(self):
        self._codec_param.setting.frm_per_pkt = self.ptime / \
                                                self._codec_param.info.frm_ptime
        self._codec_param.setting.vad = self.vad_enabled
        self._codec_param.setting.plc = self.plc_enabled
        return self._codec_param


# Library mutex
class _LibMutex:
    def __init__(self, lck):
        self._lck = lck
        self._lck.acquire()
        #_Trace(('lock acquired',))

    def __del__(self):
        try:
            self._lck.release()
            #_Trace(('lock released',))
        except:
            #_Trace(('lock release error',))
            pass


# PJSUA Library
_lib = None
enable_trace = False

class Lib:
    """Library instance.
    
    """
    _quit = False
    _has_thread = False
    _lock = None

    def __init__(self):
        global _lib
        if _lib:
            raise Error("__init()__", None, -1, 
                        "Library instance already exist")

        self._lock = threading.RLock()
        err = _pjsua.create()
        self._err_check("_pjsua.create()", None, err)
        _lib = self

    def __del__(self):
        _pjsua.destroy()
        del self._lock
        _Trace(('Lib destroyed',))

    def __str__(self):
        return "Lib"

    @staticmethod
    def instance():
        """Return singleton instance of Lib.
        """
        return _lib

    def init(self, ua_cfg=None, log_cfg=None, media_cfg=None):
        """
        Initialize pjsua with the specified configurations.

        Keyword arguments:
        ua_cfg      -- optional UAConfig instance
        log_cfg     -- optional LogConfig instance
        media_cfg   -- optional MediaConfig instance

        """
        if not ua_cfg: ua_cfg = UAConfig()
        if not log_cfg: log_cfg = LogConfig()
        if not media_cfg: media_cfg = MediaConfig()

        py_ua_cfg = ua_cfg._cvt_to_pjsua()
        py_ua_cfg.cb.on_call_state = _cb_on_call_state
        py_ua_cfg.cb.on_incoming_call = _cb_on_incoming_call
        py_ua_cfg.cb.on_call_media_state = _cb_on_call_media_state
        py_ua_cfg.cb.on_dtmf_digit = _cb_on_dtmf_digit
        py_ua_cfg.cb.on_call_transfer_request = _cb_on_call_transfer_request
        py_ua_cfg.cb.on_call_transfer_status = _cb_on_call_transfer_status
        py_ua_cfg.cb.on_call_replace_request = _cb_on_call_replace_request
        py_ua_cfg.cb.on_call_replaced = _cb_on_call_replaced
        py_ua_cfg.cb.on_reg_state = _cb_on_reg_state
        py_ua_cfg.cb.on_incoming_subscribe = _cb_on_incoming_subscribe
        py_ua_cfg.cb.on_buddy_state = _cb_on_buddy_state
        py_ua_cfg.cb.on_pager = _cb_on_pager
        py_ua_cfg.cb.on_pager_status = _cb_on_pager_status
        py_ua_cfg.cb.on_typing = _cb_on_typing
        py_ua_cfg.cb.on_mwi_info = _cb_on_mwi_info;

        err = _pjsua.init(py_ua_cfg, log_cfg._cvt_to_pjsua(), 
                          media_cfg._cvt_to_pjsua())
        self._err_check("init()", self, err)

    def destroy(self):
        """Destroy the library, and pjsua."""
        global _lib
        if self._has_thread:
            self._quit = 1
            loop = 0
            while self._quit != 2 and loop < 400:
                self.handle_events(5)
                loop = loop + 1
                time.sleep(0.050)
        _pjsua.destroy()
        _lib = None

    def start(self, with_thread=True):
        """Start the library. 

        Keyword argument:
        with_thread -- specify whether the module should create worker
                       thread.

        """
        lck = self.auto_lock()
        err = _pjsua.start()
        self._err_check("start()", self, err)
        self._has_thread = with_thread
        if self._has_thread:
            thread.start_new(_worker_thread_main, (0,))

    def handle_events(self, timeout=50):
        """Poll the events from underlying pjsua library.
        
        Application must poll the stack periodically if worker thread
        is disable when starting the library.

        Keyword argument:
        timeout -- in milliseconds.

        """
        lck = self.auto_lock()
        return _pjsua.handle_events(timeout)

    def thread_register(self, name):
        """Register external threads (threads that are not created by PJSIP,
        such as threads that are created by Python API) to PJSIP.

        The call must be made from the new thread before calling any pjlib
        functions.

        Keyword arguments:
        name -- Non descriptive name for the thread
        """
        dummy = 1
        err = _pjsua.thread_register(name, dummy)
        self._err_check("thread_register()", self, err)

    def verify_sip_url(self, sip_url):
        """Verify that the specified string is a valid URI. 
        
        Keyword argument:
        sip_url -- the URL string.
        
        Return:
            0 is the the URI is valid, otherwise the appropriate error 
            code is returned.

        """
        lck = self.auto_lock()
        return _pjsua.verify_sip_url(sip_url)

    def create_transport(self, type, cfg=None):
        """Create SIP transport instance of the specified type. 
        
        Keyword arguments:
        type    -- transport type from TransportType constant.
        cfg     -- TransportConfig instance

        Return:
            Transport object

        """
        lck = self.auto_lock()
        if not cfg: cfg=TransportConfig()
        err, tp_id = _pjsua.transport_create(type, cfg._cvt_to_pjsua())
        self._err_check("create_transport()", self, err)
        return Transport(self, tp_id)

    def create_account(self, acc_config, set_default=True, cb=None):
        """
        Create a new local pjsua account using the specified configuration.

        Keyword arguments:
        acc_config  -- AccountConfig
        set_default -- boolean to specify whether to use this as the
                       default account.
        cb          -- AccountCallback instance.

        Return:
            Account instance

        """
        lck = self.auto_lock()
        err, acc_id = _pjsua.acc_add(acc_config._cvt_to_pjsua(), set_default)
        self._err_check("create_account()", self, err)
        return Account(self, acc_id, cb)

    def create_account_for_transport(self, transport, set_default=True,
                                     cb=None):
        """Create a new local pjsua transport for the specified transport.

        Keyword arguments:
        transport   -- the Transport instance.
        set_default -- boolean to specify whether to use this as the
                       default account.
        cb          -- AccountCallback instance.

        Return:
            Account instance

        """
        lck = self.auto_lock()
        err, acc_id = _pjsua.acc_add_local(transport._id, set_default)
        self._err_check("create_account_for_transport()", self, err)
        return Account(self, acc_id, cb)

    def modify_account(self, acc_id, acc_config):
        """Modify configuration of a pjsua account.

        Keyword arguments:
        acc_id      -- ID of the account to be modified.
        acc_config  -- New account configuration.

        """
        lck = self.auto_lock()
        err = _pjsua.acc_modify(acc_id, acc_config._cvt_to_pjsua())
        self._err_check("modify_account()", self, err)

    def hangup_all(self):
        """Hangup all calls.

        """
        lck = self.auto_lock()
        _pjsua.call_hangup_all()

    # Sound device API

    def enum_snd_dev(self):
        """Enumerate sound devices in the system.

        Return:
            list of SoundDeviceInfo. The index of the element specifies
            the device ID for the device.
        """
        lck = self.auto_lock()
        sdi_list = _pjsua.enum_snd_devs()
        info = []
        for sdi in sdi_list:
            info.append(SoundDeviceInfo(sdi))
        return info

    def get_snd_dev(self):
        """Get the device IDs of current sound devices used by pjsua.

        Return:
            (capture_dev_id, playback_dev_id) tuple
        """
        lck = self.auto_lock()
        return _pjsua.get_snd_dev()

    def set_snd_dev(self, capture_dev, playback_dev):
        """Change the current sound devices.

        Keyword arguments:
        capture_dev  -- the device ID of capture device to be used
        playback_dev -- the device ID of playback device to be used.

        """
        lck = self.auto_lock()
        err = _pjsua.set_snd_dev(capture_dev, playback_dev)
        self._err_check("set_current_sound_devices()", self, err)
    
    def set_null_snd_dev(self):
        """Disable the sound devices. This is useful if the system
        does not have sound device installed.

        """
        lck = self.auto_lock()
        err = _pjsua.set_null_snd_dev()
        self._err_check("set_null_snd_dev()", self, err)

    
    # Conference bridge

    def conf_get_max_ports(self):
        """Get the conference bridge capacity.

        Return:
            conference bridge capacity.

        """
        lck = self.auto_lock()
        return _pjsua.conf_get_max_ports()

    def conf_connect(self, src_slot, dst_slot):
        """Establish unidirectional media flow from souce to sink. 
        
        One source may transmit to multiple destinations/sink. And if 
        multiple sources are transmitting to the same sink, the media 
        will be mixed together. Source and sink may refer to the same ID, 
        effectively looping the media.

        If bidirectional media flow is desired, application needs to call
        this function twice, with the second one having the arguments 
        reversed.

        Keyword arguments:
        src_slot    -- integer to identify the conference slot number of
                       the source/transmitter.
        dst_slot    -- integer to identify the conference slot number of    
                       the destination/receiver.

        """
        lck = self.auto_lock()
        err = _pjsua.conf_connect(src_slot, dst_slot)
        self._err_check("conf_connect()", self, err)
    
    def conf_disconnect(self, src_slot, dst_slot):
        """Disconnect media flow from the source to destination port.

        Keyword arguments:
        src_slot    -- integer to identify the conference slot number of
                       the source/transmitter.
        dst_slot    -- integer to identify the conference slot number of    
                       the destination/receiver.

        """
        lck = self.auto_lock()
        err = _pjsua.conf_disconnect(src_slot, dst_slot)
        self._err_check("conf_disconnect()", self, err)

    def conf_set_tx_level(self, slot, level):
        """Adjust the signal level to be transmitted from the bridge to 
        the specified port by making it louder or quieter.

        Keyword arguments:
        slot        -- integer to identify the conference slot number.
        level       -- Signal level adjustment. Value 1.0 means no level
                       adjustment, while value 0 means to mute the port.
        """
        lck = self.auto_lock()
        err = _pjsua.conf_set_tx_level(slot, level)
        self._err_check("conf_set_tx_level()", self, err)
        
    def conf_set_rx_level(self, slot, level):
        """Adjust the signal level to be received from the specified port
        (to the bridge) by making it louder or quieter.

        Keyword arguments:
        slot        -- integer to identify the conference slot number.
        level       -- Signal level adjustment. Value 1.0 means no level
                       adjustment, while value 0 means to mute the port.
        """
        lck = self.auto_lock()
        err = _pjsua.conf_set_rx_level(slot, level)
        self._err_check("conf_set_rx_level()", self, err)
        
    def conf_get_signal_level(self, slot):
        """Get last signal level transmitted to or received from the 
        specified port. The signal levels are float values from 0.0 to 1.0,
        with 0.0 indicates no signal, and 1.0 indicates the loudest signal
        level.

        Keyword arguments:
        slot        -- integer to identify the conference slot number.

        Return value:
            (tx_level, rx_level) tuple.
        """
        lck = self.auto_lock()
        err, tx_level, rx_level = _pjsua.conf_get_signal_level(slot)
        self._err_check("conf_get_signal_level()", self, err)
        return (tx_level, rx_level)
        


    # Codecs API

    def enum_codecs(self):
        """Return list of codecs supported by pjsua.

        Return:
            list of CodecInfo

        """
        lck = self.auto_lock()
        ci_list = _pjsua.enum_codecs()
        codec_info = []
        for ci in ci_list:
            cp = _pjsua.codec_get_param(ci.codec_id)
            if cp:
                codec_info.append(CodecInfo(ci, cp))
        return codec_info

    def set_codec_priority(self, name, priority):
        """Change the codec priority.

        Keyword arguments:
        name     -- Codec name
        priority -- Codec priority, which range is 0-255.

        """
        lck = self.auto_lock()
        err = _pjsua.codec_set_priority(name, priority)
        self._err_check("set_codec_priority()", self, err)

    def get_codec_parameter(self, name):
        """Get codec parameter for the specified codec.

        Keyword arguments:
        name    -- codec name.

        """
        lck = self.auto_lock()
        cp = _pjsua.codec_get_param(name)
        if not cp:
            self._err_check("get_codec_parameter()", self, -1, 
                            "Invalid codec name")
        return CodecParameter(cp)

    def set_codec_parameter(self, name, param):
        """Modify codec parameter for the specified codec.

        Keyword arguments:
        name    -- codec name
        param   -- codec parameter.

        """
        lck = self.auto_lock()
        err = _pjsua.codec_set_param(name, param._cvt_to_pjsua())
        self._err_check("set_codec_parameter()", self, err)
    
    # WAV playback and recording

    def create_player(self, filename, loop=False):
        """Create WAV file player.

        Keyword arguments
        filename    -- WAV file name
        loop        -- boolean to specify whether playback should
                       automatically restart upon EOF
        Return:
            WAV player ID

        """
        lck = self.auto_lock()
        opt = 0
        if not loop:
            opt = opt + 1
        err, player_id = _pjsua.player_create(filename, opt)
        self._err_check("create_player()", self, err)
        return player_id
        
    def player_get_slot(self, player_id):
        """Get the conference port ID for the specified player.

        Keyword arguments:
        player_id  -- the WAV player ID
        
        Return:
            Conference slot number for the player

        """
        lck = self.auto_lock()
        slot = _pjsua.player_get_conf_port(player_id)
        if slot < 0:
                self._err_check("player_get_slot()", self, -1, 
                                "Invalid player id")
        return slot

    def player_set_pos(self, player_id, pos):
        """Set WAV playback position.

        Keyword arguments:
        player_id   -- WAV player ID
        pos         -- playback position, in samples

        """
        lck = self.auto_lock()
        err = _pjsua.player_set_pos(player_id, pos)
        self._err_check("player_set_pos()", self, err)
        
    def player_destroy(self, player_id):
        """Destroy the WAV player.

        Keyword arguments:
        player_id   -- the WAV player ID.

        """
        lck = self.auto_lock()
        err = _pjsua.player_destroy(player_id)
        self._err_check("player_destroy()", self, err)

    def create_playlist(self, filelist, label="playlist", loop=True):
        """Create WAV playlist.

        Keyword arguments:
        filelist    -- List of WAV file names.
        label       -- Optional name to be assigned to the playlist
                       object (useful for logging)
        loop        -- boolean to specify whether playback should
                       automatically restart upon EOF

        Return:
            playlist_id
        """
        lck = self.auto_lock()
        opt = 0
        if not loop:
            opt = opt + 1
        err, playlist_id = _pjsua.playlist_create(label, filelist, opt)
        self._err_check("create_playlist()", self, err)
        return playlist_id 

    def playlist_get_slot(self, playlist_id):
        """Get the conference port ID for the specified playlist.

        Keyword arguments:
        playlist_id  -- the WAV playlist ID
        
        Return:
            Conference slot number for the playlist

        """
        lck = self.auto_lock()
        slot = _pjsua.player_get_conf_port(playlist_id)
        if slot < 0:
                self._err_check("playlist_get_slot()", self, -1, 
                                "Invalid playlist id")
        return slot

    def playlist_destroy(self, playlist_id):
        """Destroy the WAV playlist.

        Keyword arguments:
        playlist_id   -- the WAV playlist ID.

        """
        lck = self.auto_lock()
        err = _pjsua.player_destroy(playlist_id)
        self._err_check("playlist_destroy()", self, err)

    def create_recorder(self, filename):
        """Create WAV file recorder.

        Keyword arguments
        filename    -- WAV file name

        Return:
            WAV recorder ID

        """
        lck = self.auto_lock()
        err, rec_id = _pjsua.recorder_create(filename, 0, None, -1, 0)
        self._err_check("create_recorder()", self, err)
        return rec_id
        
    def recorder_get_slot(self, rec_id):
        """Get the conference port ID for the specified recorder.

        Keyword arguments:
        rec_id  -- the WAV recorder ID
        
        Return:
            Conference slot number for the recorder

        """
        lck = self.auto_lock()
        slot = _pjsua.recorder_get_conf_port(rec_id)
        if slot < 1:
            self._err_check("recorder_get_slot()", self, -1, 
                            "Invalid recorder id")
        return slot

    def recorder_destroy(self, rec_id):
        """Destroy the WAV recorder.

        Keyword arguments:
        rec_id   -- the WAV recorder ID.

        """
        lck = self.auto_lock()
        err = _pjsua.recorder_destroy(rec_id)
        self._err_check("recorder_destroy()", self, err)


    # Internal functions

    @staticmethod
    def strerror(err):
        return _pjsua.strerror(err)
    
    def _err_check(self, op_name, obj, err_code, err_msg=""):
        if err_code != 0:
            raise Error(op_name, obj, err_code, err_msg)

    @staticmethod
    def _create_msg_data(hdr_list):
        if not hdr_list:
            return None
        msg_data = _pjsua.Msg_Data()
        msg_data.hdr_list = hdr_list
        return msg_data
    
    def auto_lock(self):
        return _LibMutex(self._lock)

    # Internal dictionary manipulation for calls, accounts, and buddies

    def _lookup_call(self, call_id):
        return _pjsua.call_get_user_data(call_id)

    def _lookup_account(self, acc_id):
        return _pjsua.acc_get_user_data(acc_id)

    def _lookup_buddy(self, buddy_id, uri=None):
        if buddy_id != -1:
            buddy = _pjsua.buddy_get_user_data(buddy_id)
        elif uri:
            buddy_id = _pjsua.buddy_find(uri)
            if buddy_id != -1:
                buddy = _pjsua.buddy_get_user_data(buddy_id)
            else:
                buddy = None
        else:
            buddy = None
            
        return buddy 

    # Account allbacks

    def _cb_on_reg_state(self, acc_id):
        acc = self._lookup_account(acc_id)
        if acc:
            acc._cb.on_reg_state()

    def _cb_on_incoming_subscribe(self, acc_id, buddy_id, from_uri, 
                                  contact_uri, pres_obj):
        acc = self._lookup_account(acc_id)
        if acc:
            buddy = self._lookup_buddy(buddy_id)
            return acc._cb.on_incoming_subscribe(buddy, from_uri, contact_uri,
                                                 pres_obj)
        else:
            return (404, None)

    def _cb_on_incoming_call(self, acc_id, call_id, rdata):
        acc = self._lookup_account(acc_id)
        if acc:
            if 'on_incoming_call2' in acc._cb.__class__.__dict__:
                acc._cb.on_incoming_call2( Call(self, call_id), rdata )
            else:
                acc._cb.on_incoming_call( Call(self, call_id) )
        else:
            _pjsua.call_hangup(call_id, 603, None, None)

    # Call callbacks 

    def _cb_on_call_state(self, call_id):
        call = self._lookup_call(call_id)
        if call:
            if call._id == -1:
                call.attach_to_id(call_id)
            done = (call.info().state == CallState.DISCONNECTED)
            call._cb.on_state()
            if done:
                _pjsua.call_set_user_data(call_id, 0)
        else:
            pass

    def _cb_on_call_media_state(self, call_id):
        call = self._lookup_call(call_id)
        if call:
            call._cb.on_media_state()

    def _cb_on_dtmf_digit(self, call_id, digits):
        call = self._lookup_call(call_id)
        if call:
            call._cb.on_dtmf_digit(digits)

    def _cb_on_call_transfer_request(self, call_id, dst, code):
        call = self._lookup_call(call_id)
        if call:
            return call._cb.on_transfer_request(dst, code)
        else:
            return 603

    def _cb_on_call_transfer_status(self, call_id, code, text, final, cont):
        call = self._lookup_call(call_id)
        if call:
            return call._cb.on_transfer_status(code, text, final, cont)
        else:
            return cont

    def _cb_on_call_replace_request(self, call_id, rdata, code, reason):
        call = self._lookup_call(call_id)
        if call:
            return call._cb.on_replace_request(code, reason)
        else:
            return code, reason

    def _cb_on_call_replaced(self, old_call_id, new_call_id):
        old_call = self._lookup_call(old_call_id)
        new_call = self._lookup_call(new_call_id)
        if old_call and new_call:
            old_call._cb.on_replaced(new_call)

    def _cb_on_pager(self, call_id, from_uri, to_uri, contact, mime_type, 
                     body, acc_id):
        call = None
        if call_id != -1:
            call = self._lookup_call(call_id)
        if call:
            call._cb.on_pager(mime_type, body)
        else:
            acc = self._lookup_account(acc_id)
            buddy = self._lookup_buddy(-1, from_uri)
            if buddy:
                buddy._cb.on_pager(mime_type, body)
            else:
                acc._cb.on_pager(from_uri, contact, mime_type, body)

    def _cb_on_pager_status(self, call_id, to_uri, body, user_data, 
                            code, reason, acc_id):
        call = None
        if call_id != -1:
            call = self._lookup_call(call_id)
        if call:
            call._cb.on_pager_status(body, user_data, code, reason)
        else:
            acc = self._lookup_account(acc_id)
            buddy = self._lookup_buddy(-1, to_uri)
            if buddy:
                buddy._cb.on_pager_status(body, user_data, code, reason)
            else:
                acc._cb.on_pager_status(to_uri, body, user_data, code, reason)

    def _cb_on_typing(self, call_id, from_uri, to_uri, contact, is_typing, 
                      acc_id):
        call = None
        if call_id != -1:
            call = self._lookup_call(call_id)
        if call:
            call._cb.on_typing(is_typing)
        else:
            acc = self._lookup_account(acc_id)
            buddy = self._lookup_buddy(-1, from_uri)
            if buddy:
                buddy._cb.on_typing(is_typing)
            else:
                acc._cb.on_typing(from_uri, contact, is_typing)

    def _cb_on_mwi_info(self, acc_id, body):
        acc = self._lookup_account(acc_id)
        if acc:
            return acc._cb.on_mwi_info(body)

    def _cb_on_buddy_state(self, buddy_id):
        buddy = self._lookup_buddy(buddy_id)
        if buddy:
            buddy._cb.on_state()

#
# Internal
#

def _cb_on_call_state(call_id, e):
    _lib._cb_on_call_state(call_id)

def _cb_on_incoming_call(acc_id, call_id, rdata):
    _lib._cb_on_incoming_call(acc_id, call_id, rdata)

def _cb_on_call_media_state(call_id):
    _lib._cb_on_call_media_state(call_id)

def _cb_on_dtmf_digit(call_id, digits):
    _lib._cb_on_dtmf_digit(call_id, digits)

def _cb_on_call_transfer_request(call_id, dst, code):
    return _lib._cb_on_call_transfer_request(call_id, dst, code)

def _cb_on_call_transfer_status(call_id, code, reason, final, cont):
    return _lib._cb_on_call_transfer_status(call_id, code, reason, 
                                             final, cont)
def _cb_on_call_replace_request(call_id, rdata, code, reason):
    return _lib._cb_on_call_replace_request(call_id, rdata, code, reason)

def _cb_on_call_replaced(old_call_id, new_call_id):
    _lib._cb_on_call_replaced(old_call_id, new_call_id)

def _cb_on_reg_state(acc_id):
    _lib._cb_on_reg_state(acc_id)

def _cb_on_incoming_subscribe(acc_id, buddy_id, from_uri, contact_uri, pres):
    return _lib._cb_on_incoming_subscribe(acc_id, buddy_id, from_uri, 
                                          contact_uri, pres)

def _cb_on_buddy_state(buddy_id):
    _lib._cb_on_buddy_state(buddy_id)

def _cb_on_pager(call_id, from_uri, to, contact, mime_type, body, acc_id):
    _lib._cb_on_pager(call_id, from_uri, to, contact, mime_type, body, acc_id)

def _cb_on_pager_status(call_id, to, body, user_data, status, reason, acc_id):
    _lib._cb_on_pager_status(call_id, to, body, user_data, 
                             status, reason, acc_id)

def _cb_on_typing(call_id, from_uri, to, contact, is_typing, acc_id):
    _lib._cb_on_typing(call_id, from_uri, to, contact, is_typing, acc_id)

def _cb_on_mwi_info(acc_id, body):
    _lib._cb_on_mwi_info(acc_id, body)

# Worker thread
def _worker_thread_main(arg):
    global _lib
    _Trace(('worker thread started..',))
    thread_desc = 0;
    err = _pjsua.thread_register("python worker", thread_desc)
    _lib._err_check("thread_register()", _lib, err)
    while _lib and _lib._quit == 0:
        _lib.handle_events(1)
        time.sleep(0.050)
    if _lib:
        _lib._quit = 2
    _Trace(('worker thread exited..',))

def _Trace(args):
    global enable_trace
    if enable_trace:
        print("** ", end='')
        for arg in args:
            print(arg, end=' ')
        print(" **")

