Primeiros passos com as conexões de peering

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!
    }
});

Documentação da API RTCPeerConnection