Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Cloudflare platform skill covering Workers, D1, R2, KV, AI, Durable Objects, and security.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/turn/patterns.md
1# TURN Implementation Patterns23Production-ready patterns for implementing Cloudflare TURN in WebRTC applications.45## Prerequisites67Before implementing these patterns, ensure you have:8- TURN key created: see [api.md#create-turn-key](./api.md#create-turn-key)9- Worker configured: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration)1011## Basic TURN Configuration (Browser)1213```typescript14interface RTCIceServer {15urls: string | string[];16username?: string;17credential?: string;18credentialType?: "password" | "oauth";19}2021async function getTURNConfig(): Promise<RTCIceServer[]> {22const response = await fetch('/api/turn-credentials');23const data = await response.json();2425return [26{27urls: 'stun:stun.cloudflare.com:3478'28},29{30urls: [31'turn:turn.cloudflare.com:3478?transport=udp',32'turn:turn.cloudflare.com:3478?transport=tcp',33'turns:turn.cloudflare.com:5349?transport=tcp',34'turns:turn.cloudflare.com:443?transport=tcp'35],36username: data.username,37credential: data.credential,38credentialType: 'password'39}40];41}4243// Use in RTCPeerConnection44const iceServers = await getTURNConfig();45const peerConnection = new RTCPeerConnection({ iceServers });46```4748## Port Selection Strategy4950Recommended order for browser clients:51521. **3478/udp** (primary, lowest latency)532. **3478/tcp** (fallback for UDP-blocked networks)543. **5349/tls** (corporate firewalls, most reliable)554. **443/tls** (alternate TLS port, firewall-friendly)5657**Avoid port 53**—blocked by Chrome and Firefox.5859```typescript60function filterICEServersForBrowser(urls: string[]): string[] {61return urls62.filter(url => !url.includes(':53')) // Remove port 5363.sort((a, b) => {64// Prioritize UDP over TCP over TLS65if (a.includes('transport=udp')) return -1;66if (b.includes('transport=udp')) return 1;67if (a.includes('transport=tcp') && !a.startsWith('turns:')) return -1;68if (b.includes('transport=tcp') && !b.startsWith('turns:')) return 1;69return 0;70});71}72```7374## Credential Refresh (Mid-Session)7576When credentials expire during long calls:7778```typescript79async function refreshTURNCredentials(pc: RTCPeerConnection): Promise<void> {80const newCreds = await fetch('/turn-credentials').then(r => r.json());81const config = pc.getConfiguration();82config.iceServers = newCreds.iceServers;83pc.setConfiguration(config);84// Note: setConfiguration() does NOT trigger ICE restart85// Combine with restartIce() if connection fails86}8788// Auto-refresh before expiry89setInterval(async () => {90await refreshTURNCredentials(peerConnection);91}, 3000000); // 50 minutes if TTL is 1 hour92```9394## ICE Restart Pattern9596After network change, TURN server maintenance, or credential expiry:9798```typescript99pc.addEventListener('iceconnectionstatechange', async () => {100if (pc.iceConnectionState === 'failed') {101console.warn('ICE connection failed, restarting...');102103// Refresh credentials104await refreshTURNCredentials(pc);105106// Trigger ICE restart107pc.restartIce();108const offer = await pc.createOffer({ iceRestart: true });109await pc.setLocalDescription(offer);110111// Send offer to peer via signaling channel...112}113});114```115116## Credentials Caching Pattern117118```typescript119class TURNCredentialsManager {120private creds: { username: string; credential: string; urls: string[]; expiresAt: number; } | null = null;121122async getCredentials(keyId: string, keySecret: string): Promise<RTCIceServer[]> {123const now = Date.now();124125if (this.creds && this.creds.expiresAt > now) {126return this.buildIceServers(this.creds);127}128129const ttl = 3600;130if (ttl > 172800) throw new Error('TTL max 48hrs');131132const res = await fetch(133`https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`,134{135method: 'POST',136headers: { 'Authorization': `Bearer ${keySecret}`, 'Content-Type': 'application/json' },137body: JSON.stringify({ ttl })138}139);140141const data = await res.json();142const filteredUrls = data.iceServers.urls.filter((url: string) => !url.includes(':53'));143144this.creds = {145username: data.iceServers.username,146credential: data.iceServers.credential,147urls: filteredUrls,148expiresAt: now + (ttl * 1000) - 60000149};150151return this.buildIceServers(this.creds);152}153154private buildIceServers(c: { username: string; credential: string; urls: string[] }): RTCIceServer[] {155return [156{ urls: 'stun:stun.cloudflare.com:3478' },157{ urls: c.urls, username: c.username, credential: c.credential, credentialType: 'password' as const }158];159}160}161```162163## Common Use Cases164165```typescript166// Video conferencing: TURN as fallback167const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'all' };168169// IoT/predictable connectivity: force TURN170const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'relay' };171172// Screen sharing: reduce overhead173const pc = new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: 'max-bundle' });174```175176## Integration with Cloudflare Calls SFU177178```typescript179// TURN is automatically used when needed180// Cloudflare Calls handles TURN + SFU coordination181const session = await callsClient.createSession({182appId: 'your-app-id',183sessionId: 'meeting-123'184});185```186187## Debugging ICE Connectivity188189```typescript190pc.addEventListener('icecandidate', (event) => {191if (event.candidate) {192console.log('ICE candidate:', event.candidate.type, event.candidate.protocol);193}194});195196pc.addEventListener('iceconnectionstatechange', () => {197console.log('ICE state:', pc.iceConnectionState);198});199200// Check selected candidate pair201const stats = await pc.getStats();202stats.forEach(report => {203if (report.type === 'candidate-pair' && report.selected) {204console.log('Selected:', report);205}206});207```208209## See Also210211- [api.md](./api.md) - Credential generation API, types212- [configuration.md](./configuration.md) - Worker setup, environment variables213- [gotchas.md](./gotchas.md) - Common mistakes, troubleshooting214