# $Id$ # # Object oriented PJSUA wrapper. # # Copyright (C) 2003-2008 Benny Prijono # # 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. """ import _pjsua import thread 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) 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 + "" 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 "** ", for arg in args: print arg, print " **"