--- id: web-webrtc-realtime title: WebRTC — Peer / TURN / Data Channel category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [web, webrtc, realtime, vibe-coding] tech_stack: { language: "TS / WebRTC", applicable_to: ["Frontend"] } applied_in: [] aliases: [WebRTC, RTCPeerConnection, ICE, STUN, TURN, signaling, SFU, LiveKit] --- # WebRTC > Browser-to-browser audio / video / data. **Signaling (WS) + ICE / STUN / TURN + media**. P2P 가 작은 / SFU 가 multi-party 표준. **LiveKit / Daily / 100ms** 는 SFU 매니지드. ## 📖 핵심 개념 - ICE: 가능한 connection 후보 모음. - STUN: NAT 뒤 public IP 발견. - TURN: 직접 X 시 relay (큰 비용). - SFU (Selective Forwarding Unit): N 명 회의 — server 가 routing. ## 💻 코드 패턴 ### 단순 1:1 (signaling 별도) ```ts // Caller const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'turn:turn.example.com', username: '...', credential: '...' }, ], }); // Local stream const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); stream.getTracks().forEach(t => pc.addTrack(t, stream)); // Remote pc.ontrack = (e) => { remoteVideo.srcObject = e.streams[0]; }; // ICE candidates pc.onicecandidate = (e) => { if (e.candidate) signal.send({ type: 'candidate', candidate: e.candidate }); }; // Offer const offer = await pc.createOffer(); await pc.setLocalDescription(offer); signal.send({ type: 'offer', sdp: offer }); // Receive answer signal.on('answer', async (msg) => { await pc.setRemoteDescription(msg.sdp); }); signal.on('candidate', async (msg) => { await pc.addIceCandidate(msg.candidate); }); ``` ```ts // Callee signal.on('offer', async (msg) => { await pc.setRemoteDescription(msg.sdp); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); signal.send({ type: 'answer', sdp: answer }); }); ``` ### Signaling (WebSocket) ```ts // 단순 server const rooms = new Map>(); wss.on('connection', (ws, req) => { const room = ...; rooms.get(room)!.add(ws); ws.on('message', (msg) => { for (const peer of rooms.get(room)!) { if (peer !== ws) peer.send(msg); } }); }); ``` ### Data channel (P2P data) ```ts const dc = pc.createDataChannel('chat'); dc.onopen = () => dc.send('hi'); dc.onmessage = (e) => console.log(e.data); // Receive side pc.ondatachannel = (e) => { const dc = e.channel; dc.onmessage = (ev) => console.log(ev.data); }; ``` → Server 거치지 않고 직접 — 빠름. File transfer / multiplayer. ### TURN server (necessary 30%) ```bash # coturn 설치 docker run -p 3478:3478 -p 3478:3478/udp \ -p 5349:5349 -p 5349:5349/udp \ coturn/coturn -n --realm=acme.com \ --user=test:password ``` → NAT / firewall 너머 P2P 안 되면 relay. 비용: ~$0.10/GB. ### SFU (multi-party) — LiveKit ```ts import { Room, RoomEvent } from 'livekit-client'; const room = new Room(); await room.connect('wss://livekit.example.com', token); await room.localParticipant.enableCameraAndMicrophone(); room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => { if (track.kind === 'video') { const el = track.attach(); document.getElementById('videos')!.appendChild(el); } }); ``` → N 명 회의 = 각 client 가 N-1 stream 받는 mesh 비효율 → SFU 가 한 번 받고 routing. ### Daily / 100ms / Vonage - 매니지드 SFU + UI components. - Production 빠른 시작. ### Quality control ```ts // Bandwidth const sender = pc.getSenders().find(s => s.track?.kind === 'video'); const params = sender!.getParameters(); params.encodings = [{ maxBitrate: 1_000_000 }]; await sender!.setParameters(params); // Resolution await stream.getVideoTracks()[0].applyConstraints({ width: { max: 1280 }, height: { max: 720 }, frameRate: { max: 30 }, }); ``` ### Echo cancellation / noise ```ts const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); ``` ### Stats ```ts const stats = await pc.getStats(); stats.forEach(report => { if (report.type === 'inbound-rtp') { console.log('jitter:', report.jitter, 'loss:', report.packetsLost); } }); ``` ### Screen share ```ts const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); const sender = pc.addTrack(stream.getVideoTracks()[0], stream); ``` ### Recording ```ts const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); const chunks: Blob[] = []; recorder.ondataavailable = (e) => chunks.push(e.data); recorder.start(1000); // ... recorder.stop(); const blob = new Blob(chunks, { type: 'video/webm' }); ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 1:1 video | P2P + STUN + TURN fallback | | Multi-party | SFU (LiveKit / Daily / 100ms) | | Live streaming 1→N | RTMP → HLS / WHIP+WebRTC | | Game multiplayer | DataChannel (low latency) | | File transfer | DataChannel | | 통화 (PSTN) | Twilio / Voice API | ## ❌ 안티패턴 - **TURN 없음 prod**: 30% 사용자 연결 불가. - **N×N mesh + 5명+**: 각 client = N-1 upload. SFU. - **Bandwidth 무제한**: 사용자 회선 죽음. - **Echo cancellation 끔**: 짖는 소리. - **Signaling 안전 X**: room ID 추측 → 침입. - **Recording 사용자 모름**: 법적 문제. consent. - **Stats 무시 prod**: quality 추적 안 됨. ## 🤖 LLM 활용 힌트 - 1:1 = P2P + coturn TURN. - 다인 = LiveKit / Daily managed. - Bandwidth + echo + stats. ## 🔗 관련 문서 - [[AI_Voice_Agent_Realtime]] - [[Backend_WebSocket_Scaling]] - [[Web_Service_Worker_Patterns]]