mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
6.0 KiB
6.0 KiB
TURN Implementation Patterns
Production-ready patterns for implementing Cloudflare TURN in WebRTC applications.
Prerequisites
Before implementing these patterns, ensure you have:
- TURN key created: see api.md#create-turn-key
- Worker configured: see configuration.md#cloudflare-worker-integration
Basic TURN Configuration (Browser)
interface RTCIceServer {
urls: string | string[];
username?: string;
credential?: string;
credentialType?: "password" | "oauth";
}
async function getTURNConfig(): Promise<RTCIceServer[]> {
const response = await fetch('/api/turn-credentials');
const data = await response.json();
return [
{
urls: 'stun:stun.cloudflare.com:3478'
},
{
urls: [
'turn:turn.cloudflare.com:3478?transport=udp',
'turn:turn.cloudflare.com:3478?transport=tcp',
'turns:turn.cloudflare.com:5349?transport=tcp',
'turns:turn.cloudflare.com:443?transport=tcp'
],
username: data.username,
credential: data.credential,
credentialType: 'password'
}
];
}
// Use in RTCPeerConnection
const iceServers = await getTURNConfig();
const peerConnection = new RTCPeerConnection({ iceServers });
Port Selection Strategy
Recommended order for browser clients:
- 3478/udp (primary, lowest latency)
- 3478/tcp (fallback for UDP-blocked networks)
- 5349/tls (corporate firewalls, most reliable)
- 443/tls (alternate TLS port, firewall-friendly)
Avoid port 53—blocked by Chrome and Firefox.
function filterICEServersForBrowser(urls: string[]): string[] {
return urls
.filter(url => !url.includes(':53')) // Remove port 53
.sort((a, b) => {
// Prioritize UDP over TCP over TLS
if (a.includes('transport=udp')) return -1;
if (b.includes('transport=udp')) return 1;
if (a.includes('transport=tcp') && !a.startsWith('turns:')) return -1;
if (b.includes('transport=tcp') && !b.startsWith('turns:')) return 1;
return 0;
});
}
Credential Refresh (Mid-Session)
When credentials expire during long calls:
async function refreshTURNCredentials(pc: RTCPeerConnection): Promise<void> {
const newCreds = await fetch('/turn-credentials').then(r => r.json());
const config = pc.getConfiguration();
config.iceServers = newCreds.iceServers;
pc.setConfiguration(config);
// Note: setConfiguration() does NOT trigger ICE restart
// Combine with restartIce() if connection fails
}
// Auto-refresh before expiry
setInterval(async () => {
await refreshTURNCredentials(peerConnection);
}, 3000000); // 50 minutes if TTL is 1 hour
ICE Restart Pattern
After network change, TURN server maintenance, or credential expiry:
pc.addEventListener('iceconnectionstatechange', async () => {
if (pc.iceConnectionState === 'failed') {
console.warn('ICE connection failed, restarting...');
// Refresh credentials
await refreshTURNCredentials(pc);
// Trigger ICE restart
pc.restartIce();
const offer = await pc.createOffer({ iceRestart: true });
await pc.setLocalDescription(offer);
// Send offer to peer via signaling channel...
}
});
Credentials Caching Pattern
class TURNCredentialsManager {
private creds: { username: string; credential: string; urls: string[]; expiresAt: number; } | null = null;
async getCredentials(keyId: string, keySecret: string): Promise<RTCIceServer[]> {
const now = Date.now();
if (this.creds && this.creds.expiresAt > now) {
return this.buildIceServers(this.creds);
}
const ttl = 3600;
if (ttl > 172800) throw new Error('TTL max 48hrs');
const res = await fetch(
`https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${keySecret}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ ttl })
}
);
const data = await res.json();
const filteredUrls = data.iceServers.urls.filter((url: string) => !url.includes(':53'));
this.creds = {
username: data.iceServers.username,
credential: data.iceServers.credential,
urls: filteredUrls,
expiresAt: now + (ttl * 1000) - 60000
};
return this.buildIceServers(this.creds);
}
private buildIceServers(c: { username: string; credential: string; urls: string[] }): RTCIceServer[] {
return [
{ urls: 'stun:stun.cloudflare.com:3478' },
{ urls: c.urls, username: c.username, credential: c.credential, credentialType: 'password' as const }
];
}
}
Common Use Cases
// Video conferencing: TURN as fallback
const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'all' };
// IoT/predictable connectivity: force TURN
const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'relay' };
// Screen sharing: reduce overhead
const pc = new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: 'max-bundle' });
Integration with Cloudflare Calls SFU
// TURN is automatically used when needed
// Cloudflare Calls handles TURN + SFU coordination
const session = await callsClient.createSession({
appId: 'your-app-id',
sessionId: 'meeting-123'
});
Debugging ICE Connectivity
pc.addEventListener('icecandidate', (event) => {
if (event.candidate) {
console.log('ICE candidate:', event.candidate.type, event.candidate.protocol);
}
});
pc.addEventListener('iceconnectionstatechange', () => {
console.log('ICE state:', pc.iceConnectionState);
});
// Check selected candidate pair
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.selected) {
console.log('Selected:', report);
}
});
See Also
- api.md - Credential generation API, types
- configuration.md - Worker setup, environment variables
- gotchas.md - Common mistakes, troubleshooting