feat: implement WebRTC P2P file transfer with detection and fallback

Implemented complete WebRTC peer-to-peer file transfer system for match chat:

**Core WebRTC Implementation:**
- Created useWebRTC hook with RTCPeerConnection and RTCDataChannel
- P2P file transfer with 16KB chunking for large files (tested up to 700MB)
- Real-time progress monitoring for sender and receiver
- Automatic file download on receiver side
- End-to-end encryption via DTLS (native WebRTC)
- ICE candidate exchange via Socket.IO signaling
- Support for host candidates (localhost testing)

**WebRTC Detection & User Experience:**
- Automatic WebRTC capability detection on page load
- Detects if ICE candidates can be generated (fails in Opera, privacy-focused browsers, VPNs)
- User-friendly warning component with fix suggestions
- Graceful degradation: disables WebRTC button when blocked
- Suggests alternative methods (video links via Google Drive/Dropbox)

**Socket.IO Improvements:**
- Fixed multiple socket instance creation issue
- Implemented socket instance reuse pattern
- Disabled React.StrictMode to prevent reconnection loops in development

**Technical Details:**
- RTCPeerConnection with configurable STUN servers (currently using localhost config)
- RTCDataChannel with ordered delivery
- Comprehensive logging for debugging (ICE gathering, connection states, signaling)
- Match room-based signaling relay via Socket.IO
- Authorization checks for all WebRTC signaling events

**Files Changed:**
- frontend/src/hooks/useWebRTC.js - Complete WebRTC implementation
- frontend/src/utils/webrtcDetection.js - WebRTC capability detection
- frontend/src/components/WebRTCWarning.jsx - User warning component
- frontend/src/pages/MatchChatPage.jsx - WebRTC integration
- frontend/src/services/socket.js - Socket instance reuse
- frontend/src/main.jsx - Disabled StrictMode for Socket.IO stability

**Testing:**
-  Verified working in Chrome (ICE candidates generated)
-  Tested with 700MB file transfer
-  Detection working in Opera (shows warning when WebRTC blocked)
-  P2P connection establishment and DataChannel opening
-  File chunking and progress monitoring

**TODO:**
- Add STUN server configuration for production (NAT traversal)
- Consider server-based upload fallback for blocked users
This commit is contained in:
Radosław Gierwiało
2025-11-15 16:12:02 +01:00
parent 664a2865b9
commit d23a12e5e3
6 changed files with 374 additions and 41 deletions

View File

@@ -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 = () => {
</div>
<div className="flex flex-col h-[calc(100vh-320px)]">
{/* WebRTC Warning */}
{webrtcDetection && (
<div className="p-4 pb-0">
<WebRTCWarning detection={webrtcDetection} />
</div>
)}
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
@@ -472,7 +504,12 @@ const MatchChatPage = () => {
/>
<button
onClick={() => fileInputRef.current?.click()}
disabled={isTransferring || selectedFile}
disabled={isTransferring || selectedFile || (webrtcDetection && !webrtcDetection.hasIceCandidates)}
title={
webrtcDetection && !webrtcDetection.hasIceCandidates
? 'WebRTC is blocked - see warning above'
: 'Send video via P2P WebRTC'
}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
>
<Video className="w-4 h-4" />