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:
@@ -328,6 +328,104 @@ function initializeSocket(httpServer) {
|
||||
}
|
||||
});
|
||||
|
||||
// WebRTC Signaling Events
|
||||
|
||||
// Send WebRTC offer
|
||||
socket.on('webrtc_offer', async ({ matchId, offer }) => {
|
||||
try {
|
||||
const roomName = `match_${matchId}`;
|
||||
|
||||
// Verify user is part of this match
|
||||
const match = await prisma.match.findUnique({
|
||||
where: { id: parseInt(matchId) },
|
||||
select: { user1Id: true, user2Id: true },
|
||||
});
|
||||
|
||||
if (!match) {
|
||||
return socket.emit('error', { message: 'Match not found' });
|
||||
}
|
||||
|
||||
if (match.user1Id !== socket.user.id && match.user2Id !== socket.user.id) {
|
||||
return socket.emit('error', { message: 'Not authorized for this match' });
|
||||
}
|
||||
|
||||
// Forward offer to the other user in the match room
|
||||
socket.to(roomName).emit('webrtc_offer', {
|
||||
from: socket.user.id,
|
||||
offer,
|
||||
});
|
||||
|
||||
console.log(`📡 WebRTC offer sent in match ${matchId} from ${socket.user.username}`);
|
||||
} catch (error) {
|
||||
console.error('WebRTC offer error:', error);
|
||||
socket.emit('error', { message: 'Failed to send WebRTC offer' });
|
||||
}
|
||||
});
|
||||
|
||||
// Send WebRTC answer
|
||||
socket.on('webrtc_answer', async ({ matchId, answer }) => {
|
||||
try {
|
||||
const roomName = `match_${matchId}`;
|
||||
|
||||
// Verify user is part of this match
|
||||
const match = await prisma.match.findUnique({
|
||||
where: { id: parseInt(matchId) },
|
||||
select: { user1Id: true, user2Id: true },
|
||||
});
|
||||
|
||||
if (!match) {
|
||||
return socket.emit('error', { message: 'Match not found' });
|
||||
}
|
||||
|
||||
if (match.user1Id !== socket.user.id && match.user2Id !== socket.user.id) {
|
||||
return socket.emit('error', { message: 'Not authorized for this match' });
|
||||
}
|
||||
|
||||
// Forward answer to the other user in the match room
|
||||
socket.to(roomName).emit('webrtc_answer', {
|
||||
from: socket.user.id,
|
||||
answer,
|
||||
});
|
||||
|
||||
console.log(`📡 WebRTC answer sent in match ${matchId} from ${socket.user.username}`);
|
||||
} catch (error) {
|
||||
console.error('WebRTC answer error:', error);
|
||||
socket.emit('error', { message: 'Failed to send WebRTC answer' });
|
||||
}
|
||||
});
|
||||
|
||||
// Send ICE candidate
|
||||
socket.on('webrtc_ice_candidate', async ({ matchId, candidate }) => {
|
||||
try {
|
||||
const roomName = `match_${matchId}`;
|
||||
|
||||
// Verify user is part of this match
|
||||
const match = await prisma.match.findUnique({
|
||||
where: { id: parseInt(matchId) },
|
||||
select: { user1Id: true, user2Id: true },
|
||||
});
|
||||
|
||||
if (!match) {
|
||||
return socket.emit('error', { message: 'Match not found' });
|
||||
}
|
||||
|
||||
if (match.user1Id !== socket.user.id && match.user2Id !== socket.user.id) {
|
||||
return socket.emit('error', { message: 'Not authorized for this match' });
|
||||
}
|
||||
|
||||
// Forward ICE candidate to the other user in the match room
|
||||
socket.to(roomName).emit('webrtc_ice_candidate', {
|
||||
from: socket.user.id,
|
||||
candidate,
|
||||
});
|
||||
|
||||
console.log(`🧊 ICE candidate sent in match ${matchId} from ${socket.user.username}`);
|
||||
} catch (error) {
|
||||
console.error('ICE candidate error:', error);
|
||||
socket.emit('error', { message: 'Failed to send ICE candidate' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`❌ User disconnected: ${socket.user.username} (${socket.id})`);
|
||||
|
||||
Reference in New Issue
Block a user