mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
update skills
This commit is contained in:
213
.agents/skills/cloudflare-deploy/references/turn/patterns.md
Normal file
213
.agents/skills/cloudflare-deploy/references/turn/patterns.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 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](./api.md#create-turn-key)
|
||||
- Worker configured: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration)
|
||||
|
||||
## Basic TURN Configuration (Browser)
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
1. **3478/udp** (primary, lowest latency)
|
||||
2. **3478/tcp** (fallback for UDP-blocked networks)
|
||||
3. **5349/tls** (corporate firewalls, most reliable)
|
||||
4. **443/tls** (alternate TLS port, firewall-friendly)
|
||||
|
||||
**Avoid port 53**—blocked by Chrome and Firefox.
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
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](./api.md) - Credential generation API, types
|
||||
- [configuration.md](./configuration.md) - Worker setup, environment variables
|
||||
- [gotchas.md](./gotchas.md) - Common mistakes, troubleshooting
|
||||
Reference in New Issue
Block a user