Tuesday, February 3, 2009

Rich Telco Applications with Seam

UPDATE: The post was edited for clarity.

The Mobicents Seam-based Sip Servlets framework was extended with Media functions and now goes beyond SIP to unify the component models for Telco and Web applications. With this move, we are addressing the need to build quickly Media-intensive applications like PBX, conferencing, Interactive Voice Response (IVR), transaction confirmation and others.

With the help of the Seam development tools these applications can be written and tested in no time, so even if you don't want to use the Seam model in your production applications, you will find it useful for smaller "disposable" applications, or for rapid prototyping and proof-of-concept applications.

Need a conferencing application?

@Name("conference")
@Scope(ScopeType.STATELESS)
public class Conference {
@Logger Log log;
@In MediaController mediaController;
@In SipSession sipSession;

@In(scope=ScopeType.APPLICATION, required=false)
@Out(scope=ScopeType.APPLICATION, required=false)
String conferenceEndpointName;

@Observer("INVITE")
public void doInvite(SipServletRequest request) throws Exception {
String sdp = new String((byte[]) request.getContent());
request.createResponse(180).send();
sipSession.setAttribute("inviteRequest", request);
if (conferenceEndpointName == null)
conferenceEndpointName = "media/trunk/Conference/$";
mediaController.createConnection(conferenceEndpointName).modify("$",
sdp);
}

@Observer("connectionOpen")
public void doConnectionOpen(MsConnectionEvent event) throws IOException {
conferenceEndpointName = event.getConnection().getEndpoint()
.getLocalName();
SipServletRequest request = (SipServletRequest) sipSession
.getAttribute("inviteRequest");
SipServletResponse response = request.createResponse(200);
response.setContent(event.getConnection().getLocalDescriptor(),
"application/sdp");
response.send();
}

@Observer( { "BYE", "REGISTER" })
public void sayOK(SipServletRequest request) throws Exception {
request.createResponse(200).send();
}

}

That's all. No cheating.

What is happening here is that when an INVITE comes (i.e. when you dial the server), we connect the call to a conference endpoint in Mobicents Media Server. If this is the first call, the conference endpoint is not assigned yet (null) and we allocate a new endpoint (with .../Conference/$) for the application. Once the connection is established, the connectionOpen event occurs, then we store the conference endpoint name at the application scope and when the subsequent calls in this application see it they will connect to the same endpoint. We can easily extend this application to work with multiple conferences or add other media functionality. All SIP and Media events occur in a SIP Session, which is basically one call from one user (or one phone). If you want to share data in the SIP session scope simply inject the SipSession as shown in the application or create a SESSION-scoped Seam component. This conference example is available here. If you want to understand Mobicents Media Server and the MSC API you should read the Mobicents Media Server Guide.

In addition to the basic media support we are aiming to simplify the most common use-cases for media applications. While developing media applications I noticed that one SIP call is always constructed like this:

or the more simple chain:

In both chains the call is terminated at some media endpoint - IVR or Conference or an Announcement endpoint (which is not shown in the diagrams). After the call is established, the user agent is connected to exactly one media endpoint at any time, either directly or through a Packet Relay endpoint. This means that we can safely store all these links and endpoints related to the call right into the SIP session and never worry again about how to pass them to another method or component. If you need to switch to another chain just keep the session data updated. That's why we are looking into reserving a special place in the SIP session for the media objects.

To understand it better let's look at another example:
@Name("mediaFrameworkDemo")
@Scope(ScopeType.STATELESS)
public class MediaFrameworkDemo {
@Logger Log log;
@In MediaController mediaController;
@In SipSession sipSession;
@In MediaSessionStore mediaSessionStore;
@In ConnectionIVRHelper connectionIVRHelper;
@In MediaEventDispatcher mediaEventDispatcher;

@In(scope=ScopeType.APPLICATION, required=false)
@Out(scope=ScopeType.APPLICATION, required=false)

String conferenceEndpointName;

private final String announcement =
"http://mobicents.googlecode.com/svn/branches/servers/media/1.x.y/examples/" +
"mms-demo/web/src/main/webapp/audio/welcome.wav";

@Observer("INVITE")
public void doInvite(SipServletRequest request) throws Exception {
// Extract SDP from the SIp message
String sdp = new String((byte[]) request.getContent());

// Tell the other side to ring (status 180)
request.createResponse(SipServletResponse.SC_RINGING).send();

// Store the INVITE request in the sip session
sipSession.setAttribute("inviteRequest", request);

// If this is the first INVITE in the app, then we must start a new conference
if (conferenceEndpointName == null)
conferenceEndpointName = "media/trunk/Conference/$";

// Create a connection between the UA and the conference endpoint
mediaController.createConnection(conferenceEndpointName).modify("$",
sdp); // also updates the SDP in Media Server to match capabilities of UA
}

@Observer("connectionOpen")
public void doConnectionOpen(MsConnectionEvent event) throws IOException {
// Save this connection where the framework can read it
mediaSessionStore.setMsConnection(event.getConnection());

// The conference endpoint is now assiged after we are connected, so save it too
conferenceEndpointName = event.getConnection().getEndpoint()
.getLocalName();

// Recall the INVITE request that we saved in doInvite
SipServletRequest request = (SipServletRequest) sipSession
.getAttribute("inviteRequest");

// Make OK (status 200) to tell the other side that the call is established
SipServletResponse response = request.createResponse(SipServletResponse.SC_OK);

// Put the SDP inside the OK message to tell what codecs and so on we agree with
response.setContent(event.getConnection().getLocalDescriptor(),
"application/sdp");

// Now actually send the message
response.send();

// And start listening for DTMF signals
connectionIVRHelper.detectDtmf();
}

@Observer("DTMF")
public void dtmf(String button) {
// If the other side presses the button "0" stop the playback
if("0".equals(button)) {
connectionIVRHelper.endAll();
} else {
// otherwise play announcement
connectionIVRHelper.playAnnouncementWithDtmf(announcement);
}
// Also log the DTMF buttons pressed so far in this session
log.info("Current DTMF Stack for the SIP Session: "
+ mediaEventDispatcher.getDtmfArchive(sipSession));
}

// Just say OK to these messages.
@Observer( { "BYE", "REGISTER" })
public void sayOK(SipServletRequest request) throws Exception {
request.createResponse(200).send();

// And clean up the connections
MsConnection connection = mediaSessionStore.getMsConnection();
connection.release();
}

}

This is almost the same conference application with some IVR capabilities and some comments between the lines. The MediaSessionStore stores the call-related media objects (the MsConnection in this case) and ConnectionIVRHelper will read it from there when it is doing DTMF detection or playing announcement.

Once you are connected to the conference the application works like this - when you press a button 1-9 it will play a personal announcement (only the user who pressed the button can hear it). If the users presses "0" the announcement will be stopped. Note that MediaSessionStore, MediaEventDispatcher and ConnectionIVRHelper are not part of the framework right now, but you can copy and paste them into your own application from the media framework demo application in SVN (the discussed example). Eventually the classes from the org.mobicents.servlet.sip.seam.session.framework package will be moved into the main framework jar.

Development


If you've read the previous post about the Seam framework, you would already know that these applications can use almost all Seam features including hot-deployment and JBoss Tools-assisted development. Note that you don't need JBoss Tools, you can use your own IDE or no IDE with Ant or Maven or whatever you want.

Here is what you need to get started with JBoss Tools:
  • Mobicents Sip Servlets 0.7.2 with JBoss AS 4.2.3 or Mobicents Sip Servlets 0.8 with JBoss AS 4.2.3 (please do not use it with JBoss AS 5.0 for now, since the Media support there is still in technology preview stage)
  • Install the latest nightly build of JBoss Tools in Eclipse 3.4 (this is the update site) - you need JBoss Seam, JBoss AS Tools and Richfaces plug-ins as minimum. You must use the nightly builds for Seam 2.1 support. Soon, a new JBoss Developer Studio will be released with official Seam 2.1 support.
  • Get Seam 2.1.1.GA and configure it as Seam runtime in JBoss Tools when asked.
Once you create a Seam 2.1 project you can extend it with the Telco framework by following these steps:
<?xml version="1.0" encoding="UTF-8"?>

<sip-app>

<app-name>PUT_SOME_APPLICATION_NAME_HERE</app-name>
<display-name>SeamEntryPointApplication</display-name>
<description>SeamEntryPointApplication</description>

<main-servlet>
SeamEntryPointServlet
</main-servlet>

<servlet>
<servlet-name>SeamEntryPointServlet</servlet-name>
<display-name>SeamEntryPointServlet</display-name>
<description>Seam Entry Point Servlet</description>
<servlet-class>
org.mobicents.servlet.sip.seam.entrypoint.SeamEntryPointServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<listener>
<listener-class>
org.mobicents.servlet.sip.seam.entrypoint.SeamEntryPointServlet
</listener-class>
</listener>

</sip-app>
  • Now you are done. Just start adding Seam components.
Another way to get started is to checkout one of the examples mentioned above and play with them without IDE support (but it requires Maven).


Some guidelines
  • Do not subscribe methods to SIP and media events in Seam components with SESSION or CONVERSATION scopes! The reason is that each of these SESSION or CONVERSATION scoped components is likely to have multiple instances (depending on the number of the sessions) and they all will be called, which is probably not what you want.
  • Always subscribe public methods to SIP and media events. Any other access modifier will cause you method not to be called.
  • When dealing with JPA, always use your own EntityManager. Either EVENT or METHOD scoped or manage it manually through the EntityManagerFactory. The default CONVERSATION-scoped entityManager might produce "EntityManager closed" errors.
  • When initiating a SIP request from a Web session, do it in another thread! Seam uses thread-local storage and the Web contexts will collide with the SIP contexts. We are working on solving this issue and will probably be addressed in the future.
  • Keep in mind that outjection occurs at the end of a method call. If you attempt to use an outjected variable from a nested method call, it will fail.
Any feedback or contributions are welcome!