peerpigeon
Version:
WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server
559 lines (481 loc) • 26 kB
JavaScript
import { EventEmitter } from './EventEmitter.js';
import { PeerConnection } from './PeerConnection.js';
import DebugLogger from './DebugLogger.js';
/**
* Handles WebRTC signaling messages (offers, answers, ICE candidates)
*/
export class SignalingHandler extends EventEmitter {
constructor(mesh, connectionManager) {
super();
this.debug = DebugLogger.create('SignalingHandler');
this.mesh = mesh;
this.connectionManager = connectionManager;
}
async handleSignalingMessage(message) {
const { type, data, fromPeerId, targetPeerId } = message;
// CRITICAL DEBUG: Log all incoming signaling messages for answer tracking
if (type === 'answer' || type === 'renegotiation-answer') {
console.log('🚨 SIGNALING CRITICAL:', type, 'from', fromPeerId?.substring(0, 8), 'to', this.mesh.peerId?.substring(0, 8));
this.debug.log('🔍 SIGNALING DEBUG: Received answer message:', {
type,
fromPeerId: fromPeerId?.substring(0, 8) + '...',
targetPeerId: targetPeerId?.substring(0, 8) + '...',
ourPeerId: this.mesh.peerId?.substring(0, 8) + '...',
hasData: !!data,
dataType: data?.type
});
}
if (fromPeerId === this.mesh.peerId) {
if (type === 'answer' || type === 'renegotiation-answer') {
console.log('🚨 REJECTING:', type, '- from our own peer ID');
this.debug.log('🔍 SIGNALING DEBUG: Rejecting answer - from our own peer ID');
}
return;
}
if (targetPeerId && targetPeerId !== this.mesh.peerId) {
if (type === 'answer' || type === 'renegotiation-answer') {
console.log('🚨 REJECTING:', type, '- target mismatch, target:', targetPeerId?.substring(0, 8), 'us:', this.mesh.peerId?.substring(0, 8));
this.debug.log('🔍 SIGNALING DEBUG: Rejecting answer - target mismatch:', {
targetPeerId: targetPeerId?.substring(0, 8) + '...',
ourPeerId: this.mesh.peerId?.substring(0, 8) + '...'
});
}
return;
}
// Ignore cleanup messages - they are handled by the server, not as peer signaling
if (type === 'cleanup' || type === 'cleanup-all') {
this.debug.log('Ignoring cleanup message:', { type, fromPeerId });
return;
}
// Ignore ping messages - they are for keepalive, not peer signaling
if (type === 'ping' || type === 'pong') {
return;
}
this.debug.log('Processing signaling message:', { type, fromPeerId, targetPeerId });
switch (type) {
case 'announce':
this.handlePeerAnnouncement(fromPeerId);
break;
case 'peer-discovered':
// Handle peer discovery messages from the signaling server
if (data && data.peerId) {
this.handlePeerAnnouncement(data.peerId);
}
break;
case 'goodbye':
this.handlePeerGoodbye(fromPeerId);
break;
case 'offer':
await this.handleOffer(data, fromPeerId);
break;
case 'renegotiation-offer':
await this.handleRenegotiationOffer(data, fromPeerId);
break;
case 'answer':
console.log('🚨 SWITCH: Reached answer case for', fromPeerId?.substring(0, 8));
this.debug.log('🔍 SIGNALING DEBUG: Reached answer case in switch statement');
await this.handleAnswer(data, fromPeerId);
break;
case 'renegotiation-answer':
console.log('🚨 SWITCH: Reached renegotiation-answer case for', fromPeerId?.substring(0, 8));
await this.handleRenegotiationAnswer(data, fromPeerId);
break;
case 'ice-restart-offer':
await this.handleIceRestartOffer(data, fromPeerId);
break;
case 'ice-restart-answer':
await this.handleIceRestartAnswer(data, fromPeerId);
break;
case 'ice-candidate':
await this.connectionManager.handleIceCandidate(data, fromPeerId);
break;
case 'connection-rejected':
this.handleConnectionRejected(data, fromPeerId);
break;
}
}
handlePeerAnnouncement(fromPeerId) {
// Prevent duplicate announcements - only announce if we haven't seen this peer before
if (this.mesh.peerDiscovery.hasPeer(fromPeerId)) {
this.debug.log(`Peer ${fromPeerId.substring(0, 8)}... already known, skipping announcement`);
return;
}
this.mesh.emit('statusChanged', { type: 'info', message: `Peer ${fromPeerId.substring(0, 8)}... announced` });
this.debug.log(`Adding discovered peer: ${fromPeerId.substring(0, 8)}... to PeerDiscovery`);
this.debug.log(`Current peer count: ${this.connectionManager.getConnectedPeerCount()}/${this.mesh.maxPeers}`);
this.debug.log(`All discovered peers: ${Array.from(this.mesh.peerDiscovery.discoveredPeers.keys()).map(p => p.substring(0, 8)).join(', ')}`);
this.mesh.peerDiscovery.addDiscoveredPeer(fromPeerId);
}
handlePeerGoodbye(fromPeerId) {
this.mesh.emit('statusChanged', { type: 'info', message: `Peer ${fromPeerId.substring(0, 8)}... left the network` });
this.mesh.peerDiscovery.removeDiscoveredPeer(fromPeerId);
this.connectionManager.disconnectPeer(fromPeerId, 'left network');
}
async handleOffer(offer, fromPeerId) {
this.debug.log('Handling offer from', fromPeerId);
// Validate offer data structure before proceeding
if (!offer || typeof offer !== 'object') {
this.debug.error(`Invalid offer data from ${fromPeerId}:`, offer);
this.mesh.emit('statusChanged', { type: 'error', message: `Invalid offer data from ${fromPeerId.substring(0, 8)}...` });
return;
}
if (!offer.type || offer.type !== 'offer') {
this.debug.error(`Invalid offer type from ${fromPeerId}:`, offer.type);
this.mesh.emit('statusChanged', { type: 'error', message: `Invalid offer type from ${fromPeerId.substring(0, 8)}...` });
return;
}
if (!offer.sdp || typeof offer.sdp !== 'string' || offer.sdp.length < 10) {
this.debug.error(`Invalid offer SDP from ${fromPeerId}:`, offer.sdp?.substring(0, 100) || 'undefined');
this.mesh.emit('statusChanged', { type: 'error', message: `Invalid offer SDP from ${fromPeerId.substring(0, 8)}...` });
return;
}
// If we already have a connection or are connecting, handle gracefully
if (this.connectionManager.hasPeer(fromPeerId)) {
const existingPeer = this.connectionManager.getPeer(fromPeerId);
const existingStatus = existingPeer.getStatus();
// If connection is already established and working, don't interfere
if (existingStatus === 'connected') {
this.debug.log(`Connection already established with ${fromPeerId}, ignoring duplicate offer`);
return;
}
// IMPROVED RACE CONDITION HANDLING:
// Instead of ignoring offers based on initiator flags, resolve race conditions intelligently
const weShouldInitiate = this.mesh.peerId > fromPeerId;
// If both peers are trying to initiate (mutual initiation race condition)
if (weShouldInitiate && existingPeer.isInitiator) {
this.debug.log(`🔄 RACE CONDITION: Both peers trying to initiate! We should initiate (${this.mesh.peerId.substring(0, 8)}... > ${fromPeerId.substring(0, 8)}...) but received offer.`);
// CRITICAL FIX: Accept the incoming offer if our connection is stuck in "have-local-offer"
// This completes the SDP handshake that was blocked by the race condition
const ourSignalingState = existingPeer.connection?.signalingState;
if (ourSignalingState === 'have-local-offer') {
this.debug.log(`✅ RACE RESOLUTION: Our connection stuck in "${ourSignalingState}" - accepting their offer to complete handshake`);
this.connectionManager.cleanupRaceCondition(fromPeerId);
} else {
this.debug.log(`❌ RACE RESOLUTION: Our connection in "${ourSignalingState}" - ignoring their offer as we should initiate`);
return;
}
} else if (!weShouldInitiate && existingPeer.isInitiator) {
this.debug.log(`🔄 INITIATOR CORRECTION: We incorrectly initiated to ${fromPeerId.substring(0, 8)}..., backing down for their offer`);
// Cleanup our outgoing attempt and accept the incoming offer
if (existingStatus === 'connecting' || existingStatus === 'new') {
this.debug.log(`✅ Cleaning up our incorrect connection attempt to ${fromPeerId.substring(0, 8)}...`);
this.connectionManager.cleanupRaceCondition(fromPeerId);
} else {
this.debug.log('Connection in progress, not cleaning up');
return;
}
} else {
this.debug.log('Already processing incoming connection from', fromPeerId);
return;
}
}
// Check if we're already attempting to connect to this peer
if (this.connectionManager.connectionAttempts.has(fromPeerId)) {
this.debug.log('Already attempting connection to', fromPeerId, 'accepting incoming offer');
this.connectionManager.connectionAttempts.delete(fromPeerId);
}
// CRITICAL: Check capacity again right before accepting to prevent race conditions
const currentCount = this.connectionManager.getConnectedPeerCount();
const totalPeerCount = this.connectionManager.peers.size;
let canAccept = this.mesh.canAcceptMorePeers();
let evictPeerId = null;
this.debug.log(`Capacity check for ${fromPeerId.substring(0, 8)}...: ${currentCount}/${this.mesh.maxPeers} connected, ${totalPeerCount} total peers, canAccept: ${canAccept}`);
if (!canAccept) {
this.debug.log(`At capacity (${currentCount}/${this.mesh.maxPeers} connected, ${totalPeerCount} total) - checking eviction and cleanup options`);
// First try: Check if we should evict a peer for this new connection
if (this.mesh.evictionStrategy) {
evictPeerId = this.mesh.evictionManager.shouldEvictForPeer(fromPeerId);
if (evictPeerId) {
this.debug.log(`Will evict ${evictPeerId.substring(0, 8)}... for incoming connection from ${fromPeerId.substring(0, 8)}...`);
canAccept = true; // We can accept because we'll evict
} else {
this.debug.log(`No suitable peer found for eviction for ${fromPeerId.substring(0, 8)}...`);
}
} else {
this.debug.log(`Eviction strategy disabled - cannot evict for ${fromPeerId.substring(0, 8)}...`);
}
// Second try: Clean up stale peers to make room
if (!canAccept) {
const stalePeerCount = this.connectionManager.getStalePeerCount();
if (stalePeerCount > 0) {
this.debug.log(`No eviction candidate, attempting to clean up ${stalePeerCount} stale peer(s) to make room for ${fromPeerId.substring(0, 8)}...`);
this.connectionManager.cleanupStalePeers();
canAccept = this.mesh.canAcceptMorePeers();
if (canAccept) {
this.debug.log(`Successfully made room after cleanup for ${fromPeerId.substring(0, 8)}...`);
}
}
}
// Final rejection if no options worked
if (!canAccept) {
const reason = `max_peers_reached (${currentCount}/${this.mesh.maxPeers} connected, ${this.connectionManager.peers.size} total, no eviction candidate)`;
this.debug.log(`Rejecting offer from ${fromPeerId.substring(0, 8)}...: ${reason}`);
try {
await this.mesh.sendSignalingMessage({
type: 'connection-rejected',
data: {
reason: 'max_peers_reached',
details: reason,
currentCount,
maxPeers: this.mesh.maxPeers
}
}, fromPeerId);
this.debug.log(`Sent connection rejection to ${fromPeerId.substring(0, 8)}...`);
} catch (error) {
this.debug.error('Failed to send connection rejection:', error);
}
return;
}
}
// Perform eviction if we determined one was needed
if (evictPeerId) {
this.mesh.emit('statusChanged', { type: 'info', message: `Evicting ${evictPeerId.substring(0, 8)}... for incoming connection from ${fromPeerId.substring(0, 8)}...` });
await this.mesh.evictionManager.evictPeer(evictPeerId, 'incoming closer peer');
}
try {
// Get current media stream if available
const localStream = this.mesh.mediaManager.localStream;
const hasMedia = localStream !== null;
const options = {
localStream,
// ALWAYS enable both audio and video transceivers for maximum compatibility
// This allows peers to receive media even if they don't have media when connecting
enableAudio: true,
enableVideo: true
};
this.debug.log(`Creating answer connection for ${fromPeerId.substring(0, 8)}... (with media: ${hasMedia})`);
const peerConnection = new PeerConnection(fromPeerId, false, options);
// Set up event handlers BEFORE creating connection to catch all events
this.connectionManager.setupPeerConnectionHandlers(peerConnection);
this.connectionManager.peers.set(fromPeerId, peerConnection);
await peerConnection.createConnection();
this.debug.log(`Processing offer from ${fromPeerId.substring(0, 8)}...`, {
type: offer.type,
sdpLength: offer.sdp?.length || 0,
hasAudio: offer.sdp?.includes('m=audio') || false,
hasVideo: offer.sdp?.includes('m=video') || false
});
// Handle the offer using PeerConnection's method
const answer = await peerConnection.handleOffer(offer);
this.debug.log(`Answer created for ${fromPeerId.substring(0, 8)}...`, {
type: answer.type,
sdpLength: answer.sdp?.length || 0,
hasAudio: answer.sdp?.includes('m=audio') || false,
hasVideo: answer.sdp?.includes('m=video') || false
});
this.debug.log('Sending answer to', fromPeerId);
// ENHANCED DEBUGGING: Log the exact answer being sent
this.debug.log(`🔄 ANSWER SEND DEBUG: Sending answer to ${fromPeerId.substring(0, 8)}...`);
this.debug.log(`🔄 ANSWER SEND DEBUG: Answer type: ${answer.type}`);
this.debug.log(`🔄 ANSWER SEND DEBUG: Answer SDP length: ${answer.sdp?.length}`);
this.debug.log(`🔄 ANSWER SEND DEBUG: Target peer ID: ${fromPeerId}`);
// Send answer via signaling
await this.mesh.sendSignalingMessage({
type: 'answer',
data: answer,
timestamp: Date.now()
}, fromPeerId);
this.debug.log(`✅ ANSWER SEND DEBUG: Answer successfully sent to ${fromPeerId.substring(0, 8)}...`);
this.mesh.emit('statusChanged', { type: 'info', message: `Answer sent to ${fromPeerId.substring(0, 8)}...` });
// Clear connection attempt since we're now processing the connection
this.mesh.peerDiscovery.clearConnectionAttempt(fromPeerId);
} catch (error) {
this.debug.error('Failed to handle offer from', fromPeerId, ':', error);
// If it's a state error, handle gracefully
if (error.message.includes('state:')) {
this.debug.log(`Offer ignored for ${fromPeerId.substring(0, 8)}... - connection state issue (likely race condition)`);
this.mesh.emit('statusChanged', { type: 'info', message: `Offer skipped for ${fromPeerId.substring(0, 8)}... (connection state conflict)` });
} else {
this.mesh.emit('statusChanged', { type: 'error', message: `Failed to handle offer from ${fromPeerId.substring(0, 8)}...: ${error.message}` });
this.connectionManager.cleanupFailedConnection(fromPeerId);
}
}
}
async handleRenegotiationOffer(offer, fromPeerId) {
this.debug.log(`🔄 Handling renegotiation offer via signaling from ${fromPeerId.substring(0, 8)}...`);
// Find the existing peer connection
const peerConnection = this.connectionManager.getPeer(fromPeerId);
if (!peerConnection) {
this.debug.error(`No peer connection found for renegotiation from ${fromPeerId.substring(0, 8)}...`);
return;
}
try {
// Handle the renegotiation offer and get answer
const answer = await peerConnection.handleOffer(offer);
// CRITICAL FIX: Send the answer back as 'renegotiation-answer', not regular 'answer'
// This ensures the initiator receives the correct message type for renegotiation completion
await this.mesh.sendSignalingMessage({
type: 'renegotiation-answer',
data: answer,
timestamp: Date.now()
}, fromPeerId);
this.debug.log(`✅ Renegotiation completed via signaling with ${fromPeerId.substring(0, 8)}...`);
} catch (error) {
this.debug.error(`❌ Failed to handle renegotiation offer via signaling from ${fromPeerId.substring(0, 8)}...`, error);
}
}
async handleAnswer(answer, fromPeerId) {
this.debug.log('🔄 ANSWER RECEIVE DEBUG: Handling answer from', fromPeerId);
this.debug.log(`🔄 ANSWER RECEIVE DEBUG: Our peer ID: ${this.mesh.peerId.substring(0, 8)}...`);
this.debug.log(`🔄 ANSWER RECEIVE DEBUG: Answer from: ${fromPeerId.substring(0, 8)}...`);
// Validate answer data structure before proceeding
if (!answer || typeof answer !== 'object') {
this.debug.error(`Invalid answer data from ${fromPeerId}:`, answer);
this.mesh.emit('statusChanged', { type: 'error', message: `Invalid answer data from ${fromPeerId.substring(0, 8)}...` });
return;
}
if (!answer.type || answer.type !== 'answer') {
this.debug.error(`Invalid answer type from ${fromPeerId}:`, answer.type);
this.mesh.emit('statusChanged', { type: 'error', message: `Invalid answer type from ${fromPeerId.substring(0, 8)}...` });
return;
}
if (!answer.sdp || typeof answer.sdp !== 'string' || answer.sdp.length < 10) {
this.debug.error(`Invalid answer SDP from ${fromPeerId}:`, answer.sdp?.substring(0, 100) || 'undefined');
this.mesh.emit('statusChanged', { type: 'error', message: `Invalid answer SDP from ${fromPeerId.substring(0, 8)}...` });
return;
}
// CRITICAL DEBUG: Check if we have a peer connection for this answer
const peerConnection = this.connectionManager.getPeer(fromPeerId);
this.debug.log(`🔄 ANSWER RECEIVE DEBUG: Found peer connection: ${!!peerConnection}`);
if (peerConnection) {
this.debug.log(`🔄 ANSWER RECEIVE DEBUG: Peer connection state: ${peerConnection.connection?.signalingState}`);
this.debug.log(`🔄 ANSWER RECEIVE DEBUG: Is initiator: ${peerConnection.isInitiator}`);
}
if (peerConnection) {
try {
this.debug.log(`🔄 ANSWER RECEIVE DEBUG: Processing answer from ${fromPeerId.substring(0, 8)}...`);
await peerConnection.handleAnswer(answer);
this.debug.log(`✅ ANSWER RECEIVE DEBUG: Answer processed successfully from ${fromPeerId.substring(0, 8)}...`);
this.mesh.emit('statusChanged', { type: 'info', message: `Answer processed from ${fromPeerId.substring(0, 8)}...` });
} catch (error) {
this.debug.error('❌ ANSWER RECEIVE DEBUG: Failed to handle answer:', error);
// If it's a state error (connection already stable), don't treat as failure
if (error.message.includes('state:') || error.message.includes('stable')) {
this.debug.log(`Answer ignored for ${fromPeerId.substring(0, 8)}... - connection state issue (likely race condition resolved)`);
this.mesh.emit('statusChanged', { type: 'info', message: `Answer skipped for ${fromPeerId.substring(0, 8)}... (connection already stable)` });
} else {
this.mesh.emit('statusChanged', { type: 'error', message: `Failed to handle answer from ${fromPeerId.substring(0, 8)}...: ${error.message}` });
}
}
} else {
this.debug.log('❌ ANSWER RECEIVE DEBUG: No peer connection found for answer from', fromPeerId);
}
}
handleConnectionRejected(data, fromPeerId) {
this.debug.log(`Connection rejected by ${fromPeerId.substring(0, 8)}...: ${data.reason} (${data.details})`);
this.mesh.emit('statusChanged', {
type: 'info',
message: `Connection rejected by ${fromPeerId.substring(0, 8)}... (${data.reason})`
});
// Clean up any pending connection attempt
this.connectionManager.connectionAttempts.delete(fromPeerId);
// Remove the peer connection if it exists and is not connected
const peerConnection = this.connectionManager.getPeer(fromPeerId);
if (peerConnection && peerConnection.getStatus() !== 'connected') {
peerConnection.close();
this.connectionManager.peers.delete(fromPeerId);
}
// Clear any discovery connection attempt tracking
this.mesh.peerDiscovery.clearConnectionAttempt(fromPeerId);
// If we have no connections, immediately try other peers more aggressively
const connectedCount = this.connectionManager.getConnectedPeerCount();
if (connectedCount === 0) {
this.debug.log(`Connection rejected and peer is isolated (${connectedCount} connections) - trying alternative peers immediately`);
// Immediately try to connect to other discovered peers
const discoveredPeers = Array.from(this.mesh.peerDiscovery.getDiscoveredPeers());
const availablePeers = discoveredPeers.filter(peer =>
peer.peerId !== fromPeerId &&
!this.connectionManager.hasPeer(peer.peerId) &&
!this.mesh.peerDiscovery.isAttemptingConnection(peer.peerId)
);
if (availablePeers.length > 0) {
// Sort by XOR distance and try the closest available peer
const sortedByDistance = availablePeers.sort((a, b) => {
const distA = this.mesh.peerDiscovery.calculateXorDistance(this.mesh.peerId, a.peerId);
const distB = this.mesh.peerDiscovery.calculateXorDistance(this.mesh.peerId, b.peerId);
return distA < distB ? -1 : 1;
});
const nextPeer = sortedByDistance[0];
this.debug.log(`Immediately attempting connection to next closest peer: ${nextPeer.peerId.substring(0, 8)}...`);
this.connectionManager.connectToPeer(nextPeer.peerId);
}
} else {
// Regular mesh optimization for non-isolated peers
setTimeout(() => {
this.mesh.peerDiscovery.optimizeMeshConnections(this.connectionManager.peers);
}, 1000);
}
}
async handleRenegotiationAnswer(answer, fromPeerId) {
console.log('🚨 RENEGOTIATION ANSWER: IMMEDIATE PROCESSING from', fromPeerId?.substring(0, 8), 'to', this.mesh.peerId?.substring(0, 8));
this.debug.log(`🔄 CRITICAL FIX: IMMEDIATE renegotiation answer from ${fromPeerId.substring(0, 8)}...`);
// AGGRESSIVE FIX: Find connection immediately and apply answer
const peerConnection = this.connectionManager.getPeer(fromPeerId);
if (!peerConnection) {
this.debug.error(`❌ CRITICAL: No peer connection for answer from ${fromPeerId.substring(0, 8)}... - CONNECTION LOST`);
return;
}
// CHECK CONNECTION STATE BEFORE APPLYING ANSWER
const currentState = peerConnection.connection?.signalingState;
this.debug.log(`� CRITICAL: Connection state before answer: ${currentState}`);
if (currentState !== 'have-local-offer') {
this.debug.log(`⚠️ CRITICAL: Connection not waiting for answer (state: ${currentState}) - applying anyway`);
}
try {
// IMMEDIATE ANSWER APPLICATION - NO DELAYS
this.debug.log(`🔄 CRITICAL: APPLYING ANSWER IMMEDIATELY from ${fromPeerId.substring(0, 8)}...`);
await peerConnection.handleAnswer(answer);
// VERIFY STATE CHANGE
const newState = peerConnection.connection?.signalingState;
this.debug.log(`✅ CRITICAL: Answer applied - state changed from ${currentState} to ${newState}`);
if (newState === 'stable') {
this.debug.log(`🎉 SUCCESS: Connection with ${fromPeerId.substring(0, 8)}... is now STABLE - media should work!`);
} else {
this.debug.error(`❌ CRITICAL: Connection still not stable after answer - state: ${newState}`);
}
} catch (error) {
this.debug.error(`❌ CRITICAL: FAILED to apply renegotiation answer from ${fromPeerId.substring(0, 8)}...`, error);
// EMERGENCY FIX: Force connection recovery
this.debug.log(`🆘 EMERGENCY: Forcing connection recovery for ${fromPeerId.substring(0, 8)}...`);
try {
await this.connectionManager.connectToPeer(fromPeerId, false, { emergency: true });
} catch (recoveryError) {
this.debug.error(`❌ EMERGENCY: Recovery failed for ${fromPeerId.substring(0, 8)}...`, recoveryError);
}
}
}
/**
* Handle ICE restart offers sent over the mesh
*/
async handleIceRestartOffer(data, fromPeerId) {
this.debug.log(`Received ICE restart offer from ${fromPeerId.substring(0, 8)}...`);
const peerConnection = this.connectionManager.peers.get(fromPeerId);
if (!peerConnection) {
this.debug.warn(`No peer connection found for ICE restart offer from ${fromPeerId.substring(0, 8)}...`);
return;
}
try {
await peerConnection.handleIceRestartOffer(data.offer);
this.debug.log(`Successfully handled ICE restart offer from ${fromPeerId.substring(0, 8)}...`);
} catch (error) {
this.debug.error(`Failed to handle ICE restart offer from ${fromPeerId.substring(0, 8)}...:`, error);
}
}
/**
* Handle ICE restart answers sent over the mesh
*/
async handleIceRestartAnswer(data, fromPeerId) {
this.debug.log(`Received ICE restart answer from ${fromPeerId.substring(0, 8)}...`);
const peerConnection = this.connectionManager.peers.get(fromPeerId);
if (!peerConnection) {
this.debug.warn(`No peer connection found for ICE restart answer from ${fromPeerId.substring(0, 8)}...`);
return;
}
try {
await peerConnection.handleIceRestartAnswer(data.answer);
this.debug.log(`Successfully handled ICE restart answer from ${fromPeerId.substring(0, 8)}...`);
} catch (error) {
this.debug.error(`Failed to handle ICE restart answer from ${fromPeerId.substring(0, 8)}...:`, error);
}
}
}