feat(webrtc): integrate Cloudflare TURN/STUN servers

- Add backend endpoint to fetch ICE server credentials from Cloudflare
- Implement dynamic ICE server configuration in frontend
- Add fallback to public STUN servers when Cloudflare unavailable
- Create comprehensive test suite for WebRTC API endpoint
- Update environment configuration with Cloudflare TURN credentials

Backend changes:
- New route: GET /api/webrtc/ice-servers (authenticated)
- Fetches temporary credentials from Cloudflare API with 24h TTL
- Returns formatted ICE servers for RTCPeerConnection
- Graceful fallback to Google STUN servers on errors

Frontend changes:
- Remove hardcoded ICE servers from useWebRTC hook
- Fetch ICE servers dynamically from backend on mount
- Store servers in ref for peer connection initialization
- Add webrtcAPI service for backend communication

Tests:
- 9 comprehensive tests covering all scenarios
- 100% coverage for webrtc.js route
- Tests authentication, success, and all fallback scenarios
This commit is contained in:
Radosław Gierwiało
2025-12-05 21:23:50 +01:00
parent e1138c789e
commit a92d7469e4
6 changed files with 422 additions and 28 deletions

View File

@@ -1,33 +1,13 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { getSocket } from '../services/socket';
import { CONNECTION_STATE } from '../constants';
import { webrtcAPI } from '../services/api';
// WebRTC configuration with STUN and TURN servers for NAT traversal
const rtcConfig = {
iceServers: [
// STUN servers for basic NAT traversal
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
// TURN servers for symmetric NAT and strict firewalls (public relay for testing)
{
urls: 'turn:openrelay.metered.ca:80',
username: 'openrelayproject',
credential: 'openrelayproject',
},
{
urls: 'turn:openrelay.metered.ca:443',
username: 'openrelayproject',
credential: 'openrelayproject',
},
{
urls: 'turn:openrelay.metered.ca:443?transport=tcp',
username: 'openrelayproject',
credential: 'openrelayproject',
},
],
};
// Default fallback ICE servers (used if backend request fails)
const DEFAULT_ICE_SERVERS = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
];
// File chunk size (16KB recommended for WebRTC DataChannel)
const CHUNK_SIZE = 16384;
@@ -50,6 +30,7 @@ export const useWebRTC = (matchId, userId) => {
const socketRef = useRef(null);
const matchIdRef = useRef(matchId);
const userIdRef = useRef(userId);
const iceServersRef = useRef(null);
// Update refs when props change
useEffect(() => {
@@ -57,6 +38,27 @@ export const useWebRTC = (matchId, userId) => {
userIdRef.current = userId;
}, [matchId, userId]);
// Fetch ICE servers from backend on mount
useEffect(() => {
const fetchIceServers = async () => {
try {
const response = await webrtcAPI.getIceServers();
if (response.success && response.iceServers) {
iceServersRef.current = response.iceServers;
console.log('✅ Fetched ICE servers from backend:', response.iceServers.length, 'servers');
} else {
console.warn('⚠️ Using default ICE servers (backend response invalid)');
iceServersRef.current = DEFAULT_ICE_SERVERS;
}
} catch (error) {
console.error('❌ Failed to fetch ICE servers, using defaults:', error);
iceServersRef.current = DEFAULT_ICE_SERVERS;
}
};
fetchIceServers();
}, []);
// File transfer state
const fileTransferRef = useRef({
file: null,
@@ -80,10 +82,13 @@ export const useWebRTC = (matchId, userId) => {
return peerConnectionRef.current;
}
// Use full config with STUN servers for production
// Use ICE servers from backend or fallback to defaults
const iceServers = iceServersRef.current || DEFAULT_ICE_SERVERS;
const rtcConfig = { iceServers };
const pc = new RTCPeerConnection(rtcConfig);
peerConnectionRef.current = pc;
console.log('🔧 Using rtcConfig with STUN servers for NAT traversal');
console.log('🔧 Using ICE servers for NAT traversal:', iceServers.length, 'servers');
// ICE candidate handler
pc.onicecandidate = (event) => {