diff --git a/frontend/src/components/WebRTCWarning.jsx b/frontend/src/components/WebRTCWarning.jsx
new file mode 100644
index 0000000..ffc5906
--- /dev/null
+++ b/frontend/src/components/WebRTCWarning.jsx
@@ -0,0 +1,99 @@
+import { AlertTriangle, X } from 'lucide-react';
+import { useState } from 'react';
+
+/**
+ * WebRTC Warning Component
+ *
+ * Shows a dismissible warning when WebRTC is not available or blocked
+ */
+const WebRTCWarning = ({ detection, onDismiss }) => {
+ const [isDismissed, setIsDismissed] = useState(false);
+
+ if (isDismissed) {
+ return null;
+ }
+
+ // Don't show if WebRTC is working fine
+ if (detection.supported && detection.hasIceCandidates) {
+ return null;
+ }
+
+ const handleDismiss = () => {
+ setIsDismissed(true);
+ if (onDismiss) {
+ onDismiss();
+ }
+ };
+
+ const getMessage = () => {
+ if (!detection.supported) {
+ return {
+ title: 'WebRTC Not Supported',
+ description: 'Your browser does not support WebRTC. P2P video transfer is disabled.',
+ suggestions: [
+ 'Update your browser to the latest version',
+ 'Try using Chrome, Firefox, or Edge',
+ ],
+ };
+ }
+
+ if (!detection.hasIceCandidates) {
+ return {
+ title: 'WebRTC Blocked',
+ description: 'WebRTC is blocked by browser settings or extensions. P2P video transfer is disabled.',
+ suggestions: [
+ 'Check browser privacy settings (e.g., Opera: Settings โ Privacy โ WebRTC)',
+ 'Disable VPN extensions that block WebRTC',
+ 'Try using Chrome or Firefox',
+ 'Use incognito/private mode without extensions',
+ ],
+ };
+ }
+
+ return {
+ title: 'WebRTC Error',
+ description: detection.error || 'Unknown WebRTC error',
+ suggestions: ['Try refreshing the page', 'Try a different browser'],
+ };
+ };
+
+ const { title, description, suggestions } = getMessage();
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
How to fix:
+
+ {suggestions.map((suggestion, index) => (
+ - {suggestion}
+ ))}
+
+
+
+ You can still send video links via Google Drive, Dropbox, etc. using the "Link" button.
+
+
+
+
+
+
+ );
+};
+
+export default WebRTCWarning;
diff --git a/frontend/src/hooks/useWebRTC.js b/frontend/src/hooks/useWebRTC.js
index 5dd34aa..60c4685 100644
--- a/frontend/src/hooks/useWebRTC.js
+++ b/frontend/src/hooks/useWebRTC.js
@@ -8,6 +8,13 @@ const rtcConfig = {
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
],
+ iceTransportPolicy: 'all', // Use all candidates (host, srflx, relay)
+ iceCandidatePoolSize: 10, // Pre-gather candidates
+};
+
+// Alternative config for localhost testing (no STUN)
+const rtcConfigLocalhost = {
+ iceServers: [], // No STUN - use only host candidates for localhost
};
// File chunk size (16KB recommended for WebRTC DataChannel)
@@ -29,6 +36,14 @@ export const useWebRTC = (matchId, userId) => {
const peerConnectionRef = useRef(null);
const dataChannelRef = useRef(null);
const socketRef = useRef(null);
+ const matchIdRef = useRef(matchId);
+ const userIdRef = useRef(userId);
+
+ // Update refs when props change
+ useEffect(() => {
+ matchIdRef.current = matchId;
+ userIdRef.current = userId;
+ }, [matchId, userId]);
// File transfer state
const fileTransferRef = useRef({
@@ -53,20 +68,39 @@ export const useWebRTC = (matchId, userId) => {
return peerConnectionRef.current;
}
- const pc = new RTCPeerConnection(rtcConfig);
+ // Use localhost config for testing (no STUN servers)
+ const pc = new RTCPeerConnection(rtcConfigLocalhost);
peerConnectionRef.current = pc;
+ console.log('๐ง Using rtcConfigLocalhost (no STUN servers)');
// ICE candidate handler
pc.onicecandidate = (event) => {
- if (event.candidate && socketRef.current) {
- socketRef.current.emit('webrtc_ice_candidate', {
- matchId,
- candidate: event.candidate,
- });
- console.log('๐ค Sent ICE candidate');
+ 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)');
}
};
+ // ICE gathering state handler
+ pc.onicegatheringstatechange = () => {
+ console.log('๐ง ICE gathering state:', pc.iceGatheringState);
+ };
+
+ // Signaling state handler
+ pc.onsignalingstatechange = () => {
+ console.log('๐ก Signaling state:', pc.signalingState);
+ };
+
// Connection state change handler
pc.onconnectionstatechange = () => {
console.log('๐ Connection state:', pc.connectionState);
@@ -84,8 +118,9 @@ export const useWebRTC = (matchId, userId) => {
};
console.log('โ
RTCPeerConnection initialized');
+ console.log('๐ Initial states - Connection:', pc.connectionState, 'ICE:', pc.iceConnectionState, 'Signaling:', pc.signalingState);
return pc;
- }, [matchId]);
+ }, []);
/**
* Create data channel for file transfer
@@ -200,9 +235,11 @@ export const useWebRTC = (matchId, userId) => {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
+ console.log('โ
Local description set (offer). ICE gathering should start now...');
+ console.log('๐ SDP has candidates:', pc.localDescription.sdp.includes('candidate:'));
socketRef.current.emit('webrtc_offer', {
- matchId,
+ matchId: matchIdRef.current,
offer: pc.localDescription,
});
@@ -211,7 +248,7 @@ export const useWebRTC = (matchId, userId) => {
console.error('Failed to create offer:', error);
setConnectionState('failed');
}
- }, [matchId, initializePeerConnection, createDataChannel]);
+ }, [initializePeerConnection, createDataChannel]);
/**
* Handle incoming offer and create answer
@@ -230,12 +267,15 @@ export const useWebRTC = (matchId, userId) => {
};
await pc.setRemoteDescription(new RTCSessionDescription(offer));
+ console.log('โ
Remote description set (offer)');
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
+ console.log('โ
Local description set (answer). ICE gathering should start now...');
+ console.log('๐ SDP has candidates:', pc.localDescription.sdp.includes('candidate:'));
socketRef.current.emit('webrtc_answer', {
- matchId,
+ matchId: matchIdRef.current,
answer: pc.localDescription,
});
@@ -244,7 +284,7 @@ export const useWebRTC = (matchId, userId) => {
console.error('Failed to handle offer:', error);
setConnectionState('failed');
}
- }, [matchId, initializePeerConnection, setupDataChannelHandlers]);
+ }, [initializePeerConnection, setupDataChannelHandlers]);
/**
* Handle incoming answer
@@ -258,7 +298,7 @@ export const useWebRTC = (matchId, userId) => {
}
await pc.setRemoteDescription(new RTCSessionDescription(answer));
- console.log('โ
Remote description set');
+ console.log('โ
Remote description set (answer). ICE should connect now...');
} catch (error) {
console.error('Failed to handle answer:', error);
setConnectionState('failed');
@@ -385,36 +425,67 @@ export const useWebRTC = (matchId, userId) => {
socketRef.current = socket;
- // Listen for WebRTC signaling events
- socket.on('webrtc_offer', ({ from, offer }) => {
- console.log('๐ฅ Received WebRTC offer from:', from);
- if (from !== userId) {
+ // 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) {
handleOffer(offer);
+ } else {
+ console.log('โญ๏ธ Ignoring offer from self');
}
- });
+ };
- socket.on('webrtc_answer', ({ from, answer }) => {
- console.log('๐ฅ Received WebRTC answer from:', from);
- if (from !== userId) {
+ const onAnswer = ({ from, answer }) => {
+ console.log('๐ฅ Received WebRTC answer from:', from, 'My userId:', userIdRef.current);
+ if (from !== userIdRef.current) {
handleAnswer(answer);
+ } else {
+ console.log('โญ๏ธ Ignoring answer from self');
}
- });
+ };
- socket.on('webrtc_ice_candidate', ({ from, candidate }) => {
+ const onIceCandidate = ({ from, candidate }) => {
console.log('๐ฅ Received ICE candidate from:', from);
- if (from !== userId) {
+ if (from !== userIdRef.current) {
handleIceCandidate(candidate);
}
- });
+ };
- // Cleanup on unmount
+ // 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);
+
+ // Cleanup on unmount only
return () => {
- socket.off('webrtc_offer');
- socket.off('webrtc_answer');
- socket.off('webrtc_ice_candidate');
+ 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);
cleanupConnection();
};
- }, [userId, handleOffer, handleAnswer, handleIceCandidate, cleanupConnection]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [userId]); // Only re-run if userId changes
return {
connectionState,
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index b9a1a6d..1a30beb 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -1,10 +1,9 @@
-import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
+// StrictMode disabled for development - causes Socket.IO reconnection issues
+// TODO: Re-enable in production or fix Socket.IO to handle double mounting
createRoot(document.getElementById('root')).render(
-
-
- ,
+
)
diff --git a/frontend/src/pages/MatchChatPage.jsx b/frontend/src/pages/MatchChatPage.jsx
index 2c7c650..ec130e0 100644
--- a/frontend/src/pages/MatchChatPage.jsx
+++ b/frontend/src/pages/MatchChatPage.jsx
@@ -6,6 +6,8 @@ import { matchesAPI } from '../services/api';
import { Send, Video, Upload, X, Check, Link as LinkIcon, Loader2 } from 'lucide-react';
import { connectSocket, getSocket } from '../services/socket';
import { useWebRTC } from '../hooks/useWebRTC';
+import { detectWebRTCSupport } from '../utils/webrtcDetection';
+import WebRTCWarning from '../components/WebRTCWarning';
const MatchChatPage = () => {
const { slug } = useParams();
@@ -19,6 +21,7 @@ const MatchChatPage = () => {
const [showLinkInput, setShowLinkInput] = useState(false);
const [videoLink, setVideoLink] = useState('');
const [isConnected, setIsConnected] = useState(false);
+ const [webrtcDetection, setWebrtcDetection] = useState(null);
const messagesEndRef = useRef(null);
const fileInputRef = useRef(null);
@@ -33,6 +36,21 @@ const MatchChatPage = () => {
cleanupConnection,
} = useWebRTC(match?.id, user?.id);
+ // Detect WebRTC support on mount
+ useEffect(() => {
+ const runDetection = async () => {
+ const result = await detectWebRTCSupport();
+ setWebrtcDetection(result);
+
+ if (result.hasIceCandidates) {
+ console.log('โ
WebRTC detection: Working correctly');
+ } else {
+ console.warn('โ ๏ธ WebRTC detection:', result.error);
+ }
+ };
+ runDetection();
+ }, []);
+
// Fetch match data
useEffect(() => {
const loadMatch = async () => {
@@ -88,13 +106,15 @@ const MatchChatPage = () => {
return;
}
- // Socket event listeners
- socket.on('connect', () => {
+ // Helper to join match room
+ const joinMatchRoom = () => {
setIsConnected(true);
- // Join match room using numeric match ID for socket
socket.emit('join_match_room', { matchId: match.id });
console.log(`Joined match room ${match.id}`);
- });
+ };
+
+ // Socket event listeners
+ socket.on('connect', joinMatchRoom);
socket.on('disconnect', () => {
setIsConnected(false);
@@ -105,9 +125,14 @@ const MatchChatPage = () => {
setMessages((prev) => [...prev, message]);
});
+ // Join immediately if already connected
+ if (socket.connected) {
+ joinMatchRoom();
+ }
+
// Cleanup
return () => {
- socket.off('connect');
+ socket.off('connect', joinMatchRoom);
socket.off('disconnect');
socket.off('match_message');
};
@@ -315,6 +340,13 @@ const MatchChatPage = () => {
+ {/* WebRTC Warning */}
+ {webrtcDetection && (
+
+
+
+ )}
+
{/* Messages */}
{messages.length === 0 && (
@@ -472,7 +504,12 @@ const MatchChatPage = () => {
/>