Integrating WebRTC with PSTN using OpenTok & Nexmo SIP

Integrating WebRTC with PSTN using OpenTok & Nexmo SIP

In this blog we look at how to connect OpenTok Live Video sessions with traditional PSTN phone calls. We will demonstrate how to connect an OpenTok session to PSTN with an audio stream that connects through OpenTok SIP Interconnect to a Nexmo SIP-PSTN Gateway.

OpenTok SIP Interconnect is a general purpose SIP capability that can be used to connect to many different kinds of gateway or other SIP systems. TokBox is now part of Vonage, so in this blog we will use our own Nexmo programmable communications APIs to bridge the call.

We’ve created a sample application on GitHub in the opentok-sip-samples repo. The Nexmo SIP dial out sample application leverages the OpenTok Node Server SDK which allows you to create sessions, generate tokens, dial out to SIP endpoints, force disconnect clients, and much more. In this application, we will also be using Express, a Node.js framework, to create our own app server along with JavaScript on the client side for the web app.

SIP Integration Overview

We first use client-side JavaScript to communicate with the app server through HTTP requests to get session credentials. In this example we are using JavaScript – in a native iOS or Android client app you may do this in another language. The sessionId is fetched from OpenTok and the token is then generated on the app server. We then use those credentials on the client side to initialize and connect to the an OpenTok Session. After connecting successfully, we create and publish the stream. We also set event listeners so we can subscribe to any stream that is created. After publishing the WebRTC audio and/or video stream, we can then dial out to a SIP URI using OpenTok SIP Interconnect. OpenTok takes care of the dial out and forwards the stream to Nexmo which then dials out to the PSTN user.

Sample Code

Let’s take a dive into the code so that you can also build this application.

Before you start, please make sure you have these installed on your machine:

To get started, clone the the opentok-sip-samples repo and change directory to Nexmo-SIP-Dial-Out.

As you can see below, in the opentok.js file, located in the js folder, we initialize a session by calling the initSession method on the OT object. We then set event listeners on the session object for streamCreated and streamDestroyed where we subscribe to a stream when it’s created and print a message when it’s destroyed. After setting the event listeners, we connect to the session by passing in the token and an error handler to make sure there weren’t any errors. If there is no error, we proceed to creating a publisher and publishing.

const session = OT.initSession(apiKey, sessionId);

session.on({
  streamCreated: (event) => {
     const subscriberClassName = `subscriber-${event.stream.streamId}`;
     const subscriber = document.createElement('div');
     subscriber.setAttribute('id', subscriberClassName);
     document.getElementById('subscribers').appendChild(subscriber);
     session.subscribe(event.stream, subscriberClassName);
   },
  streamDestroyed: (event) => {
     console.log(`Stream ${event.stream.name} ended because ${event.reason}.`);
   },
});

session.connect(token, (error) => {
  if (error) {
     console.log('error connecting to session');
   } else {
     const publisher = OT.initPublisher('publisher');
     session.publish(publisher);
   }
});

The index.ejs file in the views folder is simply creating a few buttons which either make a fetch request to the app server to dial out or hang up. You can find more details on this here.

Let’s move on to the app server where we will create a few endpoints to render our index.ejs page, dial out to the SIP URI, and hang up.

First off, we import all of the dependencies that you need: express, body-parser, opentok, etc.

Then we will create a /room/:rid endpoint which will dynamically create sessions and tokens based on the rid parameter:

app.get('/room/:rid', (req, res) => {
  const roomId = req.params.rid;
  if (app.get(roomId)) {
     const sessionId = app.get(roomId);
     const token = generateToken(sessionId);
     renderRoom(res, sessionId, token, roomId);
   } else {
     setSessionDataAndRenderRoom(res, roomId);
   }
});

As you can see above, we either render the index.ejs with the existing sessionId in memory or create a session and then render the index.ejs page.

After this, we will create the dial-out endpoint which will allow us to make the dial out call to OpenTok:

app.get('/dial-out', (req, res) => {
  const { roomId, phoneNumber } = req.query;
  const sipTokenData = `{"sip":true, "role":"client", "name":"'${phoneNumber}'"}`;
  const sessionId = app.get(roomId);
  const token = generateToken(sessionId, sipTokenData);
  const options = setSipOptions();
  const sipUri = `sip:${phoneNumber}@sip.nexmo.com`;
  OT.dial(sessionId, token, sipUri, options, (error, sipCall) => {
     if (error) {
       res.status(400).send('There was an error dialing out');
     } else {
       app.set(phoneNumber, sipCall.connectionId);
       res.json(sipCall);
     }
   });
});

Here we generate the SIP URI based on the phone number that we receive from the client side application and also add the phone number as a part of the token data. We also use the options parameter to set the SIP options which include the Nexmo API Key and API Secret set as the username and password, respectively. These are crucial for authenticating for Nexmo’s APIs. If the dial out is successful, we can then set the phone number and SIP connectionId in memory.  Please keep in mind that we’re using memory because it’s a sample application. In production, you should use a database for the mapping.

Finally, let’s create an endpoint for hanging up the call to the PSTN user:

app.get('/hang-up', (req, res) => {
  const { roomId, phoneNumber } = req.query;
  const connectionId = app.get(phoneNumber);
  if (app.get(roomId)) {
    const sessionId = app.get(roomId);
    OT.forceDisconnect(sessionId, connectionId, (error) => {
       if (error) {
         res.status(400).send('There was an error hanging up');
       } else {
         res.status(200).send('Ok');
       }
     });
   } else {
     res.status(400).send('There was an error hanging up');
   }
});

The hang-up endpoint invokes the forceDisconnect method on the OpenTok Node SDK and passes in the sessionId and the connectionId of the PSTN user. This action then disconnects the PSTN user from the OpenTok session.

Conclusion – OpenTok and PSTN Connected

In this blog, we’ve covered the important concepts required to connect an OpenTok session through a SIP gateway to a PSTN user. To see the full code please refer to the opentok-sip-samples repo, and also sign up for my upcoming webinar on SIP Interconnect and Nexmo on September 26th to learn more.