https://d226lax1qjow5r.cloudfront.net/blog/blogposts/record-a-call-in-ruby-with-vonage-voice-api-websockets/Blog_Ruby_Record-Voice-Call_1200x600.png

Graba una llamada en Ruby con WebSockets de Voice API de Vonage

Publicado el November 5, 2020

Tiempo de lectura: 10 minutos

En WebSockets de Voice API de Vonage recientemente abandonó el estado Beta y pasó a estar disponible de manera general. WebSockets te permite crear una comunicación bidireccional a través de una única conexión TCP persistente. No necesitas manejar múltiples solicitudes y respuestas HTTP con WebSockets. Una sola conexión WebSocket puede permitir la comunicación de texto y datos binarios de forma continua, con una sola conexión abierta.

Aunque WebSockets puede simplificar el ciclo de respuesta y solicitud HTTP, es un paradigma diferente para construir una aplicación. Afortunadamente, los lenguajes de programación más utilizados disponen de herramientas WebSocket que pueden ayudar a eliminar parte de la complejidad del proceso.

En este tutorial, vamos a construir un pequeño servidor web para trabajar con WebSockets en Ruby. El servidor manejará llamadas entrantes de Voice, conexiones WebSocket, y renderizará HTML. Utilizaremos Rack como interfaz web y Thin como servidor web. Este tutorial no requiere ningún conocimiento previo sobre el trabajo con WebSockets, pero sí asume una ligera experiencia trabajando con servidores web en Ruby.

tl;dr Si quieres saltarte el proceso y simplemente ejecutar la aplicación, puedes encontrar una versión completamente funcional en GitHub.

Requisitos previos

Este tutorial requiere que Ruby v2.7 o superior esté instalado en su máquina. Además, en la aplicación se utilizan varias gemas. Cada una de ellas está listada en el archivo Gemfile que crearemos más adelante y se instalarán ejecutando bundle install desde la línea de comandos:

Ya podemos empezar a implementar nuestra aplicación.

Cuenta API de Vonage

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.

Este tutorial también utiliza un número de teléfono virtual. Para adquirir uno, vaya a Numbers > Comprar Numbers y busque uno que se ajuste a sus necesidades.

Nuestro último paso en la configuración de nuestra Account API es crear una Voice API Application. Vincularemos el número de teléfono virtual que hemos proporcionado a esta aplicación y estableceremos las URL de los webhooks.

Desde el panel de la API de Vonage, ve a Tus aplicaciones y haz clic en Crear una nueva aplicación. Esto te mostrará la siguiente página:

Dashboard Create ApplicationDashboard Create Application

Las áreas clave en las que debe centrarse para crear su solicitud aparecen resaltadas en morado:

  • Nombre: Puede dar a su aplicación el nombre que desee.

  • Clave pública y privada: Esto generará un par de claves pública y privada para la autenticación. Un archivo de clave privada se descargará en tu máquina. Nuestra aplicación sólo gestiona las llamadas de voz entrantes, por lo que no necesitamos hacer nada con ella.

  • Capacidades: Cada aplicación puede manejar múltiples funciones. Para nuestros propósitos, sólo necesitamos activar Voice.

Una vez finalizadas las opciones, puede pulsar el botón Generar nueva aplicación para finalizar.

Una vez creada la aplicación, vamos a vincularla al nuevo número de teléfono y a configurar las URL de los webhooks.

Como antes, vaya a Sus aplicaciones en el Panel de control, haga clic en las elipses junto al nombre de su aplicación y haga clic en el botón Editar Editar.

Dentro de las Capacidades de la página verá las siguientes opciones:

Application Webhook URL settingsApplication Webhook URL settings

Tenemos que rellenar la URL del evento y la URL de respuesta. La primera es a donde Vonage enviará todos los datos del ciclo de vida del evento de la llamada de voz. La segunda es a donde Vonage enviará cada nueva llamada de voz cuando se inicie. Las URL provistas aquí deben ser accesibles externamente para que Vonage pueda llegar a ellas. En otras palabras, usar localhost no funciona. Una opción de desarrollo popular es ngrok, y puedes seguir nuestro tutorial para trabajar con él.

Asegúrese de que tanto la URL del evento como la URL de la respuesta terminan en /webhooks/event y /webhooks/answerrespectivamente.

También debemos conectar nuestro número de teléfono de Vonage a esta aplicación. Para ello, ve a Numbers>Tus números y haz clic en el ícono del lápiz junto a tu número. Luego puedes seleccionar tu nueva aplicación entre las opciones para vincular el número de teléfono a ella. Una vez que pulse Guardartodas las llamadas entrantes a este número se desviarán a su aplicación.

Creación de la estructura de carpetas

Ahora que nuestra cuenta y configuración de la API de Vonage están listas, pasemos a crear la estructura de carpetas para nuestra aplicación. Al final tendrá el siguiente aspecto:

.
+-- recordings/
+-- views/
|   +-- index.html.erb
+-- app.rb
+-- Gemfile

La carpeta raíz de nuestra aplicación contendrá app.rbque será nuestro servidor web que manejará todas las llamadas de voz entrantes y la conexión WebSocket. También incluirá la carpeta Gemfileque es donde definiremos nuestras dependencias. Habrá dos carpetas más recordings/ y views/. La carpeta recordings/ será donde se guardará la grabación de la llamada telefónica desde la conexión WebSockets de la Voice API. La carpeta views/ es donde guardaremos la vista única de la aplicación.

Definición de las relaciones

Dentro del directorio Gemfile añade las siguientes gemas que utilizaremos en la aplicación:

source 'https://rubygems.org'

gem 'wavefile'
gem 'faye-websocket'
gem 'json'
gem 'rack'
gem 'thin'

Cada gema cumplirá una función específica:

  • Wavefile: Utilizaremos esta gema para convertir los datos de audio sin procesar en un archivo WAV

  • Faye: Usaremos esta gema para manejar la conexión WebSockets

  • JSON: Esta gema se utilizará para convertir en JSON la instrucción de llamada que enviamos a la Voice API de Vonage.

  • Rack: Utilizaremos Rack como nuestro framework web

  • Delgado: Esta gema nos proporciona nuestro servidor web sobre Rack

Ahora puede ejecutar bundle install desde la línea de comandos para poner todas estas gemas a disposición de tu aplicación.

Construcción del servidor

Dependencias y definición de variables

Ahora estamos listos para construir nuestro servidor web. Lo primero que haremos al construir el servidor será añadir varios ficheros require y include en la parte superior para añadir la funcionalidad de las gemas mencionadas anteriormente en nuestra aplicación:

require 'rack'
require 'erb'
require 'faye/websocket'
require 'json'
require "wavefile"
include Rack
include WaveFile

En este punto, también definiremos una variable constante que será igual a la URL accesible externamente para nuestras solicitudes de conexión WebSockets. Utilizaremos esta URL en las instrucciones que enviaremos a la Voice API de Vonage cuando recibamos una nueva llamada:

EXTERNAL_WS_URL = 'ws://example.com/cable'

Sustituya example.com en el fragmento anterior por su URL de acceso externo.

Métodos de ayuda

Nuestra aplicación se aprovechará de dos métodos de ayuda. Podemos crearlos ahora y añadirlos después de la declaración de la variable constante.

El primer método, #create_wav_fileayudará en el proceso de convertir los datos binarios de audio recibidos a través del WebSocket en un archivo WAV. Utilizará la funcionalidad de la gema Wavefile para crear el archivo WAV, y también devolverá el nombre del archivo que se utilizará posteriormente en la aplicación:

def create_wav_file(data, file_name)
  buffer = Buffer.new(data, Format.new(:mono, :pcm_16, 16000)) 
  puts "Audio Buffer Created..."
  writer = Writer.new(file_name, Format.new(:mono, :pcm_16, 16000))
  puts "New Audio File Created..."
  puts "Writing to the Buffer..."
  writer.write(buffer)
  puts "Closing Buffer Writing..."
  writer.close
  puts "WAV File Created..."
  file_name
end

El método anterior especifica que el audio entrante procede de una mono en lugar de una fuente stereo fuente. La diferencia es que hay una única pista de audio, definida por una matriz plana de datos binarios, y no una matriz de matrices de datos. El audio se define como pcm_16lo que significa que la fuente es PCM lineal de 16 bits. Por último, la fuente se define como 16000lo que significa que es una frecuencia de muestreo de 16KHz. El nuevo archivo WAV se crea con la misma configuración de audio que los datos de la fuente binaria de audio.

El segundo método, #erbes un método corto que utilizaremos para mostrar los archivos de plantilla ERB al usuario:

def erb(template)
  path = File.expand_path("#{template}")
  ERB.new(File.read(path)).result(binding)
end

El resto de nuestro código será una serie de sentencias map que conectan rutas URL a acciones específicas envueltas en Rack::Handler middleware.

Definición de las rutas

Las rutas para nuestra aplicación necesitan ser definidas dentro del middleware Rack que enlaza Rack con Thin. También utilizaremos el middleware Rack::Static para servir el archivo de audio estático en la vista. Inicializaremos el manejador Faye WebSocket aquí también:

Rack::Handler::Thin.run(Rack::Builder.new {
  Faye::WebSocket.load_adapter('thin')
  use(Rack::Static, urls: ["/recording"], root: 'recording')

Hay cuatro rutas que necesitamos construir map para: /cable, /, /webhooks/answery /webhooks/answer. Hagámoslo ahora.

La primera ruta se encargará de la conexión WebSockets:

map('/cable') do
  run(->env{
    if Faye::WebSocket.websocket?(env)
      puts "WebSockets connection opened..."
      @call_data = []
      ws = Faye::WebSocket.new(env)
  
      ws.on :message do |event|
        if event.data.is_a?(Array)
          @call_data.append(event.data.pack('c*').unpack('s*'))
        else
          puts event.data
        end
      end
  
      ws.on :close do |event|
        puts 'WebSocket connection closed...'
        create_wav_file(@call_data.flatten, 'recording/recording.wav')
      end
  
      ws.rack_response
    end
  })
end

En la ruta anterior, comprobamos si la solicitud de conexión es una solicitud WebSocket. Si lo es, instanciamos una nueva instancia de Faye::WebSocket. Hay dos tipos de datos posibles enviados a un WebSocket, texto o datos binarios. Estos últimos siempre se envían en forma de enteros del tamaño de un byte en un array.

Como tal, podemos comprobar si el event.data es un objeto array o no. Si es un array, sabemos que son los datos binarios de audio que utilizaremos para crear nuestro archivo WAV. Si no lo es, se trata de actualizaciones de estado de la Voice API de Vonage. En ese caso, podemos registrarlas en la consola.

Una nota importante a tener en cuenta: La gema Faye WebSockets convierte los datos binarios en enteros del tamaño de un byte, como hemos indicado anteriormente. Sin embargo, esto significa que los convierte exactamente en 1 byte o en enteros de 8 bits. La Voice API de Vonage envía los datos binarios de audio en enteros de 16 bits, un estándar común para el habla humana. Esto significa que nuestra aplicación necesita convertir los 8 bits de datos binarios en 16 bits. Utilizamos los métodos de la biblioteca estándar de Ruby #pack y #unpack para añadirlos a nuestra variable de instancia @call_data variable de instancia. Este es un paso necesario para producir audio comprensible.

La siguiente ruta servirá a la index vista. Comprobará si hay un archivo y, en caso afirmativo, lo pasará a la vista con variables de instancia:

map('/') do
  if File.exist?('recording.wav')
    @call_status = 'Audio Loaded!'
    @file = 'recording.wav'
  end
  run(->env{
    [200, { 'Content-Type' => 'text/html'}, [erb("views/index.html.erb")]]})
end

La ruta /webhooks/answer ruta regresará a las instrucciones especializadas de Voice API de Vonage llamadas un NCCO (objeto de control de llamadas Nexmo) que informan a la API qué hacer con la llamada que acaba de recibir. La instrucción que devolvemos como JSON le indicará a Voice API que deseamos abrir una conexión WebSocket y le proporcionaremos la URL WebSocket para iniciar la conexión. También le decimos a la Voice API que queremos que envíe a la persona que llama un breve mensaje para informarle de que su llamada se retransmitirá en un momento:

map('/webhooks/answer') do
  run(->env{
    ncco = [
      {
        "action": "talk",
        "text": "You will be streaming momentarily."
      },
      {
        "action": "connect",
        "endpoint": [
          {
            "type": "websocket",
            "uri": "#{EXTERNAL_WS_URL}",
            "content-type": "audio/l16;rate=16000",
          }
        ]
      }
    ].to_json

    [200, { 'Content-Type' => 'application/json' }, [ncco]]
  })
end

La última ruta que crearemos manejará los datos del ciclo de vida de los eventos de llamada que la Voice API envía a nuestra aplicación. No queremos hacer nada con ellos, excepto confirmar a la API que los hemos recibido con un código de estado 200 código de estado:

  map('/webhooks/event') do
  run(->env{
    [200, { 'Content-Type' => 'text/html'}, ['']]
  })
end

Por último, cerramos el bloque Rack::Builder que abrimos al principio especificando un puerto en el que ejecutar nuestra aplicación:

}, Port: 9292)

El último elemento que tenemos que crear antes de poder ejecutar nuestra aplicación es nuestra vista.

Creación de la vista

La aplicación sólo tiene una vista. Se puede acceder a esta vista yendo a la URL raíz de la aplicación, es decir localhost:9292 o 127.0.0.1:9292. La vista presentará el audio a reproducir con un elemento <audio> elemento HTML:

<html>
  <head>
    <title>Ruby Vonage WebSockets Demo</title>
  </head>
  <body>
    <h1>Vonage WebSockets + Ruby == ♥</h1>
    <p>Welcome to the Vonage WebSockets demo in Ruby</p>  
    <h2>Your Audio To Playback</h2>
    <p>Once you have finished your call, your audio will be available to playback from here.</p>
    <div id="audio-status">
      <%= @call_status %>
      <br />
      <% if @file %>
        <audio
          controls
          src="recording/<%= @file %>">
            Your browser does not support the
            <code>audio</code> element.
        </audio>
      <% end %>
    </div>
  </body>
</html>

La vista utiliza las variables de instancia que creamos en la ruta para determinar si mostrar o no el elemento <audio> elemento.

Ya estamos listos para ejecutar la aplicación.

Ejecutar la aplicación

La aplicación ya está lista para ejecutarse. Para ejecutarla, ejecute lo siguiente desde la línea de comandos en la carpeta raíz de la aplicación:

bundle exec rackup app.rb

Recuerde también asegurarse de que su servidor web es accesible externamente utilizando ngrok u otra herramienta similar.

En este punto, llama a tu aplicación llamando a tu número de teléfono virtual de Vonage. Cuando termines de hablar, puedes colgar la llamada. Si visitas tu aplicación con tu navegador web, ahora verás un reproductor de audio y reproducirás tu audio grabado. ¡Felicitaciones!

Lecturas complementarias

Este tutorial demostró la funcionalidad básica para comenzar a usar la función WebSockets de la Voice API de Vonage en Ruby. Hay mucho más que puedes hacer con esta función. Para obtener más información sobre Voice API WebSockets de Vonage, consulta lo siguiente:

Compartir:

https://a.storyblok.com/f/270183/384x384/e5480d2945/ben-greenberg.png
Ben GreenbergAntiguos alumnos de Vonage

Ben es un desarrollador de segunda carrera que anteriormente pasó una década en los campos de la educación de adultos, la organización comunitaria y la gestión de organizaciones sin ánimo de lucro. Trabajó como defensor de los desarrolladores para Vonage. Escribe regularmente sobre la intersección entre el desarrollo comunitario y la tecnología. Originario del sur de California y residente durante mucho tiempo en Nueva York, Ben reside ahora cerca de Tel Aviv (Israel).