Las conexiones de pares son la parte de las especificaciones de WebRTC que se ocupa de conectar dos aplicaciones en diferentes computadoras para comunicarse con un protocolo de igual a igual. La comunicación entre pares puede ser de video, audio o datos binarios arbitrarios (para clientes que admiten la API de RTCDataChannel
). Para descubrir cómo pueden conectarse dos pares, ambos clientes deben proporcionar una configuración de servidor de ICE. Este es un servidor STUN o TURN, y su función es proporcionar candidatos de ICE a cada cliente, que luego se transfieren al par remoto. Esta transferencia de candidatos de ICE se conoce comúnmente como señalización.
Señalización
La especificación de WebRTC incluye APIs para comunicarse con un servidor ICE (establecimiento de conectividad a Internet), pero el componente de señalización no forma parte de ella. Se necesita señalización para que dos pares compartan cómo deben conectarse. Por lo general, esto se resuelve a través de una API web normal basada en HTTP (es decir, un servicio REST o algún otro mecanismo de RPC) en el que las aplicaciones web pueden retransmitir la información necesaria antes de que se inicie la conexión entre pares.
En el siguiente fragmento de código, se muestra cómo se puede usar este servicio de señalización ficticio para enviar y recibir mensajes de forma asíncrona. Se usará en los ejemplos restantes de esta guía cuando sea necesario.
// 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!');
La señalización se puede implementar de diferentes maneras, y la especificación de WebRTC no prefiere ninguna solución específica.
Cómo iniciar conexiones de pares
Cada conexión de par se controla con un objeto RTCPeerConnection
. El constructor de esta clase toma un solo objeto RTCConfiguration
como parámetro. Este objeto define cómo se configura la conexión entre pares y debe contener información sobre los servidores de ICE que se usarán.
Una vez que se crea el RTCPeerConnection
, debemos crear una oferta o respuesta de SDP, según si somos el par emisor o receptor. Una vez que se crea la oferta o respuesta de SDP, se debe enviar al par remoto a través de un canal diferente. Pasar objetos SDP a pares remotos se denomina señalización y no está cubierto por la especificación de WebRTC.
Para iniciar la configuración de la conexión entre pares desde el lado de la llamada, creamos un objeto RTCPeerConnection
y, luego, llamamos a createOffer()
para crear un objeto RTCSessionDescription
. Esta descripción de la sesión se establece como la descripción local con setLocalDescription()
y, luego, se envía a través de nuestro canal de señalización al lado receptor. También configuramos un objeto de escucha en nuestro canal de señalización para cuando se reciba una respuesta a la descripción de la sesión ofrecida del 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});
}
En el lado receptor, esperamos una oferta entrante antes de crear nuestra instancia de RTCPeerConnection
. Una vez que lo hayas hecho, configura la oferta recibida con setRemoteDescription()
. A continuación, llamamos a createAnswer()
para crear una respuesta a la oferta recibida. Esta respuesta se establece como la descripción local con setLocalDescription()
y, luego, se envía al lado de llamada a través de nuestro servidor de señalización.
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});
}
});
Una vez que los dos pares hayan configurado las descripciones de las sesiones locales y remotas, conocerán las capacidades del par remoto. Esto no significa que la conexión entre los pares esté lista. Para que esto funcione, debemos recopilar los candidatos de ICE en cada par y transferirlos (a través del canal de señalización) al otro par.
Candidatos de ICE
Antes de que dos pares puedan comunicarse con WebRTC, deben intercambiar información de conectividad. Dado que las condiciones de la red pueden variar según una serie de factores, por lo general, se usa un servicio externo para descubrir los posibles candidatos para conectarse a un par. Este servicio se denomina ICE y usa un servidor STUN o TURN. STUN significa utilidades de recorrido de sesión para NAT y, por lo general, se usa de forma indirecta en la mayoría de las aplicaciones de WebRTC.
TURN (Recorrido mediante NAT de retransmisión) es la solución más avanzada que incorpora los protocolos STUN, y la mayoría de los servicios comerciales basados en WebRTC usan un servidor TURN para establecer conexiones entre pares. La API de WebRTC admite STUN y TURN directamente, y se agrupa bajo el término más completo de establecimiento de conectividad a Internet. Cuando creamos una conexión WebRTC, por lo general, proporcionamos uno o varios servidores ICE en la configuración del objeto RTCPeerConnection
.
ICE de goteo
Una vez que se crea un objeto RTCPeerConnection
, el framework subyacente usa los servidores de ICE proporcionados para recopilar candidatos para el establecimiento de conectividad (candidatos de ICE). El evento icegatheringstatechange
en RTCPeerConnection
indica en qué estado se encuentra la recopilación de ICE (new
, gathering
o complete
).
Si bien es posible que un par espere hasta que se complete la recopilación de ICE, por lo general, es mucho más eficiente usar una técnica de “trickle ICE” y transmitir cada candidato de ICE al par remoto a medida que se descubre. Esto reducirá significativamente el tiempo de configuración de la conectividad entre pares y permitirá que una llamada de video comience con menos demoras.
Para recopilar candidatos de ICE, simplemente agrega un objeto de escucha para el evento icecandidate
.
El RTCPeerConnectionIceEvent
emitido en ese objeto de escucha contendrá la propiedad candidate
que representa un candidato nuevo que se debe enviar al par remoto (consulta Señalización).
// 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);
}
}
});
Se estableció la conexión
Una vez que se reciban los candidatos de ICE, deberíamos esperar que el estado de nuestra conexión de pares cambie a un estado conectado. Para detectar esto, agregamos un objeto de escucha a nuestro RTCPeerConnection
en el que escuchamos los eventos connectionstatechange
.
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});