feat: implement WebRTC P2P file transfer with DataChannel
Add complete WebRTC peer-to-peer file transfer functionality: Backend changes: - Add WebRTC signaling events to Socket.IO (offer, answer, ICE candidates) - Implement authorization checks for match participants - Add signaling relay between matched users Frontend changes: - Create useWebRTC hook for RTCPeerConnection management - Implement RTCDataChannel with 16KB chunking for large files - Add real-time progress monitoring for sender and receiver - Implement automatic file download on receiver side - Add connection state tracking and error handling - Integrate WebRTC with MatchChatPage (replace mockup) Configuration: - Add Vite allowed hosts configuration via VITE_ALLOWED_HOSTS env var - Support comma-separated host list or 'all' for development - Add .env.example with configuration examples - Update docker-compose.yml with default allowed hosts Documentation: - Add comprehensive WebRTC testing guide with troubleshooting - Add quick test checklist for manual testing - Document WebRTC flow, requirements, and success criteria Features: - End-to-end encrypted P2P transfer (DTLS) - 16KB chunk size optimized for DataChannel - Buffer management to prevent overflow - Automatic connection establishment with 30s timeout - Support for files of any size - Real-time progress tracking - Clean connection lifecycle management
This commit is contained in:
@@ -5,6 +5,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
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';
|
||||
|
||||
const MatchChatPage = () => {
|
||||
const { slug } = useParams();
|
||||
@@ -15,15 +16,23 @@ const MatchChatPage = () => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [isTransferring, setIsTransferring] = useState(false);
|
||||
const [transferProgress, setTransferProgress] = useState(0);
|
||||
const [webrtcStatus, setWebrtcStatus] = useState('disconnected'); // disconnected, connecting, connected, failed
|
||||
const [showLinkInput, setShowLinkInput] = useState(false);
|
||||
const [videoLink, setVideoLink] = useState('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// WebRTC hook
|
||||
const {
|
||||
connectionState,
|
||||
isTransferring,
|
||||
transferProgress,
|
||||
receivingFile,
|
||||
createOffer,
|
||||
sendFile,
|
||||
cleanupConnection,
|
||||
} = useWebRTC(match?.id, user?.id);
|
||||
|
||||
// Fetch match data
|
||||
useEffect(() => {
|
||||
const loadMatch = async () => {
|
||||
@@ -128,61 +137,60 @@ const MatchChatPage = () => {
|
||||
if (file && file.type.startsWith('video/')) {
|
||||
setSelectedFile(file);
|
||||
} else {
|
||||
alert('Proszę wybrać plik wideo');
|
||||
alert('Please select a video file');
|
||||
}
|
||||
};
|
||||
|
||||
const simulateWebRTCConnection = () => {
|
||||
setWebrtcStatus('connecting');
|
||||
setTimeout(() => {
|
||||
setWebrtcStatus('connected');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleStartTransfer = () => {
|
||||
const handleStartTransfer = async () => {
|
||||
if (!selectedFile) return;
|
||||
|
||||
// Simulate WebRTC connection
|
||||
simulateWebRTCConnection();
|
||||
// If not connected, initiate connection first
|
||||
if (connectionState !== 'connected') {
|
||||
console.log('Creating WebRTC offer...');
|
||||
await createOffer();
|
||||
|
||||
setTimeout(() => {
|
||||
setIsTransferring(true);
|
||||
setTransferProgress(0);
|
||||
|
||||
// Simulate transfer progress
|
||||
const interval = setInterval(() => {
|
||||
setTransferProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setIsTransferring(false);
|
||||
setSelectedFile(null);
|
||||
setWebrtcStatus('disconnected');
|
||||
|
||||
// Add message about completed transfer
|
||||
const message = {
|
||||
id: messages.length + 1,
|
||||
room_id: 10,
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
content: `📹 Video sent: ${selectedFile.name} (${(selectedFile.size / 1024 / 1024).toFixed(2)} MB)`,
|
||||
type: 'video',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, message]);
|
||||
|
||||
return 0;
|
||||
// Wait for connection
|
||||
const waitForConnection = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Connection timeout')), 30000);
|
||||
const checkConnection = setInterval(() => {
|
||||
if (connectionState === 'connected') {
|
||||
clearInterval(checkConnection);
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
} else if (connectionState === 'failed') {
|
||||
clearInterval(checkConnection);
|
||||
clearTimeout(timeout);
|
||||
reject(new Error('Connection failed'));
|
||||
}
|
||||
return prev + 5;
|
||||
});
|
||||
}, 200);
|
||||
}, 2000);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForConnection;
|
||||
} catch (error) {
|
||||
alert('Failed to establish connection: ' + error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send file
|
||||
await sendFile(selectedFile);
|
||||
|
||||
// Add message about completed transfer
|
||||
const socket = getSocket();
|
||||
if (socket && socket.connected) {
|
||||
socket.emit('send_match_message', {
|
||||
matchId: match.id,
|
||||
content: `📹 Video sent: ${selectedFile.name} (${(selectedFile.size / 1024 / 1024).toFixed(2)} MB)`,
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedFile(null);
|
||||
};
|
||||
|
||||
const handleCancelTransfer = () => {
|
||||
setIsTransferring(false);
|
||||
setTransferProgress(0);
|
||||
setSelectedFile(null);
|
||||
setWebrtcStatus('disconnected');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
const handleSendLink = (e) => {
|
||||
@@ -209,7 +217,7 @@ const MatchChatPage = () => {
|
||||
};
|
||||
|
||||
const getWebRTCStatusColor = () => {
|
||||
switch (webrtcStatus) {
|
||||
switch (connectionState) {
|
||||
case 'connected':
|
||||
return 'text-green-600';
|
||||
case 'connecting':
|
||||
@@ -222,7 +230,7 @@ const MatchChatPage = () => {
|
||||
};
|
||||
|
||||
const getWebRTCStatusText = () => {
|
||||
switch (webrtcStatus) {
|
||||
switch (connectionState) {
|
||||
case 'connected':
|
||||
return 'Connected (P2P)';
|
||||
case 'connecting':
|
||||
@@ -230,7 +238,7 @@ const MatchChatPage = () => {
|
||||
case 'failed':
|
||||
return 'Connection failed';
|
||||
default:
|
||||
return 'Disconnected';
|
||||
return 'Ready to connect';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -289,14 +297,21 @@ const MatchChatPage = () => {
|
||||
{/* WebRTC Status Bar */}
|
||||
<div className="bg-gray-50 border-b px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${webrtcStatus === 'connected' ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
<div className={`w-2 h-2 rounded-full ${connectionState === 'connected' ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
<span className={`text-sm font-medium ${getWebRTCStatusColor()}`}>
|
||||
{getWebRTCStatusText()}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{webrtcStatus === 'connected' ? '🔒 E2E Encrypted (DTLS/SRTP)' : 'WebRTC ready to connect'}
|
||||
</span>
|
||||
<div className="flex items-center space-x-4">
|
||||
{receivingFile && (
|
||||
<span className="text-xs text-blue-600 font-medium">
|
||||
📥 Receiving: {receivingFile.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-500">
|
||||
{connectionState === 'connected' ? '🔒 E2E Encrypted (DTLS)' : 'WebRTC P2P Ready'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col h-[calc(100vh-320px)]">
|
||||
@@ -495,11 +510,11 @@ const MatchChatPage = () => {
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>🚀 WebRTC P2P Functionality Mockup:</strong> In the full version, videos will be transferred directly
|
||||
between users via RTCDataChannel, with chunking and progress monitoring.
|
||||
The server is only used for SDP/ICE exchange (signaling).
|
||||
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<p className="text-sm text-green-800">
|
||||
<strong>🚀 WebRTC P2P File Transfer Active!</strong> Videos are transferred directly between users via
|
||||
RTCDataChannel with 16KB chunking and real-time progress monitoring. The server is only used for
|
||||
SDP/ICE exchange (signaling). Connection is end-to-end encrypted (DTLS).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user