// 轻量信令服务器,协议与 WebRTCSignalServer (Spring) 完全兼容。 // 端点: ws://host:PORT/ws/signal // 支持消息: REGISTER / DEVICE_LIST / OFFER / ANSWER / ICE_CANDIDATE / CONTROL_COMMAND / CONNECTION_REJECTED // 支持通知: REGISTER_SUCCESS / DEVICE_LIST / TARGET_OFFLINE / REQUEST_ERROR / REQUEST_TIMEOUT import { WebSocketServer } from 'ws'; const PORT = Number(process.env.PORT || 8088); const PATH = process.env.WS_PATH || '/ws/signal'; const wss = new WebSocketServer({ port: PORT, path: PATH, // WebRTC SDP 消息可能较大,将最大负载设为 1MB(默认 100MB 但在某些代理环境下受限) maxPayload: 1024 * 1024, }); // deviceId -> { ws, type } const sessions = new Map(); // "from->to" -> true(等待被控端确认) const pending = new Map(); const keyOf = (a, b) => `${a}->${b}`; function send(ws, obj) { if (ws && ws.readyState === ws.OPEN) { try { ws.send(JSON.stringify(obj)); } catch (e) { console.error('[send] 发送消息失败:', e.message); } } } // —— 服务端主动 ping/pong 保活:每 30 秒向所有连接发送 ping, // 若 60 秒内未收到 pong 则强制关闭连接(防止僵死连接占用资源)。 const HEARTBEAT_INTERVAL = 30_000; const heartbeatTimer = setInterval(() => { wss.clients.forEach((ws) => { if (ws._alive === false) { console.warn('[heartbeat] 连接无响应,强制关闭'); return ws.terminate(); } ws._alive = false; try { ws.ping(); } catch { /* ignore */ } }); }, HEARTBEAT_INTERVAL); wss.on('close', () => clearInterval(heartbeatTimer)); wss.on('connection', (ws) => { ws._alive = true; ws.on('pong', () => { ws._alive = true; }); ws.on('message', (data) => { let msg; try { msg = JSON.parse(data.toString()); } catch { return; } const type = (msg.type || '').toUpperCase(); // 忽略客户端心跳 ping,仅用于保活 if (type === 'PING') return; switch (type) { case 'REGISTER': handleRegister(ws, msg); break; case 'DEVICE_LIST': handleDeviceList(ws); break; case 'OFFER': handleOffer(msg); break; case 'ANSWER': case 'CONNECTION_REJECTED': pending.delete(keyOf(msg.toDeviceId, msg.fromDeviceId)); forward(msg); break; case 'ICE_CANDIDATE': case 'CONTROL_COMMAND': forward(msg); break; default: forward(msg); } }); ws.on('close', () => { for (const [id, s] of sessions) { if (s.ws === ws) { sessions.delete(id); break; } } }); }); function handleRegister(ws, msg) { const id = msg.fromDeviceId; const type = (msg.deviceType || '').toUpperCase(); if (!id || !type) return; sessions.set(id, { ws, type }); send(ws, { type: 'REGISTER_SUCCESS', deviceId: id }); console.log(`设备注册: ${id} (${type})`); } function handleDeviceList(ws) { const controllers = []; const controlled = []; for (const [id, s] of sessions) { if (s.type === 'CONTROLLER') controllers.push(id); else if (s.type === 'CONTROLLED') controlled.push(id); } send(ws, { type: 'DEVICE_LIST', controllers, controlled }); } function handleOffer(msg) { const from = msg.fromDeviceId; const to = msg.toDeviceId; const fromType = sessions.get(from)?.type; const toType = sessions.get(to)?.type; if (fromType !== 'CONTROLLER' || toType !== 'CONTROLLED') { send(sessions.get(from)?.ws, { type: 'REQUEST_ERROR', toDeviceId: to, payload: '连接请求只能由 CONTROLLER 发往 CONTROLLED', }); return; } const target = sessions.get(to)?.ws; if (!target) { send(sessions.get(from)?.ws, { type: 'TARGET_OFFLINE', toDeviceId: to, payload: '目标被控端不在线,请确认设备已开启并连接服务器', }); return; } pending.set(keyOf(from, to), true); forward(msg); } function forward(msg) { const target = sessions.get(msg.toDeviceId)?.ws; if (!target) return; send(target, msg); } console.log(`信令服务器已启动: ws://localhost:${PORT}${PATH}`);