在德州扑克里加视频通话:原生 WebRTC + 复用信令服务器
在德州扑克里加视频通话:原生 WebRTC + 复用信令服务器
上一篇写了联机德州扑克的服务端架构。打牌的时候两个人没法面对面聊天,总觉得差点意思——于是给牌桌加了个视频通话:自己右下角、对手左上角各一个小浮窗,打开就”同桌”了。
实现比想象中简单,因为信令服务器早就有了(就是上一篇那个 PeerServer),视频通话只是它的”本职用法”。这篇文章记录完整方案:为什么不用 peerjs 库、信令如何与现有 Worker 打通、前端怎么写、以及测试中踩到的坑。
需求
- 两人同房打牌时,可以可选开启视频通话(尊重隐私,默认关闭,点「📹 视频」按钮开启)
- 自己的画面在右下角小窗,对手画面在左上角小窗,不挡牌桌
- 复用已有 Worker,一行服务端代码都不改
整体架构
视频通话是两条独立的通道,互不干扰:
1 | |
关键点:
- 信令只负责”牵线”:交换 SDP(offer/answer)和 ICE candidate,几 KB 的字符串。
- 音视频走 P2P 直传:不经过服务器,免费计划带宽不受影响。
- 两条通道完全独立:扑克断线不影响视频,反之亦然。
为什么不用 peerjs 库(重要)
peerjs 是最流行的 WebRTC 封装,上一篇部署 PeerServer 时也用它的客户端。但这次实测发现一个关键不兼容:
- peerjs 1.5.4 客户端:连上 WS 后,等服务器主动发
{type:'OPEN'}确认注册。 - 我们的 PeerServer DO:等客户端主动发
{type:'OPEN', src:'<peerId>'}来注册。
实测(Node 模拟 peerjs 客户端握手):WS 连上了,但服务器 5 秒不发任何消息——协议对不上,peer.on('open') 永远不触发。
教训:网上部署 PeerJS 信令服务器的教程很多,但不少是自定义简化协议,和标准 peerjs 客户端不完全兼容。用之前先实测握手流程,别直接信教程。
所以改用原生 WebRTC(RTCPeerConnection + getUserMedia),信令用裸 WebSocket 走现有协议。反而更简单:没有库依赖,协议完全可控。
信令协议(与现有 PeerServer 匹配)
复用现有 PeerServer DO 的协议,只有两条规则:
1 | |
peerId 派生规则:poker-vc-<房间号>-<座位>,座位来自扑克 welcome 消息(0 或 1)。这样同房必可达、全局唯一、跨房天然隔离。
消息类型就三种:OFFER(携带 SDP)、ANSWER(携带 SDP)、ICE(携带 candidate),外加系统级的 OPEN。
前端实现
1. 建立连接:getUserMedia + 信令
1 | |
2. 谁先发起 offer
谁先连上信令谁就”有责任”发起。约定座位 1 优先发起,座位 0 兜底:
1 | |
为什么需要兜底:座位 1 发 offer 时如果座位 0 还没注册信令,服务器会静默丢弃这条消息。兜底逻辑让连接总能建立。
3. ICE 缓冲(容易漏的细节)
信令是异步的,offer/answer 可能晚于 ICE candidate 到达。如果直接 addIceCandidate 会报错(没有 remoteDescription)。正确做法是缓冲:
1 | |
4. 视频浮窗布局(不挡牌桌)
1 | |
1 | |
5. 开关(隐私优先)
默认不开启,玩家点「📹 视频」按钮才请求摄像头:
1 | |
6. 生命周期管理(最容易出 bug 的部分)
关闭/离开时必须完整释放,否则摄像头红灯常亮、声音残留:
1 | |
踩过的坑
坑 1:localDescription 是对象不是字符串
第一版测试直接发 vc.pc.localDescription(对象),JSON 序列化后 sdp 字段变成嵌套对象,对端 new RTCSessionDescription({sdp: {...}}) 解析直接报错:
1 | |
修复:发送时取 .sdp 字符串:
1 | |
坑 2:peerjs 库与自定义信令协议不兼容
见上文「为什么不用 peerjs 库」。任何”信令服务器”教程都要先实测握手。
坑 3:NAT 穿透失败时的 TURN
多数场景 stun:stun.l.google.com 就够了;两家运营商互连失败时(connectionState 卡在 connecting 超时)需要 TURN。Cloudflare 提供免费 TURN(turn.cloudflare.com),加到 iceServers 即可,不用自己部署。
测试验证
分两层测:
- 信令层(Node 模拟):两个客户端连 PeerServer DO,验证注册、双向转发、跨房间隔离。结果全通过。
- 媒体层(Chrome headless 双窗口 + 假摄像头):模拟两个真实浏览器建立 P2P。结果:双方都收到远端视频流,
connectionState变为connected。
生产页面的开关交互(点按钮开启、再点关闭)逻辑与测试页同构,用户在真实浏览器双窗口确认即可。
经验总结
- 信令服务器是内容无关的:传扑克数据也好、传 SDP/ICE 也好,它只负责转发。已有信令 = 视频通话的地基,别再部署一套。
- 第三方库的协议假设要实测:peerjs 教程满天飞,但你的服务器协议可能和它假设的不一致。测一次握手就知道,别写完才踩。
- WebRTC 的异步细节:ICE 缓冲、.sdp 字符串,这两个小坑排查起来最隐蔽。
- 视频通话的本质是 P2P:信令只牵线,音视频直传。服务器零带宽压力,免费计划毫无压力。