https://d226lax1qjow5r.cloudfront.net/blog/blogposts/streaming-calls-to-a-browser-with-voice-websockets-dr/audio-websockets.png

Streaming de llamadas a un navegador con WebSockets de Voice

Publicado el November 5, 2020

Tiempo de lectura: 6 minutos

Recientemente anunciamos compatibilidad con WebSocket dentro de nuestra nueva Voice API. Los casos de uso iniciales para esto giran en torno a la comunicación de servidor a servidor entre la Voice API de Vonage y las plataformas de IA de voz como IBM Watson o Amazon Alexa. Sin embargo, me gustaría mostrarte una pequeña demostración que construimos para nuestro stand en AWS ReInvent que muestra otro uso. Demuestra el streaming del audio de una llamada de conferencia a un navegador web y lo reproduce con la API de audio web.

¿Por qué querrías hacer esto? Bueno, en primer lugar es una buena manera de mostrar la nueva característica WebSocket, pero cuando empecé a desarrollar esto me di cuenta de que es perfecto para los casos de uso en los que es posible que desee tener un gran número de personas en el modo de "sólo escucha" para una llamada. Por ejemplo, las típicas conferencias telefónicas de una gran empresa. Si hay más de 5-10 personas hablando, el resultado es un caos. En estas llamadas a gran escala, la mayoría de los participantes son meros oyentes pasivos que (con suerte) permanecen en silencio. Pero esto es bastante ineficaz y costoso. Además, no es especialmente escalable. Los participantes habituales no están realmente implicados en una multiconferencia; su participación es más parecida a escuchar una emisora de radio. ¿Por qué no conectarles a una tecnología más parecida a la radiodifusión?

La nueva WebSocket Voice API de Vonage es una gran solución para este caso de uso. Te explicaré cómo crear una aplicación que transmita el audio de una llamada a varios navegadores conectados.

Detalles técnicos

Voy a explicarte los detalles técnicos de cómo funciona y espero que te sirva para entender nuestras capacidades de Voice WebSocket. Estoy muy entusiasmado con esta característica, ya que abre todo un mundo de posibilidades para la integración de voz con aplicaciones web.

Aquí tienes un diagrama de cómo encaja todo esto:

architecturearchitecture

Conferencia telefónica

Lo que tenemos son dos "dominios". Hay una llamada de conferencia típica alojada en la Voice API de Vonagea la que se conectan los interlocutores. El código para esto es bastante simple, ya que todo lo que tenemos que hacer es crear una nueva aplicación de Vonage y apuntar el dominio answer_url a una NCCO que crea la conferencia. Servimos esto desde el servidor de la aplicación web.

La OCNC parece:

[
{
"action": "talk",
"text": "Connecting to Audio Socket Conf"
},
{
"action": "conversation",
"name": "audiosocket",
"eventUrl" : ["http://example.com/event"]
}
]

Así, cuando los usuarios llaman a un número vinculado a la aplicación, entran en una conferencia muy básica. Además, enviamos eventos al servidor de la aplicación web sobre el estado de la multiconferencia. Por supuesto, aquí podrías añadir funciones más avanzadas a la llamada, como moderación, un PIN, etc.

Un participante en WebSocket

Ahora viene la parte (un poco) complicada. Al interactuar con la API WebSocket de Vonage, tu aplicación no es un cliente WebSocket (por ejemplo, un navegador). Tu aplicación es un servidor WebSocket. Por lo tanto, tu servidor de aplicaciones debe realizar una solicitud a la API REST de Vonage para indicarle a la plataforma de voz que convierta tu aplicación en participante de la conferencia mediante una conexión WebSocket saliente con el servidor de tu aplicación web. Para ello, apuntamos el answer_url de la llamada saliente a la misma OCN que usamos para las llamadas telefónicas.

La solicitud para realizar una llamada saliente al websocket tiene el siguiente aspecto:

POST /v1/calls
Host: api.nexmo.com
Authorization: Bearer [YOUR_JWT_TOKEN]

{ "to": [{
"type": "websocket",
"uri": "ws://example.com/socket",
"content-type": "audio/l16;rate=16000",
"headers": {
"app": "audiosocket"
}
}],
"from": {
"type": "phone",
"number": "442037831800"
},
"answer_url": ["http://example.com/ncco"]
}

Este diagrama de secuencia muestra los flujos:

sequence diagramsequence diagram

Para garantizar que sólo se establezca una conexión websocket desde Vonage al servidor de aplicaciones, debes llevar un registro en tu aplicación del estado de esta llamada y su identificador de llamada (callid). Compruebo el número de conexiones de cliente establecidas. Cuando es cero, vuelvo a cerrar la llamada de websocket a través de la API REST. Solo la primera conexión de cliente inicia la conexión desde Vonage.

Manejo de datos WebSocket entrantes

Una vez que se establece la conexión websocket entre Vonage y el servidor de la aplicación, debemos comprender lo que envía. En la conexión inicial, Voice API enviará un único "mensaje" de texto que contiene algunos datos JSON. Esto describe principalmente el formato de audio junto con cualquier valor adicional que estés pasando desde tu aplicación a través de la OCN cuando se creó la conexión (en este caso app : audiosocket).

{
"app": "audiosocket",
"content-type": "audio/l16;rate=16000"
}

Después del mensaje de texto inicial, Vonage enviará mensajes binarios con 20 ms de audio RAW cada uno. (Nota: el audio RAW no es exactamente lo mismo que un archivo .wav.) Esto significa que en tu código deberás determinar si el mensaje recibido es de texto o binario y manejarlo según corresponda.

Envío de datos de audio al navegador

Para reproducir este audio en un navegador utilizando WebAudiotenemos que convertir el audio RAW en un archivo .wav. Esto significa añadir una pequeña cabecera de 44 bytes al archivo. Sin embargo, hacer esto para cada fotograma de 20ms sería bastante sobrecarga y dado nuestro caso de uso podemos tolerar una pequeña cantidad de latencia hacia los oyentes. Para evitarlo, podemos almacenar 10 de los mensajes de Vonage, concatenarlos y añadir la cabecera de 44 bytes. Esto nos dejará con un archivo .wav de 200ms.

A continuación, podemos difundir esos archivos .wav a los clientes iterando a través de una lista de websockets de clientes conectados y enviando a cada uno de ellos el archivo como un mensaje binario.

Reproducir el audio en un navegador

En el cliente web, necesitamos crear algo de JavaScript para conectarnos al servidor websocket y luego manejar los mensajes de audio recibidos. Debido a que el formato de audio que recibimos de Vonage es 16bit 16Khz y el formato nativo de la mayoría de los navegadores es 32bit 44.1Khz, no podemos reproducir un flujo constante con WebAudio. Tenemos que pedirle al navegador que transcodifique el audio a la velocidad de reproducción adecuada. La dirección WebAudio bufferSource hace esto muy bien y añade muy poca latencia, pero sólo puede trabajar con archivos discretos y hay que crear una nueva instancia para cada archivo. Por lo tanto, cuando un nuevo archivo de audio llega en el websocket, tenemos que pasarlo a una función que creará un nuevo bufferSource y lo reproduzca en el servidor principal audioContext.

El otro punto a tener en cuenta es la sincronización. Aunque pasar a menos muestras pero más largas (200 ms frente a 20 ms) ayuda con el jitter, los mensajes seguirán sin llegar exactamente en el intervalo correcto. Por tanto, si nos limitamos a reproducirlos uno tras otro habrá fallos. Afortunadamente, WebAudio dispone de una interfaz de temporización muy precisa que puede ayudarnos. Tomando el tiempo de la primera muestra como T0 y contando el número de mensajes recibidos y multiplicándolo por 0.2podemos programar cada muestra para que se inicie en el momento correcto y reensamblar el flujo para que esté prácticamente libre de fallos.

Esas partes del código cliente se detallan a continuación con comentarios:

var startTime; // Make startTime a global var

ws.onmessage = function(event) {
// On the first message set the startTime to the currentTime from the audio context
if (count ==0){
startTime = audioContext.currentTime;
}

audioContext.decodeAudioData(event.data, function(data) {
count ++; // Keep a count of how many messages have been received
var playTime = startTime + (count *0.2) //Play each at file 200ms
playSound(data, playTime); //call the function to play the sample at the appropriate time
});
};

function playSound(buffer, playTime) {
var source = audioContext.createBufferSource(); //Create a new BufferSource fr the
source.buffer = buffer; // Put the sample content into the buffer
source.start(playTime); // Set the starting time of the sample to the scheduled play time
source.connect(analyserNode); //Connect the source to the visualiser
source.connect(audioContext.destination); // Also Connect the source to the audio output
}

Por supuesto, aún existe el escenario de que un archivo llegue demasiado tarde para su hora de inicio programada. Sin embargo, WebAudio es así de inteligente y ajusta la reproducción para que comience desde el punto correcto, como si hubiera estado allí desde el principio. Así, si una 200ms debe reproducirse en T 1200ms pero no se invoca hasta 1300msWebAudio saltará 100ms a la muestra para comenzar a reproducirse. Esto significa que podemos tener algún fallo ocasional al perder pequeños inicios de muestras, pero esto es perfectamente aceptable para audio estilo llamada telefónica. Puede que no funcione tan bien para música de alta calidad.

Obtener el código

Y ahí lo tienes: un flujo de audio unidireccional de baja latencia de tu llamada de conferencia reproduciéndose directamente en un navegador.

Consulta el código en la Organización GitHub de la comunidad de Vonage y obtén más información sobre la Voice API de Vonage WebSocket en la documentación.

Compartir:

https://a.storyblok.com/f/270183/384x384/7fbbc7293b/sammachin.png
Sam MachinAntiguos alumnos de Vonage