Complete guide to WebRTC implementation, lifecycle, and configuration
WebRTC (Web Real-Time Communication) is a technology that enables peer-to-peer communication between browsers and mobile applications. It allows real-time audio, video, and data exchange without requiring plugins or additional software.
User initiates or joins a call by emitting rtc:joinCall with callId and optional initial media state. Backend creates the call room if it doesn't exist and adds the user as a participant. If other participants exist, they receive rtc:participantJoined event.
After joining, the client receives rtc:callStarted event with the callId and list of all current participants. This allows the client to synchronize with existing participants.
Clients fetch RTC configuration via GET /api/v1/rtc/config to get STUN/TURN servers and create RTCPeerConnection (Web) or PeerConnection (Unity) with ICE servers.
Caller creates an offer using createOffer() and sets it as local description. The SDP is sent via rtc:offer event to the target user.
Receiver sets the offer as remote description, creates an answer with createAnswer(), sets it as local description, and sends it via rtc:answer event.
As ICE candidates are discovered, they are exchanged via rtc:candidate events. This continues until a connection is established or all candidates are exhausted.
When ICE connection state becomes connected, media streams flow directly between peers (or via TURN relay if needed).
Participants can toggle audio/video/screen sharing via rtc:toggleMute, rtc:toggleVideo, rtc:toggleScreenShare. Changes are broadcast via rtc:mediaUpdated.
User emits rtc:leaveCall or disconnects. Backend removes them from the room and broadcasts rtc:participantLeft to remaining participants. When the last participant leaves, the room is automatically cleaned up.
| Event | Payload | Description |
|---|---|---|
rtc:ping |
{} |
Ping server, receive pong with config |
rtc:joinCall |
{ callId, initialMediaState? } |
Join or start a call (idempotent - creates call if doesn't exist) |
rtc:leaveCall |
{ callId } |
Leave the current call |
rtc:offer |
{ callId, toUserId, sdp } |
Send WebRTC offer SDP |
rtc:answer |
{ callId, toUserId, sdp } |
Send WebRTC answer SDP |
rtc:candidate |
{ callId, toUserId, candidate } |
Send ICE candidate |
rtc:toggleMute |
{ callId, muted } |
Toggle audio mute state |
rtc:toggleVideo |
{ callId, enabled } |
Toggle video enabled state |
rtc:toggleScreenShare |
{ callId, enabled } |
Toggle screen sharing |
| Event | Payload | Description |
|---|---|---|
rtc:pong |
{ ts, rtc: { maxParticipants }, echo } |
Response to ping |
rtc:callStarted |
{ callId, participants } |
Call started, includes all participants |
rtc:participantJoined |
{ callId, userId, mediaState } |
New participant joined |
rtc:participantLeft |
{ callId, userId } |
Participant left the call |
rtc:offer |
{ callId, fromUserId, toUserId, sdp } |
Relayed offer from another participant |
rtc:answer |
{ callId, fromUserId, toUserId, sdp } |
Relayed answer from another participant |
rtc:candidate |
{ callId, fromUserId, toUserId, candidate } |
Relayed ICE candidate |
rtc:mediaUpdated |
{ callId, userId, mediaState } |
Participant media state changed |
rtc:error |
{ code, message, details? } |
Error occurred |
STUN server URLs (comma-separated). Defaults to Google STUN servers if not set.
RTC_STUN_URLS=stun:stun.l.google.com:19302,stun:stun1.l.google.com:19302
TURN server URLs with transport types. Required for 4G/5G networks.
RTC_TURN_URLS=turn:turn.dananeer.io:3478?transport=udp,turn:turn.dananeer.io:3478?transport=tcp,turns:turn.dananeer.io:443?transport=tcp
TURN server username. Required if TURN_URLS is set.
RTC_TURN_USERNAME=dananeer_turn
TURN server password. Required if TURN_URLS is set.
RTC_TURN_CREDENTIAL=rZBSJ0z2mrDU4ja6j9DT5yPG
Maximum participants per call. Default: 4, Range: 2-10.
RTC_MAX_PARTICIPANTS=4
UDP transport (fastest)
UDP/TCP transport
TLS transport (port 443)
TLS transport (port 443)
TLS transport (port 443)
TURN relay
TLS transport (port 443)
Retrieve STUN/TURN server configuration for WebRTC peer connections.
GET /api/v1/rtc/config
Headers:
Authorization: Bearer <access_token>
Response: 200 OK
{
"turnUrls": [
"turn:turn.dananeer.io:3478?transport=udp",
"turn:turn.dananeer.io:3478?transport=tcp",
"turns:turn.dananeer.io:443?transport=tcp"
],
"turnUsername": "dananeer_turn",
"turnCredential": "rZBSJ0z2mrDU4ja6j9DT5yPG",
"stunUrls": [
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302"
],
"maxParticipants": 4
}
async function fetchRtcConfig() {
const response = await fetch('/api/v1/rtc/config', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return await response.json();
}
const config = await fetchRtcConfig();
const iceServers = [
...config.stunUrls.map(url => ({ urls: url })),
...config.turnUrls.map(url => ({
urls: url,
username: config.turnUsername,
credential: config.turnCredential
}))
];
const pc = new RTCPeerConnection({ iceServers });
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('rtc:candidate', {
callId: currentCallId,
toUserId: targetUserId,
candidate: event.candidate
});
}
};
socket.on('rtc:offer', async (data) => {
await pc.setRemoteDescription(
new RTCSessionDescription({ type: 'offer', sdp: data.sdp })
);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('rtc:answer', {
callId: data.callId,
toUserId: data.fromUserId,
sdp: pc.localDescription.sdp
});
});
socket.on('rtc:answer', async (data) => {
await pc.setRemoteDescription(
new RTCSessionDescription({ type: 'answer', sdp: data.sdp })
);
});
pc.ontrack = (event) => {
const remoteStream = event.streams[0];
remoteVideoElement.srcObject = remoteStream;
};
navigator.mediaDevices.getUserMedia({ audio: true, video: true })
.then(stream => {
localVideoElement.srcObject = stream;
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
});
Unity applications can integrate with the WebRTC backend using Socket.IO client libraries and Unity's WebRTC package. The backend API is platform-agnostic and works with any WebRTC-compatible client.
Unity WebRTC package is available through Unity Package Manager. You'll also need a Socket.IO client library compatible with Unity (C#) for WebSocket signaling. Popular options include SocketIOClient or similar C# Socket.IO implementations.
Unity WebRTC supports multiple platforms including Windows, macOS, Linux, iOS, and Android. This makes it ideal for cross-platform game development with voice communication features.
/api/v1/rtc/config endpoint with your access token to retrieve STUN/TURN server configuration.rtc:joinCall event with the callId to join or start a call.rtc:offer, rtc:answer, and rtc:candidate events and process them with Unity WebRTC APIs.rtc:callStarted, rtc:participantJoined, rtc:participantLeft) and update your game UI accordingly.The backend WebRTC API is fully compatible with Unity clients. All events, payloads, and lifecycle management work identically whether connecting from a web browser or Unity application. The only difference is the WebRTC implementation library used on the client side.
When testing Unity integration, ensure you test on the target platforms (especially mobile) as WebRTC behavior can vary between platforms. Test with different network conditions including WiFi, 4G, and 5G to verify TURN server configuration is working correctly.
Problem: WebRTC connection fails on mobile 4G networks.
Solution: Ensure TURN servers with TLS transport are configured:
RTC_TURN_URLS=turns:turn.dananeer.io:443?transport=tcp
Problem: Connection established but no media streams.
Solution: Check that tracks are added to peer connection and remote stream handlers are set up correctly.
Problem: ICE connection state remains "checking" indefinitely.
Solution: Verify STUN/TURN servers are accessible and credentials are correct. Check browser console for ICE candidate errors.
Problem: High latency in media streams.
Solution: Check if TURN relay is being used (check ICE connection type). For LAN/WiFi, ensure UDP transport is prioritized.
pc.oniceconnectionstatechange = () => {
console.log('ICE Connection State:', pc.iceConnectionState);
};
pc.onconnectionstatechange = () => {
console.log('Connection State:', pc.connectionState);
};
pc.onicegatheringstatechange = () => {
console.log('ICE Gathering State:', pc.iceGatheringState);
};