Servidor de Sinalização WebRTC: Guia Completo com Exemplos Node.js
Servidor de Sinalização WebRTC: Guia Completo com Exemplos Node.js
WebRTC permite que navegadores enviem vídeo, áudio e dados diretamente um ao outro. Sem plugins, sem Flash, sem servidores intermediários tocando seus fluxos de mídia. Mas aqui está a coisa que confunde todo desenvolvedor pela primeira vez: dois navegadores não podem apenas encontrar um ao outro magicamente na internet. Eles precisam de um intermediário. Esse intermediário é o servidor de sinalização.
Construí servidores de sinalização para plataformas de consulta por vídeo, jogos multiplayer no navegador e ferramentas de edição colaborativa. Toda vez, a camada de sinalização é onde a confusão mora -- não porque seja difícil, mas porque WebRTC intencionalmente a deixa sem especificação. A especificação diz qual informação precisa ser trocada, mas nada diz sobre como trocá-la. Isso é tanto um presente quanto uma maldição.
Este guia cobre tudo: o que a sinalização realmente faz sob o capô, o protocolo offer/answer com SDP, troca de candidatos ICE e travessia de NAT, e depois duas implementações prontas para produção -- uma com Node.js + Socket.io e outra usando Supabase Realtime. Vamos ver código real, não pseudocódigo.
Índice
- O Que Um Servidor de Sinalização Realmente Faz?
- O Protocolo Offer/Answer e SDP
- Candidatos ICE e Travessia de NAT
- Opções de Transporte de Sinalização Comparadas
- Implementação 1: Node.js + Socket.io
- Implementação 2: Supabase Realtime
- Dicas de Endurecimento para Produção
- Servidores TURN: Quando Conexões Diretas Falham
- FAQ

O Que Um Servidor de Sinalização Realmente Faz?
Vamos tirar a gíria. Um servidor de sinalização é um retransmissor de mensagens. É só isso. Dois peers precisam trocar alguns quilobytes de texto antes de poderem se conectar diretamente. O servidor de sinalização carrega essas mensagens de um lado para o outro.
Especificamente, um servidor de sinalização manipula:
- Descoberta de peers -- Usuário A precisa dizer ao Usuário B "Quero me conectar com você." Alguém tem que rotear essa mensagem.
- Troca SDP -- Ambos os peers geram blobs do Session Description Protocol que descrevem suas capacidades de mídia (codecs, criptografia, etc.). Esses precisam ir de A para B e voltar.
- Retransmissão de candidato ICE -- Cada peer descobre caminhos de rede potenciais (candidatos) e os envia ao outro peer através do servidor de sinalização.
- Ciclo de vida da sessão -- Iniciando, renegociando e derrubando conexões.
Aqui está o que o servidor de sinalização NÃO faz: ele nunca toca seus dados de áudio ou vídeo. Uma vez que os dois peers estabelecem uma conexão direta, o trabalho do servidor de sinalização é essencialmente concluído. A mídia flui peer-to-peer.
Pense nisso como apresentar duas pessoas em uma festa. Você vai lá, diz "Ei Sarah, este é Mike, vocês dois gostam de escalada." Aí você se afasta. Sarah e Mike continuam de lá. Você é o servidor de sinalização nesse cenário.
Por Que WebRTC Não Padroniza Sinalização
Esta é uma escolha deliberada, não uma omissão. Os autores da especificação WebRTC reconheceram que a maioria das aplicações já possui um canal de comunicação entre usuários -- um sistema de chat, um serviço de matchmaking, uma plataforma de colaboração. Forçar um protocolo de sinalização específico significaria que toda app precisa implementar duas camadas de comunicação em vez de aproveitar o que já existe.
Um aplicativo de namoro já possui infraestrutura de mensagens. Uma plataforma de telemedicina já possui sistemas de agendamento. Uma plataforma de gaming já possui servidores de lobby. WebRTC permite que você use o que você já tem para passar essas mensagens SDP e ICE.
Você literalmente poderia usar pombos-correio carregando pen drives se a latência não importasse. (Ela importa, mas o ponto permanece.)
O Protocolo Offer/Answer e SDP
A troca de sinalização central segue um padrão chamado modelo offer/answer, emprestado de SIP (Session Initiation Protocol). Aqui está o fluxo:
- Peer A cria um
RTCPeerConnectione chamacreateOffer() - Peer A define a oferta como sua descrição local via
setLocalDescription() - Peer A envia a oferta (um blob SDP) ao Peer B através do servidor de sinalização
- Peer B recebe a oferta e a define como sua descrição remota via
setRemoteDescription() - Peer B chama
createAnswer() - Peer B define a resposta como sua descrição local
- Peer B envia a resposta de volta através do servidor de sinalização
- Peer A recebe a resposta e a define como sua descrição remota
Após essa troca, ambos os peers conhecem as capacidades de mídia um do outro. Mas ainda não conseguem se conectar -- eles precisam de informações de caminho de rede, que vêm do ICE.
O Que Está Dentro de um Blob SDP?
SDP é um formato de texto que parece ter sido projetado em 1996. Porque foi. Aqui está um exemplo resumido:
v=0
o=- 4625943584070133116 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:aR7B
a=ice-pwd:hN3Y2mDoS68sGLprHvBGNbKp
a=fingerprint:sha-256 D1:3C:A0:...
a=setup:actpass
a=mid:0
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000
As partes importantes: lista codecs suportados (VP8, H264), impressões digitais de criptografia, credenciais ICE e tipos de mídia. Seu servidor de sinalização não precisa fazer parse de nada disso. Ele apenas passa o blob como uma string opaca.
O Offer/Answer SDP em Código
Aqui está o JavaScript do lado do cliente para o peer que faz a oferta:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
]
});
// Adicionar faixas de mídia locais
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Criar e enviar oferta
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Enviar para servidor de sinalização
signalingServer.send({
type: 'offer',
sdp: pc.localDescription,
targetPeerId: 'peer-b-id'
});
E o peer que responde:
signalingServer.on('offer', async (data) => {
await pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signalingServer.send({
type: 'answer',
sdp: pc.localDescription,
targetPeerId: data.fromPeerId
});
});
Candidatos ICE e Travessia de NAT
Negociação SDP manipula qual mídia enviar. ICE manipula como alcançar o outro peer na rede. É aqui que as coisas ficam interessantes -- e é onde a maioria das conexões WebRTC realmente falha no mundo real.
O Problema NAT
A maioria dos dispositivos fica atrás de um roteador executando NAT (Network Address Translation). Seu laptop pode ter um IP local de 192.168.1.42, mas o mundo exterior vê o IP público do seu roteador. Quando dois peers ficam atrás de NATs, nenhum deles sabe como alcançar o outro diretamente.
ICE (Interactive Connectivity Establishment) resolve isso reunindo múltiplos caminhos de candidatos e tentando todos eles:
| Tipo de Candidato | Fonte | Taxa de Sucesso | Latência |
|---|---|---|---|
| Host | Interface de rede local | Funciona apenas na mesma LAN | Mais baixa |
| Server-reflexive (srflx) | Descoberto via servidor STUN | ~80-85% das conexões | Baixa |
| Relay | Alocado via servidor TURN | ~99%+ (fallback) | Mais alta (retransmitida) |
Trickling de Candidato ICE
Aqui está uma otimização crítica: trickle ICE. Em vez de esperar que todos os candidatos sejam reunidos antes de enviá-los, você envia cada candidato ao peer remoto assim que for descoberto. Isso pode economizar segundos na estabelecimento de conexão.
// Lado do chamador: enviar candidatos conforme chegam
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate,
targetPeerId: remotePeerId
});
}
};
// Lado do receptor: adicionar candidatos conforme chegam
signalingServer.on('ice-candidate', async (data) => {
try {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch (err) {
// Candidatos podem chegar antes da descrição remota ser definida
// Armazené-los em buffer e adicione depois
console.error('Falha ao adicionar candidato ICE:', err);
}
});
Esse bloco catch dica em um problema do mundo real: candidatos ICE podem chegar antes de você chamar setRemoteDescription(). Você precisa armazená-los em buffer. Vamos lidar com isso adequadamente nas implementações de produção abaixo.
Buffering de Candidato ICE
Isto é algo que quase todo tutorial ignora, e causa bugs em produção. Aqui está o padrão:
let pendingCandidates = [];
let remoteDescriptionSet = false;
signalingServer.on('ice-candidate', async (data) => {
if (remoteDescriptionSet) {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} else {
pendingCandidates.push(data.candidate);
}
});
// Após definir descrição remota:
await pc.setRemoteDescription(remoteDesc);
remoteDescriptionSet = true;
for (const candidate of pendingCandidates) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates = [];

Opções de Transporte de Sinalização Comparadas
Você pode usar basicamente qualquer canal de comunicação bidirecional para sinalização. Aqui está como as opções comuns se comparam:
| Transporte | Prós | Contras | Melhor Para |
|---|---|---|---|
| WebSocket (raw) | Baixa latência, full-duplex, leve | Lógica de reconexão manual, sem salas | Aplicativos 1:1 simples |
| Socket.io | Reconexão automática, salas, fallback para polling | Tamanho maior da biblioteca (~45KB), não é um padrão | Maioria dos aplicativos web |
| Supabase Realtime | Infraestrutura gerenciada, auth embutida, integração Postgres | Vendor lock-in, limites de tamanho de mensagem | Aplicativos já no Supabase |
| Firebase Realtime DB | Gerenciado, boa documentação, suporte offline | Vendor lock-in, preços em escala | Aplicativos baseados em Firebase |
| HTTP polling | Funciona em todo lugar, simples de implementar | Alta latência, carga do servidor | Ambientes legados |
| SIP over WebSocket | Interoperabilidade com sistemas de telefonia | Complexo, overkill para maioria dos apps web | Integração VoIP |
Para a maioria dos projetos, Socket.io ou um serviço realtime gerenciado como Supabase é a escolha certa. Vamos construir ambos.
Implementação 1: Node.js + Socket.io
Esta é a abordagem mais comum e a que eu recomendaria para equipes que desejam controle total sobre sua infraestrutura de sinalização. Estamos construindo um servidor de sinalização que suporta salas com múltiplos peers.
Configuração do Servidor
import { createServer } from 'http';
import { Server } from 'socket.io';
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_ORIGIN || 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
// Rastrear salas e seus participantes
const rooms = new Map();
io.on('connection', (socket) => {
console.log(`Peer conectado: ${socket.id}`);
socket.on('join-room', (roomId) => {
socket.join(roomId);
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
}
rooms.get(roomId).add(socket.id);
// Notificar peers existentes na sala
socket.to(roomId).emit('peer-joined', { peerId: socket.id });
// Enviar ao novo peer uma lista de peers existentes
const existingPeers = [...rooms.get(roomId)].filter(id => id !== socket.id);
socket.emit('existing-peers', { peers: existingPeers });
});
socket.on('offer', ({ targetPeerId, sdp }) => {
io.to(targetPeerId).emit('offer', {
sdp,
fromPeerId: socket.id
});
});
socket.on('answer', ({ targetPeerId, sdp }) => {
io.to(targetPeerId).emit('answer', {
sdp,
fromPeerId: socket.id
});
});
socket.on('ice-candidate', ({ targetPeerId, candidate }) => {
io.to(targetPeerId).emit('ice-candidate', {
candidate,
fromPeerId: socket.id
});
});
socket.on('disconnect', () => {
// Limpar salas
for (const [roomId, peers] of rooms.entries()) {
if (peers.has(socket.id)) {
peers.delete(socket.id);
socket.to(roomId).emit('peer-left', { peerId: socket.id });
if (peers.size === 0) {
rooms.delete(roomId);
}
}
}
});
});
const PORT = process.env.PORT || 3001;
httpServer.listen(PORT, () => {
console.log(`Servidor de sinalização rodando na porta ${PORT}`);
});
Integração do Lado do Cliente
import { io } from 'socket.io-client';
const socket = io('wss://your-signaling-server.com');
const peerConnections = new Map();
const ICE_SERVERS = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
// Adicionar servidor TURN para produção
]
};
function createPeerConnection(remotePeerId) {
const pc = new RTCPeerConnection(ICE_SERVERS);
let pendingCandidates = [];
let isRemoteDescSet = false;
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice-candidate', {
targetPeerId: remotePeerId,
candidate: event.candidate
});
}
};
pc.ontrack = (event) => {
// Anexar fluxo remoto a um elemento de vídeo
const remoteVideo = document.getElementById(`video-${remotePeerId}`);
if (remoteVideo) {
remoteVideo.srcObject = event.streams[0];
}
};
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
console.warn(`Conexão com ${remotePeerId} ${pc.connectionState}`);
// Implementar lógica de reconexão aqui
}
};
// Armazenar a conexão e seu estado
peerConnections.set(remotePeerId, {
pc,
pendingCandidates,
isRemoteDescSet,
setRemoteDescDone() {
this.isRemoteDescSet = true;
this.pendingCandidates.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c)));
this.pendingCandidates = [];
}
});
return pc;
}
// Entrar em uma sala
socket.emit('join-room', 'my-room-id');
// Quando um novo peer entra, criar uma oferta
socket.on('peer-joined', async ({ peerId }) => {
const pc = createPeerConnection(peerId);
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', { targetPeerId: peerId, sdp: pc.localDescription });
});
socket.on('offer', async ({ sdp, fromPeerId }) => {
const pc = createPeerConnection(fromPeerId);
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
await pc.setRemoteDescription(new RTCSessionDescription(sdp));
peerConnections.get(fromPeerId).setRemoteDescDone();
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', { targetPeerId: fromPeerId, sdp: pc.localDescription });
});
socket.on('answer', async ({ sdp, fromPeerId }) => {
const conn = peerConnections.get(fromPeerId);
await conn.pc.setRemoteDescription(new RTCSessionDescription(sdp));
conn.setRemoteDescDone();
});
socket.on('ice-candidate', ({ candidate, fromPeerId }) => {
const conn = peerConnections.get(fromPeerId);
if (conn.isRemoteDescSet) {
conn.pc.addIceCandidate(new RTCIceCandidate(candidate));
} else {
conn.pendingCandidates.push(candidate);
}
});
Isso manipula múltiplos peers em uma sala, armazena em buffer candidatos ICE corretamente e faz limpeza ao desconectar. É o padrão que usamos ao construir recursos em tempo real em aplicativos Next.js.
Implementação 2: Supabase Realtime
Se você já está usando Supabase (ou quer evitar executar seu próprio servidor WebSocket), canais Supabase Realtime funcionam lindamente para sinalização. Essa abordagem usa o recurso Broadcast do Supabase -- nenhuma escrita de banco de dados necessária.
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
const roomId = 'video-call-room-123';
const myPeerId = crypto.randomUUID();
const channel = supabase.channel(roomId, {
config: { broadcast: { self: false } }
});
// Escutar mensagens de sinalização
channel
.on('broadcast', { event: 'offer' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleOffer(payload);
}
})
.on('broadcast', { event: 'answer' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleAnswer(payload);
}
})
.on('broadcast', { event: 'ice-candidate' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleIceCandidate(payload);
}
})
.on('broadcast', { event: 'peer-joined' }, ({ payload }) => {
if (payload.peerId !== myPeerId) {
initiateConnection(payload.peerId);
}
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Anunciar presença
await channel.send({
type: 'broadcast',
event: 'peer-joined',
payload: { peerId: myPeerId }
});
}
});
// Enviar oferta
async function sendOffer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'offer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// Enviar resposta
async function sendAnswer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'answer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// Enviar candidato ICE
async function sendIceCandidate(targetPeerId, candidate) {
await channel.send({
type: 'broadcast',
event: 'ice-candidate',
payload: { candidate, fromPeerId: myPeerId, targetPeerId }
});
}
A abordagem Supabase tem uma vantagem notável: você obtém rastreamento de presença de graça usando o recurso Presence do Supabase. Você pode ver quem está em uma sala sem código extra. O trade-off é que mensagens de broadcast são visíveis para todos os subscritores do canal -- você está filtrando por targetPeerId do lado do cliente, o que é bom para sinalização (não é dados sensíveis; a mídia real é criptografada).
Para equipes construindo no Supabase, isso elimina um servidor inteiro de sua infraestrutura. Usamos esse padrão em projetos de CMS headless onde colaboração em tempo real era um requisito.
Dicas de Endurecimento para Produção
Fazer sinalização funcionar em desenvolvimento é direto. Mantê-la confiável em produção é onde o trabalho real está.
1. Ordenação de Mensagens e Deduplicação
Mensagens WebSocket podem chegar fora de ordem durante reconexões. Adicione números de sequência às suas mensagens e manipule reordenação no cliente:
let messageSeq = 0;
function sendSignalingMessage(type, payload) {
socket.emit(type, { ...payload, seq: ++messageSeq, timestamp: Date.now() });
}
2. Heartbeats e Reconexão
Socket.io manipula reconexão automaticamente, mas você precisa entrar novamente nas salas após reconectar:
socket.on('connect', () => {
if (currentRoom) {
socket.emit('join-room', currentRoom);
}
});
3. Limitação de Taxa
Trickling de candidato ICE pode gerar dezenas de mensagens em rápida sucessão. Em um servidor ocupado, isso se acumula. Implementar limitação de taxa por socket:
import { RateLimiterMemory } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemory({
points: 50, // mensagens
duration: 10, // por 10 segundos
});
io.on('connection', (socket) => {
socket.use(async ([event, ...args], next) => {
try {
await rateLimiter.consume(socket.id);
next();
} catch {
next(new Error('Taxa limite excedida'));
}
});
});
4. Autenticação
Nunca execute um servidor de sinalização sem autenticação em produção. Com Socket.io, use middleware:
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
try {
const user = await verifyJWT(token);
socket.data.userId = user.id;
next();
} catch {
next(new Error('Autenticação falhou'));
}
});
5. Escalando Horizontalmente
Um único processo Node.js pode manipular milhares de conexões de sinalização (sinalização é leve -- apenas mensagens de texto). Quando você precisa escalar além de um servidor, use o @socket.io/redis-adapter:
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
Isso garante que socket.to(peerId).emit() funcione em múltiplas instâncias de servidor.
Servidores TURN: Quando Conexões Diretas Falham
STUN funciona para aproximadamente 80-85% das conexões. Para os 15-20% restantes (NATs simétricos, firewalls restritivos, redes corporativas), você precisa de um servidor TURN para retransmitir tráfego de mídia.
Em 2026, suas opções para TURN incluem:
| Provedor | Preço (aprox.) | Notas |
|---|---|---|
| Cloudflare Calls TURN | Nível gratuito disponível, baseado em uso | Melhor valor para maioria dos projetos |
| Twilio TURN | $0.40/GB | Confiável, bem documentado |
| Metered.ca | 500GB/mês grátis, depois $0.40/GB | Popular para projetos independentes |
| Self-hosted coturn | Apenas custos de servidor | Controle total, carga operacional |
| Xirsys | A partir de $24.99/mês | PoPs globais |
Sempre inclua pelo menos um servidor TURN em sua configuração ICE. Dizer aos usuários "funciona para maioria das pessoas" não é aceitável quando um VP está tentando fazer um demo do seu produto em WiFi de hotel.
Se você está construindo algo que precisa funcionar de forma confiável em redes corporativas, entre em contato conosco -- ajudamos equipes a arquitetar soluções WebRTC que manipulam os casos extremos complicados.
FAQ
O que é um servidor de sinalização WebRTC?
Um servidor de sinalização é um retransmissor de mensagens que ajuda dois peers WebRTC a trocar as informações que precisam para estabelecer uma conexão direta. Ele carrega ofertas e respostas SDP (que descrevem capacidades de mídia) e candidatos ICE (que descrevem caminhos de rede). O servidor de sinalização nunca toca dados de áudio ou vídeo -- uma vez que os peers se conectam, a mídia flui diretamente entre eles.
Por que WebRTC não inclui um protocolo de sinalização padrão?
A especificação WebRTC intencionalmente deixa sinalização sem especificação porque a maioria das aplicações já possui um canal de comunicação entre usuários. Um aplicativo de chat, um serviço de matchmaking ou uma ferramenta de colaboração podem reutilizar sua infraestrutura existente para sinalização. Essa flexibilidade significa que você pode usar WebSockets, HTTP, SIP, XMPP ou qualquer outro transporte que possa carregar mensagens de texto.
Qual é a diferença entre oferta SDP e resposta SDP?
Uma oferta SDP é criada pelo peer que inicia a conexão. Ela descreve as capacidades de mídia desse peer -- codecs suportados, métodos de criptografia e tipos de mídia. A resposta SDP é criada pelo peer receptor e confirma quais capacidades da oferta ele suporta. Juntas, elas negociam os parâmetros compartilhados que ambos os peers usarão para a sessão de mídia.
O que são candidatos ICE e por que precisam ser trocados?
Candidatos ICE são caminhos de rede potenciais através dos quais um peer pode ser alcançado. Cada peer descobre candidatos verificando suas interfaces de rede locais, consultando servidores STUN para endereços voltados para o público e alocando endereços de relay em servidores TURN. Esses candidatos devem ser enviados ao outro peer através do servidor de sinalização para que ambos os lados possam tentar múltiplos caminhos e encontrar um que funcione através de NATs e firewalls.
Posso usar Supabase Realtime como um servidor de sinalização WebRTC?
Sim. O recurso Broadcast do Supabase Realtime funciona bem para sinalização porque fornece entrega de mensagens de baixa latência entre clientes conectados sem exigir nenhuma escrita de banco de dados. Você cria um canal para cada sala, retransmite mensagens offer/answer/ICE e filtra por ID do peer alvo no lado do cliente. É uma escolha sólida para projetos já usando Supabase que querem evitar executar um servidor de sinalização separado.
Quantas conexões simultâneas um servidor de sinalização pode manipular?
Um único processo Node.js + Socket.io pode tipicamente manipular 10.000–50.000 conexões de sinalização simultâneas, dependendo da frequência de mensagens e recursos do servidor. Tráfego de sinalização é leve -- apenas pequenas mensagens JSON. Para escala maior, use o adapter Redis com Socket.io para distribuir conexões entre múltiplas instâncias de servidor. O gargalo em WebRTC raramente é o servidor de sinalização; é usualmente a largura de banda de relay TURN.
Preciso de um servidor TURN em produção?
Sim. Enquanto STUN manipula cerca de 80-85% das conexões, aproximadamente 15-20% dos usuários ficam atrás de NATs simétricos ou firewalls restritivos que impedem conexões diretas. Sem um servidor TURN, esses usuários simplesmente não conseguem se conectar. Em ambientes corporativos, a taxa de falha sem TURN pode ser ainda mais alta. Serviços como Cloudflare Calls, Twilio e Metered.ca oferecem TURN com níveis gratuitos.
O que acontece se o servidor de sinalização cair durante uma chamada ativa?
Uma vez que uma conexão WebRTC é estabelecida, ela não depende do servidor de sinalização. Uma chamada existente continuará funcionando se o servidor de sinalização falhar. No entanto, novas conexões não podem ser estabelecidas, e se a conexão existente precisar ser renegociada (por exemplo, adicionando compartilhamento de tela), isso falhará. Você deve projetar seu servidor de sinalização para alta disponibilidade usando clustering ou serviços gerenciados.