Files
VibeCoding/WebRTCControllerWeb/server/index.js

141 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 轻量信令服务器,协议与 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}`);