2025-11-15 14:12:51 +01:00
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
|
import { getSocket } from '../services/socket';
|
2025-11-23 22:28:54 +01:00
|
|
|
import { CONNECTION_STATE } from '../constants';
|
2025-12-05 21:23:50 +01:00
|
|
|
import { webrtcAPI } from '../services/api';
|
2025-11-15 14:12:51 +01:00
|
|
|
|
2025-12-05 21:23:50 +01:00
|
|
|
// Default fallback ICE servers (used if backend request fails)
|
|
|
|
|
const DEFAULT_ICE_SERVERS = [
|
|
|
|
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|
|
|
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
|
|
|
];
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
// File chunk size (16KB recommended for WebRTC DataChannel)
|
|
|
|
|
const CHUNK_SIZE = 16384;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Custom hook for managing WebRTC peer-to-peer connections and file transfers
|
|
|
|
|
*
|
|
|
|
|
* @param {number} matchId - The match ID for this connection
|
|
|
|
|
* @param {number} userId - Current user's ID
|
|
|
|
|
* @returns {Object} WebRTC state and control functions
|
|
|
|
|
*/
|
|
|
|
|
export const useWebRTC = (matchId, userId) => {
|
2025-11-23 22:28:54 +01:00
|
|
|
const [connectionState, setConnectionState] = useState(CONNECTION_STATE.DISCONNECTED);
|
2025-11-15 14:12:51 +01:00
|
|
|
const [transferProgress, setTransferProgress] = useState(0);
|
|
|
|
|
const [isTransferring, setIsTransferring] = useState(false);
|
|
|
|
|
const [receivingFile, setReceivingFile] = useState(null);
|
|
|
|
|
|
|
|
|
|
const peerConnectionRef = useRef(null);
|
|
|
|
|
const dataChannelRef = useRef(null);
|
|
|
|
|
const socketRef = useRef(null);
|
2025-11-15 16:12:02 +01:00
|
|
|
const matchIdRef = useRef(matchId);
|
|
|
|
|
const userIdRef = useRef(userId);
|
2025-12-05 21:23:50 +01:00
|
|
|
const iceServersRef = useRef(null);
|
2025-11-15 16:12:02 +01:00
|
|
|
|
|
|
|
|
// Update refs when props change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
matchIdRef.current = matchId;
|
|
|
|
|
userIdRef.current = userId;
|
|
|
|
|
}, [matchId, userId]);
|
2025-11-15 14:12:51 +01:00
|
|
|
|
2025-12-05 21:23:50 +01:00
|
|
|
// Fetch ICE servers from backend on mount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchIceServers = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await webrtcAPI.getIceServers();
|
|
|
|
|
if (response.success && response.iceServers) {
|
|
|
|
|
iceServersRef.current = response.iceServers;
|
|
|
|
|
console.log('✅ Fetched ICE servers from backend:', response.iceServers.length, 'servers');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('⚠️ Using default ICE servers (backend response invalid)');
|
|
|
|
|
iceServersRef.current = DEFAULT_ICE_SERVERS;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('❌ Failed to fetch ICE servers, using defaults:', error);
|
|
|
|
|
iceServersRef.current = DEFAULT_ICE_SERVERS;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchIceServers();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-11-15 14:12:51 +01:00
|
|
|
// File transfer state
|
|
|
|
|
const fileTransferRef = useRef({
|
|
|
|
|
file: null,
|
|
|
|
|
fileName: '',
|
|
|
|
|
fileSize: 0,
|
|
|
|
|
fileType: '',
|
|
|
|
|
chunks: [],
|
|
|
|
|
currentChunk: 0,
|
|
|
|
|
totalChunks: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Receiving file state
|
|
|
|
|
const receivingBufferRef = useRef([]);
|
|
|
|
|
const receivingMetadataRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize peer connection
|
|
|
|
|
*/
|
|
|
|
|
const initializePeerConnection = useCallback(() => {
|
|
|
|
|
if (peerConnectionRef.current) {
|
|
|
|
|
return peerConnectionRef.current;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 21:23:50 +01:00
|
|
|
// Use ICE servers from backend or fallback to defaults
|
|
|
|
|
const iceServers = iceServersRef.current || DEFAULT_ICE_SERVERS;
|
|
|
|
|
const rtcConfig = { iceServers };
|
|
|
|
|
|
2025-11-15 16:18:35 +01:00
|
|
|
const pc = new RTCPeerConnection(rtcConfig);
|
2025-11-15 14:12:51 +01:00
|
|
|
peerConnectionRef.current = pc;
|
2025-12-05 21:23:50 +01:00
|
|
|
console.log('🔧 Using ICE servers for NAT traversal:', iceServers.length, 'servers');
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
// ICE candidate handler
|
|
|
|
|
pc.onicecandidate = (event) => {
|
2025-11-15 16:12:02 +01:00
|
|
|
if (event.candidate) {
|
|
|
|
|
console.log('🧊 ICE candidate generated:', event.candidate.type);
|
|
|
|
|
if (socketRef.current) {
|
|
|
|
|
socketRef.current.emit('webrtc_ice_candidate', {
|
|
|
|
|
matchId: matchIdRef.current,
|
|
|
|
|
candidate: event.candidate,
|
|
|
|
|
});
|
|
|
|
|
console.log('📤 Sent ICE candidate');
|
|
|
|
|
} else {
|
|
|
|
|
console.error('❌ Socket not available to send ICE candidate');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log('🧊 ICE gathering complete (candidate is null)');
|
2025-11-15 14:12:51 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-15 16:12:02 +01:00
|
|
|
// ICE gathering state handler
|
|
|
|
|
pc.onicegatheringstatechange = () => {
|
|
|
|
|
console.log('🧊 ICE gathering state:', pc.iceGatheringState);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Signaling state handler
|
|
|
|
|
pc.onsignalingstatechange = () => {
|
|
|
|
|
console.log('📡 Signaling state:', pc.signalingState);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-15 14:12:51 +01:00
|
|
|
// Connection state change handler
|
|
|
|
|
pc.onconnectionstatechange = () => {
|
|
|
|
|
console.log('🔄 Connection state:', pc.connectionState);
|
|
|
|
|
setConnectionState(pc.connectionState);
|
|
|
|
|
|
2025-11-23 22:28:54 +01:00
|
|
|
if (pc.connectionState === CONNECTION_STATE.FAILED) {
|
2025-11-15 14:12:51 +01:00
|
|
|
console.error('❌ WebRTC connection failed');
|
|
|
|
|
cleanupConnection();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ICE connection state handler
|
|
|
|
|
pc.oniceconnectionstatechange = () => {
|
|
|
|
|
console.log('🧊 ICE connection state:', pc.iceConnectionState);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('✅ RTCPeerConnection initialized');
|
2025-11-15 16:12:02 +01:00
|
|
|
console.log('🔍 Initial states - Connection:', pc.connectionState, 'ICE:', pc.iceConnectionState, 'Signaling:', pc.signalingState);
|
2025-11-15 14:12:51 +01:00
|
|
|
return pc;
|
2025-11-15 16:12:02 +01:00
|
|
|
}, []);
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create data channel for file transfer
|
|
|
|
|
*/
|
|
|
|
|
const createDataChannel = useCallback((pc) => {
|
|
|
|
|
const dc = pc.createDataChannel('fileTransfer', {
|
|
|
|
|
ordered: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setupDataChannelHandlers(dc);
|
|
|
|
|
dataChannelRef.current = dc;
|
|
|
|
|
|
|
|
|
|
console.log('✅ DataChannel created');
|
|
|
|
|
return dc;
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup data channel event handlers
|
|
|
|
|
*/
|
|
|
|
|
const setupDataChannelHandlers = useCallback((dc) => {
|
|
|
|
|
dc.onopen = () => {
|
|
|
|
|
console.log('✅ DataChannel opened');
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.CONNECTED);
|
2025-11-15 14:12:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dc.onclose = () => {
|
|
|
|
|
console.log('❌ DataChannel closed');
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.DISCONNECTED);
|
2025-11-15 14:12:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dc.onerror = (error) => {
|
|
|
|
|
console.error('❌ DataChannel error:', error);
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.FAILED);
|
2025-11-15 14:12:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dc.onmessage = (event) => {
|
|
|
|
|
handleDataChannelMessage(event.data);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle incoming data channel messages
|
|
|
|
|
*/
|
|
|
|
|
const handleDataChannelMessage = useCallback((data) => {
|
|
|
|
|
// Check if it's metadata (JSON string)
|
|
|
|
|
if (typeof data === 'string') {
|
|
|
|
|
try {
|
|
|
|
|
const metadata = JSON.parse(data);
|
|
|
|
|
|
|
|
|
|
if (metadata.type === 'file_metadata') {
|
|
|
|
|
// Receiving file metadata
|
|
|
|
|
receivingMetadataRef.current = metadata;
|
|
|
|
|
receivingBufferRef.current = [];
|
|
|
|
|
setReceivingFile({
|
|
|
|
|
name: metadata.fileName,
|
|
|
|
|
size: metadata.fileSize,
|
|
|
|
|
type: metadata.fileType,
|
|
|
|
|
});
|
|
|
|
|
setIsTransferring(true);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
console.log('📥 Receiving file:', metadata.fileName);
|
|
|
|
|
} else if (metadata.type === 'file_end') {
|
|
|
|
|
// File transfer complete
|
|
|
|
|
const blob = new Blob(receivingBufferRef.current, {
|
|
|
|
|
type: receivingMetadataRef.current.fileType
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Trigger download
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = receivingMetadataRef.current.fileName;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
|
|
|
|
console.log('✅ File received and downloaded:', receivingMetadataRef.current.fileName);
|
|
|
|
|
|
|
|
|
|
// Reset state
|
|
|
|
|
setIsTransferring(false);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
setReceivingFile(null);
|
|
|
|
|
receivingBufferRef.current = [];
|
|
|
|
|
receivingMetadataRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to parse metadata:', error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Binary data - file chunk
|
|
|
|
|
receivingBufferRef.current.push(data);
|
|
|
|
|
|
|
|
|
|
const received = receivingBufferRef.current.length;
|
|
|
|
|
const total = Math.ceil(receivingMetadataRef.current.fileSize / CHUNK_SIZE);
|
|
|
|
|
const progress = Math.round((received / total) * 100);
|
|
|
|
|
|
|
|
|
|
setTransferProgress(progress);
|
|
|
|
|
console.log(`📥 Received chunk ${received}/${total} (${progress}%)`);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create and send offer to peer
|
|
|
|
|
*/
|
|
|
|
|
const createOffer = useCallback(async () => {
|
|
|
|
|
try {
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.CONNECTING);
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
const pc = initializePeerConnection();
|
|
|
|
|
createDataChannel(pc);
|
|
|
|
|
|
|
|
|
|
const offer = await pc.createOffer();
|
|
|
|
|
await pc.setLocalDescription(offer);
|
2025-11-15 16:12:02 +01:00
|
|
|
console.log('✅ Local description set (offer). ICE gathering should start now...');
|
|
|
|
|
console.log('🔍 SDP has candidates:', pc.localDescription.sdp.includes('candidate:'));
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
socketRef.current.emit('webrtc_offer', {
|
2025-11-15 16:12:02 +01:00
|
|
|
matchId: matchIdRef.current,
|
2025-11-15 14:12:51 +01:00
|
|
|
offer: pc.localDescription,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('📤 Sent WebRTC offer');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to create offer:', error);
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.FAILED);
|
2025-11-15 14:12:51 +01:00
|
|
|
}
|
2025-11-15 16:12:02 +01:00
|
|
|
}, [initializePeerConnection, createDataChannel]);
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle incoming offer and create answer
|
|
|
|
|
*/
|
|
|
|
|
const handleOffer = useCallback(async (offer) => {
|
|
|
|
|
try {
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.CONNECTING);
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
const pc = initializePeerConnection();
|
|
|
|
|
|
|
|
|
|
// Setup data channel handler for incoming connections
|
|
|
|
|
pc.ondatachannel = (event) => {
|
|
|
|
|
console.log('✅ DataChannel received');
|
|
|
|
|
dataChannelRef.current = event.channel;
|
|
|
|
|
setupDataChannelHandlers(event.channel);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
2025-11-15 16:12:02 +01:00
|
|
|
console.log('✅ Remote description set (offer)');
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
const answer = await pc.createAnswer();
|
|
|
|
|
await pc.setLocalDescription(answer);
|
2025-11-15 16:12:02 +01:00
|
|
|
console.log('✅ Local description set (answer). ICE gathering should start now...');
|
|
|
|
|
console.log('🔍 SDP has candidates:', pc.localDescription.sdp.includes('candidate:'));
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
socketRef.current.emit('webrtc_answer', {
|
2025-11-15 16:12:02 +01:00
|
|
|
matchId: matchIdRef.current,
|
2025-11-15 14:12:51 +01:00
|
|
|
answer: pc.localDescription,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('📤 Sent WebRTC answer');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to handle offer:', error);
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.FAILED);
|
2025-11-15 14:12:51 +01:00
|
|
|
}
|
2025-11-15 16:12:02 +01:00
|
|
|
}, [initializePeerConnection, setupDataChannelHandlers]);
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle incoming answer
|
|
|
|
|
*/
|
|
|
|
|
const handleAnswer = useCallback(async (answer) => {
|
|
|
|
|
try {
|
|
|
|
|
const pc = peerConnectionRef.current;
|
|
|
|
|
if (!pc) {
|
|
|
|
|
console.error('No peer connection found');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
2025-11-15 16:12:02 +01:00
|
|
|
console.log('✅ Remote description set (answer). ICE should connect now...');
|
2025-11-15 14:12:51 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to handle answer:', error);
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.FAILED);
|
2025-11-15 14:12:51 +01:00
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle incoming ICE candidate
|
|
|
|
|
*/
|
|
|
|
|
const handleIceCandidate = useCallback(async (candidate) => {
|
|
|
|
|
try {
|
|
|
|
|
const pc = peerConnectionRef.current;
|
|
|
|
|
if (!pc) {
|
|
|
|
|
console.error('No peer connection found');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
|
|
|
console.log('✅ Added ICE candidate');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to add ICE candidate:', error);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send file via data channel
|
|
|
|
|
*/
|
|
|
|
|
const sendFile = useCallback(async (file) => {
|
|
|
|
|
if (!file) {
|
|
|
|
|
console.error('No file provided');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dc = dataChannelRef.current;
|
|
|
|
|
if (!dc || dc.readyState !== 'open') {
|
|
|
|
|
console.error('DataChannel is not open');
|
|
|
|
|
alert('Connection not established. Please wait and try again.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setIsTransferring(true);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
|
|
|
|
|
// Send file metadata
|
|
|
|
|
const metadata = {
|
|
|
|
|
type: 'file_metadata',
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
fileType: file.type,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dc.send(JSON.stringify(metadata));
|
|
|
|
|
console.log('📤 Sent file metadata:', metadata);
|
|
|
|
|
|
|
|
|
|
// Read file and send in chunks
|
|
|
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
|
|
|
const totalChunks = Math.ceil(arrayBuffer.byteLength / CHUNK_SIZE);
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < totalChunks; i++) {
|
|
|
|
|
const start = i * CHUNK_SIZE;
|
|
|
|
|
const end = Math.min(start + CHUNK_SIZE, arrayBuffer.byteLength);
|
|
|
|
|
const chunk = arrayBuffer.slice(start, end);
|
|
|
|
|
|
|
|
|
|
// Wait if buffer is getting full
|
|
|
|
|
while (dc.bufferedAmount > CHUNK_SIZE * 10) {
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dc.send(chunk);
|
|
|
|
|
|
|
|
|
|
const progress = Math.round(((i + 1) / totalChunks) * 100);
|
|
|
|
|
setTransferProgress(progress);
|
|
|
|
|
console.log(`📤 Sent chunk ${i + 1}/${totalChunks} (${progress}%)`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send end marker
|
|
|
|
|
dc.send(JSON.stringify({ type: 'file_end' }));
|
|
|
|
|
console.log('✅ File transfer complete:', file.name);
|
|
|
|
|
|
|
|
|
|
setIsTransferring(false);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to send file:', error);
|
|
|
|
|
setIsTransferring(false);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
alert('Failed to send file: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cleanup connection
|
|
|
|
|
*/
|
|
|
|
|
const cleanupConnection = useCallback(() => {
|
|
|
|
|
if (dataChannelRef.current) {
|
|
|
|
|
dataChannelRef.current.close();
|
|
|
|
|
dataChannelRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (peerConnectionRef.current) {
|
|
|
|
|
peerConnectionRef.current.close();
|
|
|
|
|
peerConnectionRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 22:28:54 +01:00
|
|
|
setConnectionState(CONNECTION_STATE.DISCONNECTED);
|
2025-11-15 14:12:51 +01:00
|
|
|
setIsTransferring(false);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
setReceivingFile(null);
|
|
|
|
|
receivingBufferRef.current = [];
|
|
|
|
|
receivingMetadataRef.current = null;
|
|
|
|
|
|
|
|
|
|
console.log('🧹 WebRTC connection cleaned up');
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup Socket.IO event listeners
|
|
|
|
|
*/
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const socket = getSocket();
|
|
|
|
|
if (!socket) {
|
|
|
|
|
console.error('Socket not available');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socketRef.current = socket;
|
|
|
|
|
|
2025-11-15 16:12:02 +01:00
|
|
|
// Create stable handlers using current values from refs
|
|
|
|
|
const onOffer = ({ from, offer }) => {
|
|
|
|
|
console.log('📥 Received WebRTC offer from:', from, 'My userId:', userIdRef.current);
|
|
|
|
|
if (from !== userIdRef.current) {
|
2025-11-15 14:12:51 +01:00
|
|
|
handleOffer(offer);
|
2025-11-15 16:12:02 +01:00
|
|
|
} else {
|
|
|
|
|
console.log('⏭️ Ignoring offer from self');
|
2025-11-15 14:12:51 +01:00
|
|
|
}
|
2025-11-15 16:12:02 +01:00
|
|
|
};
|
2025-11-15 14:12:51 +01:00
|
|
|
|
2025-11-15 16:12:02 +01:00
|
|
|
const onAnswer = ({ from, answer }) => {
|
|
|
|
|
console.log('📥 Received WebRTC answer from:', from, 'My userId:', userIdRef.current);
|
|
|
|
|
if (from !== userIdRef.current) {
|
2025-11-15 14:12:51 +01:00
|
|
|
handleAnswer(answer);
|
2025-11-15 16:12:02 +01:00
|
|
|
} else {
|
|
|
|
|
console.log('⏭️ Ignoring answer from self');
|
2025-11-15 14:12:51 +01:00
|
|
|
}
|
2025-11-15 16:12:02 +01:00
|
|
|
};
|
2025-11-15 14:12:51 +01:00
|
|
|
|
2025-11-15 16:12:02 +01:00
|
|
|
const onIceCandidate = ({ from, candidate }) => {
|
2025-11-15 14:12:51 +01:00
|
|
|
console.log('📥 Received ICE candidate from:', from);
|
2025-11-15 16:12:02 +01:00
|
|
|
if (from !== userIdRef.current) {
|
2025-11-15 14:12:51 +01:00
|
|
|
handleIceCandidate(candidate);
|
|
|
|
|
}
|
2025-11-15 16:12:02 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Attach listeners
|
|
|
|
|
const attachListeners = () => {
|
|
|
|
|
socket.off('webrtc_offer', onOffer);
|
|
|
|
|
socket.off('webrtc_answer', onAnswer);
|
|
|
|
|
socket.off('webrtc_ice_candidate', onIceCandidate);
|
|
|
|
|
|
|
|
|
|
socket.on('webrtc_offer', onOffer);
|
|
|
|
|
socket.on('webrtc_answer', onAnswer);
|
|
|
|
|
socket.on('webrtc_ice_candidate', onIceCandidate);
|
|
|
|
|
|
|
|
|
|
console.log('✅ WebRTC Socket.IO listeners attached (socketId:', socket.id, ')');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Attach initially
|
|
|
|
|
attachListeners();
|
|
|
|
|
|
|
|
|
|
// Reattach on reconnect
|
|
|
|
|
const onReconnect = () => {
|
|
|
|
|
console.log('🔄 Socket reconnected - reattaching WebRTC listeners');
|
|
|
|
|
attachListeners();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.on('connect', onReconnect);
|
2025-11-15 14:12:51 +01:00
|
|
|
|
2025-11-15 16:12:02 +01:00
|
|
|
// Cleanup on unmount only
|
2025-11-15 14:12:51 +01:00
|
|
|
return () => {
|
2025-11-15 16:12:02 +01:00
|
|
|
console.log('🧹 Removing WebRTC Socket.IO listeners');
|
|
|
|
|
socket.off('connect', onReconnect);
|
|
|
|
|
socket.off('webrtc_offer', onOffer);
|
|
|
|
|
socket.off('webrtc_answer', onAnswer);
|
|
|
|
|
socket.off('webrtc_ice_candidate', onIceCandidate);
|
2025-11-15 14:12:51 +01:00
|
|
|
cleanupConnection();
|
|
|
|
|
};
|
2025-11-15 16:12:02 +01:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [userId]); // Only re-run if userId changes
|
2025-11-15 14:12:51 +01:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
connectionState,
|
|
|
|
|
isTransferring,
|
|
|
|
|
transferProgress,
|
|
|
|
|
receivingFile,
|
|
|
|
|
createOffer,
|
|
|
|
|
sendFile,
|
|
|
|
|
cleanupConnection,
|
|
|
|
|
};
|
|
|
|
|
};
|