Skip to main content Início Criadores marvinrichter clarc webrtc-patterns
webrtc-patterns WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
Ir para a instalação Skills Marketplace Descubra e explore skills de IA criadas pela comunidade.
Ocupações relacionadas SOC
Baseado na classificação ocupacional SOC
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Copiar promptMostrar detalhes do prompt Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/marvinrichter/clarc --skill webrtc-patternsO comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Baixar Zip Baixando... name webrtc-patterns description WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
WebRTC Patterns
When to Activate
Building real-time audio/video calls (1:1 or group)
Implementing screen sharing
Building a live streaming feature with low latency (<500ms)
Choosing between P2P, SFU (LiveKit, Mediasoup), or MCU architectures
Deploying TURN servers for NAT traversal
Designing WebRTC signaling with WebSocket
Debugging ICE connection failures or poor video quality
WebRTC Core Concepts
WebRTC is a browser/native API for peer-to-peer real-time audio, video, and data. Three core APIs:
API Purpose RTCPeerConnectionManages the peer connection — media, codec negotiation, ICE RTCDataChannelArbitrary binary/text data between peers (without a server) MediaStreamAudio/video track container from camera, microphone, or screen
What WebRTC does NOT define: signaling. The mechanism to exchange offer/answer/ICE is left to the application — typically WebSocket.
ICE, STUN, and TURN
Browser A ─────── STUN Server ──── discovers public IP
│
│ ICE Candidates exchanged via Signaling (WebSocket)
│
Browser B ─────── STUN Server ──── discovers public IP
│
└──────── Direct P2P connection (if NAT allows) ────────┘
OR
└──── TURN Server (relay) ── if P2P impossible (symmetric NAT) ──┘
STUN (Session Traversal Utilities for NAT): Browser asks "what's my public IP:port?" — free to run, minimal bandwidth
TURN (Traversal Using Relays around NAT): Full media relay through server — required for ~15-20% of connections behind corporate/symmetric NAT
ICE (Interactive Connectivity Establishment): Tries all possible connection paths (host, srflx, relay) and selects the best one
Offer/Answer Flow (Signaling)
const pc = new RTCPeerConnection ({ iceServers : [
{ urls : 'stun:stun.l.google.com:19302' },
{ urls : 'turn:turn.myapp.com:3478' , username : 'user' , credential : 'pass' },
]});
const stream = await navigator.mediaDevices .getUserMedia ({ video : true , audio : true });
stream.getTracks ().forEach (track => pc.addTrack (track, stream));
const offer = await pc.createOffer ();
await pc.setLocalDescription (offer);
signalingSocket.emit ('offer' , { to : remoteUserId, sdp : offer });
pc.onicecandidate = ({ candidate } ) => {
if (candidate) signalingSocket.emit ('ice-candidate' , { to : remoteUserId, candidate });
};
signalingSocket.on ('answer' , async ({ sdp }) => {
await pc.setRemoteDescription (new RTCSessionDescription (sdp));
});
signalingSocket.on ('ice-candidate' , async ({ candidate }) => {
await pc.addIceCandidate (new RTCIceCandidate (candidate));
});
pc.ontrack = ({ streams } ) => {
remoteVideoElement.srcObject = streams[0 ];
};
signalingSocket.on ('offer' , async ({ from , sdp }) => {
await pc.setRemoteDescription (new RTCSessionDescription (sdp));
const localStream = await navigator.mediaDevices .getUserMedia ({ video : true , audio : true });
localStream.getTracks ().forEach (track => pc.addTrack (track, localStream));
const answer = await pc.createAnswer ();
await pc.setLocalDescription (answer);
signalingSocket.emit ('answer' , { to : from , sdp : answer });
});
Media Server Architectures
P2P Mesh (no server)
Every participant connects to every other: N*(N-1)/2 connections
Scales to ~4-6 participants before bandwidth and CPU collapse
Best for: 1:1 calls, small groups with good network
SFU — Selective Forwarding Unit (recommended for group calls)
Each participant sends one upstream to the SFU; receives N-1 downstreams from it
Server does NOT decode/re-encode (just forwards) → low server CPU
Scales to 50-200 active speakers in one room with simulcast
Best for: group video calls, webinars, live streaming
MCU — Multipoint Control Unit (rarely used)
Server decodes all streams, mixes them into one composite, re-encodes and sends it
Client bandwidth: only one stream regardless of participants
Tradeoff: very high server CPU + cost, encoding delay, lower quality
Best for: SIP gateway, legacy systems, very limited client bandwidth
LiveKit (Modern SFU — Recommended) LiveKit is an open-source SFU written in Go. Production-ready, batteries-included.
Architecture ┌─────────────────┐
Client App ───────► │ LiveKit Server │ ◄────── Access Token (JWT)
(Web/iOS/Android) │ (SFU in Go) │
└────────┬────────┘
│ Egress API
┌────▼────┐
│ S3 │ (recording)
└─────────┘
Server Setup
docker run -d \
-p 7880:7880 \
-p 7881:7881 \
-p 7882:7882/udp \
-e LIVEKIT_KEYS="devkey: devsecret" \
livekit/livekit-server:latest
helm repo add livekit https://helm.livekit.io
helm install livekit livekit/livekit-server \
--set livekit.keys.mykey=mysecret \
--set livekit.turn.enabled=true
Access Tokens (Server-Side)
import { AccessToken } from 'livekit-server-sdk' ;
async function createRoomToken (
roomName : string ,
participantIdentity : string ,
participantName : string ,
): Promise <string > {
const token = new AccessToken (
process.env .LIVEKIT_API_KEY !,
process.env .LIVEKIT_API_SECRET !,
{
identity : participantIdentity,
name : participantName,
ttl : '2h' ,
},
);
token.addGrant ({
room : roomName,
roomJoin : true ,
canPublish : true ,
canSubscribe : true ,
canPublishData : true ,
});
return token.toJwt ();
}
app.get ('/api/rooms/:roomId/token' , authenticate, async (req, res) => {
const token = await createRoomToken (
req.params .roomId ,
req.user .id ,
req.user .displayName ,
);
res.json ({ data : { token, serverUrl : process.env .LIVEKIT_URL } });
});
Client Integration (Web) import { Room , RoomEvent , RemoteParticipant , Track } from 'livekit-client' ;
async function joinRoom (serverUrl : string , token : string ) {
const room = new Room ({
adaptiveStream : true ,
dynacast : true ,
videoCaptureDefaults : {
resolution : VideoPresets .h720 .resolution ,
},
});
room
.on (RoomEvent .TrackSubscribed , (track, publication, participant ) => {
if (track.kind === Track .Kind .Video ) {
const videoElement = document .getElementById (`video-${participant.identity} ` );
track.attach (videoElement as HTMLVideoElement );
}
})
.on (RoomEvent .ParticipantConnected , (participant : RemoteParticipant ) => {
console .log ('Participant joined:' , participant.identity );
})
.on (RoomEvent .Disconnected , (reason ) => {
console .log ('Disconnected from room:' , reason);
});
await room.connect (serverUrl, token);
await room.localParticipant .enableCameraAndMicrophone ();
return room;
}
function leaveRoom (room : Room ) {
room.disconnect ();
}
LiveKit iOS/Android SDKs
import LiveKit
let room = Room ()
let url = "wss://my-livekit-server.com"
let token = await fetchTokenFromAPI()
try await room.connect(url, token)
try await room.localParticipant.setCamera(enabled: true )
try await room.localParticipant.setMicrophone(enabled: true )
val room = Room(context)
room.connect("wss://my-livekit-server.com" , token) { result ->
result.onSuccess {
room.localParticipant.setMicrophoneEnabled(true )
room.localParticipant.setCameraEnabled(true )
}
}
Mediasoup (Low-Level SFU) Mediasoup is a Node.js SFU that gives you full control but requires building your own signaling protocol. Choose Mediasoup over LiveKit when:
You need complete control over the media pipeline
You want to process/inspect media server-side (recording at RTP level)
You need ORTC compatibility
const mediasoup = require ('mediasoup' );
const worker = await mediasoup.createWorker ({ logLevel : 'warn' });
const router = await worker.createRouter ({
mediaCodecs : [
{ kind : 'audio' , mimeType : 'audio/opus' , clockRate : 48000 , channels : 2 },
{ kind : 'video' , mimeType : 'video/VP8' , clockRate : 90000 },
{ kind : 'video' , mimeType : 'video/H264' , clockRate : 90000 ,
parameters : { 'packetization-mode' : 1 , 'profile-level-id' : '42e01f' } },
],
});
const transport = await router.createWebRtcTransport ({
listenIps : [{ ip : '0.0.0.0' , announcedIp : process.env .ANNOUNCED_IP }],
enableUdp : true ,
enableTcp : true ,
preferUdp : true ,
});
const producer = await transport.produce ({ kind : 'video' , rtpParameters });
const consumer = await transport.consume ({
producerId : producer.id ,
rtpCapabilities : peerRtpCapabilities,
});
Simulcast Simulcast allows a sender to transmit multiple quality layers (e.g., 1080p + 540p + 270p) simultaneously. The SFU forwards only the layer appropriate for each receiver's bandwidth.
const videoTrack = await navigator.mediaDevices .getUserMedia ({ video : true });
pc.addTransceiver (videoTrack, {
sendEncodings : [
{ rid : 'high' , maxBitrate : 1_200_000 , scaleResolutionDownBy : 1 },
{ rid : 'medium' , maxBitrate : 400_000 , scaleResolutionDownBy : 2 },
{ rid : 'low' , maxBitrate : 100_000 , scaleResolutionDownBy : 4 },
],
});
room.localParticipant .publishVideoTrack (track, {
simulcast : true ,
videoSimulcastLayers : [VideoPresets .h1080 , VideoPresets .h540 , VideoPresets .h216 ],
});
TURN Server Deployment (Coturn) TURN is required for ~15-20% of users behind corporate firewalls or symmetric NAT. Without it, those calls fail.
apt-get install coturn
listening-port=3478
tls-listening-port=5349
listening-ip=0.0.0.0
relay-ip=YOUR_PUBLIC_IP
external-ip=YOUR_PUBLIC_IP
realm=turn.myapp.com
server-name=turn.myapp.com
lt-cred-mech
user=username:password
use-auth-secret
static-auth-secret=your-secret-key
cert=/etc/letsencrypt/live/turn.myapp.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.myapp.com/privkey.pem
Time-limited TURN credentials (more secure — no static password):
function generateTurnCredentials (userId : string ): { username : string ; credential : string } {
const timestamp = Math .floor (Date .now () / 1000 ) + 3600 ;
const username = `${timestamp} :${userId} ` ;
const credential = createHmac ('sha1' , process.env .TURN_SECRET !)
.update (username)
.digest ('base64' );
return { username, credential };
}
const iceServers = [
{ urls : 'stun:stun.l.google.com:19302' },
{
urls : ['turn:turn.myapp.com:3478' , 'turns:turn.myapp.com:5349' ],
...generateTurnCredentials (userId),
},
];
Alternatives to self-hosted TURN:
Twilio Network Traversal Service (pay-per-use)
Cloudflare Calls (includes TURN)
Xirsys TURN-as-a-service
Recording with LiveKit Egress import { EgressClient , EncodedFileOutput , RoomCompositeEgressRequest } from 'livekit-server-sdk' ;
const egress = new EgressClient (process.env .LIVEKIT_URL !, process.env .LIVEKIT_API_KEY !, process.env .LIVEKIT_API_SECRET !);
const egressInfo = await egress.startRoomCompositeEgress (roomName, {
file : new EncodedFileOutput ({
filepath : `recordings/${roomName} -${Date .now()} .mp4` ,
output : {
case : 's3' ,
value : {
accessKey : process.env .AWS_ACCESS_KEY_ID !,
secret : process.env .AWS_SECRET_ACCESS_KEY !,
bucket : 'my-recordings-bucket' ,
region : 'us-east-1' ,
},
},
}),
});
await egress.stopEgress (egressInfo.egressId );
Testing WebRTC
chromium --use-fake-ui-for-media-stream \
--use-fake-device-for-media-stream \
--allow-file-access-from-files
livekit-cli load-test \
--url wss://my-server.com \
--api-key devkey --api-secret devsecret \
--room load-test-room \
--publishers 10 \
--subscribers 50 \
--duration 60s
use : {
permissions : ['camera' , 'microphone' ],
launchOptions : {
args : ['--use-fake-ui-for-media-stream' , '--use-fake-device-for-media-stream' ],
},
},
Security Checklist - [ ] Access tokens are short-lived (max 2-4 hours) and participant-scoped
- [ ] TURN credentials use HMAC time-limited method (not static passwords)
- [ ] Room creation is server-side only (clients cannot create arbitrary rooms)
- [ ] Recording requires explicit participant consent (GDPR)
- [ ] Room permissions enforced server-side (canPublish, canSubscribe per role)
- [ ] TURN server uses TLS (turns://) not plaintext (turn://) in production
- [ ] LiveKit API key/secret is server-side only — never in client bundle
Reference