Skip to content

WebRTC 音视频通话

聊天模块内置 1v1 语音 / 视频通话、屏幕共享、群语音。信令复用现有 WebSocket(/api/ws),媒体走 P2P(群聊为 mesh 拓扑),服务端不转发媒体流。

架构概览

┌──────────┐   rtc:* 信令(WS 中继)   ┌──────────┐
│ 浏览器 A │ ──────────────────────▶ │  服务端  │ ──▶ 浏览器 B
│RTCPeerCon│ ◀────────────────────── │ (仅中继) │ ◀──
└────┬─────┘                         └──────────┘
     │   媒体(音视频/屏幕)P2P 直连,经 STUN/TURN 协商
     └──────────────────────────────────────────────▶ 浏览器 B
  • 信令invite / accept / offer / answer / ICE candidate 等通过 WebSocket 中继。事件清单见 WebSocket 事件 · rtc
  • 媒体:浏览器间 P2P。服务端不经手媒体流,仅做信令转发、群通话 join 房间登记和断线清理。
  • 关键文件
    • 服务端:routes/platform/ws.ts(信令中继)、lib/rtc-manager.ts(群通话房间)、services/chat/chat.service.tsgetRtcConfig / postCallRecord)。
    • 前端:webrtc/callManager.ts(单例通话管理器)、webrtc/CallOverlayHost.tsx(全局来电/通话宿主,挂载于 AdminLayout)、CallWindow.tsx / MediaTile.tsx

配置(ICE 服务器)

浏览器创建 RTCPeerConnection 需要 ICE 服务器做 NAT 穿透。服务端通过 GET /api/chat/rtc/config 下发,取值来自环境变量:

变量说明默认
WEBRTC_STUN_URLSSTUN 地址(逗号分隔),用于发现公网地址stun:stun.l.google.com:19302
WEBRTC_TURN_URLSTURN 中继地址(逗号分隔)
WEBRTC_TURN_USERNAMETURN 用户名
WEBRTC_TURN_CREDENTIALTURN 凭证
dotenv
# 仅 STUN:同 LAN / 本机可连通
WEBRTC_STUN_URLS=stun:stun.l.google.com:19302

# 跨 NAT / 公网需 TURN(自建 coturn 或云服务)
WEBRTC_TURN_URLS=turn:turn.example.com:3478,turns:turn.example.com:5349
WEBRTC_TURN_USERNAME=zenith
WEBRTC_TURN_CREDENTIAL=your_turn_password

必须配置 TURN 的场景

仅配 STUN 时,若双方处于对称型 NAT / 不同公网且无法直连,通话会协商失败。生产环境(跨公网用户)务必部署 TURN 中继服务器。

浏览器限制

getUserMedia / getDisplayMedia 仅在 HTTPS 或 localhost 下可用。生产环境必须启用 HTTPS,否则无法获取麦克风/摄像头/屏幕。

通话流程

1v1(p2p)

  1. 呼叫方点击「语音/视频通话」→ 获取本地媒体 → 发送 rtc:inviteto 为被叫用户)。
  2. 被叫弹出来电窗口;接听 → 获取本地媒体 → 回 rtc:accept拒绝 → 回 rtc:reject;已在 1v1 通话中 → 自动回 rtc:busy
  3. 呼叫方收到 accept 后创建 RTCPeerConnection 并发 rtc:offer,被叫 rtc:answer,双方交换 rtc:ice → 连通。
  4. 呼叫方等待接听最长 35 秒,超时发送 rtc:cancel 并写入未接听通话记录。
  5. 任一方挂断发 rtc:leave(呼叫前取消发 rtc:cancel)→ 前端清理 peer,并写入通话记录系统消息(时长 / 未接听 / 已拒绝 / 已取消)。

群语音(group,mesh)

  1. 发起者广播 rtc:invite(会话内其他成员)并发送 rtc:join 登记房间。
  2. 成员接听后发送 rtc:join;服务端回 rtc:room-participants(房间现有成员),加入者主动向每个现有成员建连发 offer(现有成员仅应答),因此天然无 glare。
  3. 成员离开发 rtc:leave,服务端按会话转发给其他成员;前端收到后移除对应 peer。
  4. WebSocket 断线时服务端调用 leaveAllRooms() 清理该用户所在房间,并向剩余成员发送 rtc:leave

屏幕共享、纯语音添加视频轨等 renegotiation 场景,采用 perfect-negotiation(按 userId 决定 polite/impolite)处理协商冲突。

通话能力

能力说明
语音 / 视频单聊支持语音与视频;群聊入口提供群语音
屏幕共享通话中切换;有视频 sender 时用 replaceTrack,没有视频 sender 时 addTrack 并触发重新协商
静音 / 关摄像头切换本地轨道 enabled
最小化通话窗口可最小化为悬浮条,远端音频持续播放(独立 <audio> 元素)
来电提示音WebAudio 合成,无需音频资源
通话记录1v1 结束写入会话系统消息(POST /api/chat/conversations/:id/call-record

排错

现象可能原因
无法获取麦克风/摄像头非 HTTPS/localhost;或浏览器权限被拒
接通后听不到/看不到对方缺少 TURN,双方无法 P2P 直连;检查 WEBRTC_TURN_*
来电窗口不弹对方 WebSocket 未连接(未登录/断线);通话宿主仅在登录态挂载
群通话有人听不到新加入者新加入者需向现有成员发起连接,确认 rtc:joinrtc:room-participants 往返正常

部署约束

群通话房间状态为单进程内存(与 WebSocket 连接管理同等约束)。多实例水平扩展时,需将 rtc-manager 的房间状态与 ws-manager 的连接路由改造为共享存储(如 Redis Pub/Sub)。

Built with VitePress for local documentation preview.