WebRTC 生產實戰:信令架構、NAT穿透、SFU架構與10個常見陷阱

网络协议

那場掉線40%使用者的通話故障

「視訊通話全掛了。」儀表板顯示40%的連線失敗。4G使用者、企業防火牆後面的使用者、CGNAT地區的使用者——全部連不上。我們的單台TURN伺服器達到了瓶頸:10000個併發中繼連線,每個2 Mbps,頂著20 Gbps的流量。核心在悄悄丟棄UDP封包,因為接收緩衝區滿了。WebRTC的demo在localhost沒問題,但生產環境需要完全不同的認知。

我部署過四個WebRTC生產系統——從處理5000併發問診的遠距醫療平台,到支撐50000觀眾的直播服務。這是一份從零到一的實戰筆記。


核心概念速查

概念 角色 關鍵機制 生產影響
信令 在媒體傳輸前交換會話元資料 WebSocket/SSE/HTTP長輪詢 重連、訊息順序、房間狀態
ICE 互動式連線建立 STUN + TURN 候選位址 85%直連P2P,15%需要中繼
STUN 穿透NAT發現公網IP:Port 查詢公網STUN伺服器 對對稱型NAT無效
TURN 直連失敗時的媒體中繼 專用中繼伺服器 頻寬消耗大,每流約500 kbps
SDP 媒體能力協商 編解碼器、格式、ICE候選位址 Offer/Answer模型
SFU 選擇性轉發單元 路由流而不解碼 多人通話頻寬高效

信令架構:看不見的骨架

信令是WebRTC最被低估的部分。規範故意不定義它,意味著每個團隊都自己實作——多數都搞砸了。

生產信令伺服器(Node.js + Socket.IO)

// server/signaling.ts
import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import { Redis } from "ioredis";

const io = new Server(3000, {
  pingInterval: 10000,
  pingTimeout: 5000,
  connectTimeout: 10000,
});

// Redis 配接器支援水平擴展
const pubClient = new Redis({ host: "redis" });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));

io.on("connection", (socket) => {
  socket.on("join-room", async ({ roomId, userId }) => {
    // 檢查房間容量
    const count = await redis.scard(`room:${roomId}:participants`);
    if (count >= 12) {
      socket.emit("error", { message: "房間已滿" });
      return;
    }

    socket.join(roomId);
    await redis.sadd(`room:${roomId}:participants`, userId);

    // 通知其他參與者
    socket.to(roomId).emit("participant-joined", { userId });

    // 發送現有參與者列表
    const participants = await redis.smembers(`room:${roomId}:participants`);
    socket.emit("room-state", { participants });
  });

  // 轉發信令訊息
  socket.on("offer", ({ to, sdp }) => {
    io.to(to).emit("offer", { from: socket.id, sdp });
  });

  socket.on("answer", ({ to, sdp }) => {
    io.to(to).emit("answer", { from: socket.id, sdp });
  });

  socket.on("ice-candidate", ({ to, candidate }) => {
    io.to(to).emit("ice-candidate", { from: socket.id, candidate });
  });

  socket.on("disconnect", async () => {
    const rooms = Array.from(socket.rooms);
    for (const roomId of rooms) {
      if (roomId !== socket.id) {
        await redis.srem(`room:${roomId}:participants`, socket.id);
        socket.to(roomId).emit("participant-left", { userId: socket.id });
      }
    }
  });
});

信令坑點

  1. 訊息順序很重要:如果ICE候選在SDP answer之前到達,瀏覽器會忽略它。需要實作緩衝。
  2. 重連:Socket.IO處理傳輸層重連,但你需要應用層的信令狀態恢復。把待處理操作存在Redis裡。
  3. 過期狀態:參與者異常斷連而不走正常離開流程,成員記錄會殘留。用TTL過期的Redis key管理房間成員。

NAT穿透:殘酷的現實

NAT為什麼阻斷P2P

沒NAT:
  主機A (1.2.3.4:50000) ←──────────→ 主機B (5.6.7.8:60000)
  直接UDP正常工作。

有NAT:
  主機A (192.168.1.5:50000) ──NAT──▶ 公網 (1.2.3.4:12345)
  主機B (10.0.0.3:60000)    ──NAT──▶ 公網 (5.6.7.8:54321)
  
  A發包到5.6.7.8:54321 → NAT B丟棄(沒有映射記錄)

NAT只允許來自內網主機最近聯繫過的位址的入站包。

ICE過程逐步解析

1. 收集候選位址
   └─ Host: 192.168.1.5:50000(本地,基本不可用)
   └─ SRFLX(STUN): 1.2.3.4:12345(反射位址)
   └─ RELAY(TURN): turn.example.com:3478(中繼,永遠可用)

2. 按優先級排序並交換

3. 連通性檢查
   └─ 逐對嘗試候選位址
   └─ 第一個成功的對獲選

4. 提名 → 媒體開始傳輸

NAT類型與其影響

NAT類型 P2P可能? 需要TURN? 普遍性
全錐形 少見
受限錐形 常見
埠受限錐形 非常常見
對稱NAT 企業、行動、CGNAT

實踐中,10-15%的P2P連線需要TURN中繼。行動網路使用者跳到25-30%。

TURN伺服器設定(coturn)

# /etc/coturn/turnserver.conf
listening-port=3478
tls-listening-port=5349
relay-ip=10.0.1.5
external-ip=203.0.113.5/10.0.1.5

lt-cred-mech
user=myapp:myapp_turn_secret
realm=myapp.com

max-bps=2000000
no-multicast-peers
no-loopback-peers

cert=/etc/letsencrypt/live/turn.myapp.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.myapp.com/privkey.pem

prometheus

SFU架構:超越P2P

超過3個人,P2P網格就崩潰了:

P2P Mesh(4人間): 6條連線,每人上傳3倍碼率
P2P Mesh(8人間): 28條連線,每人上傳7倍碼率

SFU: 8人 × 1條連線 = 8條連線
每人上傳一次,SFU轉發給其他7人
上傳:1倍碼率。下載:7倍碼率

mediasoup SFU實作

// server/sfu.ts
import * as mediasoup from "mediasoup";

const worker = await mediasoup.createWorker({
  logLevel: "warn",
  rtcMinPort: 40000,
  rtcMaxPort: 49999,
});

const router = await worker.createRouter({
  mediaCodecs: [
    {
      kind: "video", mimeType: "video/VP8", clockRate: 90000,
      parameters: { "x-google-start-bitrate": 1000 },
    },
    {
      kind: "video", mimeType: "video/H264", clockRate: 90000,
      parameters: { "packetization-mode": 1, "profile-level-id": "42e01f" },
    },
    { kind: "audio", mimeType: "audio/opus", clockRate: 48000, channels: 2 },
  ],
});

async function handleNewParticipant(transport, roomId) {
  const producer = await transport.produce({
    kind: "video",
    rtpParameters: clientRtpParameters,
    encodings: [
      { rid: "h", maxBitrate: 1500000, scalabilityMode: "L1T3" },
      { rid: "m", maxBitrate: 500000, scalabilityMode: "L1T3" },
      { rid: "l", maxBitrate: 150000, scalabilityMode: "L1T3" },
    ],
  });
}

Simulcast:一條串流三種畫質

const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({
  video: { width: 1280, height: 720 },
});

const transceiver = pc.addTransceiver(stream.getVideoTracks()[0], {
  sendEncodings: [
    { rid: "h", maxBitrate: 1500_000, scaleResolutionDownBy: 1 },
    { rid: "m", maxBitrate: 500_000, scaleResolutionDownBy: 2 },
    { rid: "l", maxBitrate: 150_000, scaleResolutionDownBy: 4 },
  ],
});

監控WebRTC統計

setInterval(async () => {
  const stats = await pc.getStats();
  for (const [_, report] of stats) {
    if (report.type === "remote-inbound-rtp" && report.kind === "video") {
      console.log({
        packetLoss: report.fractionLost * 100 + "%",
        jitter: report.jitter * 1000 + "ms",
      });
    }
  }
}, 2000);

10個常見生產陷阱

陷阱1:沒有TURN伺服器

10-15%的使用者會連不上。行動端25-30%。從第一天就為TURN頻寬做預算。

陷阱2:單台TURN — 單點故障。至少2台。

陷阱3:忽略ICE候選滴漏

每發現一個就發送,別等所有都就緒。

陷阱4:getUserMedia不加約束

記得指定合理的解析度、幀率。

陷阱5:不處理getUserMedia錯誤

權限拒絕、設備未找到等都需友善提示。

陷阱6:通話中網路切換 — 實作ICE重啟。

陷阱7:瀏覽器相容性

瀏覽器 問題 方案
Chrome VP9 simulcast丟層 用VP8/H264
Firefox H264不支援simulcast 用VP8
Safari getUserMedia需使用者手勢 放在click裡
Chrome Android 部分設備限制720p 顯式請求720p

陷阱8:不監控統計

陷阱9:碼率過高 — 限制maxBitrate

陷阱10:忘記清理 — 正確關閉PeerConnection


監控儀表板指標

指標 健康範圍 告警閾值
ICE連線時間 1-3秒 > 10秒
丟包率 0-2% > 5%
抖動 0-30ms > 50ms
RTT 20-80ms > 300ms
幀率 25-30 < 15
TURN使用率 10-20% > 30%

總結:WebRTC生產部署是20%媒體和80%基礎設施。三條最致命的問題:不部署TURN不處理網路切換所有東西都跑單機。從第一天就建監控。給TURN頻寬做預算,在真實行動網路上測試。


線上工具

本站提供瀏覽器本地工具,免註冊即可試用 →

#WebRTC#实时通信#STUN#TURN#SFU#音视频#2026