WebRTC RTCPeerConnection is the API which 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 connect to a common signalling server and also provide an ICE Server configuration. The ice server can either be a STUN or a TURN-server, and their role is to provide ICE candidates to each client, which are then transferred to the remote peer. This transfer of ICE candidates is commonly called signalling. All these new terminologies may sound alien at first, but these are the secrets behind successfully connecting a video call between 2 computers using only browsers.
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 like a web socket), where web applications can relay the necessary information before the peer connection is initiated. Signaling can be implemented in many different ways, and the WebRTC specification doesn't prefer any specific solution.
Peer connection initiation
RTCPeerConnection API is responsible for creating the RTCPeerConnection object by instantiating it, as described in the code snippet below. 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 the 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 signalling, to be specific and is not covered by the WebRTC specification.
To initiate the peer connection setup from the calling side, we create an 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 signalling channel to the receiving side. We also set up a listener to our signalling channel for when an answer to our offered session description is received from the receiving side.
Simple signalling server
'
// 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!');
Initiating the call from browser A
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 signalling 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 their respective remote peer. This doesn't mean that the connection between the peers has already been established. For this to work, we need to collect the ICE candidates at each peer and transfer them over the signalling channel to the other peer in order to establish a connection between them.
ICE Candidates
ICE means Internet Connectivity Establishment. Before two peers can communicate 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 to discover the possible candidates for connecting to a peer. This service is called ICE and uses either a STUN or a TURN server. STUN stands for Session Traversal of User Datagram Protocol 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
Trickle ICE is a technique which is used to reduce the call setup time between two peers. Once a RTCPeerConnection object is created, the underlying framework uses the provided ICE servers to gather candidates for establishing connectivity based on the 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 this technique and transmit each ICE candidate to the remote peer as it gets discovered. This significantly reduces the setup time for peer connectivity and allows a video call to get started with fewer delays.
To gather ICE candidates, simply add a listener for the icecandidate event. The RTCPeerConnectionIceEvent emitted on that listener will contain candidate property that represent a new candidate that should be sent to the remote peer using the signaling mechanism as mentioned above.
// 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);
}
}
});
Once ICE candidates are being received, we should expect the state for our peer connection to eventually change to a connected state. To detect this, we add a listener to our RTCPeerConnection, where we listen for connection state change events.
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});
If you wish to know more about a webRTC tailored to your specific use case, please feel free to drop an email at hello@centedge.io to kickstart a conversation with us. If you wish to schedule a discussion with one of our principal engineers to discuss your use case in detail, feel free to use the Meetnow button available at the top of this page to schedule it.