Primeiros passos com as conexões de peering

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)