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:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user