blob: a0fd56cc1717cf98df729d9b8bc1342dfe84e292 [file] [log] [blame]
# $Id: pjsua_app.py 1438 2007-09-17 15:44:47Z bennylp $
#
# Sample and simple Python script to make and receive calls, and do
# presence and instant messaging/IM using PJSUA-API binding for Python.
#
# Copyright (C) 2003-2007 Benny Prijono <benny@prijono.org>
#
import py_pjsua
import sys
import thread
#
# Configurations
#
THIS_FILE = "pjsua_app.py"
C_QUIT = 0
C_LOG_LEVEL = 4
# STUN config.
# Set C_STUN_HOST to the address:port of the STUN server to enable STUN
#
C_STUN_HOST = ""
#C_STUN_HOST = "192.168.0.2"
#C_STUN_HOST = "stun.iptel.org:3478"
# SIP port
C_SIP_PORT = 5060
# Globals
#
g_ua_cfg = None
g_acc_id = py_pjsua.PJSUA_INVALID_ID
g_current_call = py_pjsua.PJSUA_INVALID_ID
g_wav_files = []
g_wav_id = 0
g_wav_port = 0
g_rec_file = ""
g_rec_id = 0
g_rec_port = 0
# Utility: display PJ error and exit
#
def err_exit(title, rc):
py_pjsua.perror(THIS_FILE, title, rc)
py_pjsua.destroy()
exit(1)
# Logging function (also callback, called by pjsua-lib)
#
def log_cb(level, str, len):
if level <= C_LOG_LEVEL:
print str,
def write_log(level, str):
log_cb(level, str + "\n", 0)
# Utility to get call info
#
def call_name(call_id):
ci = py_pjsua.call_get_info(call_id)
return "[Call " + `call_id` + " " + ci.remote_info + "]"
# Callback when call state has changed.
#
def on_call_state(call_id, e):
global g_current_call
ci = py_pjsua.call_get_info(call_id)
write_log(3, call_name(call_id) + " state = " + `ci.state_text`)
if ci.state == py_pjsua.PJSIP_INV_STATE_DISCONNECTED:
g_current_call = py_pjsua.PJSUA_INVALID_ID
# Callback for incoming call
#
def on_incoming_call(acc_id, call_id, rdata):
global g_current_call
if g_current_call != py_pjsua.PJSUA_INVALID_ID:
# There's call in progress - answer Busy
py_pjsua.call_answer(call_id, 486, None, None)
return
g_current_call = call_id
ci = py_pjsua.call_get_info(call_id)
write_log(3, "*** Incoming call: " + call_name(call_id) + "***")
write_log(3, "*** Press a to answer or h to hangup ***")
# Callback when media state has changed (e.g. established or terminated)
#
def on_call_media_state(call_id):
ci = py_pjsua.call_get_info(call_id)
if ci.media_status == py_pjsua.PJSUA_CALL_MEDIA_ACTIVE:
py_pjsua.conf_connect(ci.conf_slot, 0)
py_pjsua.conf_connect(0, ci.conf_slot)
write_log(3, call_name(call_id) + ": media is active")
else:
write_log(3, call_name(call_id) + ": media is inactive")
# Callback when account registration state has changed
#
def on_reg_state(acc_id):
acc_info = py_pjsua.acc_get_info(acc_id)
if acc_info.has_registration != 0:
cmd = "registration"
else:
cmd = "unregistration"
if acc_info.status != 0 and acc_info.status != 200:
write_log(3, "Account " + cmd + " failed: rc=" + `acc_info.status` + " " + acc_info.status_text)
else:
write_log(3, "Account " + cmd + " success")
# Callback when buddy's presence state has changed
#
def on_buddy_state(buddy_id):
write_log(3, "On Buddy state called")
buddy_info = py_pjsua.buddy_get_info(buddy_id)
if buddy_info.status != 0 and buddy_info.status != 200:
write_log(3, "Status of " + `buddy_info.uri` + " is " + `buddy_info.status_text`)
else:
write_log(3, "Status : " + `buddy_info.status`)
# Callback on incoming pager (MESSAGE)
#
def on_pager(call_id, strfrom, strto, contact, mime_type, text):
write_log(3, "MESSAGE from " + `strfrom` + " : " + `text`)
# Callback on the delivery status of outgoing pager (MESSAGE)
#
def on_pager_status(call_id, strto, body, user_data, status, reason):
write_log(3, "MESSAGE to " + `strto` + " status " + `status` + " reason " + `reason`)
# Received typing indication
#
def on_typing(call_id, strfrom, to, contact, is_typing):
str_t = ""
if is_typing:
str_t = "is typing.."
else:
str_t = "has stopped typing"
write_log(3, "IM indication: " + strfrom + " " + str_t)
# Received the status of previous call transfer request
#
def on_call_transfer_status(call_id,status_code,status_text,final,p_cont):
strfinal = ""
if final == 1:
strfinal = "[final]"
write_log(3, "Call " + `call_id` + ": transfer status= " + `status_code` + " " + status_text+ " " + strfinal)
if status_code/100 == 2:
write_log(3, "Call " + `call_id` + " : call transfered successfully, disconnecting call")
status = py_pjsua.call_hangup(call_id, 410, None, None)
p_cont = 0
# Callback on incoming call transfer request
#
def on_call_transfer_request(call_id, dst, code):
write_log(3, "Call transfer request from " + `call_id` + " to " + dst + " with code " + `code`)
#
# Initialize pjsua.
#
def app_init():
global g_acc_id, g_ua_cfg
# Create pjsua before anything else
status = py_pjsua.create()
if status != 0:
err_exit("pjsua create() error", status)
# Create and initialize logging config
log_cfg = py_pjsua.logging_config_default()
log_cfg.level = C_LOG_LEVEL
log_cfg.cb = log_cb
# Create and initialize pjsua config
# Note: for this Python module, thread_cnt must be 0 since Python
# doesn't like to be called from alien thread (pjsua's thread
# in this case)
ua_cfg = py_pjsua.config_default()
ua_cfg.thread_cnt = 0
ua_cfg.user_agent = "PJSUA/Python 0.1"
ua_cfg.cb.on_incoming_call = on_incoming_call
ua_cfg.cb.on_call_media_state = on_call_media_state
ua_cfg.cb.on_reg_state = on_reg_state
ua_cfg.cb.on_call_state = on_call_state
ua_cfg.cb.on_buddy_state = on_buddy_state
ua_cfg.cb.on_pager = on_pager
ua_cfg.cb.on_pager_status = on_pager_status
ua_cfg.cb.on_typing = on_typing
ua_cfg.cb.on_call_transfer_status = on_call_transfer_status
ua_cfg.cb.on_call_transfer_request = on_call_transfer_request
# Configure STUN setting
if C_STUN_HOST != "":
ua_cfg.stun_host = C_STUN_HOST;
# Create and initialize media config
med_cfg = py_pjsua.media_config_default()
med_cfg.ec_tail_len = 0
#
# Initialize pjsua!!
#
status = py_pjsua.init(ua_cfg, log_cfg, med_cfg)
if status != 0:
err_exit("pjsua init() error", status)
# Configure UDP transport config
transport_cfg = py_pjsua.transport_config_default()
transport_cfg.port = C_SIP_PORT
# Create UDP transport
status, transport_id = \
py_pjsua.transport_create(py_pjsua.PJSIP_TRANSPORT_UDP, transport_cfg)
if status != 0:
err_exit("Error creating UDP transport", status)
# Create initial default account
status, acc_id = py_pjsua.acc_add_local(transport_id, 1)
if status != 0:
err_exit("Error creating account", status)
g_acc_id = acc_id
g_ua_cfg = ua_cfg
# Add SIP account interractively
#
def add_account():
global g_acc_id
acc_domain = ""
acc_username = ""
acc_passwd =""
confirm = ""
# Input account configs
print "Your SIP domain (e.g. myprovider.com): ",
acc_domain = sys.stdin.readline()
if acc_domain == "\n":
return
acc_domain = acc_domain.replace("\n", "")
print "Your username (e.g. alice): ",
acc_username = sys.stdin.readline()
if acc_username == "\n":
return
acc_username = acc_username.replace("\n", "")
print "Your password (e.g. secret): ",
acc_passwd = sys.stdin.readline()
if acc_passwd == "\n":
return
acc_passwd = acc_passwd.replace("\n", "")
# Configure account configuration
acc_cfg = py_pjsua.acc_config_default()
acc_cfg.id = "sip:" + acc_username + "@" + acc_domain
acc_cfg.reg_uri = "sip:" + acc_domain
cred_info = py_pjsua.Pjsip_Cred_Info()
cred_info.realm = "*"
cred_info.scheme = "digest"
cred_info.username = acc_username
cred_info.data_type = 0
cred_info.data = acc_passwd
acc_cfg.cred_info.append(1)
acc_cfg.cred_info[0] = cred_info
# Add new SIP account
status, acc_id = py_pjsua.acc_add(acc_cfg, 1)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error adding SIP account", status)
else:
g_acc_id = acc_id
write_log(3, "Account " + acc_cfg.id + " added")
def add_player():
global g_wav_files
global g_wav_id
global g_wav_port
file_name = ""
status = -1
wav_id = 0
print "Enter the path of the file player(e.g. /tmp/audio.wav): ",
file_name = sys.stdin.readline()
if file_name == "\n":
return
file_name = file_name.replace("\n", "")
status, wav_id = py_pjsua.player_create(file_name, 0)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error adding file player ", status)
else:
g_wav_files.append(file_name)
if g_wav_id == 0:
g_wav_id = wav_id
g_wav_port = py_pjsua.player_get_conf_port(wav_id)
write_log(3, "File player " + file_name + " added")
def add_recorder():
global g_rec_file
global g_rec_id
global g_rec_port
file_name = ""
status = -1
rec_id = 0
print "Enter the path of the file recorder(e.g. /tmp/audio.wav): ",
file_name = sys.stdin.readline()
if file_name == "\n":
return
file_name = file_name.replace("\n", "")
status, rec_id = py_pjsua.recorder_create(file_name, 0, None, 0, 0)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error adding file recorder ", status)
else:
g_rec_file = file_name
g_rec_id = rec_id
g_rec_port = py_pjsua.recorder_get_conf_port(rec_id)
write_log(3, "File recorder " + file_name + " added")
def conf_list():
ports = None
print "Conference ports : "
ports = py_pjsua.enum_conf_ports()
for port in ports:
info = None
info = py_pjsua.conf_get_port_info(port)
txlist = ""
for listener in info.listeners:
txlist = txlist + "#" + `listener` + " "
print "Port #" + `info.slot_id` + "[" + `(info.clock_rate/1000)` + "KHz/" + `(info.samples_per_frame * 1000 / info.clock_rate)` + "ms] " + info.name + " transmitting to: " + txlist
def connect_port():
src_port = 0
dst_port = 0
print "Connect src port # (empty to cancel): "
src_port = sys.stdin.readline()
if src_port == "\n":
return
src_port = src_port.replace("\n", "")
src_port = int(src_port)
print "To dst port # (empty to cancel): "
dst_port = sys.stdin.readline()
if dst_port == "\n":
return
dst_port = dst_port.replace("\n", "")
dst_port = int(dst_port)
status = py_pjsua.conf_connect(src_port, dst_port)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error connecting port ", status)
else:
write_log(3, "Port connected from " + `src_port` + " to " + `dst_port`)
def disconnect_port():
src_port = 0
dst_port = 0
print "Disconnect src port # (empty to cancel): "
src_port = sys.stdin.readline()
if src_port == "\n":
return
src_port = src_port.replace("\n", "")
src_port = int(src_port)
print "From dst port # (empty to cancel): "
dst_port = sys.stdin.readline()
if dst_port == "\n":
return
dst_port = dst_port.replace("\n", "")
dst_port = int(dst_port)
status = py_pjsua.conf_disconnect(src_port, dst_port)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error disconnecting port ", status)
else:
write_log(3, "Port disconnected " + `src_port` + " from " + `dst_port`)
def dump_call_quality():
global g_current_call
buf = ""
if g_current_call != -1:
buf = py_pjsua.call_dump(g_current_call, 1, 1024, " ")
write_log(3, "\n" + buf)
else:
write_log(3, "No current call")
def xfer_call():
global g_current_call
if g_current_call == -1:
write_log(3, "No current call")
else:
call = g_current_call
ci = py_pjsua.call_get_info(g_current_call)
print "Transfering current call ["+ `g_current_call` + "] " + ci.remote_info
print "Enter sip url : "
url = sys.stdin.readline()
if url == "\n":
return
url = url.replace("\n", "")
if call != g_current_call:
print "Call has been disconnected"
return
msg_data = py_pjsua.msg_data_init()
status = py_pjsua.call_xfer(g_current_call, url, msg_data);
if status != 0:
py_pjsua.perror(THIS_FILE, "Error transfering call ", status)
else:
write_log(3, "Call transfered to " + url)
def xfer_call_replaces():
if g_current_call == -1:
write_log(3, "No current call")
else:
call = g_current_call
ids = py_pjsua.enum_calls()
if len(ids) <= 1:
print "There are no other calls"
return
ci = py_pjsua.call_get_info(g_current_call)
print "Transfer call [" + `g_current_call` + "] " + ci.remote_info + " to one of the following:"
for i in range(0, len(ids)):
if ids[i] == call:
continue
call_info = py_pjsua.call_get_info(ids[i])
print `ids[i]` + " " + call_info.remote_info + " [" + call_info.state_text + "]"
print "Enter call number to be replaced : "
buf = sys.stdin.readline()
buf = buf.replace("\n","")
if buf == "":
return
dst_call = int(buf)
if call != g_current_call:
print "Call has been disconnected"
return
if dst_call == call:
print "Destination call number must not be the same as the call being transfered"
return
if dst_call >= py_pjsua.PJSUA_MAX_CALLS:
print "Invalid destination call number"
return
if py_pjsua.call_is_active(dst_call) == 0:
print "Invalid destination call number"
return
py_pjsua.call_xfer_replaces(call, dst_call, 0, None)
#
# Worker thread function.
# Python doesn't like it when it's called from an alien thread
# (pjsua's worker thread, in this case), so for Python we must
# disable worker thread in pjsua and poll pjsua from Python instead.
#
def worker_thread_main(arg):
global C_QUIT
thread_desc = 0;
status = py_pjsua.thread_register("python worker", thread_desc)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error registering thread", status)
else:
while C_QUIT == 0:
py_pjsua.handle_events(50)
print "Worker thread quitting.."
C_QUIT = 2
# Start pjsua
#
def app_start():
# Done with initialization, start pjsua!!
#
status = py_pjsua.start()
if status != 0:
err_exit("Error starting pjsua!", status)
# Start worker thread
thr = thread.start_new(worker_thread_main, (0,))
print "PJSUA Started!!"
# Print account and buddy list
def print_acc_buddy_list():
global g_acc_id
acc_ids = py_pjsua.enum_accs()
print "Account list:"
for acc_id in acc_ids:
acc_info = py_pjsua.acc_get_info(acc_id)
if acc_info.has_registration == 0:
acc_status = acc_info.status_text
else:
acc_status = `acc_info.status` + "/" + acc_info.status_text + " (expires=" + `acc_info.expires` + ")"
if acc_id == g_acc_id:
print " *",
else:
print " ",
print "[" + `acc_id` + "] " + acc_info.acc_uri + ": " + acc_status
print " Presence status: ",
if acc_info.online_status != 0:
print "Online"
else:
print "Invisible"
if py_pjsua.get_buddy_count() > 0:
print ""
print "Buddy list:"
buddy_ids = py_pjsua.enum_buddies()
for buddy_id in buddy_ids:
bi = py_pjsua.buddy_get_info(buddy_id)
print " [" + `buddy_id` + "] " + bi.status_text + " " + bi.uri
# Print application menu
#
def print_menu():
print ""
print ">>>"
print_acc_buddy_list()
print """
+============================================================================+
| Call Commands : | Buddy, IM & Presence: | Account: |
| | | |
| m Make call | +b Add buddy | +a Add account |
| a Answer current call | -b Delete buddy | -a Delete accnt |
| h Hangup current call | | |
| H Hold call | i Send instant message | rr register |
| v re-inVite (release Hold) | s Subscribe presence | ru Unregister |
| # Send DTMF string | u Unsubscribe presence | |
| dq Dump curr. call quality | t ToGgle Online status | |
| +--------------------------+------------------+
| x Xfer call | Media Commands: | Status: |
| X Xfer with Replaces | | |
| | cl List ports | d Dump status |
| | cc Connect port | dd Dump detail |
| | cd Disconnect port | |
| | +p Add file player | |
|------------------------------+ +r Add file recorder | |
| q Quit application | | |
+============================================================================+"""
print "You have " + `py_pjsua.call_get_count()` + " active call(s)"
print ">>>",
# Menu
#
def app_menu():
global g_acc_id
global g_current_call
quit = 0
while quit == 0:
print_menu()
choice = sys.stdin.readline()
if choice[0] == "q":
quit = 1
elif choice[0] == "i":
# Sending IM
print "Send IM to SIP URL: ",
url = sys.stdin.readline()
if url == "\n":
continue
# Send typing indication
py_pjsua.im_typing(g_acc_id, url, 1, None)
print "The content: ",
message = sys.stdin.readline()
if message == "\n":
py_pjsua.im_typing(g_acc_id, url, 0, None)
continue
# Send the IM!
py_pjsua.im_send(g_acc_id, url, None, message, None, 0)
elif choice[0] == "m":
# Make call
print "Using account ", g_acc_id
print "Make call to SIP URL: ",
url = sys.stdin.readline()
url = url.replace("\n", "")
if url == "":
continue
# Initiate the call!
status, call_id = py_pjsua.call_make_call(g_acc_id, url, 0, 0, None)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error making call", status)
else:
g_current_call = call_id
elif choice[0] == "+" and choice[1] == "b":
# Add new buddy
bc = py_pjsua.Buddy_Config()
print "Buddy URL: ",
bc.uri = sys.stdin.readline()
if bc.uri == "\n":
continue
bc.uri = bc.uri.replace("\n", "")
bc.subscribe = 1
status, buddy_id = py_pjsua.buddy_add(bc)
if status != 0:
py_pjsua.perror(THIS_FILE, "Error adding buddy", status)
elif choice[0] == "-" and choice[1] == "b":
print "Enter buddy ID to delete : "
buf = sys.stdin.readline()
buf = buf.replace("\n","")
if buf == "":
continue
i = int(buf)
if py_pjsua.buddy_is_valid(i) == 0:
print "Invalid buddy id " + `i`
else:
py_pjsua.buddy_del(i)
print "Buddy " + `i` + " deleted"
elif choice[0] == "+" and choice[1] == "a":
# Add account
add_account()
elif choice[0] == "-" and choice[1] == "a":
print "Enter account ID to delete : "
buf = sys.stdin.readline()
buf = buf.replace("\n","")
if buf == "":
continue
i = int(buf)
if py_pjsua.acc_is_valid(i) == 0:
print "Invalid account id " + `i`
else:
py_pjsua.acc_del(i)
print "Account " + `i` + " deleted"
elif choice[0] == "+" and choice[1] == "p":
add_player()
elif choice[0] == "+" and choice[1] == "r":
add_recorder()
elif choice[0] == "c" and choice[1] == "l":
conf_list()
elif choice[0] == "c" and choice[1] == "c":
connect_port()
elif choice[0] == "c" and choice[1] == "d":
disconnect_port()
elif choice[0] == "d" and choice[1] == "q":
dump_call_quality()
elif choice[0] == "x":
xfer_call()
elif choice[0] == "X":
xfer_call_replaces()
elif choice[0] == "h":
if g_current_call != py_pjsua.PJSUA_INVALID_ID:
py_pjsua.call_hangup(g_current_call, 603, None, None)
else:
print "No current call"
elif choice[0] == "H":
if g_current_call != py_pjsua.PJSUA_INVALID_ID:
py_pjsua.call_set_hold(g_current_call, None)
else:
print "No current call"
elif choice[0] == "v":
if g_current_call != py_pjsua.PJSUA_INVALID_ID:
py_pjsua.call_reinvite(g_current_call, 1, None);
else:
print "No current call"
elif choice[0] == "#":
if g_current_call == py_pjsua.PJSUA_INVALID_ID:
print "No current call"
elif py_pjsua.call_has_media(g_current_call) == 0:
print "Media is not established yet!"
else:
call = g_current_call
print "DTMF strings to send (0-9*#A-B)"
buf = sys.stdin.readline()
buf = buf.replace("\n", "")
if buf == "":
continue
if call != g_current_call:
print "Call has been disconnected"
continue
status = py_pjsua.call_dial_dtmf(g_current_call, buf)
if status != 0:
py_pjsua.perror(THIS_FILE, "Unable to send DTMF", status);
else:
print "DTMF digits enqueued for transmission"
elif choice[0] == "s":
print "Subscribe presence of (buddy id) : "
buf = sys.stdin.readline()
buf = buf.replace("\n","")
if buf == "":
continue
i = int(buf)
py_pjsua.buddy_subscribe_pres(i, 1)
elif choice[0] == "u":
print "Unsubscribe presence of (buddy id) : "
buf = sys.stdin.readline()
buf = buf.replace("\n","")
if buf == "":
continue
i = int(buf)
py_pjsua.buddy_subscribe_pres(i, 0)
elif choice[0] == "t":
acc_info = py_pjsua.acc_get_info(g_acc_id)
if acc_info.online_status == 0:
acc_info.online_status = 1
else:
acc_info.online_status = 0
py_pjsua.acc_set_online_status(g_acc_id, acc_info.online_status)
st = ""
if acc_info.online_status == 0:
st = "offline"
else:
st = "online"
print "Setting " + acc_info.acc_uri + " online status to " + st
elif choice[0] == "r":
if choice[1] == "r":
py_pjsua.acc_set_registration(g_acc_id, 1)
elif choice[1] == "u":
py_pjsua.acc_set_registration(g_acc_id, 0)
elif choice[0] == "d":
py_pjsua.dump(choice[1] == "d")
elif choice[0] == "a":
if g_current_call != py_pjsua.PJSUA_INVALID_ID:
py_pjsua.call_answer(g_current_call, 200, None, None)
else:
print "No current call"
#
# main
#
app_init()
app_start()
app_menu()
#
# Done, quitting..
#
print "PJSUA shutting down.."
C_QUIT = 1
# Give the worker thread chance to quit itself
while C_QUIT != 2:
py_pjsua.handle_events(50)
print "PJSUA destroying.."
py_pjsua.destroy()