Peer connections is the part of the WebRTC specifications that deals with
connecting two applications on different computers to communicate using a
peer-to-peer protocol. The communication between peers can be video, audio or
arbitrary binary data (for clients supporting the RTCDataChannel
API). In
order to discover how two peers can connect, both clients need to provide an ICE
Server configuration. This is either a STUN or a TURN-server, and their role is
to provide ICE candidates to each client which is then transferred to the remote
peer. This transferring of ICE candidates is commonly called signaling.
Signaling
The WebRTC specification includes APIs for communicating with an ICE (Internet Connectivity Establishment) Server, but the signaling component is not part of it. Signaling is needed in order for two peers to share how they should connect. Usually this is solved through a regular HTTP-based Web API (i.e., a REST service or other RPC mechanism) where web applications can relay the necessary information before the peer connection is initiated.
The follow code snippet shows how this fictious signaling service can be used to send and receive messages asynchronously. This will be used in the remaining examples in this guide where necessary.
// 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!');
Signaling can be implemented in many different ways, and the WebRTC specification doesn't prefer any specific solution.
Initiating peer connections
Each peer connection is handled by a RTCPeerConnection
object. The constructor
for this class takes a single RTCConfiguration
object as its parameter. This
object defines how the peer connection is set up and should contain information
about the ICE servers to use.
Once the RTCPeerConnection
is created we need to create an SDP offer or
answer, depending on if we are the calling peer or receiving peer. Once the SDP
offer or answer is created, it must be sent to the remote peer through a
different channel. Passing SDP objects to remote peers is called signaling and
is not covered by the WebRTC specification.
To initiate the peer connection setup from the calling side, we create a
RTCPeerConnection
object and then call createOffer()
to create a
RTCSessionDescription
object. This session description is set as the local
description using setLocalDescription()
and is then sent over our signaling
channel to the receiving side. We also set up a listener to our signaling
channel for when an answer to our offered session description is received from
the receiving side.
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});
}
On the receiving side, we wait for an incoming offer before we create our
RTCPeerConnection
instance. Once that is done we set the received offer using
setRemoteDescription()
. Next, we call createAnswer()
to create an answer to
the received offer. This answer is set as the local description using
setLocalDescription()
and then sent to the calling side over our signaling
server.
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});
}
});
Once the two peers have set both the local and remote session descriptions they know the capabilities of the remote peer. This doesn't mean that the connection between the peers is ready. For this to work we need to collect the ICE candidates at each peer and transfer (over the signaling channel) to the other peer.
ICE candidates
Before two peers can communitcate using WebRTC, they need to exchange connectivity information. Since the network conditions can vary depending on a number of factors, an external service is usually used for discovering the possible candidates for connecting to a peer. This service is called ICE and is using either a STUN or a TURN server. STUN stands for Session Traversal Utilities for NAT, and is usually used indirectly in most WebRTC applications.
TURN (Traversal Using Relay NAT) is the more advanced solution that incorporates
the STUN protocols and most commercial WebRTC based services use a TURN server
for establishing connections between peers. The WebRTC API supports both STUN
and TURN directly, and it is gathered under the more complete term Internet
Connectivity Establishment. When creating a WebRTC connection, we usually
provide one or several ICE servers in the configuration for the
RTCPeerConnection
object.
Trickle ICE
Once a RTCPeerConnection
object is created, the underlying framework uses the
provided ICE servers to gather candidates for connectivity establishment (ICE
candidates). The event icegatheringstatechange
on RTCPeerConnection
signals
in what state the ICE gathering is (new
, gathering
or complete
).
While it is possible for a peer to wait until the ICE gathering is complete, it is usually much more efficient to use a "trickle ice" technique and transmit each ICE candidate to the remote peer as it gets discovered. This will significantly reduce the setup time for the peer connectivity and allow a video call to get started with less delays.
To gather ICE candidates, simply add a listener for the icecandidate
event.
The RTCPeerConnectionIceEvent
emitted on that listener will contain
candidate
property that represents a new candidate that should be sent to the
remote peer (See 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);
}
}
});
Connection established
Once ICE candidates are being received, we should expect the state for our peer
connection will eventually change to a connected state. To detect this, we add a
listener to our RTCPeerConnection
where we listen for connectionstatechange
events.
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});