Overview
MeetMates implements real-time video chat using WebRTC (Web Real-Time Communication) for peer-to-peer connections. The system uses Socket.io for signaling and supports dynamic video/audio toggling, connection recovery, and graceful fallback to text-only chat.
Architecture
Client A Server (Socket.io) Client B
| | |
|--- ready-to-connect --------->| |
| |<------- ready-to-connect ----|
| | |
|<--- create-offer -------------| |
| | |
|--- webrtc-offer ------------->| |
| |------- webrtc-offer -------->|
| | |
| |<------ webrtc-answer --------|
|<--- webrtc-answer ------------| |
| | |
|--- ice-candidate ------------>| |
| |------ ice-candidate -------->|
| | |
|<========== Direct P2P Connection Established =============>|
WebRTC Signaling (Server)
The server acts as a signaling intermediary to establish peer connections. Located in server.js:224-267:
Socket Events
server.js (Ready Signal)
server.js (Offer/Answer Exchange)
server.js (ICE Candidates)
// Track users ready for WebRTC
let rtcReadyUsers = new Set ();
socket . on ( "ready-to-connect" , () => {
rtcReadyUsers . add ( socket . id );
if ( chatPairs [ socket . id ]) {
const partnerId = chatPairs [ socket . id ]. partner ;
// Both users ready - initiate offer from lower socket ID
if ( rtcReadyUsers . has ( partnerId )) {
if ( socket . id < partnerId ) {
socket . emit ( "create-offer" );
}
}
}
});
The server uses deterministic offer creation (lower socket.id creates the offer) to prevent offer/answer collisions.
Client Implementation
The WebRTC client is implemented in VideoChat.jsx with comprehensive connection management.
ICE Configuration
const iceServers = {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
{ urls: "stun:stun2.l.google.com:19302" },
],
};
STUN (Session Traversal Utilities for NAT) servers help establish connections through NAT/firewalls by discovering public IP addresses.
Peer Connection Setup
VideoChat.jsx (Create Connection)
VideoChat.jsx (Media Capture)
const createPeerConnection = () => {
// Close existing connection
if ( peerConnectionRef . current ) {
peerConnectionRef . current . close ();
}
const pc = new RTCPeerConnection ( iceServers );
peerConnectionRef . current = pc ;
// Add local media tracks
if ( localStreamRef . current ) {
localStreamRef . current . getTracks (). forEach (( track ) => {
pc . addTrack ( track , localStreamRef . current );
});
}
// Handle remote tracks
pc . ontrack = ( event ) => {
console . log ( "Remote track received" , event . streams [ 0 ]);
if ( remoteVideoRef . current && event . streams [ 0 ]) {
remoteVideoRef . current . srcObject = event . streams [ 0 ];
setIsConnected ( true );
}
};
// ... connection state handlers
};
Connection State Management
pc . onconnectionstatechange = () => {
const state = pc . connectionState ;
console . log ( "Connection state:" , state );
setConnectionState ( state );
if ( state === "connected" ) {
setIsConnected ( true );
} else if ( state === "failed" || state === "disconnected" || state === "closed" ) {
setIsConnected ( false );
// Retry connection (max 3 attempts)
if ( state === "failed" && connectionAttempts < 3 ) {
console . log ( `Retrying... (Attempt ${ connectionAttempts + 1 } )` );
setTimeout (() => {
setConnectionAttempts (( prev ) => prev + 1 );
createPeerConnection ();
socket . emit ( "ready-to-connect" );
}, 1000 );
}
}
};
new : Connection object created, no networking yet
connecting : ICE agent is gathering candidates or establishing connection
connected : Connection successfully established
disconnected : At least one transport has failed (may recover)
failed : One or more transports have failed permanently
closed : Connection has been closed
Offer/Answer Exchange
VideoChat.jsx (Handle Offer)
VideoChat.jsx (Handle Answer)
VideoChat.jsx (Create Offer)
const handleOffer = async ( data ) => {
const pc = peerConnectionRef . current ;
if ( ! pc ) return ;
// Prevent duplicate processing
const offerId = JSON . stringify ( data . offer );
if ( processedSignalsRef . current . has ( offerId )) {
console . log ( "Duplicate offer - ignoring" );
return ;
}
processedSignalsRef . current . add ( offerId );
// Handle signaling state conflicts
if ( pc . signalingState === "have-local-offer" ) {
console . log ( "Rolling back local offer" );
await pc . setLocalDescription ({ type: "rollback" });
}
// Set remote offer
await pc . setRemoteDescription ( new RTCSessionDescription ( data . offer ));
// Create and send answer
const answer = await pc . createAnswer ();
await pc . setLocalDescription ( answer );
socket . emit ( "webrtc-answer" , { answer });
};
ICE Candidate Exchange
// Local ICE candidate generation
pc . onicecandidate = ( event ) => {
if ( event . candidate ) {
socket . emit ( "ice-candidate" , {
candidate: event . candidate ,
});
}
};
// Remote ICE candidate handling
const handleIceCandidate = async ( data ) => {
if ( data . candidate && peerConnectionRef . current ) {
await peerConnectionRef . current . addIceCandidate (
new RTCIceCandidate ( data . candidate )
);
}
};
ICE candidates must be added after remote description is set. Queue candidates if received early.
The client provides real-time media track controls:
VideoChat.jsx (Toggle Microphone)
VideoChat.jsx (Toggle Camera)
const toggleMute = () => {
if ( localStreamRef . current ) {
const audioTracks = localStreamRef . current . getAudioTracks ();
audioTracks . forEach (( track ) => {
track . enabled = ! track . enabled ;
});
setIsMuted ( ! isMuted );
}
};
Disabling tracks with track.enabled = false preserves the stream but stops transmission, reducing bandwidth while maintaining the connection.
Connection Recovery
The implementation includes automatic reconnection logic:
if ( state === "failed" && connectionAttempts < 3 ) {
console . log ( `Connection failed. Retrying... (Attempt ${ connectionAttempts + 1 } )` );
setTimeout (() => {
setConnectionAttempts (( prev ) => prev + 1 );
createPeerConnection ();
socket . emit ( "ready-to-connect" );
}, 1000 );
}
Manual reconnection is also available:
const reconnect = () => {
setConnectionAttempts ( 0 );
createPeerConnection ();
socket . emit ( "ready-to-connect" );
};
Cleanup and Resource Management
return () => {
isComponentMounted = false ;
// Remove socket listeners
socket . off ( "webrtc-offer" );
socket . off ( "webrtc-answer" );
socket . off ( "ice-candidate" );
socket . off ( "create-offer" );
socket . off ( "partnerLeft" );
// Stop all media tracks
if ( localStreamRef . current ) {
localStreamRef . current . getTracks (). forEach (( track ) => track . stop ());
}
// Close peer connection
if ( peerConnectionRef . current ) {
peerConnectionRef . current . close ();
}
};
Proper cleanup is critical to prevent memory leaks and camera/microphone permission issues.
Integration with Chat Flow
Video chat integrates seamlessly with the matching system:
socket . on ( "chatStart" , ( data ) => {
const { withVideo } = data || {};
setCurrentScreen ( "chat" );
setIsVideoChat ( withVideo );
console . log ( "✅ Chat started, Video:" , withVideo );
setMessages ([{
type: "system" ,
text: "Connected to chat partner"
}]);
// Signal readiness for WebRTC
socket . emit ( "ready-to-connect" );
});
Key Features
Peer-to-Peer Direct browser-to-browser connections reduce latency and server costs
Auto Recovery Automatic reconnection with up to 3 retry attempts
Dynamic Controls Real-time mute/unmute and camera toggle without reconnection
NAT Traversal STUN server integration for connections across networks
Technical Considerations
Why deterministic offer creation?
Using socket.id < partnerId ensures only one peer creates the offer, preventing offer/answer collisions when both users signal readiness simultaneously.
Why track processed signals?
Socket.io can deliver duplicate messages. Tracking processed offers/answers prevents state machine errors in RTCPeerConnection.
Why rollback on offer collision?
If a peer receives an offer while in have-local-offer state, it must rollback to stable before processing the remote offer, following the WebRTC specification.
Currently using only STUN. For production, add TURN servers for users behind restrictive firewalls: { urls : "turn:turn.example.com" , username : "user" , credential : "pass" }