As conexões de mesmo nível são a parte das especificações do WebRTC que trata da
conexão de dois aplicativos em computadores diferentes para comunicação usando um
protocolo ponto a ponto. A comunicação entre os participantes pode ser por vídeo, áudio ou
dados binários arbitrários (para clientes que oferecem suporte à API RTCDataChannel). Para descobrir como dois peers podem se conectar, os dois clientes precisam fornecer uma configuração de servidor ICE. Ele é um servidor STUN ou TURN, e a função dele é fornecer candidatos ICE a cada cliente, que são transferidos para o peer remoto. Essa transferência de candidatos ICE é comumente chamada de sinalização.
Sinalização
A especificação WebRTC inclui APIs para comunicação com um servidor ICE (Internet Connectivity Establishment), mas o componente de sinalização não faz parte dela. A sinalização é necessária para que dois participantes compartilhem como devem se conectar. Normalmente, isso é resolvido por uma API da Web HTTP comum (ou seja, um serviço REST ou outro mecanismo RPC) em que os aplicativos da Web podem transmitir as informações necessárias antes que a conexão de mesmo nível 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. Ele 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 WebRTC não prefere nenhuma solução específica.
Como iniciar conexões de ponto a ponto
Cada conexão de mesmo nível é 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 mesmo nível é configurada e deve conter informações sobre os servidores ICE a serem usados.
Depois que o RTCPeerConnection é criado, precisamos criar uma oferta ou
resposta de SDP, dependendo se somos o peer de chamada ou de recebimento. Depois que a oferta ou resposta do SDP
é criada, ela precisa ser enviada ao peer remoto por um
canal diferente. A transmissão de objetos SDP para peers remotos é chamada de sinalização e
não é coberta pela especificação WebRTC.
Para iniciar a configuração da conexão de mesmo nível do lado da chamada, criamos um objeto RTCPeerConnection e chamamos createOffer() para criar um objeto RTCSessionDescription. Essa descrição da sessão é definida como a descrição local usando setLocalDescription() e é 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 receptor, aguardamos uma oferta recebida antes de criar nossa 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 da 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 peers está pronta. Para que isso funcione, precisamos coletar os candidatos do ICE em cada peer e transferir (pelo canal de sinalização) para o outro peer.
Candidatos do ICE
Antes que dois usuários 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, geralmente é usado um serviço externo para descobrir os possíveis candidatos a conexão com um peer. Esse serviço é chamado de ICE e usa 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 peers. A API WebRTC é compatível com STUN
e TURN diretamente, e é reunida no termo mais completo "Estabelecimento de
conectividade com a 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 subjacente usa os servidores ICE fornecidos para coletar 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 "ICE por gotejamento" e transmitir cada candidato ICE ao peer remoto à medida que ele é descoberto. Isso reduz significativamente o tempo de configuração da conectividade entre dispositivos e permite que uma videochamada seja iniciada com menos atrasos.
Para coletar candidatos ICE, basta adicionar um listener ao evento icecandidate.
O RTCPeerConnectionIceEvent emitido nesse listener vai conter a propriedade
candidate, que representa um novo candidato a ser enviado ao
peer remoto (consulte "Sinalização").
// 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
Depois que os candidatos ICE forem recebidos, o estado da conexão
de pareamento vai mudar para um estado conectado. Para detectar isso, adicionamos um
listener ao nosso RTCPeerConnection, em que detectamos eventos connectionstatechange.
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});