P2P SIP Call with Python and PJSUA
Goal
Establish a SIP call between your own computer and an embedded device within the same network. A script on the device will detect an incoming call and asks the user to accept through the command line. If accepted, an audio file from the file system of the device will be played.
Overview
We have to write 2 scripts:
*make_call.py *
- On the computer, making a call to the SIP uri (address) of the device
receive_call.py
- On the device, receiving the call and accepting output of audio
Usage
The order is not required, but we suggest starting the scripts like this.
Device:
python receive_call.py
Computer:
python make_call.py <SIP uri of device>
Example: python make_call.py sip:192.168.XX.XXX:port
If you don't know the SIP uri of your device, invoke the script like this (on the device):
python receive_call.py show_uri
This will print the SIP uri to the console and you can copy it to your computer.
In-Depth Explanation
make_call.py
First, import modules and decleration of a LOG_LEVEL variable, which specifies the input verbosity level of our callbacks. We also define a CALL_STATUS dictionary which we will use to change the state of our later call.
import sys
import time
import pjsua as pj
LOG_LEVEL = 3
CALL_STATUS = {
"quit": 0,
"start": 1,
"ongoing": 2,
}
Definition of a logging function for later callbacks.
def log_cb(level, str, len):
print str,
All operations that involve sending and receiving SIP messages are asynchronous. Functions that invoke an operation will complete immediately and will give the completion status as callbacks to our running program.
Before we can have a look at our execution pipeline, we need to define a MyCallCallback(pjsua.CallCallback) class. This class inherits attributes/methods from the pjsua.CallCallback class.
It is used to receive/handle events from an ongoing call.
class MyCallCallback(pj.CallCallback):
def __init__(self, call=None):
pj.CallCallback.__init__(self, call)
def on_state(self):
global current_status
print "Call is ", self.call.info().state_text,
print "last code =", self.call.info().last_code,
print "(" + self.call.info().last_reason + ")"
if self.call.info().state == pj.CallState.DISCONNECTED:
print "Call again? y=yes, n=quit"
input = sys.stdin.readline().rstrip("\r\n")
if input == "y":
current_status = CALL_STATUS["start"]
else:
current_status = CALL_STATUS["quit"]
Method | Description |
---|---|
init(self, call=None) | Will be called after a new class instance was created; connect the class to a specific call object. |
on_state(self) | Print the status of the call to the console (when status has changed), handle status of call. |
Now, we are ready to take a look into the execution logic of the script, starting at the bottom.
if name == "main":
main()
As we can see, the main() function of the script gets invoked first.
def main():
if len(sys.argv) != 2:
print "Usage: make_call.py <dst-SIP-URI>"
sys.exit(1)
lib = pj.Lib()
Here, we check if the script was called with an additional argument specified (i.e. the SIP uri of our device). If no argument was provided, the script will stop (sys.exit(1)). See Section "Usage" for more info.
lib.init(log_cfg = pj.LogConfig(
level=LOG_LEVEL,
callback=log_cb
))
transport = lib.create_transport(pj.TransportType.UDP)
lib.start()
acc = lib.create_account_for_transport(transport)
global current_status
current_status = CALL_STATUS["start"]
Initializing of the pjsua lib instance, create transport (type UDP). Then set status of call to "start".
while True:
if not current_status:
print "Quitting..."
break
elif current_status == 1:
call = acc.make_call(sys.argv[1], MyCallCallback())
current_status = CALL_STATUS["ongoing"]
This is the main (menu) loop of the program. Here we wait till the callbacks of the call arrive and handle the call status accordingly.
transport = None
acc.delete()
acc = None
lib.destroy()
lib = None
Finally, clean up and delete the lib instance.
receive_call.py
A usually, imports and parameter defintion.
import json
import os
import subprocess
import sys
import threading
import time
import pjsua as pj
PROCESS_ID_PATH = "/var/run-flexa.pid"
SOUND_F_PATH = "/mnt/data/package/sound.wav"
LOG_LEVEL = 3
current_call = None
Then, helper functions to save the process ID, logging the callbacks and play the audio file.
def daemonize_app():
pid = os.getpid()
with open(PROCESS_ID_PATH, "w") as op_file:
op_file.write("%s" % pid)
def log_cb(level, str, len):
print str,
def play_file():
time.sleep(0.5)
print("\nPlay back announcement")
playback = subprocess.Popen(" ".join(("aplay", SOUND_F_PATH)), shell=True)
playback.wait()
print("Finished\n")
time.sleep(0.5)
Now 2 classes to handle callbacks from a call. For an explanation, please the Apx II inside the SIP Announcement Player Code Documentation document.
class MyAccountCallback(pj.AccountCallback):
sem = None
def __init__(self, account=None):
pj.AccountCallback.__init__(self, account)
def wait(self):
self.sem = threading.Semaphore(0)
self.sem.acquire()
def on_reg_state(self):
if self.sem:
if self.account.info().reg_status >= 200:
self.sem.release()
def on_incoming_call(self, call):
global current_call
if current_call:
call.answer(486, "Busy")
return
current_call = call
call_cb = MyCallCallback(current_call)
current_call.set_callback(call_cb)
current_call.answer(180)
print "Incoming call from ", current_call.info().remote_uri
print "Accept and play audio: a, hangup: h, quit: q"
class MyCallCallback(pj.CallCallback):
def __init__(self, call=None):
pj.CallCallback.__init__(self, call)
def on_state(self):
global current_call
print "Call with", self.call.info().remote_uri,
print "is", self.call.info().state_text,
print "last code =", self.call.info().last_code,
print "(" + self.call.info().last_reason + ")"
if self.call.info().state == pj.CallState.DISCONNECTED:
current_call = None
print 'Current call is', current_call
Execution starts at the bottom of the script.
if __name__ == "__main__":
main()
We jump to the main() function. Invoke the function to save the process ID from above and create a pjsua library instance.
def main():
daemonize_app()
lib = pj.Lib()
Initialize the new library instance with the callback logging function and verbosity level. Create SIP transport instance of the specified type (UDP).
lib.init(log_cfg = pj.LogConfig(
level = LOG_LEVEL,
callback = log_cb,
))
transport = lib.create_transport(
pj.TransportType.UDP,
pj.TransportConfig(port=50112),
)
If the scirpt is called with an additional argument "show_uri", print the SIP uri to the console (see Usage section above).
if len(sys.argv) == 2 and sys.argv[1] == "show_uri":
my_sip_uri = ":".join((
"sip",
transport.info().host,
str(transport.info().port)
))
print "\nYour SIP uri:", my_sip_uri, "\n"
transport = None
lib.destroy()
lib = None
sys.exit(1)
Start the library and create an account for transport, attaching the MyAccountCallback.
lib.start()
acc_cb = MyAccountCallback()
acc = lib.create_account_for_transport(transport, cb=acc_cb)
Main (menu) loop to to handle incoming call.
while True:
print "\n--- Call menu ---"
print "Current call status is:", current_call
print "answer: a, hangup: h, quit: q\n"
input = sys.stdin.readline().rstrip('\r\n')
if input == "h":
if not current_call:
print "There is no call\n"
continue
current_call.hangup()
elif input == "a":
if not current_call:
print "There is no call\n"
continue
current_call.answer(200)
play_file()
elif input == "q":
break
Shut down the library instance after everything is done.
transport = None
acc.delete()
acc = None
lib.destroy()
lib = None