
Compartilhar:
Hui Jing é Developer Advocate na Nexmo. Ela tem um amor imenso por CSS e tipografia e, de modo geral, é apaixonada por tudo que diz respeito à web.
Adicione a funcionalidade de mensagens de texto a um bate-papo por vídeo com a Video API da Vonage
Tempo de leitura: 10 minutos
Esta série de tutoriais explorará a Video API da Vonage (anteriormente TokBox OpenTok) e o que você pode criar com ela. A Video API é muito robusta e altamente personalizável, e em cada postagem mostraremos como implementar um recurso específico usando a API. Desta vez, veremos como adicionar mensagens de texto a um bate-papo básico de áudio e vídeo.
Como essa aplicação exigirá algum código do lado do servidor, vamos usar o Glitch para facilitar a configuração. Você também pode baixar o código deste projeto do Glitch e implantá-lo em seu próprio servidor ou na plataforma de hospedagem de sua preferência (provavelmente será necessário fazer alguns ajustes de configuração de acordo com os requisitos da sua plataforma).
Não utilizaremos nenhum framework de front-end nesta série, apenas JavaScript puro, para manter o foco na própria Video API. Ao final deste tutorial, você deverá ser capaz de criar um aplicativo de bate-papo com áudio e vídeo, com funcionalidade de bate-papo por texto. O bate-papo por texto é implementado usando a API de sinalização.
Screenshot of video chat with text chat
O código final desta aplicação pode ser encontrado neste repositório do GitHub ou modificado no Glitch.
Pré-requisitos
Antes de começarmos, você precisará de um Account da Video API da Vonage, que pode ser criado gratuitamente aqui. Você também precisará do do Node.js instalado (caso você não esteja usando o Glitch).
Este tutorial dá continuidade ao primeiro post introdutório da série: Criação de um chat de vídeo básico. Se esta for a primeira vez que você usa a Video API, recomendamos fortemente que você leia esse post, pois ele aborda a configuração básica a seguir:
Criar um projeto da Video API da Vonage
Configuração no Glitch
Estrutura básica do projeto
Inicializando uma sessão
Conectando-se à sessão, assinando e publicando
Estilos básicos de layout para um bate-papo por Video
Noções básicas sobre bate-papo por texto com a Video API da Vonage
A implementação do chat de texto com a Video API é feita por meio da API de sinalização. Esse mecanismo de sinalização permite que os clientes conectados a uma sessão enviem mensagens de texto e dados uns aos outros. Por enquanto, vamos nos concentrar apenas no texto.
O Client SDK da Video API acionará um evento quando o cliente receber um sinal. Para um chat de texto básico, no qual as mensagens ficam visíveis para todos os clientes conectados, usaremos o signal() método da objeto Session . Os clientes participantes receberão esse sinal ao escutar o evento de sinal disparado pelo Session .
Para uma análise detalhada do que está disponível na objeto Session , suas propriedades, métodos e eventos, consulte a referência do SDK para o objeto Session.
Configuração inicial
Como vamos desenvolver um chat de vídeo básico, comece remixando o projeto do chat de vídeo básico criado no tutorial anterior. Clique no grande botão “Remix” abaixo para fazer isso. 👇

Sua estrutura de pastas deve ficar mais ou menos assim:
Folder structure of the project
Conforme mencionado no início, o TokBox OpenTok agora é a Video API da Vonage. Não fizemos nenhuma alteração nos nomes dos nossos pacotes, portanto, você continuará fazendo referência ao OpenTok em seu código.
Se você tivesse feito um remix do projeto Glitch, seu server.js arquivo já deveria estar assim:
const express = require("express");
const app = express();
const OpenTok = require("opentok");
const OT = new OpenTok(process.env.API_KEY, process.env.API_SECRET);
let sessions = {};
app.use(express.static("public"));
app.get("/", (request, response) => {
response.sendFile(__dirname + "/views/landing.html");
});
app.get("/session/:room", (request, response) => {
response.sendFile(__dirname + "/views/index.html");
});
app.post("/session/:room", (request, response) => {
const roomName = request.params.room;
// Check if the session already exists
if (sessions[roomName]) {
// Generate the token
generateToken(roomName, response);
} else {
// If the session does not exist, create one
OT.createSession((error, session) => {
if (error) {
console.log("Error creating session:", error);
} else {
// Store the session in the sessions object
sessions[roomName] = session.sessionId;
// Generate the token
generateToken(roomName, response);
}
});
}
});
function generateToken(roomName, response) {
// Configure token options
const tokenOptions = {
role: "publisher",
data: `roomname=${roomName}`
};
// Generate token with the Video API Client SDK
let token = OT.generateToken(
sessions[roomName],
tokenOptions
);
// Send the required credentials back to to the client
// as a response from the fetch request
response.status(200);
response.send({
sessionId: sessions[roomName],
token: token,
apiKey: process.env.API_KEY
});
}
const listener = app.listen(process.env.PORT, () => {
console.log("Your app is listening on port " + listener.address().port);
});
Para colocar o chat por vídeo em funcionamento, acesse o .env arquivo e preencha com sua chave de API e seu segredo do projeto, que você encontra no painel de controle. Depois disso, vamos trabalhar no código do lado do cliente para fazer o chat de texto funcionar antes de revisitar o server.js arquivo novamente.
Adicione a marcação necessária
Nosso aplicativo será composto por duas páginas. Uma página inicial com dois campos de texto. Um para que os usuários criem uma sessão, que chamaremos de “Sala”, para que participantes posteriores possam entrar nessa mesma “Sala”. O outro campo de texto serve para que os usuários insiram um nome de exibição que possam usar para se identificar.
A página terá um elemento de formulário simples com dois campos de entrada para que os usuários insiram o nome da sala e o nome de usuário. Vamos adicionar o campo de nome de usuário ao formulário.
<form class="registration" id="registration">
<label>
<span>Room</span>
<input
type="text"
name="room-name"
placeholder="Enter room name"
required
/>
</label>
<!-- Add the user name input field and label -->
<label>
<span>User name</span>
<input
type="text"
name="user-name"
placeholder="Enter your name"
required
/>
</label>
<button>Enter</button>
</form>
Também precisamos adicionar uma janela de bate-papo à index.html página. Vamos adicionar a marcação para uma janela de bate-papo com um cabeçalho, uma área para exibir mensagens e um campo de entrada na parte inferior para digitar e enviar mensagens. Para maximizar o espaço na tela, a janela de bate-papo ficará fora da tela por padrão e só será ativada quando você clicar no botão de bate-papo no canto inferior direito da página.
Chat button to trigger chat window
Adicione a seguinte marcação à sua página; na próxima seção, vamos aplicar os estilos para que o botão fique com a aparência do design acima.
<button class="btn-chat" id="showChat" aria-label="Show chat">
<svg viewBox="0 0 512 512">
<path
fill="white"
d="m512 346.5c0-63.535156-36.449219-120.238281-91.039062-147.820312-1.695313 121.820312-100.460938 220.585937-222.28125 222.28125 27.582031 54.589843 84.285156 91.039062 147.820312 91.039062 29.789062 0 58.757812-7.933594 84.210938-23.007812l80.566406 22.285156-22.285156-80.566406c15.074218-25.453126 23.007812-54.421876 23.007812-84.210938zm0 0"
/>
<path
fill="white"
d="m391 195.5c0-107.800781-87.699219-195.5-195.5-195.5s-195.5 87.699219-195.5 195.5c0 35.132812 9.351562 69.339844 27.109375 99.371094l-26.390625 95.40625 95.410156-26.386719c30.03125 17.757813 64.238282 27.109375 99.371094 27.109375 107.800781 0 195.5-87.699219 195.5-195.5zm-225.5-45.5h-30c0-33.085938 26.914062-60 60-60s60 26.914062 60 60c0 16.792969-7.109375 32.933594-19.511719 44.277344l-25.488281 23.328125v23.394531h-30v-36.605469l35.234375-32.25c6.296875-5.761719 9.765625-13.625 9.765625-22.144531 0-16.542969-13.457031-30-30-30s-30 13.457031-30 30zm15 121h30v30h-30zm0 0"
/>
</svg>
</button>
Também queremos adicionar a marcação da janela de bate-papo ao index.html arquivo.
Bare bones chat window which will slide in from the right
<aside id="chatWindow">
<header class="chat-header">
<h1><span id="roomName"></span>Chat</h1>
<button class="btn-close" id="closeChat" aria-label="Close chat">
<svg viewBox="0 0 47.971 47.971" role="img">
<path
d="M28.228 23.986L47.092 5.122a2.998 2.998 0 000-4.242 2.998 2.998 0 00-4.242 0L23.986 19.744 5.121.88a2.998 2.998 0 00-4.242 0 2.998 2.998 0 000 4.242l18.865 18.864L.879 42.85a2.998 2.998 0 104.242 4.241l18.865-18.864L42.85 47.091c.586.586 1.354.879 2.121.879s1.535-.293 2.121-.879a2.998 2.998 0 000-4.242L28.228 23.986z"
/>
</svg>
</button>
</header>
<section id="messageArea" class="messages"></section>
<form class="chat-form" id="chatForm">
<input id="chatInput" type="text" />
<input type="submit" value="Send" />
</form>
</aside>
Definir o estilo dos elementos relacionados ao chat
Nossa interface de bate-papo ficará oculta até que seja necessária, já que o principal recurso deste aplicativo é o bate-papo por vídeo. Para ativar ou desativar a interface de bate-papo, os usuários deverão clicar no botão de bate-papo no canto inferior direito da página. Esse botão possui um ícone SVG para indicar que ele aciona o bate-papo.
.btn-chat {
height: 3.5em;
width: 3.5em;
background-color: black;
border-radius: 50%;
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.2), 0 3px 6px 0 rgba(0, 0, 0, 0.19);
position: fixed;
right: 1em;
bottom: 1em;
}
.btn-chat svg {
height: 1.5em;
width: 1.5em;
}Por padrão, estamos usando CSS para ocultar a janela de bate-papo fora da área visível. Quando alguém clica no ícone de bate-papo, isso ativa uma classe CSS que altera o translateX valor de forma que a janela deslize para dentro da tela a partir do lado direito da tela.
aside {
position: fixed;
top: 0;
right: 0;
transform: translateX(100%);
display: flex;
flex-direction: column;
min-width: 20em;
width: 25%;
height: 100%;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
transition: transform 0.5s ease;
}
aside.active {
transform: translateX(0);
}Vamos adicionar alguns estilos ao cabeçalho, à área de mensagens e também ao formulário de inserção de mensagens.
.chat-header {
background-color: white;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
display: flex;
align-items: center;
}
.btn-close {
margin-left: auto;
height: 2em;
width: 2em;
background: transparent;
border: none;
cursor: pointer;
}
.btn-close svg {
height: 1em;
width: 1em;
}
.messages {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: scroll;
padding: 0.5em;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
background-color: rgba(255, 255, 255, 0.75);
}
.messages p {
margin-bottom: 0.5em;
display: flex;
word-break: break-word;
}
.chat-form {
padding: 0.5em;
background-color: white;
display: flex;
align-items: center;
}
.chat-form input[type="text"] {
flex: 1;
}
.chat-form input[type="submit"] {
margin-left: 0.5em;
align-self: stretch;
}Com esses estilos, você deverá ver um ícone de bate-papo no canto inferior direito do index.html arquivo depois de entrar na sala. Mas clicar nele ainda não faz nada.
Vamos adicionar um manipulador de eventos para ativar ou desativar uma classe CSS que faça a janela de bate-papo deslizar até ficar visível no client.js arquivo. Além disso, vamos adicionar outro no ícone de fechar da janela de bate-papo para ocultá-la novamente.
const showChatBtn = document.getElementById("showChat");
showChatBtn.addEventListener(
"click",
event => {
const chatWindow = document.getElementById("chatWindow");
chatWindow.classList.toggle("active");
},
false
);
const closeChatBtn = document.getElementById("closeChat");
closeChatBtn.addEventListener(
"click",
event => {
const chatWindow = document.getElementById("chatWindow");
chatWindow.classList.remove("active");
},
false
);
Essa não é a única maneira de personalizar uma interface de bate-papo, então fique à vontade para fazer as alterações que achar necessárias.
Como usar a API do Signal para bate-papo por texto
Vamos fazer mais alguns ajustes no projeto básico e colocar o chat de texto mais simples para funcionar.
Vamos mover a session variável para fora da initializeSession função e vamos declará-la globalmente no início do client.js arquivo.
let session;
function initializeSession(apiKey, sessionId, token) {
// Create a session object with the sessionId
session = OT.initSession(apiKey, sessionId);
// All the rest of the code
}Adicione um ouvinte de evento ao formulário de bate-papo, que enviará um sinal a todos os clientes conectados à sessão quando o formulário for enviado.
const chat = document.getElementById("chatForm");
const msgTxt = document.getElementById("chatInput");
chat.addEventListener(
"submit",
event => {
event.preventDefault();
session.signal(
{
type: "msg",
data: `${msgTxt.value}`
},
() => {
msgTxt.value = "";
}
);
},
false
);
Também precisamos de outro ouvinte de eventos para receber essa mensagem, monitorando o signal evento disparado pelo objeto Session. A carga útil desse evento será usada para exibir a mensagem na área de mensagens da janela de bate-papo.
function initializeSession(apiKey, sessionId, token) {
// Create a session object with the sessionId
session = OT.initSession(apiKey, sessionId);
// All the rest of the code
// Event listener for the msg signal
session.on("signal:msg", event => {
const content = event.data;
updateChat(content);
});
}
function updateChat(content) {
const msgHistory = document.getElementById("messageArea");
const msg = document.createElement("p");
msg.textContent = content;
msgHistory.appendChild(msg);
msgHistory.scroll({
top: msgHistory.scrollHeight,
behavior: "smooth"
});
}
Esse é o mecanismo básico do chat de texto com a Video API. Se você digitar algo na janela de chat e clicar em “Enviar”, a mensagem deve aparecer na tela.
Identificar os participantes do chat
No entanto, para tornar o chat mais fácil de usar, também queremos adicionar uma forma de identificar quem disse o quê no chat. Utilizaremos o campo de nome de usuário na página inicial para obter essa informação, enviando-a ao servidor como uma string de consulta na URL.
O script a seguir na landing.html página passa o nome da sala e o nome de usuário inseridos para a index.html página.
const form = document.getElementById("registration");
form.addEventListener("submit", event => {
event.preventDefault();
location.href = `/session/${form.elements["room-name"].value}?username=${form.elements["user-name"].value}`;
});
Quando a index.html página for carregada, isso acionará uma POST solicitação para a session/:name rota, extraindo o nome de usuário enviado da URL e repassando-o ao servidor.
const url = new URL(window.location.href);
const roomName = url.pathname.split("/")[2];
const userName = url.searchParams.get("username");
fetch(location.pathname, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: userName })
})
.then(res => {
return res.json();
})
.then(res => {
const apiKey = res.apiKey;
const sessionId = res.sessionId;
const token = res.token;
const streamName = res.streamName;
initializeSession(apiKey, sessionId, token, streamName);
})
.catch(handleCallback);
Agora, precisaremos modificar essa rota no server.js arquivo para que ela processe o nome da sala e o nome do usuário e retorne as informações necessárias para a initializeSession função. Também precisamos incluir um middleware para processar a carga útil da solicitação.
// Middleware to read the body of the request
app.use(express.json());
app.post("/session/:room", (request, response) => {
const roomName = request.params.room;
const streamName = request.body.username;
// Check if the session already exists
if (sessions[roomName]) {
// Generate the token
generateToken(roomName, streamName, response);
} else {
// If the session does not exist, create one
OT.createSession((error, session) => {
if (error) {
console.log("Error creating session:", error);
} else {
// Store the session in the sessions object
sessions[roomName] = session.sessionId;
// Generate the token
generateToken(roomName, streamName, response);
}
});
}
});
function generateToken(roomName, streamName, response) {
// Configure token options
const tokenOptions = {
role: "publisher",
data: `roomname=${roomName}?streamname=${streamName}`
};
// Generate token with the Video API Client SDK
let token = OT.generateToken(sessions[roomName], tokenOptions);
// Send the required credentials back to to the client
// as a response from the fetch request
response.status(200);
response.send({
sessionId: sessions[roomName],
token: token,
apiKey: process.env.API_KEY
});
}
Na client.js, agora podemos exibir o nome de usuário do participante que digitou a mensagem, modificando a data propriedade do session.signal() método.
session.signal(
{
type: "msg",
data: `${session.connection.data.split("=")[2]}: ${msgTxt.value}`
},
() => {
msgTxt.value = "";
}
);
Agora, quando você enviar uma mensagem de texto, ela será precedida pelo nome de usuário que você usou ao entrar na sala.
Configurar o PouchDB como um repositório de dados
No entanto, se você atualizar a página, todas as mensagens anteriores do chat desaparecerão. Isso ocorre porque não armazenamos as mensagens, mas apenas as exibimos na tela. Vamos implementar algum tipo de armazenamento de dados para as mensagens.
Vamos usar PouchDB no servidor neste tutorial, mas você pode substituí-lo por qualquer banco de dados de sua preferência.
Instale pouchdb-node com o seguinte comando (observe que usar pnpm é uma característica do Glitch):
pouchdb-node installation on Glitch
Vamos verificar se tudo está funcionando como esperado.
const PouchDB = require("pouchdb-node");
const sessionDb = new PouchDB("sessionDb");
sessionDb.info().then(info => console.log(info));
Você deve ver o seguinte nos registros do Glitch.
Database information printed to console
O PouchDB oferece uma API totalmente assíncrona, embora também inclua a opção de os usuários escolherem entre o formato de callback e o formato de promise. Usaremos o formato de promise neste tutorial, e o código refletirá essa escolha.
Em vez de armazenar as informações da sessão em uma variável de objeto, vamos armazená-las em nosso novo banco de dados. Remova a seguinte linha do server.js arquivo.
// We no longer need this object
let sessions = {};Vamos fazer mais ajustes na /session/:name rota. Primeiro, verificamos o banco de dados para ver se existe uma sessão; se houver, recuperamos as informações associadas e geramos o token a partir delas. Se a sessão não existir, criaremos uma nova sessão, a armazenaremos no banco de dados e, em seguida, geraremos o token de acordo com isso.
app.post("/session/:room", (request, response) => {
const roomName = request.params.room;
const streamName = request.body.username;
const isExistingSession = checkSession(roomName);
isExistingSession.then(sessionExists => {
if (sessionExists) {
sessionDb
.get(roomName)
.then(sessionInfo => {
generateToken(roomName, streamName, sessionInfo, response);
})
.catch(error => error);
} else {
OT.createSession((error, session) => {
if (error) {
console.log("Error creating session:", error);
} else {
const sessionInfo = {
_id: roomName,
sessionId: session.sessionId,
messages: []
};
sessionDb.put(sessionInfo);
generateToken(roomName, streamName, sessionInfo, response);
}
});
}
});
});
function checkSession(roomName) {
return sessionDb
.get(roomName)
.then(() => {
console.log(roomName + "exists");
return Promise.resolve(true);
})
.catch(() => {
console.log("Room does not exist");
return Promise.resolve(false);
});
}
function generateToken(roomName, streamName, sessionInfo, response) {
const tokenOptions = {
role: "publisher",
data: `roomname=${roomName}?streamname=${streamName}`
};
let token = OT.generateToken(sessionInfo.sessionId, tokenOptions);
response.status(200);
response.send({
sessionId: sessionInfo.sessionId,
token: token,
apiKey: process.env.API_KEY,
streamName: streamName
});
}
Adicionar nomes de fluxos à interface do usuário
Podemos usar o nome da transmissão na resposta para identificar as transmissões, de modo que os participantes possam passar o mouse sobre a transmissão de Video de cada um para ver o nome. Tanto o initPublisher() método quanto o subscribe() aceitam um properties , o que nos permite passar opções de personalização para a transmissão.
function initializeSession(apiKey, sessionId, token, streamName) {
// Create a session object with the sessionId
session = OT.initSession(apiKey, sessionId);
// Create a publisher
const publisher = OT.initPublisher(
"publisher",
{
insertMode: "append",
width: "100%",
height: "100%",
name: streamName
},
handleCallback
);
// Subscribe to a newly created stream
session.on("streamCreated", event => {
session.subscribe(
event.stream,
"subscriber",
{
insertMode: "append",
width: "100%",
height: "100%",
name: event.stream.name
},
handleCallback
);
});
}
Hover over a video stream to see the stream name
Salvar mensagens no banco de dados
Quando os participantes enviam mensagens de texto, queremos enviá-las via POST ao servidor para que sejam armazenadas no banco de dados. Vamos criar uma saveMessage() função para fazer isso.
function saveMessage(content) {
const message = {
_id: Date.now().toString(),
content: content,
roomname: name,
user: username
};
fetch("/message", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message)
}).catch(handleCallback);
}Modifique o ouvinte de eventos no formulário de bate-papo para acionar essa função sempre que um participante enviar uma mensagem de texto.
chat.addEventListener(
"submit",
event => {
event.preventDefault();
session.signal(
{
type: "msg",
data: `${session.connection.data.split("=")[2]}: ${msgTxt.value}`
},
() => {
saveMessage(msgTxt.value);
msgTxt.value = "";
}
);
},
false
);
No server.js arquivo, precisamos adicionar um manipulador para essa POST solicitação também.
app.post("/message", (request, response) => {
const roomName = request.body.roomName;
const message = {
timeStamp: request.body._id,
content: request.body.content,
user: request.body.user
};
sessionDb
.get(roomName)
.then(result => {
result.messages = [...result.messages, message];
return sessionDb.put(result);
})
.then(() => {
return sessionDb.get(roomName);
})
.then(result => {
response.status(200);
response.send({
latestMessage: result.messages[result.messages.length - 1]
});
})
.catch(error => console.log(error));
});
Agora que nossas mensagens estão sendo armazenadas, queremos exibi-las sempre que a página for carregada. Vamos adicionar uma getChatHistory() função no lado do cliente que acione uma GET solicitação para recuperar todas as mensagens armazenadas para aquela sessão e exibi-las na janela de bate-papo.
function getChatHistory() {
fetch(`/messages/${roomName}`)
.then(res => {
return res.json();
})
.then(res => {
const messageArea = document.getElementById("messageArea");
res.messagesArray.forEach(message => {
const msg = document.createElement("p");
msg.textContent = `${message.user}: ${message.content}`;
messageArea.appendChild(msg);
});
messageArea.scroll({
top: messageArea.scrollHeight,
behavior: "smooth"
});
})
.catch(handleCallback);
}
E a rota correspondente no lado do servidor para enviar as mensagens de volta ao cliente na forma de uma matriz.
app.get("/messages/:room", (request, response) => {
const roomName = request.params.room;
sessionDb
.get(roomName)
.then(result => {
response.status(200);
response.send({
messagesArray: result.messages
});
})
.catch(error => console.log(error));
});
Portanto, agora, mesmo que você atualize a página enquanto a sessão ainda estiver ativa, as mensagens continuarão lá. Além disso, se você digitar o mesmo nome de sala de uma sessão anterior com histórico de bate-papo salvo, esse histórico será exibido.
E agora?
O código final sobre Glitch e GitHub contém tudo o que abordamos neste post bastante extenso, mas reorganizado para que o código fique mais limpo e fácil de manter. Fique à vontade para remixar ou clonar o código e brincar com ele por conta própria.
Existem funcionalidades adicionais que podemos desenvolver com a Video API da Vonage, que serão abordadas em tutoriais futuros; mas, enquanto isso, você pode saber mais em nosso site de documentação completa. Se você tiver algum problema ou dúvida, entre em contato conosco na nossa Slack da Comunidade. Obrigado pela leitura!