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.pyComputer:
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_uriThis 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 sysimport timeimport pjsua as pjLOG_LEVEL = 3CALL_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_statuscurrent_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 = Noneacc.delete()acc = Nonelib.destroy()lib = NoneFinally, clean up and delete the lib instance.
receive_call.py
A usually, imports and parameter defintion.
import jsonimport osimport subprocessimport sysimport threadingimport timeimport pjsua as pjPROCESS_ID_PATH = "/var/run-flexa.pid"SOUND_F_PATH = "/mnt/data/package/sound.wav"LOG_LEVEL = 3current_call = NoneThen, 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_callExecution 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": breakShut down the library instance after everything is done.
transport = Noneacc.delete()acc = Nonelib.destroy()lib = None