What are ICE Candidates?
After exchanging SDP (Session Description Protocol) messages between peers, peers should connect to each other to transfer real-time data. However, connecting between peers is a bit complex because each peer is behind a NAT/Firewall in their local network, and they don't know how to find right paths to the destination to exchange stream data.
The challenge of peer-to-peer connectivity in modern networks stems from the widespread use of Network Address Translation (NAT) and firewall systems. These network devices, while essential for security and address space management, create significant barriers to direct communication between devices on different networks. NAT devices modify packet headers as they traverse network boundaries, making it difficult for peers to establish direct connections using only the information available in SDP messages.
Furthermore, different NAT implementations exhibit varying behaviors, from simple port mapping to more complex symmetric NAT configurations that create unique mappings for each destination. Firewalls add another layer of complexity by blocking unsolicited incoming connections, requiring sophisticated techniques to establish bidirectional communication channels. Enterprise networks often employ multiple layers of NAT and firewall protection, creating even more challenging connectivity scenarios.
To solve this issue, WebRTC uses ICE (Interactive Connectivity Establishment) protocol, and peers can negotiate the actual connection between them by exchanging ICE candidates.
ICE represents a comprehensive framework that systematically explores all possible connectivity options between peers. The protocol operates by discovering multiple potential communication paths, testing their viability, and selecting the most optimal route for media transmission. This approach ensures robust connectivity even in complex network environments where simple direct connections would fail.
The ICE framework implements a sophisticated prioritization system that favors direct connections when possible while maintaining fallback options for challenging network scenarios. It coordinates the timing of connectivity checks to avoid overwhelming networks with excessive traffic while ensuring rapid connection establishment when conditions permit.
ICE is utilized to find all possible methods for establishing peer-to-peer connections through NAT. This is achieved by combining the functionalities of STUN (Session Traversal Utilities for NAT) servers and TURN (Traversal Using Relays around NAT) servers.
STUN servers enable peers to discover their public-facing network addresses and understand the NAT binding behavior of their network infrastructure. When a peer contacts a STUN server, it receives information about how its packets appear on the public internet, including the external IP address and port mappings created by intervening NAT devices. This information becomes crucial for generating server reflexive candidates that other peers can use to establish connections.
TURN servers provide a more robust fallback mechanism by offering relay services when direct or STUN-assisted connections prove impossible. These servers allocate public IP addresses and ports that peers can use as intermediaries for their communication. While TURN relay introduces additional latency and bandwidth costs, it ensures connectivity in the most restrictive network environments where peer-to-peer connections would otherwise be impossible.
The combination of STUN and TURN services creates a comprehensive connectivity solution that adapts to various network topologies. ICE intelligently coordinates these services, attempting more efficient direct connections first before falling back to relay mechanisms when necessary.
By adding an ICE candidate received from the remote peer via its signaling server, you can effectively manage and modify the media stream data, facilitating a successful peer-to-peer communication even across different types of NAT configurations.
The ICE candidate exchange process enables peers to share their discovered connectivity options and collaboratively determine the best communication path. Each candidate represents a potential network route with associated priority information that helps peers make informed decisions about which connection attempts to prioritize. This distributed approach to connectivity establishment ensures that peers can adapt to changing network conditions and find optimal communication paths even in dynamic environments.
Add an ICE Candidate
You can set an RTCPeerConnection
s remote description, which is a standard format for describing multimedia communication sessions for a peer-to-peer connection by calling RTCPeerConnection.addIceCandidate() method like the code below:
await peerConnection.addIceCandidate(candidate)
The addIceCandidate()
method performs several critical operations beyond simply storing the candidate information. When a new ICE candidate is added, the peer connection immediately evaluates whether this candidate provides a viable connectivity option for the current session. The method validates the candidate's format, checks its compatibility with the local network configuration, and integrates it into the ongoing connectivity establishment process.
The timing of ICE candidate addition is crucial for optimal connection performance. Candidates should be added as soon as they are received from the remote peer to minimize connection establishment delays. The peer connection maintains an internal queue of pending candidates and processes them according to ICE prioritization rules, ensuring that the most promising connectivity options are tested first.
This method adds a new ICE candidate to the connection. You can obtain the candidate from the remote peer via the signaling channel. The candidate contains the network connection information such as the IP address and port of the remote peer like the example below:
a=candidate:7344997325 1 udp 2043216322 192.168.0.42 44323 typ host
This candidate string follows the ICE candidate format specified in RFC standards, where each component provides specific information about the connectivity option. The foundation (7344997325) serves as a unique identifier for candidates that share the same base address. The component identifier (1) indicates whether this candidate is for RTP (1) or RTCP (2) traffic. The transport protocol (udp) specifies the underlying network protocol, while the priority (2043216322) indicates the preference level for this candidate.
The IP address (192.168.0.42) and port (44323) define the actual network endpoint for this connectivity option. The candidate type (typ host) indicates this is a host candidate representing a direct interface on the peer's device. Other candidate types include server reflexive (srflx) for STUN-discovered addresses, peer reflexive (prflx) for addresses discovered during connectivity checks, and relay candidates for TURN-allocated addresses.
Additional attributes may be present in ICE candidate strings, including related addresses for server reflexive and relay candidates, generation numbers for ICE restart scenarios, and extension attributes for advanced ICE features. Understanding these components helps developers debug connectivity issues and optimize their ICE server configurations.
The RTCPeerConnection.addIceCandidate()
receives a candidate
parameter, which describes the properties of the candidate from the SDP attribute. Now, you should get an ICE candidate object from the remote peer.
Exchange ICE Candidates
You may already be noticed, if you want to add an ICE candidate with the addIceCandidate()
method, you should have a candidate
instance. You can get a candidate instance by adding an event listener, which listens to ICE candidates from a peer connection:
localPeerConnection.addEventListener('icecandidate', e => onIceCandidate(peerConnection, e));
The icecandidate
event represents a fundamental mechanism in WebRTC's connectivity establishment process. This event fires whenever the local ICE agent discovers a new potential connectivity path, providing applications with the opportunity to share these discoveries with remote peers. The event-driven nature of ICE candidate discovery allows for asynchronous and efficient connectivity establishment that doesn't block other WebRTC operations.
ICE candidate generation occurs in multiple phases, starting with host candidate discovery from local network interfaces, followed by server reflexive candidate gathering through STUN servers, and finally relay candidate allocation from TURN servers if configured. Each phase may generate multiple candidates as the ICE agent explores different network interfaces, protocols, and server configurations.
The timing and frequency of icecandidate events depend on various factors including network topology, ICE server configuration, and local network interface availability. Applications should be prepared to handle multiple candidate events over several seconds, especially in complex network environments or when multiple ICE servers are configured.
The icecandidate event is sent to an peer connection when the local peer set the SDP with RTCPeerConnection.setLocalDescription() method. Then the candidate should be sent to the remote peer over the signaling server, so the remote peer can add it to its set of remote candidates:
const candidate = getCandidateFromSignalingServer();
await remotePeerConnection.addIceCandidate(candidate);
The relationship between SDP setting and ICE candidate generation is intrinsic to WebRTC's design. Setting the local description triggers the ICE gathering process because the peer connection now knows what types of media streams will be transmitted and can configure appropriate ICE components. The number of ICE components depends on whether RTP and RTCP are multiplexed or use separate ports, which is determined by the SDP negotiation.
Signaling servers play a crucial role in ICE candidate exchange by providing reliable, ordered delivery of candidate information between peers. The server must handle candidate messages with appropriate prioritization to ensure time-sensitive connectivity information reaches peers quickly. Modern signaling implementations often include features like candidate trickling, which allows incremental candidate delivery rather than waiting for complete candidate gathering.
The local peer also should receive ICE candidates from the signaling server and add the candidates to the peer connection.
const candidate = getCandidateFromSignalingServer();
await localPeerConnection.addIceCandidate(candidate);
Bidirectional ICE candidate exchange creates a comprehensive connectivity matrix where both peers share their available network paths. This mutual sharing enables the ICE framework to evaluate all possible connectivity combinations and select the optimal path based on factors like latency, packet loss, and network type preferences. The process continues until both peers have successfully established connectivity or all options have been exhausted.
Error handling during candidate addition is important because network conditions can change rapidly, and some candidates may become invalid between generation and application. Applications should implement robust error handling that gracefully manages candidate addition failures without disrupting the overall connection establishment process.
Now you understand how to exchange ICE candidates between the local peer and the remote peer.
Local and Remote Peer Connection
Now let's combine all the concepts above and establish a peer connection by exchanging ICE candidates between a local peer (p1
) and a remote peer (p2
):
let pc1, pc2;
const offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
};
function getName(pc) {
return (pc === pc1) ? 'pc1' : 'pc2';
}
function getOtherPc(pc) {
return (pc === pc1) ? pc2 : pc1;
}
async function call() {
pc1 = new RTCPeerConnection();
pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
pc2 = new RTCPeerConnection();
pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
}
async function onIceCandidate(pc, event) {
try {
await (getOtherPc(pc).addIceCandidate(event.candidate));
console.log(`${getName(pc)} addIceCandidate success`);
} catch (e) {
onCatch(pc, e);
}
console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
}
function onCatch(error) {
const errorElement = document.querySelector('#errorMsg');
errorElement.innerHTML += `<p>Something went wrong: ${error.name}</p>`;
}
This comprehensive example demonstrates the complete ICE candidate exchange workflow in a controlled local environment. The code creates two peer connections that simulate the behavior of peers on different networks, even though they're actually running on the same page. Each peer connection is configured with an icecandidate event listener that automatically forwards discovered candidates to the other peer, simulating the role of a signaling server.
The onIceCandidate function includes important error handling and logging that helps developers understand the ICE candidate exchange process. The null candidate check is particularly important because the ICE gathering process concludes with a null candidate event, indicating that candidate gathering is complete. This final event helps applications determine when they can stop expecting additional candidates from a particular peer.
The example also demonstrates the asynchronous nature of ICE candidate processing. Candidates are generated and exchanged independently of other WebRTC operations, allowing the connection establishment process to proceed efficiently. The error handling ensures that failed candidate additions don't disrupt the overall connectivity establishment process, as multiple candidates provide redundancy for connection establishment.
Typically, exchanging ICE candidates should be done by the signaling server, but in this tutorial, we don't use a signaling server and set up a connection between two RTCPeerConnection
objects (known as peers) on the same page to help you better grasp how ICE candidates are exchanged.
In production environments, signaling servers implement sophisticated ICE candidate handling strategies that optimize connection establishment performance. These may include candidate buffering to reduce signaling overhead, priority-based candidate ordering to accelerate connection establishment, and intelligent candidate filtering to reduce unnecessary network traffic.
Modern signaling implementations often support advanced features like ICE candidate trickling, which allows candidates to be shared incrementally as they're discovered rather than waiting for complete gathering. This approach significantly reduces connection establishment latency, especially in scenarios with multiple ICE servers or complex network topologies.
The signaling server also plays a crucial role in handling ICE restart scenarios, where network conditions change during an active call and new candidates must be gathered and exchanged. These scenarios require careful coordination to ensure media continuity while establishing new connectivity paths.
You've learned the essential concepts to establish a peer connection. Now, let's build a demo project that simulates a peer connection.