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

5.6 KiB
Raw Blame History

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
web-webrtc-realtime WebRTC — Peer / TURN / Data Channel Coding draft B conceptual 2026-05-09 2026-05-09
web
webrtc
realtime
vibe-coding
language applicable_to
TS / WebRTC
Frontend
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 별도)

// 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);
});
// 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)

// 단순 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)

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%)

# 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

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

// 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

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
});

Stats

const stats = await pc.getStats();
stats.forEach(report => {
  if (report.type === 'inbound-rtp') {
    console.log('jitter:', report.jitter, 'loss:', report.packetsLost);
  }
});

Screen share

const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
const sender = pc.addTrack(stream.getVideoTracks()[0], stream);

Recording

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.

🔗 관련 문서