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

@@ -0,0 +1,100 @@
const express = require('express');
const { authenticate } = require('../middleware/auth');
const router = express.Router();
/**
* GET /api/webrtc/ice-servers
* Get ICE servers configuration (STUN/TURN) for WebRTC
* Requires authentication
*/
router.get('/ice-servers', authenticate, async (req, res) => {
try {
const turnTokenId = process.env.CLOUDFLARE_TURN_TOKEN_ID;
const turnApiToken = process.env.CLOUDFLARE_TURN_API_TOKEN;
if (!turnTokenId || !turnApiToken) {
console.error('Cloudflare TURN credentials not configured');
// Fallback to public STUN servers if TURN not configured
return res.json({
success: true,
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
],
});
}
// Request TURN credentials from Cloudflare
const cloudflareUrl = `https://rtc.live.cloudflare.com/v1/turn/keys/${turnTokenId}/credentials/generate`;
const response = await fetch(cloudflareUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${turnApiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
ttl: 86400, // 24 hours
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error('Cloudflare TURN API error:', response.status, errorText);
// Fallback to public STUN servers
return res.json({
success: true,
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
],
});
}
const data = await response.json();
// Cloudflare returns: { iceServers: { urls: [...], username: "...", credential: "..." } }
// We need to return it in the format expected by RTCPeerConnection
const cloudflareIceServers = data.iceServers;
if (!cloudflareIceServers || !cloudflareIceServers.urls) {
console.error('Invalid response from Cloudflare TURN API:', data);
// Fallback to public STUN servers
return res.json({
success: true,
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
],
});
}
// Return in RTCPeerConnection format
// Cloudflare provides all URLs (STUN + TURN) in one object with credentials
res.json({
success: true,
iceServers: [
{
urls: cloudflareIceServers.urls,
username: cloudflareIceServers.username,
credential: cloudflareIceServers.credential,
}
],
});
} catch (error) {
console.error('Error fetching TURN credentials:', error);
// Fallback to public STUN servers on error
res.json({
success: true,
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
],
});
}
});
module.exports = router;