You are on page 1of 22

SEMS application plugin

development tutorial
writing SEMS applications
[ Version 0.1 / 12.10.2006 ]

iptego GmbH
Am Borsigturm 40
D-13507 Berlin
Germany
0700 - IPTEGODE
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page1/22

Table of Contents
1 Introduction.......................................................................................................................................................3
2 Writing a C++ application plugin........................................................................................................................4
2.1 Overview...................................................................................................................................................4
2.2 MyApp the empty template.....................................................................................................................4
2.3 MyConfigurableApp adding configurability..............................................................................................7
2.4 MyAnnounceApp an announcement.......................................................................................................8
2.5 MyJukebox - reacting to user input .........................................................................................................9
2.6 Calling Card application in B2BUA mode................................................................................................11
3 Writing an IVR application...............................................................................................................................18
3.1 Ivr Overview............................................................................................................................................18
3.2 Simple announcement script...................................................................................................................18
3.3 RFC 4240 announcement service...........................................................................................................19

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page2/22

1 Introduction
This is the application development tutorial for SEMS-ng. SEMS-ng is a high performance, carrier grade,
extensible media server for SIP (RFC3261) based VoIP services.
SEMS-ng can be extended through loadable modules, so called plug-ins. Application modules that implement
applications logic can use the core SEMS-ng API on various levels of abstraction, therefore flexibility in the
development of services is provided. Other modules extend SEMS' functionality, for example with additional
codecs, file formats, or signaling logic.
While a part of this guide also applies to older versions of SEMS (SEMS v0.8.12, SEMS v0.9.0), this
documentation is for SEMS-ng (SEMS v0.10.0). For further information about SEMS version 0.8.12 and 0.9.0
please refer to the documentation provided with the respective source packages. Nevertheless, for simplicity
from now on SEMS-ng is referred to in this document as SEMS. For further information about SEMS versioning
please refer to the SEMS-versions document.
This tutorial has two parts: The first one describes how to write application modules in C++ using the SEMS
framework API, the second part explains how to write an application in the IVR as python script. While the API
and the use of the framework is different or sometimes has differing names, one basically does the same things
in both C++ plugins: On session start one sets input and output, and then one reacts to various events (BYE,
DTMF, ...). So even if the reader only wants to use the IVR, the first part gives some probably useful insight.
The C++ tutorial starts from an empty application plugin template, and subsequently adds functionality, while the
code is explained. First, the plugin is made configurable, and then, the plugin will play an announcement. As a
next step we react on the caller's keypresses. Then a calling card application will describe how to use
component plugins and SEMS' B2BUA capabilities.
The IVR tutorial has a minimal announcement application and announcement service following rfc4240: Basic
Media Services with SIP.
The reader is encouraged to try the plugins while reading this document, modify and adapt them. For each step
there is a separate archive with all the source available.
The author welcomes comments, suggestions, and corrections, and can best be reached via email at the sems
or semsdev mailing lists (see http://lists.iptel.org) or directly at mailto:stefan.sayer@iptego.de.

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page3/22

2 Writing a C++ application plugin


2.1 Overview
Application plugins written in C++ can use the SEMS API on various levels of abstraction. Both on signaling and
on media side the application can either use some high level functions - which in most cases should be
sufficient - or take the freedom to control precisely call flow or media processing on a low level. This provides
the flexibility that both simple application modules can be written with a few lines of code, while also complex
applications can build on the SEMS core.
It is therefore not possible to define a closed set of SEMS API functions the SEMS core should rather be seen
and used as a framework for developing VoIP applications.
This tutorial-style guide will present only high level functions, further uses of the SEMS API can be found in the
application modules that come with SEMS. It should be sufficient though to get the reader started to
programming her or his own applications and provide a basis for understanding of the other applications.

2.2 MyApp the empty template

sems-tutorial-appmyapp.tgz

empty template for c++ plugin

An application plugin's source resides in the SEMS source tree under the /apps folder. In this folder there is a
directory for each plugin. The empty, but functional template contains the following files:
myapp/MyApp.h

Header file

myapp/MyApp.cpp

C++ source file

myapp/Makefile

The Makefile

myapp/Readme

Documentation

Lets start with the Makefile:


plug_in_name = myapp
Here we define the plugin-name for the build system
module_ldflags =
module_cflags

we do not need additional compiler flags


COREPATH ?=../../core
include $(COREPATH)/plug-in/Makefile.app_module
This includes the Makefile for application modules from the core path, which will build the module for us.

Now lets move on to the Header file:


#include "AmSession.h"

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page4/22

From AmSession.h we get most of the Session related stuff


class MyAppFactory: public AmSessionFactory
{
public:
MyAppFactory(const string& _app_name);

int onLoad();
AmSession* onInvite(const AmSipRequest& req);
};
This is the session factory class for our application. It will be instantiated on startup, and at this point onLoad() is
called. Whenever an INVITE comes for this application, it is requested to react on this, and return an
AmSession object. This object is the one that will handle the call itself. As parameter we get the SIP request, so
we can act accordingly.
Lets have a look at the class we derive from AmSession that will handle our call:
class MyAppDialog : public AmSession
{
public:
MyAppDialog();
~MyAppDialog();

void onSessionStart(const AmSipRequest& req);


void onBye(const AmSipRequest& req);
};
As we see there is two event handlers: OnSessionStart, which will be called when the session starts with the
INVITE requests that started the session, and onBye, which is called if a BYE is received in the session. There
are of course more event handlers, which we'll look at later.

Now to the implementation, which is almost empty yet:


#include "MyApp.h"
#include "log.h"
we need this to display debug messages on SEMS' log (DEBUG, WARN and ERR macros)
#define MOD_NAME "myapp"

EXPORT_SESSION_FACTORY(MyAppFactory,MOD_NAME);
This is a macro so that SEMS finds the exports while loading the module
MyAppFactory::MyAppFactory(const string& _app_name)
: AmSessionFactory(_app_name)
{

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page5/22

int MyAppFactory::onLoad()
{
return 0;
}

AmSession* MyAppFactory::onInvite(const AmSipRequest& req)


{
return new MyAppDialog();
}
Here we create a new Session object and return it.
MyAppDialog::MyAppDialog()
{
}

MyAppDialog::~MyAppDialog()
{
}

void MyAppDialog::onSessionStart(const AmSipRequest& req)


{
DBG("MyAppDialog::onSessionStart: Hello World!\n");
}
Until now we don't want to do anything with the call...
void MyAppDialog::onBye(const AmSipRequest& req)
{
DBG("onBye: stopSession\n");
setStopped();
}
...only if we receive a BYE we have to stop the Session. This will stop the session thread and eventually the
Session will be removed from the SessionContainer.

How to start that example:

chdir to sems/

unpack myapp.tgz (tar xzvf myapp.tgz)

rebuild (make all)

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page6/22

restart sems

In the SEMS log you should see a line like:


(7419) DEBUG: load (AmPlugIn.cpp:115): loading lib/myapp.so ...
(7419) DEBUG: loadAppPlugIn (AmPlugIn.cpp:327): application 'myapp' loaded.

If you then route a call from ser's ser.cfg to sems using


t_write_unix("/tmp/your_sems_socket","myapp")
you should see in the logfile something like:
(7419) DEBUG: onSessionStart (MyApp.cpp:35): MyAppDialog::onSessionStart: Hello
World!
(7419) ERROR: onSipRequest (AmSession.cpp:484): missing audio input and/or ouput.
As we can see, the myapp got the call, an then SEMS complained that it does not have something to send or
record, which is perfectly ok because so far the app is empty.
If you hangup the phone, you will see something like this in the log:
(7419) DEBUG: onBye (MyApp.cpp:40): onBye: stopSession
If you do not get these log entries, there is probably something wrong with your ser configuration, or you did not
place the module in the correct path where SEMS searches for plugins, in this case have a look at your
sems.conf configuration.

2.3 MyConfigurableApp adding configurability

sems-tutorial-appmyconfigurableapp.tgz

application module with configurability

The next step is to add configurability, so we can set the file which will be played in the following chapter, which
will be a simple announcement application.
We call this application myconfigurableapp. So we have to rename the source files, the module name in the
Makefile and MOD_NAME in the source. In
SEMS has one main configuration file (sems.conf) , and each module has its own configuration file, which
usually has the name of the module plus .conf.

In the MyConfigurableAppFactory, we add a static member to remember the configuration:


static string AnnouncementFile;

We use the ConfigReader from AmConfigReader.h, which will read the config file for us:
#include "AmConfigReader.h"
When the module is loaded, we configure it in the onLoad function:

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page7/22

int MyConfigurableAppFactory::onLoad()
{
AmConfigReader cfg;
if(cfg.loadFile(AmConfig::ModConfigPath + string(MOD_NAME ".conf")))
return -1;

AnnouncementFile = cfg.getParameter("announcement_file","/tmp/default.wav");
Read the parameter, the second is the default value which will be used if the there is no configuration for
announcement_file.
if(!file_exists(AnnouncementFile)){
ERROR("announcenment file for configurableApp module does not exist
('%s').\n",
AnnouncementFile.c_str());
return -1;
}
We check whether the file exists with the file_exists function form AmUtils. If we return error (-1) here, SEMS
will not startup on failure.
return 0;
}
Return OK.
Now that this application is called myconfigurableapp and not myapp any more, we must not forget to change
the line in the ser.cfg to execute the new application:
t_write_unix("/tmp/your_sems_socket","myconfigurableapp")

2.4 MyAnnounceApp an announcement

sems-tutorial-appmyannounceapp.tgz

play an announcement from a (configurable) file

While the plugin didn't do much with the caller until now, now the time has come to let it do something: it will play
the file to the caller indicated in the configuration.
To read a file, we use AmAudioFile from AmAudio, which already does all the work for us: it opens the file, looks
for the appropriate file format and codec. We add a member wav_file to the session class as
MyAnnounceAppDialog::wav_file;

When the session starts, we open the file and set it as output:
void MyAnnounceAppDialog::onSessionStart(const AmSipRequest& req)

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page8/22

{
DBG("MyAnnounceAppDialog::onSessionStart - file is '%s'\n",
MyAnnounceAppFactory::AnnouncementFile.c_str());

if(wav_file.open(MyAnnounceAppFactory::AnnouncementFile,AmAudioFile::Read))
throw string("MyAnnounceAppDialog::onSessionStart: Cannot open file\n");
If an error has occured, the return value is != 0
setOutput(&wav_file);

}
As we maybe remember from the developersguide, each AmSession has an input and an output. Audio is read
from the output and sent via RTP, and received audio is written to the input. It is sufficient for the Session to set
the output the SEMS core does the rest.
Additionally, when all the file is played, the audio engine sends an event to the session (an AmAudio event with
the type AmAudio::cleared), and the default behaviour in this case is to stop the session, which will send a bye.
We can set anything derived from AmAudio as input or output. In the core there is several useful things already
implemented:
AmAudioFile

Write or read from or to file

AmPlaylist

Has several items in the playlist that are played sequentially.

AmConferenceChannel

Connects to a conference (a mixer of several streams)

AmAudioBridge

Connects in and out (write and read)

AmAudioDelay

Connects in and out with a delay

AmAudioQueue

Holds several items, the Audio is written/read through all of them (e.g. for putting filters beore
a conference)

For a more detailed description and examples have a look at the developersguide section
insert reference

2.5

MyJukebox - reacting to user input

sems-tutorial-appmyjukebox.tgz

react to key input

use playlist

This application shows how to react on user input via DTMF. Additionally the playlist (AmPlaylist) is introduced.
If the user presses a key while in the call, the corresponding file is added to the back of the playlist.
The MyJukeboxDialog gets an AmPlaylist as member:

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page9/22

class MyJukeboxDialog : public AmSession


{
AmPlaylist playlist;

public:
...

The playlist is initialized with the dialog object as event receiver; when the playlist is empty we will get an event
from the playlist.
MyJukeboxDialog::MyJukeboxDialog()
: playlist(this)
{
}

When the session starts, we set input and output to the playlist (well... we don't need input actually, but we'll still
set it), and enable DTMF detection:
void MyJukeboxDialog::onSessionStart(const AmSipRequest& req)
{
DBG("MyJukeboxDialog::onSessionStart - jukedir is '%s'\n",
MyJukeboxFactory::JukeboxDir.c_str());

setInOut(&playlist, &playlist);
setDtmfDetectionEnabled(true);
}
By default DTMF detection is disabled as the signal processing involved use quite some processing power.
When the caller presses a key, the onDtmf event handler is executed, where we just open the file and add it to
the playlist:
void MyJukeboxDialog::onDtmf(int event, int duration) {
DBG("MyJukeboxDialog::onDtmf, got event %d, duration %d.\n", event, duration);

AmAudioFile* wav_file = new AmAudioFile();


if(wav_file->open(MyJukeboxFactory::JukeboxDir + int2str(event) +
".wav",AmAudioFile::Read)) {
ERROR("MyJukeboxDialog::onSessionStart: Cannot open file\n");
delete wav_file;
return;
}
AmPlaylistItem*

item = new AmPlaylistItem(wav_file, NULL);

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
10/22

playlist.addToPlaylist(item);
}
We also get an event when the playlist is empty. As this is an AmAudio event, we have to filter this on the
process routine before the AmAudio base class processes the events, while not forgetting to send the rest of
the events to AmAudio::process for correct handling:
void MyJukeboxDialog::process(AmEvent* ev)
{
DBG("MyJukeboxDialog::process\n");

AmAudioEvent* audio_ev = dynamic_cast<AmAudioEvent*>(ev);


if(audio_ev && (audio_ev->event_id == AmAudioEvent::noAudio)){
DBG("MyJukeboxDialog::process: Playlist is empty!\n");
return;
}

AmSession::process(ev);
}

To try this application, we create a config file myjukebox.conf which contains for example the following:
#CFGOPTION_SEMS_MYJUKEBOX_JUKEBOXDIR
jukebox_dir=/tmp/jukebox/
#ENDCFGOPTION

and we create a directory /tmp/jukebox where we put some sound files, named 0.wav, 1.wav, 2.wav, and so on.
Even though this not that much fun like real music files, using the following line we can get the numbers as wav
files from the CVS of the 0.9 version of SEMS:
for ((f = 0; f < 10; f++)); do cvs -d
:pserver:anonymous@cvs.berlios.de:/cvsroot/sems co answer_machine/wav/$f.wav; done
; mv answer_machine/wav /tmp/jukebox; rm -rf answer_machine

2.6 Calling Card application in B2BUA mode


sems-tutorial-appcc_acc.tgz

create component module

use DI API

sems-tutorial-appmycc.tgz

use user_timer

ccard_wav.tar

B2BUA application

As next example we will use SEMS' B2BUA capabilities to create a calling card application, for example for a
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
11/22

PSTN gateway. When a user dials in, his PIN number is collected and verified, and if his credit is sufficient, he
is asked for a number to dial. With SEMS acting as Back-to-Back user agent with this number then a session is
established (the callee leg). The session is terminated when the user is out of credit. On termination, the users'
credit is reduced by the amount of time the session was established.
To test this application there is a set of wave files available with the messages to be played back to the caller, as
ccard_wav.tar. They were created with the flite speech synthesizer and are of course only for testing purposes.
We want to build the application in a modular manner: On module, called cc_acc provides the accounting,
which is used by the other one, mycc, which is the implementation of the call logic. This shows how to provide
and use a di-interface (di stands for dynamic invocation), and how to create component modules. The
application also uses the user_timer, a timer from the session_timer plugin, which has seconds granularity,
which should be enough for a calling card application.
Lets start with the accounting module cc_acc, it should provide only two functions:
/** returns credit for pin, -1 if pin wrong */
int getCredit(string pin);
/** returns remaining credit */
int subtractCredit(string pin, int amount);

The implementation of these functions is straightforward, we simply use a singleton CCAcc, in which we have a
map to hold the credits. As an example we add a credit for 12345 on startup.
As every class that provides a di-interface, the class must implement AmDynInvoke (from AmApi.h), which
consists of the function invoke(method, args, ret):
#include "AmApi.h"

/**
* accounting class for calling card.
* this illustrates the DI interface
* and component modules
*/
class CCAcc : public AmDynInvoke

{
/** returns credit for pin, -1 if pin wrong */
int getCredit(string pin);
/** returns remaining credit */
int subtractCredit(string pin, int amount);

map<string, unsigned int> credits;


// as this is used from various sessions,
// it must be protected by a mutex
AmMutex credits_mut;
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
12/22

static CCAcc* _instance;


public:
CCAcc();
~CCAcc();
static CCAcc* instance();
void invoke(const string& method, const AmArgArray& args, AmArgArray& ret);
};

To use the API , the caller gets the DI Interface and calls invoke with the function name as the first parameter,
the arguments as the second, and an empty array for return values as the third. The AmArgArray type is a
dynamic array that can hold entries of types int, double or c string.
In the implementation of the invoke function we check the function name and call the appropriate function:
void CCAcc::invoke(const string& method, const AmArgArray& args, AmArgArray& ret)
{
if(method == "getCredit"){
ret.push(getCredit(args.get(0).asCStr()));
}
else if(method == "subtractCredit"){
ret.push(subtractCredit(args.get(0).asCStr(),
args.get(1).asInt()));
}
else
throw AmDynInvoke::NotImplemented(method);
}

To export the interface, so that the plugin-loader knows that we export a DI-API from the plugin, we must export
a class that implements AmDynInvokeFactory, which is the factory class for instances of the DI-interface. In our
case we have the implementation of the accounting as singleton as well so we just give out the same
CCAcc::instance every time:
class CCAccFactory : public AmDynInvokeFactory
{
public:
CCAccFactory(const string& name)
: AmDynInvokeFactory(name) {}

AmDynInvoke* getInstance(){
return CCAcc::instance();

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
13/22

int onLoad(){
DBG("CCAcc calling card accounting loaded.\n");
return 0;
}
};
As every plugin factory (AmDynInvokeFactory is a AmPluginFactory), it also has a function onLoad().
To export the CCAccFactory for the plugin loader, we use
EXPORT_PLUGIN_CLASS_FACTORY(CCAccFactory,"cc_acc");
The second parameter is the interface factory name, which is used by the user of the DI-API.

Lets now see how we use the DI API, in the application mycc (apps/MyCC.cpp and apps/MyCC.h). In the
SessionFactory we get a Factory for the Interface, in
int MyCCFactory::onLoad()

...
cc_acc_fact = AmPlugIn::instance()->getFactory4Di("cc_acc");
if(!cc_acc_fact){
ERROR("could not load cc_acc accounting, please provide a module\n");
return -1;
}
...

This Factory, AmDynInvokeFactory* cc_acc_fact, is later used to get a DI interface, which we pass to the
CCDialog class (the session class):
AmSession* MyCCFactory::onInvite(const AmSipRequest& req)
{
...
AmDynInvoke* cc_acc = cc_acc_fact->getInstance();
if(!user_timer){
ERROR("could not get a cc acc reference\n");
throw AmSession::Exception(500,"could not get a cc acc reference");
}

return new MyCCDialog(cc_acc, user_timer);


}
We get a user_timer DI interface as well and pass it to the CCDialog class. We will use the user_timer, which
is a timer with seconds granularity provided by the session_timer plugin, to limit the call time to the maximum
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
14/22

available credit.
To use the DI interface we fill the args with the arguments, call the function with the appropriate name, and read
the result from the return value array, as here in MyCCDialog::onDtmf:
AmArgArray di_args,ret;
di_args.push(pin.c_str());
cc_acc->invoke("getCredit", di_args, ret);
credit = ret.get(0).asInt();
Now to the call control class, the MyCCDialog. As we want to get into B2BUA mode later, we derive the Call
control class from AmB2BCallerSession. A AmB2BCallerSession behaves like a normal AmSession, if
sip_relay_only == false. We create another leg of the call, the so-called callee leg, with the function
connectCallee, which will set sip_relay_only to true, such that all events (SIP requests and replys) coming from
the caller will be relayed to the other leg to the callee leg.
A AmB2BCallerSession also has two more callbacks: onOtherReply and onOtherBye. onOtherReply is
called when there is a reply in the callee leg; if we sense this we can start the accounting when the callee is
connected:
void MyCCDialog::onOtherReply(const AmSipReply& reply) {
DBG("OnOtherReply \n");
if (state == CC_Dialing) {
if (reply.code < 200) {
DBG("Callee is trying... code %d\n", reply.code);
} else if(reply.code < 300){
if (getCalleeStatus()

== Connected) {

state = CC_Connected;
start the accounting:
startAccounting();
set our input and output to NULL such that we don't send RTP to the caller:
setInOut(NULL, NULL);
set the call timer from the session timer, this will post us an event on timeout:
// set the call timer
AmArgArray di_args,ret;
di_args.push(TIMERID_CREDIT_TIMEOUT);
di_args.push(credit); // in seconds
di_args.push(dlg.local_tag.c_str());
user_timer->invoke("setTimer", di_args, ret);
}
We can get the call back and start again with collecting the number if the callee could not be reached:
} else {
DBG("Callee final error with code %d\n",reply.code);
addToPlaylist(MyCCFactory::DialFailed);

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
15/22

number = "";
state = CC_Collecting_Number;
}
}
// we dont call
//

AmB2BCallerSession::onOtherReply(reply);

// as it tears down the call if callee could


// not be reached
}

By sensing onOtherBye we can stop the accounting when the other side hangs up:
void MyCCDialog::onOtherBye(const AmSipRequest& req) {
DBG("onOtherBye\n");
stopAccounting();
AmB2BCallerSession::onOtherBye(req); // will stop the session
}
We also stop the accounting when we receive a BYE from the caller leg:
void MyCCDialog::onBye(const AmSipRequest& req)
{
DBG("onBye: stopSession\n");
if (state == CC_Connected) {
stopAccounting();
}
terminateOtherLeg();
setStopped();
}
Our implementation of the calling card service logic uses a state machine with four states, Collecting_PIN,
Collecting_ Number, CC_Dialing and CC_Connected. Depending on this state we add the number to the PIN or
the Number in onDtmf, or we do connectCallee.
void MyCCDialog::onDtmf(int event, int duration) {
DBG("MyCCDialog::onDtmf, got event %d, duration %d.\n", event, duration);

switch (state) {
...
case CC_Collecting_Number:
if(event <10) {
number +=int2str(event);
DBG("number is now '%s'\n", number.c_str());
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
16/22

} else {
if (getCalleeStatus() == None) {
state = CC_Dialing;
connectCallee(number+MyCCFactory::ConnectSuffix,
"sip:"+number+MyCCFactory::ConnectSuffix);
addToPlaylist(MyCCFactory::Dialing);
}
}

break;

If there is a timeout of the timer we set before using the user_timer API, we get an event in our event queue.
This event is of the type AmPluginEvent, and has name==timer_timeout . So in MyCCDialog::process we filter
out this event, and tear down the call:
void MyCCDialog::process(AmEvent* ev)
{
...
AmPluginEvent* plugin_event = dynamic_cast<AmPluginEvent*>(ev);
if(plugin_event && plugin_event->name == "timer_timeout") {
int timer_id = plugin_event->data.get(0).asInt();
if (timer_id == TIMERID_CREDIT_TIMEOUT) {
DBG("timer timeout: no credit...\n");
stopAccounting();
terminateOtherLeg();
terminateLeg();

ev->processed = true;
return;
}
}

AmB2BCallerSession::process(ev);
}

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
17/22

3 Writing an IVR application


3.1 Ivr Overview
With the ivr plugin, applications can be written as a Python script which will be executed in a python interpreter
embedded in the ivr module. On SEMS startup, when the ivr is loading, it loads all scripts that are in the path
script_path, a configuration parameter of the ivr plugin, and registers the applications with the core. If for
example the script mailbox is loaded, the ivr registers the application name mailbox to this script, and if an
INVITE is passed to SEMS with mailbox as application name, the application will be executed.
The script itself is executed on startup of the script. Every application script has to define one class derived from
IvrDialogBase. An instance of this class will be created if a call is received for the application, and its
onSessionStart will be executed. IvrDialogBase also has other event handlers: onDtmf, onBye,
onEmptyQueue.
The IvrDialogBase class has as attribute the SEMS' representation of the SIP dialog class, named
IvrDialogBase::dialog, which contains to, from etc.
There is a dictionary config, which holds the configuration for the application.
With the methods stopSession and bye, the call can be ended or a bye sent.
IvrDialogBase already has a playlist integrated. From the python script, the function enqueue(AmAudio) can be
used to put an entry in the playlist, for example an IvrAudioFile, which is already implemented in the ivr library.
flush empties the playlist, while mute/unmute control whether the session is muted.
If key presses should be received, enableDTMFDetection should be called. If a keypress is received, the
onDtmf event handler is executed. disableDTMFDetection should be called if key presses are ignored, as the
processing takes some CPU power.
The IvrDialogBase also has the user_timer integrated; it is used with functions setTimer, clearTimer,
clearTimers, and the onTimer callback.
The ivr can be used in B2BUA mode. In B2BUA mode the first leg of the call is called CallerLeg, and the second
one CalleeLeg. connectCallee connects another leg, and terminateOtherLeg terminates it, while terminateLeg
tears down the caller leg itself (the script is running in the CallerLeg). setRelayonly controls whether SEMS
should only relay SIP events or react to them itself.
In the log library the logging functions known from SEMS are defined: error, warn, and debug. They will log
using SEMS' logging facility.

3.2 Simple announcement script

sems-tutorial-appivr_announce.tgz

use playlist for announcement

configurable announcement file

A basic ivr script which plays back a file configured as announcement configuration parameter is a twelve
liner:
from log import *
from ivr import *

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
18/22

class IvrDialog(IvrDialogBase):
announcement = None

def onSessionStart(self, hdrs):


debug("onSessionStart of ivr announcement app")
self.announcement = IvrAudioFile()
self.announcement.open(config['announcement'], ivr.AUDIO_READ)
self.enqueue(self.announcement, None)

def onEmptyQueue(self):
self.bye()
self.stopSession()

onEmptyQueue is called every time that the playlist is empty, and then we have to end the call ourselves.

3.3 RFC 4240 announcement service

sems-tutorial-appannc_service.tgz

process request uri

react on empty playlist

use user_timers

This script implements announcement service following rfc4240. The file to be played back is encoded in the
request uri of the INVITE as parameter play (e.g. INVITE
sip:annc@mediaserver.net:5060;play=http://mediaserver.net/example.wav). It can be a remote (http uri) or a
local file. The script uses urlparse and urllib libraries to parse the parameter and get the file.
Additional optional parameters are the maximum play time, the repeat count and delay between two repeats.
The script uses two timers from user_timer (session_timer plugin) to control this. Every timer has an ID, which
must be >0. This timer id is used when setting the timer with the setTimer method, and on timeout the onTimer
method is called with the corresponding timer ID. The timers have seconds granularity so the second parameter
to the setTimer function is the timeout in seconds.
We react to onEmptyQueue (empty playlist) and onTimer events, by hanging up, setting the timer or rewinding
and enqueuing the file once more.
# Simple implementation of (a part) of RFC4240
# announcement service.
#
# supported parameters:
#

play, repeat, duration, delay

from log import *


ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
19/22

from ivr import *

from urlparse import urlparse, urlsplit


from urllib import urlretrieve
from os import unlink

TIMEOUT_TIMER_ID = 1
DELAY_TIMER_ID

= 2

class IvrDialog(IvrDialogBase):

announcement=None
filename = ""
repeat="1"
delay=0
duration=-1
play=""
delete_onbye = False
repeat_left = 0

def onSessionStart(self,hdrs):

debug("configuration: %s" % repr(config))


debug("local_uri = " + self.dialog.local_uri);
# we use urlsplit as urlparse only returns the
# parameters of the last path
params = urlsplit(self.dialog.local_uri)[2].split(";")
debug("parameters are " + str(params))
for param in params[0:len(params)]:
if (param.startswith("play=")):
self.play=param[5:len(param)]
elif (param.startswith("repeat=")):
self.repeat=param[7:len(param)]
elif (param.startswith("delay=")):
self.delay=int(param[6:len(param)])
elif (param.startswith("duration=")):
self.duration=int(param[9:len(param)])

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
20/22

resource = urlparse(self.play)
if (resource[0] == "http"):
self.delete_onbye = True
self.filename = urlretrieve(self.play)[0]

debug("play: "+self.play+" repeat: "+self.repeat+" delay:"+


str(self.delay)+" duration: "+str(self.duration))
self.announcement = IvrAudioFile()
self.announcement.open(self.filename,AUDIO_READ)
if (self.repeat!="forever"):
self.repeat_left=int(self.repeat)-1
else:
self.repeat_left=500 # maximum

if (int(self.duration) > 0):


self.setTimer(TIMEOUT_TIMER_ID, self.duration/1000)

self.enqueue(self.announcement, None)

def onBye(self):
self.stopSession()
self.cleanup()

def onEmptyQueue(self):
if (self.repeat_left>0):
if (int(self.delay) > 0):
self.setTimer(DELAY_TIMER_ID, int(self.delay)/1000)
else:
self.repeat_left-=1
self.announcement.rewind()
self.enqueue(self.announcement, None)
else:
self.bye()
self.stopSession()
self.cleanup()

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
21/22

def onDtmf(self,key,duration):
pass

def onTimer(self, timer_id):


if (timer_id == TIMEOUT_TIMER_ID):
self.bye()
self.stopSession()
self.cleanup()
elif (timer_id == DELAY_TIMER_ID):
self.repeat_left-=1
self.announcement.rewind()
self.enqueue(self.announcement, None)

def cleanup(self):
if (self.delete_onbye):
unlink(self.filename)
debug("cleanup..." + self.filename + " deleted.")
self.removeTimers()

References

ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de

Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1

Page
22/22

You might also like