| 組件 | 作用 | 部署建議 |
| 信令服務器 | WebSocket 交換 SDP/ICE Candidate | Node.js + ws / Socket.io / Go + Gorilla WebSocket,需 HTTPS/WSS |
| STUN 服務器 | 獲取公網 IP:Port (NAT 穿透) | 免費:`stun:stun.l.google.com:19302` 或自建 coturn |
| TURN 服務器 | 中繼流量 (對稱 NAT/防火牆) | **生產必須自建** coturn,配置長期憑證 `turn:turn.example.com:3478?transport=udp` |
| HTTPS 憑證 | 瀏覽器強制安全上下文 | Let's Encrypt / CF SSL,配置 HSTS |
最小可行前端代碼 (原生 JS, 無框架)
// app.js
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com:3478?transport=udp', username: 'user', credential: 'pass' }
]
};
const pc = new RTCPeerConnection(config);
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const signaling = new WebSocket('wss://signal.example.com');
// 1. 獲取本地媒體流
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
localVideo.srcObject = stream;
stream.getTracks().forEach(track => pc.addTrack(track, stream));
})
.catch(console.error);
// 2. 處理遠端軌道
pc.ontrack = e => remoteVideo.srcObject = e.streams[0];
// 3. ICE Candidate 交換
pc.onicecandidate = e => {
if (e.candidate) signaling.send(JSON.stringify({ type: 'candidate', candidate: e.candidate }));
};
// 4. 信令消息處理
signaling.onmessage = async ({ data }) => {
const msg = JSON.parse(data);
switch (msg.type) {
case 'offer':
await pc.setRemoteDescription(new RTCSessionDescription(msg));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send(JSON.stringify({ type: 'answer', sdp: answer }));
break;
case 'answer':
await pc.setRemoteDescription(new RTCSessionDescription(msg));
break;
case 'candidate':
await pc.addIceCandidate(new RTCIceCandidate(msg.candidate));
break;
case 'bye':
pc.close();
break;
}
};
// 5. 發起呼叫
document.getElementById('btnCall').onclick = async () => {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send(JSON.stringify({ type: 'offer', sdp: offer }));
};
// 6. 掛斷
document.getElementById('btnHangup').onclick = () => {
signaling.send(JSON.stringify({ type: 'bye' }));
pc.close();
};
信令服務器極簡實現 (Node.js + ws)
// signal.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8443 }); // 需配置 SSL 憑證
const rooms = new Map();
wss.on('connection', (ws, req) => {
const room = new URL(req.url, `https://${req.headers.host}`).searchParams.get('room') || 'default';
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(ws);
ws.on('message', (msg) => {
// 廣播給房間內其他人
for (const client of rooms.get(room)) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(msg);
}
}
});
ws.on('close', () => rooms.get(room).delete(ws));
});
coturn 部署要點 (Ubuntu 22.04)
apt update && apt install -y coturn
# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
use-auth-secret
static-auth-secret=your_long_random_secret
realm=example.com
total-quota=100
stale-nonce=600
# systemctl enable --now coturn
常見坑點速查
| 現象 | 原因 | 解決 |
| 本地能通、跨網不通 | 缺 TURN 或 TURN 埠未放行 | 部署 coturn、防火牆開 3478/5349 UDP/TCP |
| 視頻黑屏/綠屏 | 編碼器協商失敗 (H.264/VP8/VP9) | SDp 中確保雙端支援共同編碼,或強制 `RTCRtpTransceiver.setCodecPreferences` |
| 移動端無聲音 | 自動播放策略、AudioContext 需用戶手勢啟動 | 按鈕點擊時 `audioContext.resume()` |
| 信令 WebSocket 頻繁斷開 | 負載均衡器無 Sticky Session / 超時 | 配置 WebSocket 親和性、心跳 Ping/Pong |
進階資源
總結
**WebRTC = getUserMedia + RTCPeerConnection + 信令 + STUN/TURN**。前端代碼不足 100 行,**關鍵在於基礎設施 (TURN、HTTPS、信令高可用)**。建議先跑通官方樣例,再逐步替換信令/ TURN 為自建服務。
- 上一篇文章: 多網站會員帳號互通與數據共享架構設計:SSO、OAuth2、OpenID Connect 方案選型
- 下一篇文章: Java「Cannot inherit from final class」類衝突排查:HttpClient 4.x 多版本 Jar 包導致驗證錯誤
Copyright ©
程式碼之家 All Rights Reserved