Files
2nd/10_Wiki/Topics/Coding/Web_WebRTC_Realtime.md
T
2026-05-09 21:08:02 +09:00

218 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<string, Set<WebSocket>>();
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]]