
Compartilhar:
Javier studied Industrial Engineering back in Madrid where he's from. He is now one of our Solution Engineers, so if you get into trouble using our APIs he may be the one that gives you a hand. Out of work he loves playing football and travelling as much as he can.
Melhores práticas para sala de espera e pré-chamada com a Video API da Vonage
Tempo de leitura: 7 minutos
Ao desenvolver sua própria solução de videoconferência, é fundamental oferecer uma boa experiência antes da chamada. Certifique-se de que o usuário possa escolher os dispositivos de áudio e vídeo a serem utilizados, verifique se o microfone detecta sua voz e se o sinal de rede é bom o suficiente. Verificar todos esses pontos ajudará você a criar um aplicativo mais robusto e a identificar alguns problemas que, esperamos, reduzirão o atrito com os usuários finais do seu aplicativo.
Atualmente, também é muito comum ter diferentes funções em seu aplicativo; se você atua nas áreas de saúde, educação ou webinars, talvez seja interessante contar com um moderador. Nesta postagem do blog, vamos explicar como fazer com que os demais participantes aguardem a entrada do moderador na sessão antes de começarem a publicar.
Resumindo, este aplicativo de exemplo implementa:
Moderação. Aguarde até que o moderador/apresentador comece a transmitir na sessão. Seleção de dispositivos. Práticas recomendadas antes da chamada. Isso inclui um teste de conectividade e qualidade antes da chamada, além de outras práticas recomendadas, como indicadores de nível de áudio.
Se isso lhe parecer um bom plano, fique por aqui. Se estiver com um pouco de preguiça e quiser, você pode conferir o repositório finalizado aqui.
Estrutura do projeto
Lado do servidor
O arquivo principal do servidor é um servidor Express básico do Node.js que serve dois arquivos HTML, dependendo da rota escolhida (/host ou /participant). O servidor também é responsável por gerar as credenciais da sessão (token e IDs de sessão).
O servidor irá gerar um token de moderador para o anfitrião ou um token de editor para um participante. Para obter mais informações sobre a criação de tokens, acesse este link. O servidor armazenará um mapa das sessões e roomNames na memória. Para uma aplicação em produção, você precisará armazenar essas sessões em um banco de dados ou algo semelhante.
Lado do cliente
O aplicativo utiliza o Webpack para agrupar todos os arquivos JavaScript, tornando-o mais escalável e mais fácil de entender. Ele também utiliza o Bootstrap para simplificar o processo de design da interface do usuário.
Todos os arquivos JavaScript estão na pasta src:
O ponto de entrada principal é o arquivo index.js na pasta src. Esse arquivo irá extrair o roomName da URL e, dependendo da rota visitada, criará uma instância de Host ou Participant. Em seguida, ele inicializará o processo.
import { Host } from "./Host";
import { Participant } from "./Participant";
(() => {
const urlParams = new URLSearchParams(window.location.search);
const roomName = urlParams.get("room");
if (window.location.pathname === "/host") {
const host = new Host(roomName);
host.init();
} else if (window.location.pathname === "/participant") {
const participant = new Participant(roomName);
participant.init();
}
})();
A lógica da aplicação ocorre na Classe Host e na Classe Participant , dependendo da função do usuário conectado. Essas classes utilizarão arquivos adicionais para melhorar a legibilidade do código. Você pode conferir os diferentes arquivos que nossa aplicação utiliza.
Seleção de dispositivos
A seleção do dispositivo será implementada em ambas as visualizações (Participante e Anfitrião). Conforme explicado na referência da API do MediaDevices, nosso aplicativo precisa ser robusto o suficiente para lidar com a conexão e desconexão de dispositivos durante a chamada.
O que acontece se um dispositivo for desconectado durante a chamada ou se um novo dispositivo for conectado? Nosso aplicativo precisa ser inteligente o suficiente para detectar uma alteração na lista de dispositivos disponíveis. Vamos configurar um ouvinte de eventos para que, quando houver uma alteração nos dispositivos disponíveis, ele acione uma chamada de função para atualizar nossa interface do usuário e exibir os dispositivos disponíveis mais recentes.
Primeiro, vamos acionar a primeira chamada para atualizar nossa lista de dispositivos assim que o usuário conceder permissão para o uso da câmera e/ou do microfone. Podemos fazer isso aproveitando os accessAllowed eventos emitidos pelo publisher.
this.publisher.on("accessAllowed", () => {
refreshDeviceList(this.publisher);
});
Em seguida, configuraremos um ouvinte de eventos que recalculará os dispositivos disponíveis caso haja uma atualização nos dispositivos de mídia disponíveis durante a chamada.
navigator.mediaDevices.ondevicechange = () => {
refreshDeviceList(this.publisher);
};
A refreshDeviceList função é responsável por anexar a lista de dispositivos de áudio e Video a um elemento DOM. Neste caso, vou usar um menu suspenso para simplificar. Se você quiser ver mais detalhes sobre essa função, fique à vontade para conferir o código-fonte da implementação da função . Também adicionaremos uma tag HTML `selected` às fontes de áudio e Video atuais retornadas por getAudioSource() e getVideoSource , respectivamente.
Quando se trata de lidar com a troca de dispositivo durante a chamada, vamos utilizar o setVideoSource e setAudioSource respectivamente. Vou descrever aqui o processo para um deles, para que você entenda melhor.
const onVideoSourceChanged = async (event, publisher) => {
const labelToFind = event.target.value;
const videoDevices = await listVideoInputs();
const deviceId = videoDevices.find((e) => e.label === labelToFind)?.deviceId;
if (deviceId != null) {
publisher.setVideoSource(deviceId);
}
};
Vamos definir um ouvinte de evento para detectar alterações em nosso menu suspenso, que acionará a onVideoSourceChanged função. Essa função procurará o ID do dispositivo cujo rótulo estamos selecionando. Em seguida, ela chamará o setVideoSource método do objeto publisher para alterar a fonte do Video.
document.getElementById("audioInputs").addEventListener("change", (e) => {
onAudioSourceChanged(e, this.waitingRoompublisher);
});
Aguarde o anfitrião
Nosso aplicativo precisa saber se o usuário que está entrando é um Anfitrião ou um Participante. Nesse caso, estou servindo um arquivo HTML diferente do lado do servidor, dependendo da função do usuário, já que nosso Anfitrião poderá desconectar todos os Participantes da chamada. Nosso ponto de entrada instanciará um Anfitrião ou um Participante, dependendo da URL para a qual navegarmos. Lembre-se de que este não é um aplicativo pronto para produção, e você deve implementar a autenticação nas rotas.
Toda a lógica começa com nossa init função index.js na pasta /src , que será executada em uma instância de Host ou de Participante, dependendo de para onde navegarmos.
A init função chamará nossa getCredentials função credentials.js com a função definida como “admin” para o Anfitrião ou “Participante” para o Participante.
const getCredentials = async (roomName, role) => {
try {
const url = `/api/room/${roomName}?role=${role}`;
const config = {};
const response = await fetch(`${url}`, config);
const data = await response.json();
if (data.apiKey && data.sessionId && data.token) {
return Promise.resolve(data);
}
return Promise.reject(new Error("Credentials Not Valid"));
} catch (error) {
console.log(error.message);
return Promise.reject(error);
}
};
Nosso servidor irá, então, gerar um token de moderador para o administrador/anfitrião ou um token de publicador para o participante. Para obter mais informações sobre a criação de tokens e funções, consulte nossa documentação sobre criação de tokens.
Dê uma olhada em a geração de tokens no lado do servidor.
Assim que o token for recebido no lado do cliente, podemos saber se um Anfitrião ou um Participante se conecta à sessão, monitorando eventos de conexão enviados pelo nosso SDK. O fluxo do aplicativo segue da seguinte forma:
Se um Anfitrião entrar na chamada, ele se conectará à sessão e começará a transmitir imediatamente. Se um Participante entrar na chamada, realizaremos um teste pré-chamada e, em seguida, o Participante se conectará à sessão. Se já houver um Anfitrião conectado à sessão, o Participante começará a transmitir. Caso contrário, o participante permanecerá conectado até que um anfitrião entre na chamada e só então começará a transmitir.
Participante
É assim que a init função do nosso Participante. Primeiro, obtemos as credenciais de um Participante (função de publisher). Também solicitamos credenciais separadas para nosso teste pré-chamada, a fim de evitar que a sessão principal seja afetada por conexões/fluxos provenientes do teste pré-chamada. Em seguida, iniciamos o teste pré-chamada (explicarei como fazer isso daqui a pouco) e, assim que o teste for concluído, nos conectaremos à sessão.
init() {
getCredentials(this.roomName, 'participant')
.then(data => {
this.roomToken = data.token;
this.initializeSession(data);
getCredentials(`${this.roomName}-precall`, 'participant').then(
precallCreds => {
startTest(precallCreds)
.then(results => {
this.precallTestDone = true;
this.connect();
})
.catch(e => console.log(e));
}
);
this.registerEvents();
})
.catch(e => console.log(e));
}
A função “connect” verificará se já há um Host conectado à sessão ou não. Se já houver um Host, começaremos a publicar; caso contrário, permaneceremos conectados.
connect() {
this.session.connect(this.roomToken, error => {
if (error) {
handleError(error);
} else {
if (isHostPresent()) {
this.handlePublisher();
}
console.log('Session Connected');
}
});
}
A isHostPresent função retornará true se um host estiver conectado à sessão e false caso contrário.
const isHostPresent = () => {
if (usersConnected.find((e) => e.data === "admin")) {
return true;
} else {
return false;
}
};
A usersConnected matriz manterá o controle das conexões na sessão. Nós a incrementaremos a cada connectionCreated evento e o diminuiremos a cada connectionDestroyed evento. É importante observar que essa variável será incrementada por ambas as classes (Host e Participant) quando houver uma nova conexão. Portanto, precisaremos que essa variável seja acessível por ambas as classes.
this.session.on("connectionCreated", (event) => {
connectionCount += 1;
console.log("[connectionCreated]", connectionCount);
usersConnected.push(event.connection);
console.log(usersConnected);
if (event.connection.data === "admin") {
this.handlePublisher();
}
});
this.session.on("connectionDestroyed", (event) => {
connectionCount -= 1;
console.log("[connectionDestroyed]", connectionCount);
usersConnected = usersConnected.filter((connection) => {
return connection.id != event.connection.id;
});
connectionCount -= 1;
console.log(usersConnected);
});
Se o anfitrião não estiver presente quando o participante se conectar à sessão, esperaremos até que um novo anfitrião entre na sessão e, só então, começaremos a publicar.
Teste pré-chamada
Outro aspecto importante para proporcionar uma boa experiência ao cliente é realizar uma verificação de conectividade e qualidade, a fim de garantir que tudo corra da maneira mais tranquila possível. Se os participantes precisam esperar que o moderador entre na reunião, por que não aproveitamos esse tempo precioso para fazer um teste antes da chamada?
Vamos utilizar o módulo de teste de rede para verificar se o Participante tem conectividade com os servidores de log, mensagens, mídia e Video API da Vonage, bem como para verificar a qualidade esperada durante a chamada. Lembre-se de que o comportamento da rede é dinâmico, o que significa que um resultado positivo no teste pré-chamada não garante que sua largura de banda disponível não mude durante a chamada.
Para simplificar, realizaremos apenas um teste pré-chamada com nossos participantes, mas não com o anfitrião. É claro que você pode realizá-lo com ambos.
Criei alguns arquivos para lidar com a resposta do teste de conectividade e qualidade e também uma barra de progresso para indicar o status do teste. É uma simples barra de progresso do Bootstrap que se preenche após 30 segundos, o que corresponde aproximadamente ao tempo que o teste leva para ser concluído. Você pode modificar isso definindo um valor de tempo limite ao instanciar o NetworkTest. No entanto, quanto mais tempo o teste durar, mais precisos serão os resultados. Se o teste falhar, também removeremos o indicador de progresso.
const handleTestProgressIndicator = () => {
const progressIndicator = setInterval(() => {
let currentProgress = progressBar.value;
progressBar.value += 3.3;
if (currentProgress === 100) {
clearInterval(progressIndicator);
progressBar.value = 0;
progressBar.style.display = "none";
}
}, 1000);
};
const removeProgressIndicator = () => {
progressBar.style.display = "none";
};
Se você quiser dar uma olhada na implementação do teste de rede, consulte o arquivo onde eu trato a lógica pré-chamada.
Os resultados do teste pré-chamada também fornecem uma solução recomendada e uma pontuação MOS de 0 a 4,5.
Como isso é um pouco subjetivo, vamos adicionar a opção de decidir se queremos exibir a resolução preferida e uma descrição do resultado com base na pontuação MOS, ou seja, (Bom, Ruim, Excelente...).
Você pode decidir se deseja incluir a resolução recomendada e o rótulo da partitura ativando ou desativando a addFeedback variável em /src/variables.js. Você também pode utilizar os ErrorNames do módulo npm para adicionar seus próprios erros, dependendo do erro gerado, e fornecer algumas recomendações aos usuários.
E agora?
O projeto concluído está disponível no GitHub, e você pode ler mais sobre a Video API da Vonage em nossa documentação.