
Transmissão de chamadas para um navegador com Voice WebSockets
Tempo de leitura: 6 minutos
Anunciamos recentemente o suporte a WebSocket em nossa nova Voice API. Os casos de uso iniciais para isso envolvem a comunicação servidor a servidor entre a Voice API da Vonage e plataformas de IA de voz, como o IBM Watson ou a Amazon Alexa. No entanto, gostaria de mostrar a vocês uma pequena demonstração que criamos para nosso estande na AWS ReInvent, que mostra outro uso. Ela demonstra a transmissão do áudio de uma teleconferência para um navegador da web e sua reprodução com a API Web Audio.
Por que você faria isso? Bem, em primeiro lugar, é uma boa maneira de mostrar o novo recurso WebSocket, mas, quando comecei a desenvolver isso, percebi que é perfeito para casos em que você pode querer ter um grande número de pessoas no modo “somente escuta” durante uma chamada. Veja, por exemplo, as típicas teleconferências com “todos os funcionários” em grandes empresas. Se mais de 5 a 10 pessoas estiverem falando, o resultado é o caos. Nessas chamadas em grande escala, a maioria dos participantes são apenas ouvintes passivos que (espero) permanecem com o microfone desligado. Mas isso é bastante ineficiente e oneroso. Também não é muito escalável. Esses participantes regulares não estão realmente envolvidos na teleconferência; a participação deles se assemelha mais a ouvir uma emissora de rádio. Então, por que não conectá-los a uma tecnologia mais parecida com a de uma transmissão de rádio?
A nova Voice API WebSocket da Vonage é uma ótima solução para esse caso de uso. Vou explicar como criar um aplicativo que transmita o áudio de uma chamada para vários navegadores conectados.
Os detalhes técnicos
Vou explicar detalhadamente como isso funciona e, espero, ajudar vocês a entenderem nossos recursos de Voice WebSocket. Estou muito animado com esse recurso, pois ele abre um mundo inteiro de possibilidades para integrar voz a aplicativos da web.
Aqui está um diagrama que mostra como tudo isso se encaixa:
architecture
A teleconferência
O que temos são dois “domínios”. Há uma teleconferência típica hospedada na API de voz da Vonage Voice API, para a qual os participantes ligam. O código para isso é bastante simples, pois tudo o que precisamos fazer é criar um novo aplicativo Vonage e direcioná-lo answer_url para um NCCO que cria a conferência. Servimos isso a partir do servidor de aplicativos web.
O NCCO tem a seguinte aparência:
[
{
"action": "talk",
"text": "Connecting to Audio Socket Conf"
},
{
"action": "conversation",
"name": "audiosocket",
"eventUrl" : ["http://example.com/event"]
}
]Assim, quando os usuários ligam para um número vinculado ao aplicativo, eles são colocados em uma conferência bem básica. Além disso, enviamos eventos ao servidor do aplicativo web informando o status da chamada em conferência. É claro que você poderia adicionar recursos mais avançados à sua chamada, como moderação, um PIN etc.
Um participante do WebSocket
Agora vem a parte (um pouco) complicada. Ao interagir com a API WebSocket da Vonage, seu aplicativo não é um cliente WebSocket (por exemplo, um navegador). Seu aplicativo é um servidor WebSocket. Portanto, o servidor do seu aplicativo precisa enviar uma solicitação à API REST da Vonage para instruir a plataforma de voz a tornar seu aplicativo um participante da conferência, estabelecendo uma conexão WebSocket de saída com o servidor do seu aplicativo web. Para fazer isso, direcionamos o answer_url da chamada de saída para o mesmo NCCO que usamos para as chamadas telefônicas.
A solicitação para fazer uma chamada de saída para o WebSocket é a seguinte:
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 sequência mostra os fluxos:
sequence diagram
Para garantir que apenas uma conexão WebSocket seja estabelecida da Vonage para o servidor do aplicativo, você precisa acompanhar, em seu aplicativo, o estado dessa chamada e seu identificador (callid). Eu verifico o número de conexões de clientes estabelecidas. Quando esse número é zero, eu encerro a chamada WebSocket novamente por meio da API REST. Apenas a primeira conexão de cliente inicia a conexão a partir da Vonage.
Tratamento de dados recebidos via WebSocket
Assim que a conexão WebSocket for estabelecida entre a Vonage e o servidor do aplicativo, precisamos entender o que ela envia. Na conexão inicial, a Voice API enviará uma única “mensagem” de texto que contém alguns dados JSON. Esses dados descrevem principalmente o formato de áudio, juntamente com quaisquer valores adicionais que você esteja passando do seu aplicativo por meio do NCCO no momento em que a conexão foi criada (neste caso, app: audiosocket).
{
"app": "audiosocket",
"content-type": "audio/l16;rate=16000"
}Após a mensagem de texto inicial, a Vonage enviará mensagens binárias, cada uma contendo 20 ms de áudio RAW. (Observação: o áudio RAW não é exatamente o mesmo que um arquivo .wav.) Isso significa que, em seu código, você precisará determinar se a mensagem recebida é de texto ou binária e tratá-la de acordo.
Envio de dados de áudio para o navegador
Para reproduzir esse áudio em um navegador usando WebAudio, precisamos converter o áudio RAW em um arquivo .wav. Isso significa adicionar um pequeno cabeçalho de 44 bytes ao arquivo. No entanto, fazer isso para cada quadro de 20 ms representaria uma sobrecarga considerável e, dado o nosso caso de uso, podemos tolerar uma pequena latência para os ouvintes. Para evitar isso, podemos armazenar em buffer até 10 mensagens da Vonage, concatená-las e anexar o cabeçalho de 44 bytes no início. Isso nos deixará com um arquivo .wav de 200 ms.
Podemos, então, transmitir esses arquivos .wav aos clientes percorrendo uma lista de websockets de clientes conectados e enviando a cada um deles o arquivo como uma mensagem binária.
Reprodução do áudio em um navegador
No cliente web, precisamos criar um código em JavaScript para nos conectarmos ao servidor WebSocket e, em seguida, processar as mensagens de áudio recebidas. Como o formato de áudio que recebemos da Vonage é de 16 bits e 16 kHz, e o formato nativo da maioria dos navegadores é de 32 bits e 44,1 kHz, não é possível reproduzir um fluxo contínuo com o WebAudio. Precisamos solicitar ao navegador que transcode o áudio para a taxa de reprodução adequada. O bufferSource do WebAudio faz isso muito bem e adiciona pouquíssima latência, mas só funciona com arquivos discretos, e é preciso criar uma nova instância para cada arquivo. Portanto, quando um novo arquivo de áudio chega pelo WebSocket, precisamos passá-lo para uma função que criará um novo bufferSource e reproduzi-lo no principal audioContext.
Outro ponto a ser considerado é o tempo. Embora a mudança para amostras menos frequentes, porém mais longas (200 ms em vez de 20 ms), ajude a reduzir o jitter, as mensagens ainda assim não chegarão exatamente no intervalo correto. Portanto, se simplesmente as reproduzirmos uma após a outra, ocorrerão falhas. Felizmente, o WebAudio possui uma interface de temporização muito precisa que pode ajudar. Ao registrar o tempo da primeira amostra como T0 e, em seguida, contarmos o número de mensagens recebidas e multiplicarmos esse valor por 0.2, podemos programar cada amostra para começar no momento correto e remontar o fluxo de forma que fique praticamente livre de falhas.
Essas partes do código do cliente estão detalhadas a seguir, acompanhadas de comentários:
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
}É claro que ainda existe a possibilidade de um arquivo chegar tarde demais para o horário de início programado. No entanto, o WebAudio é inteligente nesse sentido e, na verdade, ajusta a reprodução para começar no ponto correto, como se o arquivo já estivesse lá desde o início. Portanto, se uma 200ms amostra deva ser reproduzida em T 1200ms , mas só for acionada a partir de 1300ms, o WebAudio vai pular 100ms para dentro da amostra para começar a reproduzi-la. Isso significa que podemos ter algumas falhas ocasionais devido à perda de pequenos trechos iniciais das amostras, mas isso é perfeitamente aceitável para áudio do tipo de chamada telefônica. Talvez não funcione tão bem para música de alta qualidade.
Obter o código
E aí está: um fluxo de áudio unidirecional de baixa latência da sua teleconferência sendo reproduzido diretamente no navegador.
Confira o código na Organização da Comunidade Vonage no GitHub e saiba mais sobre a Voice API WebSocket da Vonage na documentação.