As conexões de pares são a parte das especificações do WebRTC que lidam com
a conexão de dois aplicativos em computadores diferentes para se comunicar usando um
protocolo peer-to-peer. A comunicação entre pares pode ser de vídeo, áudio ou
dados binários arbitrários (para clientes que oferecem suporte à API RTCDataChannel
). Para
descobrir como dois pares podem se conectar, ambos os clientes precisam fornecer uma configuração de
servidor ICE. Esse é um servidor STUN ou TURN, e a função dele é
fornecer candidatos ICE para cada cliente, que é transferido para o peer
remoto. Essa transferência de candidatos ICE é comumente chamada de sinalização.
Sinalização
A especificação do WebRTC inclui APIs para se comunicar com um servidor ICE (Estabelecimento de Conexão de Internet), mas o componente de sinalização não faz parte dela. A sinalização é necessária para que dois pares compartilhem como eles devem se conectar. Isso geralmente é resolvido com uma API da Web regular baseada em HTTP (ou seja, um serviço REST ou outro mecanismo de RPC), em que os aplicativos da Web podem transmitir as informações necessárias antes que a conexão de peer seja iniciada.
O snippet de código a seguir mostra como esse serviço de sinalização fictício pode ser usado para enviar e receber mensagens de forma assíncrona. Esse valor será usado nos exemplos restantes deste guia, quando necessário.
// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
// New message from remote client received
});
// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');
A sinalização pode ser implementada de várias maneiras, e a especificação do WebRTC não prefere nenhuma solução específica.
Como iniciar conexões de ponto a ponto
Cada conexão de peer é processada por um objeto RTCPeerConnection
. O construtor
dessa classe usa um único objeto RTCConfiguration
como parâmetro. Esse
objeto define como a conexão de peer é configurada e precisa conter informações
sobre os servidores ICE a serem usados.
Depois que o RTCPeerConnection
for criado, será necessário criar uma oferta ou
resposta do SDP, dependendo se somos o par de chamada ou o par de recebimento. Depois que a oferta ou resposta do SDP
for criada, ela precisa ser enviada ao peer remoto por um
canal diferente. A transmissão de objetos SDP para pares remotos é chamada de sinalização e
não está incluída na especificação do WebRTC.
Para iniciar a configuração de conexão de peer do lado de chamada, criamos um
objeto RTCPeerConnection
e chamamos createOffer()
para criar um
objeto RTCSessionDescription
. Essa descrição de sessão é definida como a descrição local
usando setLocalDescription()
e, em seguida, é enviada pelo nosso canal de
sinalização para o lado receptor. Também configuramos um listener para nosso canal
de sinalização quando uma resposta à descrição da sessão oferecida é recebida
do lado receptor.
async function makeCall() {
const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.answer) {
const remoteDesc = new RTCSessionDescription(message.answer);
await peerConnection.setRemoteDescription(remoteDesc);
}
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingChannel.send({'offer': offer});
}
No lado do destinatário, aguardamos uma oferta de entrada antes de criar a
instância RTCPeerConnection
. Depois disso, definimos a oferta recebida usando
setRemoteDescription()
. Em seguida, chamamos createAnswer()
para criar uma resposta à
oferta recebida. Essa resposta é definida como a descrição local usando
setLocalDescription()
e enviada ao lado de chamada pelo nosso servidor
de sinalização.
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.offer) {
peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
signalingChannel.send({'answer': answer});
}
});
Depois que os dois peers definem as descrições de sessão local e remota, eles conhecem os recursos do peer remoto. Isso não significa que a conexão entre os pares esteja pronta. Para que isso funcione, precisamos coletar os candidatos ICE em cada peer e transferir (pelo canal de sinalização) para o outro peer.
Candidatos ao ICE
Antes que dois pares possam se comunicar usando o WebRTC, eles precisam trocar informações de conectividade. Como as condições de rede podem variar dependendo de vários fatores, um serviço externo geralmente é usado para descobrir os possíveis candidatos para conexão com um peer. Esse serviço é chamado de ICE e está usando um servidor STUN ou TURN. STUN significa "Session Traversal Utilities for NAT" e geralmente é usado indiretamente na maioria dos aplicativos WebRTC.
O TURN (Traversal Using Relay NAT) é a solução mais avançada que incorpora
os protocolos STUN, e a maioria dos serviços comerciais baseados em WebRTC usa um servidor TURN
para estabelecer conexões entre pares. A API WebRTC oferece suporte direto a STUN
e TURN e é coletada sob o termo mais completo "Estabelecimento de conectividade
da Internet". Ao criar uma conexão WebRTC, geralmente
fornecemos um ou vários servidores ICE na configuração do
objeto RTCPeerConnection
.
ICE de gotejamento
Depois que um objeto RTCPeerConnection
é criado, a estrutura usa os
servidores ICE fornecidos para reunir candidatos para o estabelecimento de conectividade (candidatos
ICE). O evento icegatheringstatechange
em RTCPeerConnection
indica
em que estado está a coleta de ICE (new
, gathering
ou complete
).
Embora seja possível que um peer aguarde até que a coleta de ICE seja concluída, geralmente é muito mais eficiente usar uma técnica de "gotejamento" e transmitir cada candidato ICE para o peer remoto conforme ele é descoberto. Isso reduz significativamente o tempo de configuração da conectividade de pares e permite que uma chamada de vídeo seja iniciada com menos atrasos.
Para coletar candidatos ICE, basta adicionar um listener para o evento icecandidate
.
O RTCPeerConnectionIceEvent
emitido nesse listener vai conter
a propriedade candidate
, que representa um novo candidato que precisa ser enviado ao
peer remoto (consulte "Sinais").
// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
if (event.candidate) {
signalingChannel.send({'new-ice-candidate': event.candidate});
}
});
// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
if (message.iceCandidate) {
try {
await peerConnection.addIceCandidate(message.iceCandidate);
} catch (e) {
console.error('Error adding received ice candidate', e);
}
}
});
Conexão estabelecida
Quando os candidatos do ICE estiverem sendo recebidos, o estado da nossa conexão
peer vai mudar para um estado conectado. Para detectar isso, adicionamos um
listener ao RTCPeerConnection
, em que detectamos eventos
connectionstatechange
.
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});
Documentação da API RTCPeerConnection (link em inglês)