[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user