Este artigo demonstra como construir uma aplicação de videochamada peer-to-peer utilizando WebRTC no front end e um servidor Node.js com Socket.IO para sinalização.
Servidor de Sinalização (Node.js + Express + Socket.IO)
O servidor atua como intermediário para trocar mensagesn de sinalização (ofertas SDP, respostas SDP e candidatos ICE) antre os peers conectados:
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const app = express();
const httpServer = http.createServer(app);
const io = new Server(httpServer, {
cors: { origin: '*' }
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// Mapa de clientes conectados
const connectedClients = new Map();
io.on('connection', (client) => {
console.log(`Cliente conectado: ${client.id}`);
connectedClients.set(client.id, client);
console.log(`Total online: ${connectedClients.size}`);
// Repassar candidatos ICE
client.on('relay_ice', (candidate) => {
for (const [id, peer] of connectedClients) {
if (id !== client.id) {
peer.emit('relay_ice', candidate);
}
}
});
// Repassar oferta SDP
client.on('relay_offer', (sdp) => {
for (const [id, peer] of connectedClients) {
if (id !== client.id) {
peer.emit('relay_offer', sdp);
}
}
});
// Repassar resposta SDP
client.on('relay_answer', (sdp) => {
for (const [id, peer] of connectedClients) {
if (id !== client.id) {
peer.emit('relay_answer', sdp);
}
}
});
client.on('disconnect', () => {
console.log(`Cliente desconectado: ${client.id}`);
connectedClients.delete(client.id);
});
});
httpServer.listen(5544, () => {
console.log(`Servidor ativo em http://localhost:5544`);
});
Frontend: index.html
A página HTML captura o stream de mídia local (câmera ou compartilhamento de tela), estabelece a conexão peer-to-peer via RTCPeerConnection e gerencia a troca de SDP e ICE candidates através do Socket.IO:
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Videochamada WebRTC</title>
<style>
.video-box {
width: 320px;
height: 200px;
margin: 10px;
background: #ddd;
}
#local-stream { border: 3px solid #2ecc71; }
#remote-stream { border: 3px solid #3498db; }
.layout {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
</head>
<body>
<div class="layout">
<h2>Videochamada WebRTC</h2>
<div>
<p>Stream Local:</p>
<video id="local-stream" class="video-box" autoplay muted></video>
</div>
<div>
<p>Stream Remoto:</p>
<video id="remote-stream" class="video-box" autoplay></video>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
const peerConn = new RTCPeerConnection(rtcConfig);
const socketClient = io('ws://127.0.0.1:5544');
socketClient.on('connect', () => {
console.log('Conectado ao servidor de sinalização');
});
socketClient.on('relay_ice', async (candidate) => {
try {
await peerConn.addIceCandidate(JSON.parse(candidate));
} catch (e) {
console.error('Erro ao adicionar ICE candidate:', e);
}
});
socketClient.on('relay_offer', async (sdp) => {
const desc = JSON.parse(sdp);
await peerConn.setRemoteDescription(desc);
const answer = await peerConn.createAnswer();
await peerConn.setLocalDescription(answer);
socketClient.emit('relay_answer', JSON.stringify(answer));
});
socketClient.on('relay_answer', async (sdp) => {
const desc = JSON.parse(sdp);
await peerConn.setRemoteDescription(desc);
});
peerConn.ontrack = (event) => {
document.getElementById('remote-stream').srcObject = event.streams[0];
};
peerConn.onicecandidate = (event) => {
if (event.candidate) {
socketClient.emit('relay_ice', JSON.stringify(event.candidate));
}
};
async function initMedia() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false
});
attachStream(stream);
} catch (e) {
console.log('Câmera indisponível, tentando compartilhamento de tela...');
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false
});
attachStream(screenStream);
}
}
async function attachStream(stream) {
document.getElementById('local-stream').srcObject = stream;
stream.getTracks().forEach((track) => {
peerConn.addTrack(track, stream);
});
const offer = await peerConn.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
await peerConn.setLocalDescription(offer);
socketClient.emit('relay_offer', JSON.stringify(offer));
}
initMedia();
</script>
</body>
</html>
Como executar
Instale as dependências necessárias:
npm install express socket.io
Inicie o servidor com node server.js e acesse http://localhost:5544 em duas abas ou navegadores diferentes. O servidor STUN público do Google é utilizado para descoberta de ICE candidates. Como não há HTTPS configurado, a aplicação deve ser executada em ambiente local. Se a câmera não estiver disponível, o código automaticamente tenta utilizar o compartilhamento de tela como fallback.