Primeiros passos com as conexões de peering

As conexões ponto a ponto são parte das especificações do WebRTC que lidam com a conexão de dois aplicativos em computadores diferentes para que se comuniquem usando um protocolo ponto a ponto. A comunicação entre pares pode ser vídeo, áudio ou dados binários arbitrários (para clientes compatíveis com a API RTCDataChannel). Para descobrir como dois peerings podem se conectar, os dois clientes precisam fornecer uma configuração do servidor CEI. Esse é um servidor STUN ou turn e o papel dele é fornecer candidatos ICE a cada cliente que for transferido para o peering remoto. Essa transferência de candidatos ICE é geralmente chamada de sinalização.

Sinalização

A especificação WebRTC inclui APIs para se comunicar com um servidor de estabelecimento de conectividade de Internet (ICE, na sigla em inglês), mas o componente de sinalização não faz parte dele. A sinalização é necessária para que dois apps semelhantes se comuniquem. Geralmente, isso é resolvido com uma API da Web baseada em HTTP regular (ou seja, um serviço REST ou outro mecanismo de RPC), em que os aplicativos da Web podem redirecionar as informações necessárias antes que a conexão de peering seja iniciada.

O snippet de código a seguir mostra como esse serviço fictício de sinalização 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 muitas maneiras diferentes, e a especificação WebRTC não prefere nenhuma solução específica.

Iniciar conexões de peering

Cada conexão de peering é 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 peering é configurada e precisa conter informações sobre os servidores de ICE a serem usados.

Depois que o RTCPeerConnection for criado, precisaremos criar uma oferta ou resposta de SDP, dependendo se somos o peering que faz a chamada ou o recebedor. 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 pares remotos é chamada de sinalização e não é coberta pela especificação WebRTC.

Para iniciar a configuração da conexão de peering no lado da chamada, criamos um objeto RTCPeerConnection e, em seguida, chamamos createOffer() para criar um objeto RTCSessionDescription. Essa descrição da sessão é definida como a descrição local usando setLocalDescription() e, em seguida, enviada pelo nosso canal de sinalização para o lado de recebimento. Também configuramos um listener no nosso canal de sinalização para quando uma resposta à descrição da sessão oferecida for recebida do lado do destinatário.

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 recebimento, aguardamos uma oferta recebida antes de criar nossa instância RTCPeerConnection. Quando isso é feito, 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, em seguida, enviada para o lado da chamada usando 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 apps definem as descrições da sessão local e remota, eles sabem os recursos do par remoto. Isso não significa que a conexão entre os pares está pronta. Para que isso funcione, precisamos coletar os candidatos ICE em cada peering e transferir (pelo canal de sinalização) para o outro par.

Candidatos à ICE

Antes de dois pares se comunicarem usando o WebRTC, eles precisam trocar informações de conectividade. Como as condições da rede podem variar de acordo com vários fatores, um serviço externo geralmente é usado para descobrir os possíveis candidatos a conexão com um peering. 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.

Essa é a solução mais avançada que incorpora os protocolos STUN e a maioria dos serviços comerciais com base em WebRTC usa um servidor SET para estabelecer conexões entre pares. A API WebRTC aceita STUN e turn diretamente e é coletada com o termo de conexão de Internet mais completo. Ao criar uma conexão WebRTC, geralmente fornecemos um ou vários servidores ICE na configuração do objeto RTCPeerConnection.

Trickle ice

Depois que um objeto RTCPeerConnection é criado, o framework subjacente usa os servidores de ICE fornecidos para coletar candidatos para o estabelecimento de conectividade (candidatos do ICE). O evento icegatheringstatechange na RTCPeerConnection sinaliza em que estado a coleta de ICE está (new, gathering ou complete).

Embora seja possível aguardar até que a coleta de ICE seja concluída, geralmente é muito mais eficiente usar uma técnica de "trickle ice" e transmitir cada candidato ao ICE ao par remoto conforme ele é descoberto. Isso reduzirá significativamente o tempo de configuração da conectividade de peering e permitirá que uma videochamada comece com menos atrasos.

Para coletar candidatos ICE, basta adicionar um listener ao evento icecandidate. O RTCPeerConnectionIceEvent emitido nesse listener conterá a propriedade candidate, que representa um novo candidato que precisa ser enviado ao par remoto (consulte "Signaling").

// 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 nossa conexão de peering mudará para um estado conectado. Para detectar isso, adicionamos um listener ao RTCPeerConnection, em que ouvimos eventos connectionstatechange

// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
    if (peerConnection.connectionState === 'connected') {
        // Peers connected!
    }
});

Documentação da API RTCPeerConnection