WebRTC Deep Dive

Phân tích toàn bộ WebRTC protocol từ lớp network đến JavaScript API — dành cho bạn muốn hiểu tại sao chứ không chỉ biết cách dùng. Tài liệu offline, không cần internet.

Toàn bộ quá trình WebRTC — từ lúc bắt đầu đến khi media chạy (click vào từng phase)

1. getUserMedia Camera / Mic Video File 2. SDP Negotiation Offer / Answer Signaling Server 3. ICE Gathering host / srflx / relay STUN / TURN 4. ICE Checks Hole Punching Nomination 5. DTLS Handshake Key Exchange 6. SRTP Stream ✓ Encrypted Video P2P Direct ← click từng phase để đọc chi tiết →
01 WebRTC là gì

WebRTC (Web Real-Time Communication) là tập hợp các API và protocol mở cho phép trình duyệt trao đổi audio, video, và dữ liệu tùy ý trực tiếp với nhau — không cần plugin, không cần server trung gian cho media. Được chuẩn hóa bởi W3C và IETF, WebRTC được Chrome, Firefox, Safari, Edge hỗ trợ đầy đủ.

Kiến trúc tổng thể

JS API
RTCPeerConnection · RTCDataChannel · MediaStream · getUserMedia()
Media Engine
SRTP/SRTCP — mã hóa audio/video  |  RTCP — feedback, congestion control
Data Engine
SCTP — DataChannel transport, multiplexing nhiều stream
Security
DTLS 1.2/1.3 — TLS over UDP, thiết lập session key, xác thực certificate qua fingerprint
Transport
ICE — chọn đường tốt nhất  |  STUN — discover public IP  |  TURN — relay fallback
Network
UDP (ưu tiên) · TCP fallback · IPv4 · IPv6

Tại sao WebRTC phức tạp?

Ba thách thức cốt lõi:

  1. NAT traversal — hầu hết thiết bị không có IP công khai, nằm sau NAT. WebRTC phải "đục lỗ" qua NAT để 2 peer tìm thấy nhau.
  2. Bảo mật bắt buộc — DTLS và SRTP không thể tắt. Mọi kết nối WebRTC đều phải mã hóa. Đây là yêu cầu của spec, không phải tùy chọn.
  3. Codec negotiation — mỗi browser/thiết bị hỗ trợ codec khác nhau. SDP offer/answer là quá trình "thương lượng" codec, resolution, bitrate.
Điểm mấu chốt
Server chỉ cần thiết trong giai đoạn signaling (trao đổi SDP + ICE candidates). Sau khi kết nối P2P thiết lập xong, media stream đi trực tiếp giữa 2 peer. Server signaling có thể đóng kết nối mà không ảnh hưởng đến stream.
02 NAT & vấn đề nó tạo ra

NAT (Network Address Translation) là kỹ thuật cho phép nhiều thiết bị trong mạng LAN chia sẻ một IP công khai. Router NAT duy trì một bảng ánh xạ (private IP, private port) ↔ (public IP, public port).

Tại sao NAT chặn WebRTC

Hai peer đằng sau NAT — không thể kết nối trực tiếp nếu không có STUN/ICE

STUN Server stun.l.google.com:19302 Router A (NAT) public: 1.2.3.4 Router B (NAT) public: 5.6.7.8 Internet public routing Peer A / Drone private: 192.168.1.5 public: ??? Peer B / Ground private: 10.0.0.8 public: ??? STUN req srflx: 1.2.3.4 STUN req srflx: 5.6.7.8 ✗ Direct không được — Router B không có NAT entry ✓ ICE hole punching: cả 2 peer gửi đồng thời → tạo NAT entry 2 phía → kết nối được

4 loại NAT và khả năng traversal

Loại NATQuy tắc inboundICE vượt được?
Full Cone Bất kỳ ai biết public IP:port đều gửi được vào Có — dùng srflx
Restricted Cone Chỉ IP đã từng gửi outbound mới vào được Có — ICE tạo "hole" trước
Port Restricted Chỉ IP:port đã từng gửi outbound mới vào được Có — ICE tạo "hole" trước
Symmetric Mỗi đích đến nhận port ngoại khác nhau; không đoán được port Khó — cần TURN relay
Thực tế production
Mạng doanh nghiệp, carrier-grade NAT (CGN), và 4G/5G thường dùng Symmetric NAT. Đây là lý do hệ thống WebRTC production luôn cần TURN server làm fallback. Tỷ lệ kết nối thất bại mà không có TURN có thể lên đến 15–20%.

Hole Punching — kỹ thuật vượt NAT

ICE dùng kỹ thuật hole punching: cả 2 peer gửi packet đến nhau đồng thời. Packet outbound từ A tạo entry trong NAT table của Router A, cho phép packet từ B qua Router A đến A.

Ví dụ từng bước — Port Restricted Cone NAT (phổ biến nhất)

Peer A 192.168.1.5:5000 Router A 1.2.3.4 : port? Internet Router B 5.6.7.8 : port? Peer B 10.0.0.8:6000 Bước 0 — Trạng thái ban đầu: NAT table trống, 2 peer không biết port của nhau NAT Table A src → dst : mapping (trống) NAT Table B src → dst : mapping (trống) ✗ A muốn gửi tới 5.6.7.8 nhưng không biết port nào — Router B sẽ DROP vì không có NAT entry gửi tới 5.6.7.8:??? — DROP ✗ Bước 1 — A và B gửi STUN request đồng thời (ICE đã biết srflx của nhau qua signaling) STUN req → tới 5.6.7.8:srflx-port ← STUN req tới 1.2.3.4:srflx-port → Router A tạo entry: 192.168.1.5:5000 ↔ 1.2.3.4:48291 (ánh xạ outbound tới B) → Router B tạo entry: 10.0.0.8:6000 ↔ 5.6.7.8:62100 (ánh xạ outbound tới A) Bước 2 — Packets chéo nhau, NAT entries tồn tại → cả 2 chiều mở A → B: Router B có entry → FORWARD ✓ B → A: Router A có entry → FORWARD ✓ ✓ Hole punched — STUN Binding Response thành công → ICE valid pair → P2P connected
Tại sao phải "đồng thời"?
Nếu A gửi trước và B chưa gửi, packet của A đến Router B nhưng Router B chưa có entry cho IP của A → DROP. B phải gửi ra trước để tạo entry ở Router B, tạo "cửa sổ" cho packet của A đi vào. ICE sử dụng STUN credentials (ice-ufrag/ice-pwd) để xác thực cả 2 phía đang trong cùng 1 session.
  Ví dụ thực tế — Port Restricted Cone NAT:

  Signaling đã trao đổi srflx:
    A biết B tại: 5.6.7.8:62100
    B biết A tại: 1.2.3.4:48291

  T=0ms  A gửi: src=1.2.3.4:48291  dst=5.6.7.8:62100
         Router A tạo: {192.168.1.5:5000 → 5.6.7.8:62100} ← NAT entry A
         Packet đến Router B → DROP (chưa có entry cho 1.2.3.4:48291)

  T=0ms  B gửi: src=5.6.7.8:62100  dst=1.2.3.4:48291
         Router B tạo: {10.0.0.8:6000 → 1.2.3.4:48291} ← NAT entry B
         Packet đến Router A → DROP (chưa có entry cho 5.6.7.8:62100)

  T=1ms  Packet của A đến Router B lần 2 (ICE retransmit)
         Router B tra bảng: có entry cho 1.2.3.4:48291 → FORWARD → Peer B nhận được ✓

  T=1ms  Packet của B đến Router A lần 2 (ICE retransmit)
         Router A tra bảng: có entry cho 5.6.7.8:62100 → FORWARD → Peer A nhận được ✓

  → STUN Binding Response gửi về → ICE marks pair as "valid" → Nomination → P2P ✓
03 ICE Framework

ICE (Interactive Connectivity Establishment — RFC 8445) là framework tổng hợp giải quyết bài toán NAT traversal. ICE không phải một protocol đơn lẻ mà là quy trình phối hợp STUN, TURN, và hole punching để tìm đường kết nối tốt nhất.

Ba giai đoạn ICE

Giai đoạn 1 — Candidate Gathering

Mỗi peer thu thập tất cả địa chỉ có thể kết nối — gọi là candidates:

host 192.168.1.5:52341 — IP LAN trực tiếp. Lấy từ network interface. Nhanh nhất, chỉ dùng được trong LAN.
srflx 203.0.113.1:48291 — Server Reflexive. IP công khai được STUN server trả về. Dùng để vượt Full/Restricted Cone NAT.
relay turn.example.com:3478 — Relay. Địa chỉ TURN server sẽ forward thay. Luôn hoạt động, latency cao nhất, tốn băng thông server.

Mỗi candidate có priority số — host > srflx > relay. ICE thử candidates theo thứ tự priority giảm dần.

Ba loại ICE candidate — priority từ cao xuống thấp, và ma trận candidate pairs

Priority (cao → thấp) host 192.168.1.5:52341 IP LAN trực tiếp priority ≈ 2,122,194,943 Nhanh nhất · LAN only srflx 203.0.113.1:48291 IP công khai (STUN reflect) priority ≈ 1,686,052,607 Vượt được Full/Restricted NAT relay turn.example.com:3478 Địa chỉ TURN server priority ≈ 16,777,215 Luôn hoạt động · latency cao Candidate Pair Matrix (A × B) — ICE thử theo thứ tự ưu tiên: B:host B:srflx B:relay A:host A:srflx A:relay ① Highest priority ⑨ Lowest

Giai đoạn 2 — Connectivity Checks

Hai peer trao đổi candidate lists qua signaling, sau đó tạo tất cả candidate pairs (candidate của A × candidate của B). Mỗi pair được check bằng STUN Binding Request theo cả 2 chiều.

  Peer A candidates: [host-A, srflx-A, relay-A]
  Peer B candidates: [host-B, srflx-B, relay-B]

  Candidate pairs (sắp theo priority):
  1. host-A   ↔ host-B    (ưu tiên cao nhất)
  2. host-A   ↔ srflx-B
  3. srflx-A  ↔ host-B
  4. host-A   ↔ relay-B
  5. srflx-A  ↔ srflx-B
  ...

  Mỗi pair: A gửi STUN request → B, B gửi STUN request → A
  Pair nào nhận được response hợp lệ → "valid pair"

Giai đoạn 3 — Nomination

Controlling peer (bên tạo offer) chọn valid pair tốt nhất và gửi STUN request với flag USE-CANDIDATE. Đây là "nominated pair" — traffic chuyển sang pair này.

Trickle ICE — tại sao quan trọng

Trickle ICE cho phép gửi candidates ngay khi tìm thấy, không cần chờ gather xong tất cả. Điều này giảm thời gian kết nối đáng kể vì:

Không có Trickle (slow)

  1. Gather tất cả candidates (có thể mất 2–5s)
  2. Gửi toàn bộ qua signaling
  3. Bắt đầu checks

Trickle ICE (fast)

  1. Tìm được host candidate → gửi ngay
  2. Check bắt đầu ngay, song song với gathering
  3. Thường connected trước khi gather xong
Browser hiện đại
Chrome, Firefox, Safari đều dùng Trickle ICE mặc định. SDP sẽ có a=ice-options:trickle. Candidates được gửi qua signaling channel dưới dạng các message riêng biệt, không cần gửi lại SDP.

ICE Restart

Khi kết nối bị disconnected (ví dụ đổi WiFi sang 4G), ICE có thể restart bằng cách tạo offer mới với iceRestart: true. Credentials ICE mới được tạo, quá trình gathering/checking lặp lại. Media stream không bị gián đoạn nếu restart thành công đủ nhanh.

04 STUN — Session Traversal Utilities for NAT

STUN (RFC 8489) là protocol đơn giản: client gửi request đến STUN server, server trả về IP:port mà server thấy — tức là IP:port công khai sau NAT.

  [Peer A]                         [STUN Server]
  private: 192.168.1.5:52341
  public: ???
       |                                |
       |-- STUN Binding Request ------->|
       |                                |   Server thấy packet đến từ:
       |<-- STUN Binding Response ------|   203.0.113.1:48291
       |    XOR-MAPPED-ADDRESS:         |
       |    203.0.113.1:48291           |
       |                                |
  → Peer A biết public IP:port của mình là 203.0.113.1:48291
  → Tạo srflx candidate: "candidate:2 1 UDP ... 203.0.113.1 48291 typ srflx"

STUN Message Format

FieldSizeMô tả
Message Type2 bytesBinding Request (0x0001) hoặc Binding Response (0x0101)
Message Length2 bytesĐộ dài phần attributes
Magic Cookie4 bytesCố định: 0x2112A442 — dùng để phân biệt STUN với RTP
Transaction ID12 bytesRandom ID để match request với response
AttributesvariableKey-value: XOR-MAPPED-ADDRESS, USERNAME, MESSAGE-INTEGRITY...

STUN cho ICE Connectivity Checks

STUN không chỉ dùng để discover IP. Trong ICE, STUN Binding Request còn là cơ chế connectivity check — peer gửi STUN request có chứa USERNAME (ice-ufrag kết hợp) và MESSAGE-INTEGRITY (HMAC-SHA1). Điều này vừa verify kết nối, vừa authenticate peer đang kết nối.

STUN server miễn phí
stun:stun.l.google.com:19302 — Google cung cấp free cho development. Production nên tự host hoặc dùng dịch vụ như Twilio / Cloudflare Calls để có SLA.
05 TURN — Traversal Using Relays around NAT

TURN (RFC 8656) là fallback cuối cùng khi direct P2P và STUN-based connection đều thất bại. TURN server đứng giữa, relay toàn bộ traffic. Peer gửi data đến TURN, TURN forward đến peer kia.

  [Peer A]                [TURN Server]               [Peer B]
      |                        |                           |
      |-- Allocate Request --→ |                           |
      |←- Allocate Response -- |  relay addr: 1.2.3.4:50000|
      |-- CreatePermission --> |  (allow B to send)        |
      |                        |←-- Connect/Send from B --|
      |←- Data from B ---------|                           |
      |-- Send to B --------→ |-- Forward to B ----------→|

  Mọi packet đều đi qua TURN server.
  Không còn P2P, nhưng luôn hoạt động kể cả Symmetric NAT.

Khi nào cần TURN?

  • Một hoặc cả hai peer đằng sau Symmetric NAT
  • Mạng doanh nghiệp có firewall chặn UDP outbound
  • Carrier-grade NAT (CGN) trên mạng mobile 4G/5G

Chi phí TURN

Latency

Thêm 1 hop → tăng RTT. Nếu TURN server đặt gần người dùng, overhead chỉ ~10–30ms thêm vào.

Bandwidth

TURN server phải xử lý toàn bộ traffic media. 1 video call 1080p30 ≈ 3–5 Mbps qua TURN.

TURN với DTLS
TURN chỉ relay packets — nó không thể đọc nội dung vì DTLS/SRTP vẫn được mã hóa end-to-end. TURN server chỉ thấy encrypted bytes. Đây là lý do WebRTC an toàn ngay cả khi buộc phải dùng TURN.

Cấu hình ICEServer với TURN

  {
    iceServers: [
      { urls: "stun:stun.l.google.com:19302" },
      {
        urls: "turn:turn.example.com:3478",
        username: "user",
        credential: "password"
      },
      {
        urls: "turns:turn.example.com:5349",  // TURN over TLS (TCP)
        username: "user",
        credential: "password"
      }
    ]
  }
06 SDP Anatomy — Session Description Protocol

SDP (RFC 8866) là định dạng text mô tả "tôi có thể làm gì" — không phải protocol truyền tải, chỉ là metadata được trao đổi qua signaling. SDP dùng format type=value, mỗi dòng 1 attribute.

SDP Offer thực tế — annotated

v=0← version, luôn là 0
o=- 8447722553217982387 2 IN IP4 127.0.0.1← origin: session ID, version
s=-← session name (unused)
t=0 0← timing: 0 0 = không có thời hạn
a=group:BUNDLE 0 1← gộp audio + video vào 1 ICE connection
a=msid-semantic: WMS stream1← WebRTC MediaStream semantic
 
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99← media: video, payload types 96-99
c=IN IP4 0.0.0.0← connection (overridden bởi ICE candidates)
a=ice-ufrag:xKkPaZ3m← ICE username fragment (8 ký tự, dùng authentication)
a=ice-pwd:bZ4tMnop7QrsXyz123456789← ICE password (22+ ký tự)
a=ice-options:trickle← hỗ trợ gửi candidates từng cái (Trickle ICE)
a=fingerprint:sha-256 4A:3B:...← hash của DTLS cert để xác thực peer
a=setup:actpass← "tôi có thể làm cả DTLS client lẫn server"
a=mid:0← media ID (dùng với BUNDLE group)
a=sendrecv← direction: gửi VÀ nhận (alternatives: sendonly, recvonly, inactive)
a=rtcp-mux← gộp RTP và RTCP vào cùng 1 port
a=rtpmap:96 VP8/90000← payload type 96 = VP8, clock rate 90kHz
a=rtpmap:97 rtx/90000← payload type 97 = RTX (retransmission cho VP8)
a=fmtp:97 apt=96← RTX liên kết với VP8 (payload 96)
a=rtpmap:98 H264/90000← payload type 98 = H264
a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f← H264 profile: Baseline, Level 3.1
a=ssrc:3421698765 cname:abc123def456← SSRC: định danh track, CNAME: canonical name
 
m=audio 9 UDP/TLS/RTP/SAVPF 111← audio section
a=rtpmap:111 opus/48000/2← Opus, 48kHz, stereo
a=fmtp:111 minptime=10;useinbandfec=1← Opus FEC (forward error correction)

Offer/Answer Model

SDP negotiation dùng mô hình Offer/Answer (RFC 3264):

  1. Peer A gọi createOffer() → browser tạo SDP liệt kê tất cả capability
  2. Peer A gọi setLocalDescription(offer) → ICE gathering bắt đầu
  3. Peer A gửi offer qua signaling đến Peer B
  4. Peer B gọi setRemoteDescription(offer)
  5. Peer B gọi createAnswer() → chọn subset capability từ offer
  6. Peer B gọi setLocalDescription(answer)
  7. Peer B gửi answer về Peer A
  8. Peer A gọi setRemoteDescription(answer) → negotiation hoàn tất
Lỗi phổ biến
Thứ tự gọi API phải đúng: setLocalDescription trước khi ICE gathering, setRemoteDescription trước khi createAnswer. Sai thứ tự → InvalidStateError.
07 Signaling — Thứ WebRTC KHÔNG định nghĩa

Signaling là quá trình 2 peer trao đổi thông tin để thiết lập kết nối — cụ thể là SDP offer/answerICE candidates. WebRTC chủ động không định nghĩa cơ chế signaling.

Tại sao không định nghĩa signaling?
Vì signaling chỉ cần truyền text — bất kỳ channel nào cũng làm được: WebSocket, HTTP, XMPP, SIP, QR code, copy-paste tay. WebRTC để ngỏ để phù hợp với mọi use case.

Signaling Sequence Diagram — toàn bộ flow

Từ WebSocket connect đến P2P video stream — mọi message theo đúng thứ tự thực tế

Drone / Peer A drone.html Signaling Server server.js WebSocket Ground Station ground-station.html ① WebSocket Connect connect WS connect WS ② SDP Offer — createOffer() → setLocalDescription() createOffer() {type:"offer", sdp:…} relay offer → ③ SDP Answer — setRemoteDescription() → createAnswer() createAnswer() {type:"answer", sdp:…} ← relay answer ④ Trickle ICE Candidates (đồng thời 2 chiều, qua server relay) candidate: host (192.168.x.x) candidate: host (10.0.0.x) candidate: srflx (1.2.3.4) candidate: srflx (5.6.7.8) ⑤ ICE Connectivity Checks — trực tiếp P2P, không qua server STUN Binding Request (hole punching) STUN Binding Response ✓ ICE Connected — nominated pair ⑥ DTLS Handshake — P2P, server không nhìn thấy ClientHello → ServerHello → Certificate → KeyExchange… ✓ DTLS Channel established — SRTP keys derived ⑦ Media Stream — P2P trực tiếp, signaling server không liên quan ══ Encrypted Video Stream (SRTP) ══▶ ─ ─ DataChannel: chat + file transfer ─ ─▶ Server chỉ relay SDP + ICE, không thấy media ✗

Demo App Architecture

Kiến trúc hai giai đoạn của ứng dụng demo

Phase 1 — Loopback (index.html, zero-dependency) Browser Tab (1 file HTML) RTCPeerConnection A getUserMedia() → tracks JS signaling (function calls) RTCPeerConnection B ontrack → <video> loopback ICE / SRTP media Phase 2 — Drone Streaming (Node.js + 2 tabs) drone.html 📷 Webcam / Video File captureStream() addTrack() → RTCPeerConn DataChannel initiator sendonly + uplink stats server.js Node.js http + ws@8 Serve static HTML files WebSocket signaling relay Forward SDP + ICE only 30 lines of code ground-station.html ontrack → <video> display Text chat (DataChannel) File receive + download getStats() panel 1s recvonly + downlink stats SDP/ICE SDP/ICE SRTP video stream (P2P — bypasses server)

Signaling chỉ truyền 2 loại data

SDP Offer / Answer

Chuỗi text mô tả capability. Gửi 1 lần duy nhất khi bắt đầu negotiation.

{
  "type": "offer",
  "sdp": "v=0\r\no=-..."
}

ICE Candidates

Địa chỉ mạng tìm được. Gửi từng cái khi tìm thấy (Trickle ICE).

{
  "type": "ice",
  "candidate": {
    "candidate": "candidate:...",
    "sdpMid": "0"
  }
}

Signaling server tối giản — WebSocket relay

// server.js — 30 dòng là đủ
const http = require('http');
const { WebSocketServer } = require('ws');

const server = http.createServer((req, res) => {
  // serve static files
});

const wss = new WebSocketServer({ server });
const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);
  ws.on('message', (data) => {
    // forward message đến tất cả clients còn lại
    for (const client of clients) {
      if (client !== ws && client.readyState === 1) {
        client.send(data);
      }
    }
  });
  ws.on('close', () => clients.delete(ws));
});

server.listen(3000);

Sau khi signaling xong

Khi iceConnectionState === "connected", media flow đi P2P. Signaling server không liên quan đến media nữa. Có thể đóng WebSocket connection mà video stream vẫn chạy bình thường.

08 DTLS Handshake

DTLS (Datagram TLS — RFC 6347) là TLS chạy trên UDP. Sau khi ICE tìm được candidate pair, DTLS handshake thiết lập channel mã hóa và xác thực 2 peer với nhau.

DTLS vs TLS

TLSDTLS
TransportTCP (reliable)UDP (unreliable)
RetransmissionTCP tự handleDTLS tự retransmit handshake packets
Record layerSequentialCó epoch + sequence number để handle reorder
LatencyCao hơn (TCP 3-way handshake)Thấp hơn (UDP)

DTLS Handshake Flow

  Peer A (DTLS Client)              Peer B (DTLS Server)
       |                                    |
       |--- ClientHello ------------------>|  (cipher suites, random)
       |<-- HelloVerifyRequest ------------|  (cookie, tránh DoS)
       |--- ClientHello (with cookie) ---->|
       |<-- ServerHello --------------------|  (chosen cipher)
       |<-- Certificate --------------------|  (self-signed cert)
       |<-- ServerKeyExchange --------------|  (ECDH params)
       |<-- CertificateRequest ------------|
       |<-- ServerHelloDone ----------------|
       |--- Certificate ------------------->|  (Peer A's self-signed cert)
       |--- ClientKeyExchange ------------->|  (ECDH public key)
       |--- CertificateVerify ------------->|  (signature với private key)
       |--- ChangeCipherSpec -------------->|
       |--- Finished ---------------------->|  (HMAC của toàn bộ handshake)
       |<-- ChangeCipherSpec ---------------|
       |<-- Finished -----------------------|
       |                                    |
       |====== DTLS Channel Established =====|

Xác thực Certificate qua Fingerprint

Browser tự tạo self-signed certificate cho mỗi RTCPeerConnection — không cần CA (Certificate Authority). Certificate được xác thực qua fingerprint trong SDP:

  1. Peer A tạo self-signed cert, hash nó → fingerprint
  2. Đặt fingerprint vào SDP: a=fingerprint:sha-256 4A:3B:...
  3. SDP được gửi qua signaling channel (WebSocket — có thể trust)
  4. Trong DTLS handshake, Peer A gửi cert
  5. Peer B verify: hash của cert nhận được phải khớp fingerprint trong SDP
  6. Nếu không khớp → reject kết nối (man-in-the-middle attack)
Tại sao an toàn
Kẻ tấn công có thể relay ICE traffic nhưng không thể giả mạo cert vì không biết private key. Fingerprint trong SDP được gửi qua kênh signaling mà attacker không control → MITM không thể thành công.
09 SRTP — Secure Real-time Transport Protocol

Media (audio/video) không đi qua DTLS channel mà đi qua SRTP (RFC 3711). Lý do: DTLS record layer có overhead lớn, không phù hợp cho hàng nghìn packets/giây của video.

DTLS-SRTP Key Derivation

Keys cho SRTP được derive từ DTLS handshake qua cơ chế DTLS-SRTP (RFC 5764):

  DTLS Master Secret
       |
       ↓ DTLS-SRTP key derivation (RFC 5705 exporter)
       |
  ┌────┴────────────────────────────────┐
  │  Client SRTP Master Key  (16 bytes) │
  │  Server SRTP Master Key  (16 bytes) │
  │  Client SRTP Master Salt (14 bytes) │
  │  Server SRTP Master Salt (14 bytes) │
  └─────────────────────────────────────┘
       |
       ↓ KDF (AES Counter Mode)
       |
  ┌────┴──────────────────────────────────────┐
  │  Cipher Key   (AES-128-CM hoặc AES-256-CM) │
  │  Authentication Key  (HMAC-SHA1, 160-bit)  │
  │  Salting Key  (thêm vào IV)                │
  └────────────────────────────────────────────┘

SRTP Packet Structure

  ┌─────────────────────────────────────────────────────┐
  │  RTP Header (12+ bytes) — KHÔNG MÃ HÓA              │
  │  [V=2][P][X][CC][M][PT][Sequence Number][Timestamp] │
  │  [SSRC] [CSRC list]                                 │
  ├─────────────────────────────────────────────────────┤
  │  RTP Payload — MÃ HÓA (AES-128-CTR)                 │
  │  (H264 NAL units, VP8 frames, Opus frames...)       │
  ├─────────────────────────────────────────────────────┤
  │  Authentication Tag (HMAC-SHA1, 80-bit) — THÊM VÀO  │
  └─────────────────────────────────────────────────────┘
  • Sequence Number trong header không mã hóa → receiver có thể detect reorder/replay
  • Payload mã hóa bằng AES-CTR → TURN server không đọc được nội dung
  • Auth Tag verify integrity — detect packet tampering

SRTP vs DTLS cho Media

Nếu dùng DTLS cho media

  • Record layer overhead: ~13 bytes/packet
  • Phải track sequence riêng
  • Fragmentation phức tạp
  • ~1000 packets/s × 13 bytes = 13 KB/s overhead không cần thiết

DTLS-SRTP (thực tế)

  • DTLS chỉ dùng để exchange keys 1 lần
  • SRTP dùng RTP header sẵn có
  • Overhead chỉ thêm auth tag 10 bytes
  • Latency thấp, throughput cao
10 Codec Negotiation

Codec negotiation xác định ngôn ngữ chung giữa 2 peer. Offerer liệt kê tất cả codec hỗ trợ theo thứ tự ưu tiên. Answerer chọn codec đầu tiên trong danh sách mà cả hai đều hỗ trợ.

Video Codecs

H264 / AVC
Video
Support: Chrome, Firefox, Safari, Edge
Hardware enc: Rộng rãi nhất
Latency: Thấp
Drone use: Tốt nhất — camera hardware H264
VP8
Video
Support: Chrome, Firefox, Safari
Hardware enc: Ít hơn H264
Chất lượng: Tốt ở bitrate thấp
License: Royalty-free
VP9
Video
Support: Chrome, Firefox
Nén: ~50% tốt hơn VP8
CPU: Cao hơn VP8
Safari: Hạn chế
AV1
Video
Support: Chrome 90+, Firefox
Nén: Tốt nhất hiện tại
CPU: Rất cao
WebRTC: Thêm gần đây

Audio Codecs

CodecBitrateĐặc điểm
Opus6–510 kbpsBắt buộc trong WebRTC. Adaptive bitrate, FEC, DTX. Tốt nhất cho mọi use case.
G.711 (PCMU/PCMA)64 kbpsLegacy, dùng cho interop với SIP/PSTN.
G.72264 kbpsWideband, chất lượng tốt hơn G.711.

Force codec trong JavaScript

  // Ưu tiên H264 cho drone streaming
  async function preferH264(pc) {
    const offer = await pc.createOffer();
    const sdp = offer.sdp.replace(
      /m=video (\d+) UDP\/TLS\/RTP\/SAVPF ([\d ]+)/,
      (match, port, payloads) => {
        const lines = offer.sdp.split('\n');
        // tìm payload type của H264
        const h264 = lines
          .filter(l => l.includes('H264'))
          .map(l => l.match(/a=rtpmap:(\d+)/)?.[1])
          .filter(Boolean);
        const rest = payloads.split(' ').filter(p => !h264.includes(p));
        return `m=video ${port} UDP/TLS/RTP/SAVPF ${[...h264, ...rest].join(' ')}`;
      }
    );
    await pc.setLocalDescription({ type: 'offer', sdp });
  }
Drone streaming recommendation
Ưu tiên H264 Baseline Profile (profile-level-id=42e01f). Hầu hết camera module (Raspberry Pi, GoPro, DJI) có hardware H264 encoder, zero CPU overhead. VP8/VP9 cần software encoding, tốn CPU pin của drone.
11 DataChannel — SCTP over DTLS

DataChannel cho phép truyền arbitrary data (text, binary) qua WebRTC. Không dùng RTP — thay vào đó dùng SCTP (RFC 4960) chạy trên DTLS channel.

SCTP vs TCP vs UDP

Tính năngTCPUDPSCTP (DataChannel)
Ordered deliveryLuôn cóKhôngTùy chọn
Reliable deliveryLuôn cóKhôngTùy chọn
Multi-streamKhôngKhông — nhiều channels, không block nhau
Head-of-line blockingCó (giữa messages)KhôngKhông (giữa channels)
Partial reliabilityKhôngKhông — maxRetransmits / maxPacketLifeTime
Message boundariesStream, không có boundary — message framing

Các chế độ DataChannel

  // Reliable + Ordered (mặc định) — dùng cho: chat, commands, file transfer
  const channel = pc.createDataChannel("chat");

  // Unreliable + Unordered — dùng cho: game state, telemetry (giống UDP)
  const channel = pc.createDataChannel("telemetry", {
    ordered: false,
    maxRetransmits: 0
  });

  // Reliable nhưng có timeout — dùng cho: sensor data có deadline
  const channel = pc.createDataChannel("sensor", {
    ordered: false,
    maxPacketLifeTime: 100  // ms — discard nếu chưa gửi được sau 100ms
  });

File Transfer qua DataChannel

  // Sender
  async function sendFile(channel, file) {
    const CHUNK = 16384; // 16KB — max chunk size an toàn
    channel.send(JSON.stringify({ type:'file-start', name:file.name, size:file.size }));

    const buffer = await file.arrayBuffer();
    for (let offset = 0; offset < buffer.byteLength; offset += CHUNK) {
      // throttle nếu buffer đầy
      if (channel.bufferedAmount > channel.bufferedAmountLowThreshold) {
        await new Promise(r => channel.onbufferedamountlow = r);
      }
      channel.send(buffer.slice(offset, offset + CHUNK));
    }
    channel.send(JSON.stringify({ type:'file-end' }));
  }

  // Receiver
  let chunks = [], meta = null;
  channel.onmessage = (e) => {
    if (typeof e.data === 'string') {
      const msg = JSON.parse(e.data);
      if (msg.type === 'file-start') meta = msg;
      if (msg.type === 'file-end') {
        const blob = new Blob(chunks);
        const url = URL.createObjectURL(blob);
        const a = Object.assign(document.createElement('a'), { href:url, download:meta.name });
        a.click();
        chunks = []; meta = null;
      }
    } else {
      chunks.push(e.data); // ArrayBuffer chunk
    }
  };
Lưu ý bufferedAmount
DataChannel có buffer nội bộ. Gửi quá nhanh mà không check bufferedAmount sẽ làm tràn buffer → browser crash hoặc data loss. Luôn throttle bằng bufferedAmountLowThreshold.
12 getStats() API — Connection Diagnostics

RTCPeerConnection.getStats() trả về Map của hàng chục RTCStats objects, mỗi object là một "report" về một khía cạnh của kết nối. Đây là công cụ chẩn đoán mạnh nhất của WebRTC.

Các report type quan trọng

Report TypeMetrics quan trọngDùng để
inbound-rtp bytesReceived, packetsLost, jitter, framesDecoded, framesPerSecond Tính bitrate nhận, packet loss %, framerate thực tế
outbound-rtp bytesSent, packetsSent, retransmittedPacketsSent, qualityLimitationReason Bitrate gửi, retransmit rate, lý do giảm chất lượng
candidate-pair currentRoundTripTime, availableOutgoingBitrate, bytesDiscardedOnSend RTT, estimated bandwidth, dropped packets
local-candidate candidateType, ip, port, protocol Xem đang dùng host/srflx/relay, UDP hay TCP
codec mimeType, clockRate, sdpFmtpLine Verify codec đang dùng (video/H264, audio/opus)
media-source frameWidth, frameHeight, framesPerSecond Resolution và fps của local camera/source

Tính bitrate thực tế

  let prevBytes = 0, prevTime = 0;

  setInterval(async () => {
    const stats = await pc.getStats();
    const now = Date.now();

    for (const [, report] of stats) {
      if (report.type === 'inbound-rtp' && report.kind === 'video') {
        const bytes = report.bytesReceived;
        const dt = (now - prevTime) / 1000; // seconds

        const bitrateKbps = ((bytes - prevBytes) * 8 / dt / 1000).toFixed(1);
        console.log(`Video bitrate: ${bitrateKbps} kbps`);
        console.log(`Packet loss: ${report.packetsLost}`);
        console.log(`Jitter: ${(report.jitter * 1000).toFixed(1)} ms`);
        console.log(`FPS: ${report.framesPerSecond}`);

        prevBytes = bytes;
        prevTime = now;
      }

      if (report.type === 'candidate-pair' && report.nominated) {
        console.log(`RTT: ${(report.currentRoundTripTime * 1000).toFixed(0)} ms`);
        console.log(`Available bandwidth: ${(report.availableOutgoingBitrate/1000).toFixed(0)} kbps`);
      }
    }
  }, 1000);

qualityLimitationReason — tại sao video giảm chất lượng

ValueNghĩa
"none"Đang gửi ở chất lượng tối đa
"bandwidth"Mạng không đủ — encoder giảm bitrate/resolution
"cpu"CPU không đủ — encoder giảm fps/resolution
"other"Lý do khác (ví dụ: encoder constraint)

Connection State Machine

RTCPeerConnection state machine — tất cả transitions và nguyên nhân

new RTCPeerConn created checking ICE candidates checking connected P2P link + DTLS ✓ disconnected connection lost / switch failed ICE / DTLS failed closed pc.close() called setLocalDesc ICE nominated network lost ICE restart timeout all candidates failed / Symmetric NAT pc.close()

Theo dõi bằng pc.oniceconnectionstatechangepc.onconnectionstatechange. Hai event này độc lập — ICE có thể connected trong khi DTLS vẫn đang handshake.

Debug tip
Chrome có built-in WebRTC debugger tại chrome://webrtc-internals — hiển thị toàn bộ getStats() theo real-time với biểu đồ. Công cụ chẩn đoán mạnh nhất để phân tích WebRTC issues trong production.

Các điểm cốt lõi cần nhớ

Bản chắt lọc từ 12 sections — những điểm hay bị nhầm, hay gặp trong interviews và hay phát sinh bug trong production WebRTC.

Connection Flow

  • Thứ tự: getUserMedia → createOffer → setLocalDescription → ICE gathering → signaling → setRemoteDescription → DTLS → SRTP
  • ICE gathering bắt đầu ngay khi setLocalDescription được gọi — không phải sau
  • ICE connected ≠ DTLS complete — hai state machine độc lập, dùng cả oniceconnectionstatechangeonconnectionstatechange
  • Trickle ICE: gửi candidates ngay khi gather được → giảm latency đáng kể vs batch

NAT & ICE

  • 4 loại NAT: Full Cone, Restricted, Port Restricted, Symmetric — chỉ Symmetric không thể hole-punch
  • Candidate priority: host > srflx > relay — ICE chọn pair có priority tổng cao nhất
  • Symmetric NAT → STUN không đủ → bắt buộc TURN relay
  • TURN credentials: dùng time-limited HMAC token (RFC 5766) — không expose static credentials
  • ICE restarts khi network thay đổi — pc.restartIce()

SDP

  • SDP = negotiate parameters, không truyền data — chỉ là text description
  • a=fingerprint trong SDP → verify DTLS certificate (chống MITM)
  • sdpMid + sdpMLineIndex: định vị candidate thuộc media line nào
  • Offer/answer phải match: codec PT, direction (sendrecv/sendonly/recvonly), ice-ufrag/ice-pwd
  • Unified Plan (mặc định): mỗi track = 1 m-line; Plan B (deprecated): nhiều tracks/m-line

DTLS & SRTP

  • DTLS chạy sau ICE connected, trước khi media flow — không bỏ qua được
  • DTLS fingerprint = SHA-256 của cert, so sánh với a=fingerprint trong SDP → chống MITM
  • SRTP key material được derive từ DTLS handshake (DTLS-SRTP, RFC 5763)
  • RTCP-MUX: RTP và RTCP dùng chung port → giảm số port cần mở qua NAT
  • Cipher suites: AES-128-GCM (mới) hoặc AES-128-CM-HMAC-SHA1-80 (legacy)

Codec & DataChannel

  • Opus bắt buộc cho audio (RFC 7874) — tất cả browsers support
  • Video codecs: VP8/VP9/H.264/AV1 — dùng RTCRtpSender.getCapabilities('video') để kiểm tra
  • DataChannel = SCTP trên DTLS — ordered: false + maxRetransmits: 0 cho unreliable (gaming)
  • DataChannel không cần media tracks — có thể dùng độc lập
  • Simulcast: gửi nhiều resolutions (rid: low/mid/high) → receiver chọn layer

Debugging & Stats

  • chrome://webrtc-internals — real-time graphs, ICE candidates, SDP history — công cụ mạnh nhất
  • RTCInboundRtpStreamStats: jitter, packetsLost, bytesReceived, framesDecoded
  • Packet loss = packetsLost / (packetsReceived + packetsLost) — >5% là degraded
  • RTCIceCandidatePairStats: currentRoundTripTime, availableOutgoingBitrate
  • Jitter target < 30ms cho audio — jitter buffer sẽ tăng delay khi jitter cao
Câu hỏi hay gặp trong interviews
  • Tại sao cần TURN server? — Symmetric NAT block hole-punching; TURN relay traffic qua server, đảm bảo kết nối được nhưng tốn bandwidth
  • SDP offer/answer exchange xảy ra ở đâu? — Qua signaling channel tùy chọn (WebSocket, HTTP) — WebRTC không định nghĩa signaling protocol
  • Khác nhau giữa ICE connected và connection state connected? — ICE connected = transport layer OK; connection state connected = ICE + DTLS đều xong
  • Tại sao DTLS fingerprint quan trọng? — Chống MITM attack: attacker có thể relay ICE nhưng không thể forge certificate khớp fingerprint trong SDP
  • DataChannel có thể dùng mà không cần video/audio không? — Có — createDataChannel() tạo RTCPeerConnection chỉ với SCTP transport, không cần addTrack()