跳到主要内容

WebRTC 全景实战 (15):Capstone — 生产级视频会议系统

Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡
Rainy
16 MIN READ... VIEWS

"站在巨人的肩膀上。" — WebRTC 标准化团队的共识(Curious — 历史

本系列最后一篇,整合 Ch0–Ch14 全部知识,构建可部署的生产级视频会议系统。Serge Lachapelle 在 Curious 访谈 中回顾:从 Marratech 的瑞典企业网实验,到 Google Meet 的全球部署,WebRTC 的终极形态不是某个协议细节,而是一套可运维、可扩展、可观测的实时通信系统

配套代码:examples/webrtc-lab/(全栈整合)

推荐先读

Capstone 以 LiveKit 为 SFU 参考实现。请先阅读本站 LiveKit 介绍,了解 Room/Participant/Track 模型、SDK 选型与 Agents 扩展路径。


本篇术语表

术语英文解释
Capstone毕业设计本系列综合实战项目
Control Plane控制面信令、鉴权、Room 管理
Media Plane媒体面SFU 媒体转发 + TURN 中继
Access Token访问令牌JWT 编码 Room 权限和 Identity
Egress出口LiveKit 录制/直播推流服务
Ingress入口LiveKit 外部媒体源接入
Agent智能体LiveKit AI 参与者(STT/LLM/TTS)
Perfect Negotiation完美协商避免 Offer/Answer glare 的协商模式
Graceful Degradation优雅降级网络差时自动降质而非断连
Chaos Engineering混沌工程故意注入故障验证系统韧性
Waiting Room等候室主持人批准后才允许加入
Active Speaker活跃说话者大会议 UI 聚焦当前发言人
Webhook回调Room 事件推送后端

一、系统架构

组件技术选型对应章节
客户端React + @livekit/components-reactCh1, Ch2
信令Node.js + ws(或 LiveKit 内置)Ch3
SFULiveKit Server(基于 Pion)Ch12
TURNcoturn 集群Ch5, Ch14
认证JWT Access TokenCh3, Ch7
监控getStats → PrometheusCh8, Ch13
拥塞控制GCC + SimulcastCh10, Ch11

LiveKit 介绍 详细说明了 LiveKit 如何整合上述组件——从 Pion 底层到 Room SDK、Agents、Egress 的完整产品栈。Capstone 在此基础上给出可部署的端到端方案。


二、功能清单


三、关键流程:用户加入 Room

3.1 后端 Join API 完整实现

// examples/webrtc-lab/signaling/ 扩展为完整 API
import express from "express";
import crypto from "crypto";
import { AccessToken } from "livekit-server-sdk";

const app = express();
app.use(express.json());

function generateTurnCredentials(secret, identity, ttl = 86400) {
const timestamp = Math.floor(Date.now() / 1000) + ttl;
const username = `${timestamp}:${identity}`;
const hmac = crypto.createHmac("sha1", secret);
hmac.update(username);
const credential = hmac.digest("base64");

return {
iceServers: [
{
urls: [
"turn:turn.example.com:443?transport=tcp",
"turns:turn.example.com:443?transport=tcp",
],
username,
credential,
},
{ urls: "stun:stun.example.com:3478" },
],
};
}

function generateLiveKitToken(roomId, identity, grants = {}) {
const token = new AccessToken(
process.env.LIVEKIT_API_KEY,
process.env.LIVEKIT_API_SECRET,
{ identity, ttl: "24h" }
);
token.addGrant({
roomJoin: true,
room: roomId,
canPublish: true,
canSubscribe: true,
canPublishData: true,
...grants,
});
return token.toJwt();
}

app.post("/rooms/join", async (req, res) => {
const { roomId, identity } = req.body;
if (!roomId || !identity) {
return res.status(400).json({ error: "roomId and identity required" });
}

const token = await generateLiveKitToken(roomId, identity);
const turn = generateTurnCredentials(process.env.TURN_SECRET, identity);

console.log(JSON.stringify({
layer: "signaling",
event: "room_join",
roomId,
identity,
timestamp: Date.now(),
}));

res.json({
token,
livekitUrl: process.env.LIVEKIT_URL || "ws://localhost:7880",
iceServers: turn.iceServers,
roomId,
identity,
});
});

app.listen(3000, () => console.log("API server :3000"));

四、客户端完整实现

4.1 React 视频会议组件

// examples/webrtc-lab/client/ch15-capstone/App.jsx
import {
LiveKitRoom,
VideoConference,
RoomAudioRenderer,
useRoomContext,
} from "@livekit/components-react";
import "@livekit/components-styles";
import { useState, useEffect } from "react";
import { RoomEvent, DisconnectReason } from "livekit-client";

function CapstoneMeeting() {
const [token, setToken] = useState(null);
const [livekitUrl, setLivekitUrl] = useState(null);
const [roomId, setRoomId] = useState("");
const [identity, setIdentity] = useState("");

async function joinRoom(rId, id) {
const res = await fetch("/api/rooms/join", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomId: rId, identity: id }),
});
const data = await res.json();
setToken(data.token);
setLivekitUrl(data.livekitUrl);
setRoomId(rId);
setIdentity(id);
}

if (!token) {
return <JoinForm onJoin={joinRoom} />;
}

return (
<LiveKitRoom
token={token}
serverUrl={livekitUrl}
connectOptions={{ autoSubscribe: true }}
video={true}
audio={true}
onDisconnected={(reason) => {
if (reason !== DisconnectReason.CLIENT_INITIATED) {
console.log("[Layer2] disconnected:", reason);
}
setToken(null);
}}
>
<VideoConference />
<RoomAudioRenderer />
<MetricsReporter roomId={roomId} identity={identity} />
<ReconnectHandler roomId={roomId} identity={identity} livekitUrl={livekitUrl} />
</LiveKitRoom>
);
}

4.2 指标上报组件

function MetricsReporter({ roomId, identity }) {
const room = useRoomContext();

useEffect(() => {
if (!room) return;
const interval = setInterval(() => {
console.log(JSON.stringify({
layer: "media",
room: room.name || roomId,
identity,
participants: room.numParticipants,
timestamp: Date.now(),
}));
}, 5000);
return () => clearInterval(interval);
}, [room, roomId, identity]);

return null;
}

4.3 屏幕共享

import { Track } from "livekit-client";

async function shareScreen(room) {
await room.localParticipant.setScreenShareEnabled(true, {
simulcast: true,
videoEncoding: { maxBitrate: 3_000_000, maxFramerate: 15 },
});
}

room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.source === Track.Source.ScreenShare) {
const el = track.attach();
document.getElementById("screen-share").appendChild(el);
}
});

五、快速部署

5.1 本地开发环境

# 1. LiveKit SFU
brew install livekit
livekit-server --dev
# → ws://localhost:7880

# 2. TURN(可选,本地开发通常不需要)
cd examples/webrtc-lab/docker/coturn
docker compose up -d

# 3. 信令 + API
cd examples/webrtc-lab/signaling
npm install && npm start
# → ws://localhost:8080 + http://localhost:3000

# 4. 生成 Token(CLI 方式)
brew install livekit-cli
lk token create \
--api-key devkey \
--api-secret secret \
--join --room capstone \
--identity user1 \
--valid-for 24h

# 5. 客户端
npx serve examples/webrtc-lab/client/ch12-sfu-client

5.2 生产部署拓扑

步骤命令/配置说明
LiveKit 配置livekit.yaml + Redis 地址分布式 Mesh
TURN 部署coturn × N 区域GeoDNS 解析
API 部署Docker/K8sToken + TURN 凭证
监控Prometheus scrapeLiveKit :6789 + coturn :9641
证书Let's EncryptWSS + TURNS 443

5.3 livekit.yaml 生产示例

port: 7880
rtc:
tcp_port: 7881
port_range_start: 50000
port_range_end: 60000
use_external_ip: true
redis:
address: redis.example.com:6379
turn:
enabled: true
domain: turn.example.com
tls_port: 443
keys:
APIKey: <your-api-key>
APISecret: <your-api-secret>
webhook:
urls:
- https://api.example.com/webhook/livekit
logging:
level: info
room:
empty_timeout: 300
max_participants: 100

六、性能基线与 SLO

指标目标测量方法对应章节
首帧时间< 2sframesDecoded 首次 > 0Ch13
端到端延迟< 300msRTT/2 + jitterBufferDelayCh8
100 Room 并发无 ICE failed压测 + PrometheusCh5
TURN 占比< 25%candidateType=relay 统计Ch14
通话成功率> 99%connectionState=connected 比例Ch13
Simulcast 切换< 1s限速后 rid 变化时间Ch11

6.1 压测脚本

import { Room } from "livekit-client";

async function loadTest(roomId, count) {
const results = [];
for (let i = 0; i < count; i++) {
const identity = `bot-${i}`;
const res = await fetch("/api/rooms/join", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomId, identity }),
});
const { token, livekitUrl } = await res.json();

const room = new Room();
const start = Date.now();
try {
await room.connect(livekitUrl, token);
results.push({
identity,
connectTime: Date.now() - start,
state: room.state,
});
} catch (err) {
results.push({ identity, connectTime: -1, state: "failed", error: err.message });
}
}
console.table(results);
const success = results.filter((r) => r.state === "connected").length;
const avgConnect = results
.filter((r) => r.connectTime > 0)
.reduce((s, r) => s + r.connectTime, 0) / success;
console.log(`Success: ${success}/${count}, Avg connect: ${avgConnect}ms`);
}

七、断线重连与容错

function ReconnectHandler({ roomId, identity, livekitUrl }) {
const room = useRoomContext();

useEffect(() => {
if (!room) return;

room.on(RoomEvent.Reconnecting, () => {
console.log(JSON.stringify({ layer: "ice", event: "reconnecting", roomId }));
});

room.on(RoomEvent.Reconnected, () => {
console.log(JSON.stringify({ layer: "ice", event: "reconnected", roomId }));
});

room.on(RoomEvent.Disconnected, async (reason) => {
if (reason === DisconnectReason.CLIENT_INITIATED) return;
try {
const res = await fetch("/api/rooms/join", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomId, identity }),
});
const { token } = await res.json();
await room.connect(livekitUrl, token);
} catch (err) {
console.error("Reconnect failed:", err);
}
});
}, [room, roomId, identity, livekitUrl]);

return null;
}

八、LiveKit Agents 扩展

Serge Lachapelle 在 Curious 访谈中提到对未来最兴奋的方向:云计算 + AI 算法(降噪、背景分离、实时翻译)。LiveKit Agents 让 AI 作为 Room 中的普通 Participant:

Agent 类型用途示例
Voice Assistant语音问答会议 AI 助手
Translator实时翻译多语言会议
Transcriber实时字幕无障碍/accessibility
Moderator内容审核违规检测
# LiveKit Agent 最小示例
from livekit.agents import AutoSubscribe, JobContext, WorkerOptions, cli
from livekit.agents.voice import Agent

async def entrypoint(ctx: JobContext):
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
agent = Agent(instructions="You are a helpful meeting assistant.")
agent.start(ctx.room)

if __name__ == "__main__":
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))

详见 LiveKit 介绍Agents 文档


九、Egress 录制与 Ingress 接入

lk egress start --room capstone --layout grid --output s3://bucket/recording.mp4
lk egress start --room capstone --stream rtmp://a.rtmp.youtube.com/live2/KEY

十、examples/webrtc-lab 全栈整合

模块路径章节状态
媒体设备client/ch01-media-devicesCh1
P2P 通话client/ch02-p2p-basicCh2
信令服务器signaling/server.jsCh3
Data Channelclient/ch06-data-channelCh6📋 规划中
SFU 客户端client/ch12-sfu-clientCh12📋 规划中
TURN 部署docker/coturn/Ch5, Ch14
Capstone全栈整合Ch15📋 规划中

十一、安全清单

检查项实现章节
Token 权限最小化canPublish / canSubscribe 按需Ch12
TURN 短期凭证use-auth-secret + TTLCh14
传输加密WSS + DTLS/SRTPCh7
Secret 管理环境变量 / K8s Secret
监控告警ICE failed / relay 占比Ch13

十二、常见陷阱

#陷阱现象修复
1Token 权限过大任意用户可 publishJWT grant 最小权限
2未配 TURN部分用户 ICE failed部署 coturn + REST 凭证
3单 SFU 节点跨区延迟高Redis Mesh 分布式
4无监控故障不可见Prometheus + 三层日志
5Simulcast 未开大会议带宽爆炸publishTrack simulcast:true
6硬编码 API Key安全风险环境变量 + Secret 管理
7忽略 Token 刷新长会议中断过期前自动 refresh
8信令与 API 混端口CORS/路由混乱API :3000 + 信令 :8080 分离
9无 emptyTimeout空 Room 占资源livekit.yaml 配置
10压测未模拟 TURN生产 relay 爆满压测含 relay 场景

十三、系列回顾

阶段章节核心能力
认知Ch0–Ch2WebRTC 架构、浏览器 API、首个 P2P 通话
连接Ch3–Ch6信令、SDP、ICE/TURN、Data Channel
媒体Ch7–Ch9DTLS/SRTP、RTP/RTCP、Codec/Simulcast
SFUCh10–Ch12GCC 拥塞控制、Simulcast/SVC、SFU 架构
生产Ch13–Ch15调试可观测、TURN 部署、Capstone 系统
能力域你现在应该能
浏览器 API独立实现 P2P + DataChannel + Simulcast
协议栈读懂 SDP、抓包分析 DTLS/SRTP、理解 GCC
信令与 SFU设计 Room 模型、部署 LiveKit、Token 鉴权
生产运维部署 TURN 集群、监控 stats、SLO 告警
权威资料查阅 RFC / Curious / webrtcH4cKS / LiveKit Docs

十四、Marratech 到 Meet 的完整弧线

Serge Lachapelle 的 Curious 访谈是整个系列的历史锚点——从 Marratech 的多播幻想,到 packet shufflers 的现实,再到今天 AI Agent 作为 Room 参与者,WebRTC 的故事仍在续写。本 Capstone 站在 Meet/LiveKit 的肩膀上,把 Ch0–Ch14 的知识落成一套可部署系统。


十五、实战 Lab:Capstone 验收清单

#验收项操作通过标准
1本地启动livekit-server --dev + 客户端3 人互见视频
2Token 鉴权API 生成 JWT无 Token 无法加入
3TURN fallback关 STUN 仅留 TURNICE relay 成功
4Simulcast限速 300kbps自动降层
5断线重连断网 5s 恢复自动 reconnect
6指标上报MetricsReporter 运行5s 间隔日志
7屏幕共享publishScreenShare其他参与者可见
850 人压测loadTest(50)成功率 > 99%
#!/bin/bash
set -e

echo "=== Capstone WebRTC 视频会议系统 ==="

livekit-server --dev &
sleep 2

cd examples/webrtc-lab/docker/coturn && docker compose up -d
cd -

cd examples/webrtc-lab/signaling && npm start &
sleep 2

TOKEN=$(lk token create --api-key devkey --api-secret secret \
--join --room capstone --identity tester --valid-for 1h 2>/dev/null)

echo "LiveKit: ws://localhost:7880"
echo "Token: $TOKEN"
echo "打开 examples/webrtc-lab/client/ch12-sfu-client 加入 room=capstone"

十六、推荐阅读


系列导航 — WebRTC 全景实战(全 16 章)

章节主题状态
0架构全景与协议栈地图✅ 已发布
1浏览器媒体 API 与设备管理✅ 已发布
2第一个 P2P 视频通话✅ 已发布
3信令服务器设计与会话状态机✅ 已发布
4SDP 解剖与媒体协商✅ 已发布
5ICE、STUN、TURN 与 NAT 穿透✅ 已发布
6Data Channel 与 SCTP over DTLS✅ 已发布
7DTLS 握手与 SRTP 加密体系✅ 已发布
8RTP/RTCP 媒体传输与 QoS✅ 已发布
9音视频编解码与 Simulcast 入门✅ 已发布
10带宽估计与拥塞控制 GCC✅ 已发布
11Simulcast、SVC 与选择性订阅✅ 已发布
12SFU/MCU/Mesh 架构与 Pion 实战✅ 已发布
13调试工具链与可观测性✅ 已发布
14TURN 集群部署与多区域扩展✅ 已发布
15Capstone 生产级视频会议系统✅ 已发布

🎉 恭喜完成 WebRTC 全景实战全系列!


References

Logo
RainLib

探索技术、设计与分布式系统的边界。构建面向未来的开发者工具。

留言与建议

© 2026 RainLib. 为未来构建。(Built for the Future)
版权所有。
系统正常