SIP Announcement Player Code Documentation
Goal
The aim of this python code is to establish (register) a SIP call to signal the output of audio data (e.g. an Announcement/Music etc.) on the target device (receiver).
The Session Initiation Protocol (SIP) is a protocol which is used for establishing a two-party or multy-party connection (session) to send media (i.e. voice/video).
Quick Start
To get started as fast as possible, only 3 steps are necessary:
- Obtain SIP registry, Auth ID and secret from linuquick
- Update the config.json file with your new credentials (path /mnt/data/package/config.json)
- Change SOUND_F_PATH variable in line 9 of callbacks.py so that it points to the desired audio (.wav) file
In-Depth Explanation
All the action happens in the main script run.py. From here, all other files or scripts will be called.
Let's dive into the run.py file and analyse what is happening:
(i)
First, we need to import all packages and (our own) classes we want to use. These are already pre-installed on the device, so we don't need to take care of that.
import json
import os
import sys
import pjsua as pj
import get_update
from callbacks import MyAccountCallback, MyCallCallback
(ii)
Secondly, we define some (constant) paths, file names and variables we will need throughout the script:
PROCESS_ID_PATH = "/var/run-flexa.pid"
CONFIG_PATH = "/mnt/data/package/config.json"
LOG_LEVEL = 3
Variable | Description |
---|---|
PROCESS_ID_PATH | Path where the current process ID will be saved |
CONFIG_PATH | Path to the file where the linuquick user identity is saved |
LOG_LEVEL | Input verbosity level of callback log |
(iii)
Next, some helper functions we will need later.
def daemonize_app():
pid = os.getpid()
with open(PROCESS_ID_PATH, "w") as op:
op.write("%s" % pid)
def get_linu_config():
with open(os.path.join(CONFIG_PATH)) as config:
config = json.load(config)
return config
def log_cb(level, str, len):
print str,
Function | Description |
---|---|
daemonize_app() | Accesses the current process ID and saves it into a file, which path is defined in the beginning of the script (PROCESS_ID_PATH, see (ii) above). |
get_linu_config() | Reads the linuquick config parameters from the config.json file and stores them in the variable config, which will be returned in the end. |
log_cb(level, str, len) | Logging function for later callbacks. |
(iv)
After this is done, we can analyse the execution logic of the script. We jump to the end of the script (lines 91-92):
if __name__ == "__main__":
main()
This is the first part that will start the script execution. Since we are running the run.py script as the main program, the "name == "main" condition evaluates to true.
As you can see, the script continues with invoking the main() function, which definition starts in line 31.
(v)
In a first step, 2 helper functions from above (see (ii)) will be called:
daemonize_app()
config = get_linu_config()
(vi)
Next, we create a pjsua library instance
lib = pj.Lib()
and invoke the call_update function from the *get_update.py *script.
get_update.call_update()
(Please see Apx I **below for more information about the *get_update.py*** script.)
(vii)
We initialize the library, create a UDP transport and set user identity information as well as a callback for the account (see Apx II).
lib.init(log_cfg = pj.LogConfig(
level = LOG_LEVEL,
callback = log_cb,
))
transport = lib.create_transport(
pj.TransportType.UDP,
pj.TransportConfig(0),
)
lib.start()
acc = lib.create_account(pj.AccountConfig(
str(config["sip registry"]),
str(config["Auth ID"]),
str(config["secret"]),
))
acc_cb = MyAccountCallback(acc)
acc.set_callback(acc_cb)
acc_cb.wait()
(For a closer look into the MyAccountCallback class, please see (Apx II) below.)
(viii)
try:
while True:
pass
except KeyboardInterrupt:
print "\nQuitting..."
transport = None
acc.delete()
acc = None
lib.destroy()
lib = None
This is the main loop of our script. We create a loop that will run forever unless we stop the program and close the connection/lib instance (pressing Ctrl+c). Since we put the pass keyword here, no further code will be executed inside the body of the loop.
All the action is happening inside our callback classes, which will handle the events from the ongoing call.
(Apx I - get_update.py)
This script is updating the json.config parameters and will change the value of the audio output.
Again, first the necessary imports.
import control_volume
import datetime
import json
import os
import requests
import threading
import time
Followed by the declaration of the (constant) variables we will need in the script.
REG_PATH = os.path.abspath("/mnt/data/regId")
SERV_F_PATH = "/mnt/data/package/service.json"
CONFIG_PATH = os.getcwd()
UPDATE_INTERVAL = 1
LINU_Q_URL = "http://linuquick.net/dev/public/api/"
Variable | Description |
---|---|
REG_PATH | Path to the regID |
SERV_F_PATH | Path to service.json file |
CONFIG_PATH | Path of current working directory |
UPDATE_INTERVAL | Update step for next call |
LINU_Q_URL | URL to linuquick api |
Let's take a look at the execution logic from the run.py viewpoint. The function call_update() (which is part of the get_update.py script) gets invoked in line 98 of run.py.
get_update.call_update()
The function is defined between lines 63-74 of get_update.py.
def call_update():
global next_call
with open(REG_PATH,'r') as reg:
regId = reg.read()
next_call = time.time()
next_call += UPDATE_INTERVAL
threading.Timer(
next_call - time.time(),
get_update
).start()
First, a global variable next_call is define (accessible from everywhere in the script). This variable is used to schedule the update intervall for the next call. Next, we load the regID and store it in the variable regID. We initialize the next_call variable with the current time, defined in seconds (see here). Now we increment the next_call variable by the value of UPDATE_INTERVAL (i.e. 1 second).
Finally, we can create a thread that executes the function get_update() after a specified time interval (i.e. next_call - time.time()) has passed.
To follow the execution logic, we now take a look at the get_update() function, starting in line 17. Make variables accessible for the whole script (global keyword).
global CONFIG_PATH, regId, next_call
Load service.json file.
with open(SERV_F_PATH) as service_json:
service_data = json_load(service_json)
POST request to url.
url = "".join((
LINU_Q_URL,
regId,
"/SetStatus",
))
set_state_request = requests.post(
url,
json = {"regId":regId, "status":"Alive"}
)
Get state of the request.
state = set_state_request.json()
If config state of request is "new", send POST request with configUrl and regID.
config_request = requests.post(
service_data["configUrl"],
json = {"regId", regID}
)
Save new config parameters to config.json file.
with open(os.path.join(CONFIG_PATH,"config.json"),'w') as config:
json.dump(config_json, config)
Finally, set new volume value for the audio output.
control_volume.set_lineout_vol(
config_json["AppParam"]["volume"]
)
(See Apx III for control_volume.py description.)
(Apx II - callbacks.py)
Again, imports, parameter and variable declarations in the beginning.
import subprocess
import threading
import time
import pjsua as pj
SOUND_F_PATH = "/mnt/data/package/sound.wav"
current_call = None
Here we define the path to the desired audio file (.wav) as well as a flag to check if a call is ongoing.
def play_file():
time.sleep(0.5)
print("\nPlay back announcement")
playback = subprocess.Popen(" ".join(("aplay", SOUND_F_PATH)), shell=True)
plaback.wait()
print("Finished\n")
time.sleep(0.5)
Here, we define a function to handle the output of the audio. The function will wait till the audio has finished.
Going on, MyAccountCallback class is next.
This is a callback class for the pjsua SIP account. It will handle incoming notifications from account events. Hence, it inherits attributes from the pjsua AccountCallback class.
At the top, we declare a placeholder for the semaphore variable we will need later.
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 "receiving call"
play_file()
print "Hangup call"
print "Ctrl+c for exit"
current_call.hangup()
The class has 4 methods:
Method | Description |
---|---|
init(self, account=None) | Class constructor, will be called after a new class instance has been created. |
wait(self) | Initialize the semaphore variable; increment it by 1 (self.sem.acquire()). |
on_reg_state(self) | Decrement semaphore if account registration was successful. |
on_incoming_call(self, call) | Gets invoked if there is an incoming call. First, check if there is an ongoing call. If not, we answer the call and enable the playback of the audio (i.e. play_file()). Finally, we hangup the call. |
The last class is defined at the bottom of the script, MyCallCallback.
This is a callback class for the call itself. It will handle the status of our call. It inherits attributes from the pjsua CallCallback class.** **
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
Method | Description |
---|---|
init(self, call=None) | Class constructor, will be called after a new class instance has been created. |
on_state(self) | Notifies when call state has changed, handels call status if call gets disconnected (current_call = None) |
(**Apx III - control_volume.py)**
Script to control output volume.
import subprocess
import json
import alsaaudio
def set_lineout_vol(volume):
mixer = alsaaudio.Mixer('Line Out')
mixer.setvolume(int(volume))
Create a mixer instance and set its volume.