https://d226lax1qjow5r.cloudfront.net/blog/blogposts/connecting-webrtc-and-pstn-with-opentok-and-nexmo-dr/opentok-sip.png

Conexión de WebRTC y PSTN con OpenTok y Nexmo

Publicado el May 13, 2021

Tiempo de lectura: 5 minutos

En Nexmo, hemos anunciado recientemente SIP Connect que permite conectar terminales WebRTC con la Voice API de Nexmo. Esta característica hace posible que los usuarios PSTN marquen en una sesión de Video OpenTok.

En este post, vamos a construir una aplicación web de video en tiempo real usando OpenTok y conectar usuarios PSTN con ella usando SIP Connect y el Voice API.

Requisitos previos

Antes de empezar, asegúrate de que tienes lo siguiente:

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Primeros pasos

Por favor, crea un proyecto API TokBox porque necesitarás un proyecto apiKey y apiSecret para añadir la voz y el Video en tiempo real a la aplicación web. Además de las credenciales de TokBox, necesitarás crear una aplicación Nexmo Voice y establecer webhooks de eventos y respuestas.

No te preocupes por cuáles son ahora porque te los iremos explicando a medida que trabajemos con ellos. Por último, tendrás que comprar un número virtual Nexmo y desviar todas las llamadas entrantes al número a la aplicación que has creado.

Visión general

architecture

Código de muestra

Para empezar, clone el archivo opentok-nexmo-sip y cambie a la carpeta Dial-In-Conference directorio.

En el directorio del proyecto, verá un archivo config.example.js archivo. Por favor, siga adelante y copie el contenido de este archivo en un nuevo archivo llamado config.js.

Asegúrate de añadir las credenciales de TokBox y Nexmo que generaste antes al archivo config.js porque las usaremos para la aplicación.

Código cliente

En este caso, estamos utilizando JavaScript para la web, pero puede utilizar los mismos conceptos con el OpenTok iOS, Androidy Windows SDKs.

Como puede ver a continuación, en el archivo opentok.js ubicado en la carpeta public/js inicializamos una sesión llamando al método initSession en el objeto OT objeto. A continuación creamos un objeto objeto Editor utilizando el método initPublisher método

A continuación, procedemos a configurar los siguientes eventos de sesión:

  • streamCreated

  • streamDestroyed

  • sessionConnected

Estos eventos se activan cuando se crea un flujo, se destruye un flujo o cuando el cliente se conecta a la sesión, respectivamente.

Después de configurar los escuchadores de eventos, nos conectamos a la sesión pasando el token y un manejador de errores. El manejador de errores se utiliza para asegurarse de que no hubo ningún error al intentar conectarse a la sesión. En nuestra aplicación, si hay un error, lo registramos en la consola, pero en una aplicación de producción, debemos mostrar un elemento de interfaz de usuario y tratar de volver a conectar.

const session = OT.initSession(apiKey, sessionId);
const publisher = OT.initPublisher('publisher');
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}.`);
 },
 sessionConnected: (event) => {
   session.publish(publisher);
 },
});

session.connect(token, (error) => {
 if (error) {
   console.log('error connecting to session');
 }
});

Este archivo opentok.js se importa en la vista situada en views/index.ejs archivo.

Además de este código, también creamos un par de botones que activan peticiones API al servidor de la aplicación para marcar vía SIP.

En esta aplicación, esta vista está siendo renderizada por nuestro servidor, pero puedes elegir renderizarla como quieras. Para ver el código en el que se representa esta vista, consulte el siguiente enlace.

Código del servidor

Ahora que tenemos nuestro cliente configurado, vamos a ver el código del código del servidor.

Notarás que estamos importando express, opentoky body-parser paquetes. Estamos usando Express.js para nuestro servidor, el OpenTok Node SDKy el analizador del cuerpo que se utilizará para analizar el cuerpo de las solicitudes entrantes.

Tenga en cuenta que también estamos importando el config que establecimos en el archivo config.js archivo.

A continuación, pasamos a crear los siguientes puntos finales:

  • /room/:roomId

  • /dial-out

  • /hang-up

  • /nexmo-answer

  • /nexmo-dtmf

  • /nexmo-events

La ruta /room/:roomId es la ruta principal de nuestra aplicación. Renderiza la index.ejs con las credenciales OpenTok apropiadas. Cuando esto sucede, el servidor de aplicaciones hace una petición a OpenTok para crear una sesión que luego responde con un objeto de sesión que contiene el archivo sessionId.

La dirección sessionId se utiliza para generar un token OpenTok. Luego generamos un código pin de 4 dígitos y lo asignamos al sessionId y el nombre de la habitación. Esto es importante porque lo necesitaremos para buscar el sessionId cuando el usuario PSTN marque. También hemos añadido algo de lógica para poner a los usuarios en la misma sesión cuando hacen una solicitud con el mismo roomId.

/**
* When the room/:roomId request is made, either a template is rendered is served with the
* sessionid, token, pinCode, roomId, and apiKey.
*/

app.get('/room/:roomId', (req, res) => {
  const { roomId } = req.params;
  let pinCode;
  if (app.get(roomId)) {
    const sessionId = app.get(roomId);
    const token = generateToken(sessionId);
    pinCode = app.get(sessionId);
    renderRoom(res, sessionId, token, roomId, pinCode);
  } else {
    pinCode = generatePin();
    OT.createSession({
      mediaMode: 'routed',
    }, (error, session) => {
      if (error) {
        return res.send('There was an error').status(500);
      }
      const { sessionId } = session;
      const token = generateToken(sessionId);
      app.set(roomId, sessionId);
      app.set(pinCode, sessionId);
      renderRoom(res, sessionId, token, roomId, pinCode);
    });
  }
});

Marcación de salida

Para llamar al punto final SIP, el navegador hará una petición al punto final /dial-out y el servidor generará un token y usará nuestras credenciales Nexmo (API Key y API Secret) junto con la uri SIP (sip:lvn@sip.nexmo.com) para hacer una petición a OpenTok para marcar la sesión. Si esto tiene éxito, obtenemos información de conexión a través de la devolución de llamada para los participantes SIP.

/**
* When the dial-out get request is made, the dial method of the OpenTok Dial API is invoked
*/

app.get('/dial-out', (req, res) => {
  const { roomId } = req.query;
  const { conferenceNumber } = config;
  const sipTokenData = `{"sip":true, "role":"client", "name":"'${conferenceNumber}'"}`;
  const sessionId = app.get(roomId); // grabbing the sessionId from the mapping we created earlier
  const token = generateToken(sessionId, sipTokenData);
  const options = setSipOptions();
  const sipUri = `sip:${conferenceNumber}@sip.nexmo.com;transport=tls`;
  OT.dial(sessionId, token, sipUri, options, (error, sipCall) => {
    if (error) {
      res.status(500).send('There was an error dialing out');
    } else {
      app.set(conferenceNumber + roomId, sipCall.connectionId);
      res.json(sipCall);
    }
  });
});

Al llamar a la Voice API de Nexmo a través de SIP se activará el evento evento webhook, /nexmo-events en nuestro caso, para cualquier cambio de estado, es decir started, ringingetc.

app.get('/nexmo-events', (req, res) => {
  console.log('call event', req.query);
  res.status(200).send();
});

Además de la url del evento, un webhook de respuesta para que el servidor de aplicaciones pueda indicar a Voice API qué hacer con esa llamada.

La Voice API espera esto en el formato de un NCCOun objeto JSON en el que el servidor de aplicaciones especifica la(s) acción(es).

En nuestro caso, el servidor de aplicaciones especificará el icono action como conversation y utilizará sessionId como nombre del archivo conversation. Determinamos el sessionId utilizando las cabeceras SIP que OpenTok añade al marcar o a través del código pin de cuatro dígitos que el usuario introduce mediante DTMF.

app.get('/nexmo-answer', (req, res) => {
  const { serverUrl } = config;
  const ncco = [];
  if (req.query['SipHeader_X-OpenTok-SessionId']) {
    ncco.push({
      action: 'conversation',
      name: req.query['SipHeader_X-OpenTok-SessionId'],
    });
  } else {
    ncco.push(
      {
        action: 'talk',
        text: 'Please enter a a pin code to join the session'
      },
      {
        action: 'input',
        eventUrl: [`${serverUrl}/nexmo-dtmf`]
      }
    )
  }

  res.json(ncco);
});

En el código anterior, tenemos una declaración condicional que comprueba las cabeceras para coger el archivo sessionId. En este caso, cuando no hay ninguna cabecera SIP, podemos asumir que es un usuario PSTN marcando para que podamos pedirle un pin. Hacemos esto estableciendo nuestro action a talk con algo de text. El servidor de aplicaciones también indica a la Voice API que queremos recibir el código dtmf código en el /nexmo-dtmf webhook.

Solicitud de PIN

Cuando se llama al /nexmo-dtmf webhook, buscamos el código dtmf en el cuerpo de la solicitud y buscamos el sessionId basado en el mapeo. Esto se hace para asegurarnos de que estamos conectando a los usuarios WebRTC y PSTN en la misma conversación. Esto también le permite reutilizar el mismo número de teléfono virtual con múltiples códigos pin para facilitar conferencias concurrentes.

app.post('/nexmo-dtmf', (req, res) => {
  const { dtmf } = req.body;
  let sessionId;

  if (app.get(dtmf)) {
    sessionId = app.get(dtmf);
  }

  const ncco = [
  {
    action: 'conversation',
    name: sessionId,
  }];

  res.json(ncco);
});

Creamos un /hang-up endpoint donde podemos utilizar el método forceDisconnect en el objeto OpenTok para desconectar al participante SIP.

/**
* When the hang-up get request is made, the forceDisconnect method of the OpenTok API is invoked
*/
app.get('/hang-up', (req, res) => {
  const { roomId } = req.query;
  const { conferenceNumber } = config;
  if (app.get(roomId) + app.get(conferenceNumber + roomId)) {
    const sessionId = app.get(roomId);
    const connectionId = app.get(conferenceNumber + roomId); 
    OT.forceDisconnect(sessionId, connectionId, (error) => {
      if (error) {
        res.status(500).send('There was an error hanging up');
      } else {
        res.status(200).send('Ok');
      }
    });
  } else {
    res.status(400).send('There was an error hanging up');
  }
});

Por último, especificamos un puerto y ejecutamos el servidor exprés:

const port = process.env.PORT || '3000';
app.listen(port, () => console.log(`listening on port ${port}`));

Conclusión

En este post, hemos cubierto la conexión de una Sesión OpenTok con usuarios PSTN usando SIP Connect y el Nexmo Voice API. Para ver el código completo con otros ejemplos de SIP, por favor revisa el archivo opentok-nexmo-sip repo.

Compartir:

https://a.storyblok.com/f/270183/384x384/63f654d765/manik.png
Manik SachdevaAntiguos alumnos de Vonage

Manik es ingeniero de software sénior. Le gusta trabajar con desarrolladores y crear API. Cuando no está construyendo APIs o SDKs, se le puede encontrar hablando en conferencias y meetups.