diff --git a/src/class/httphandler.ts b/src/class/httphandler.ts index 259cf45..1ee3b7a 100644 --- a/src/class/httphandler.ts +++ b/src/class/httphandler.ts @@ -7,7 +7,7 @@ import Offer from './offer'; import Answer from './answer'; import Candidate from './candidate'; import { v4 as uuid } from 'uuid'; -import { onGetAllConnectionIds, onGetOnlineUsers as onGetWsOnlineUsers } from './websockethandler'; +import { onGetAllConnectionIds, onGetOnlineUsers as onGetWsOnlineUsers, onGetRooms as onGetWsRooms } from './websockethandler'; import { log, LogLevel } from '../log'; /** * 断开连接记录类 @@ -1039,40 +1039,13 @@ function postCandidate(req: Request, res: Response): void { * description: 总房间数 */ function onGetConnections(req: Request, res: Response): void { + const connectionId = typeof req.query.connectionId === 'string' ? req.query.connectionId : undefined; + const wsRooms = onGetWsRooms(connectionId).map((room) => ({ + ...room, + users: room.members + })); - // 收集所有房间ID和链接用户信息 - const rooms = []; - - // 遍历所有连接对 - for (const [connectionId, pair] of Array.from(connectionPair.entries())) { - // 收集房间中的用户信息 - const users = []; - - // 添加第一个用户 - if (pair[0] && clients.has(pair[0])) { - users.push({ - sessionId: pair[0], - connected: true - }); - } - - // 添加第二个用户 - if (pair[1] && clients.has(pair[1])) { - users.push({ - sessionId: pair[1], - connected: true - }); - } - - // 添加房间信息 - rooms.push({ - roomId: connectionId, - users: users, - userCount: users.length - }); - } - - res.json({ rooms: rooms, totalRooms: rooms.length }); + res.json({ rooms: wsRooms, totalRooms: wsRooms.length }); } /** diff --git a/src/class/websockethandler.ts b/src/class/websockethandler.ts index 7f6cba0..b40f54b 100644 --- a/src/class/websockethandler.ts +++ b/src/class/websockethandler.ts @@ -53,12 +53,37 @@ interface OnlineUser { avatar: string; } +interface RoomMemberInfo extends OnlineUser { + joinedAt: number; + updatedAt: number; +} + +interface RoomSnapshot { + roomId: string; + connectionId: string; + hostSocketId: string; + createdAt: number; + updatedAt: number; + members: RoomMemberInfo[]; + userCount: number; +} + +interface StoredRoom { + roomId: string; + connectionId: string; + hostSocketId: string; + createdAt: number; + updatedAt: number; + members: Map; +} + /** * 连接组映射 * 键: connectionId * 值: ConnectionGroup(1个host + 多个participants) */ const connectionGroup: Map = new Map(); +const rooms: Map = new Map(); function asAppWebSocket(ws: WebSocket): AppWebSocket { return ws as AppWebSocket; @@ -89,7 +114,12 @@ function getUserInfo(ws: WebSocket): UserInfo { } function setUserInfo(ws: WebSocket, userInfo: UserInfo): void { - asAppWebSocket(ws).userInfo = userInfo; + asAppWebSocket(ws).userInfo = { + id: userInfo.id || '', + name: userInfo.name || '', + avatar: userInfo.avatar || '' + }; + updateRoomMembersForSocket(ws); } function safeSend(ws: WebSocket, payload: unknown): boolean { @@ -111,6 +141,106 @@ function findParticipantSocket(group: ConnectionGroup, participantId: string): W return null; } +function getSocketRoleInRoom(ws: WebSocket, connectionId: string): 'host' | 'participant' | 'idle' { + const group = connectionGroup.get(connectionId); + if (group) { + if (group.host === ws) { + return 'host'; + } + if (group.participants.has(ws)) { + return 'participant'; + } + } + + return isPrivate ? 'idle' : 'participant'; +} + +function toRoomMember(ws: WebSocket, connectionId: string, existing?: RoomMemberInfo): RoomMemberInfo { + const userInfo = getUserInfo(ws); + const now = Date.now(); + return { + socketId: ensureSocketId(ws), + connectionId, + participantId: ensureParticipantId(ws), + role: getSocketRoleInRoom(ws, connectionId), + userId: userInfo.id || '', + name: userInfo.name || '', + avatar: userInfo.avatar || '', + joinedAt: existing ? existing.joinedAt : now, + updatedAt: now + }; +} + +function getOrCreateRoom(connectionId: string, ws: WebSocket): StoredRoom { + let room = rooms.get(connectionId); + const now = Date.now(); + const socketId = ensureSocketId(ws); + if (!room) { + room = { + roomId: connectionId, + connectionId, + hostSocketId: '', + createdAt: now, + updatedAt: now, + members: new Map() + }; + rooms.set(connectionId, room); + } + + if (!room.hostSocketId || getSocketRoleInRoom(ws, connectionId) === 'host') { + room.hostSocketId = socketId; + } + room.updatedAt = now; + return room; +} + +function saveRoomMember(ws: WebSocket, connectionId: string): void { + const room = getOrCreateRoom(connectionId, ws); + const socketId = ensureSocketId(ws); + const existing = room.members.get(socketId); + room.members.set(socketId, toRoomMember(ws, connectionId, existing)); + room.updatedAt = Date.now(); +} + +function updateRoomMembersForSocket(ws: WebSocket): void { + const connectionIds = clients.get(ws); + if (!connectionIds) { + return; + } + + connectionIds.forEach(connectionId => { + if (rooms.has(connectionId)) { + saveRoomMember(ws, connectionId); + } + }); +} + +function removeRoomMember(ws: WebSocket, connectionId: string): void { + const room = rooms.get(connectionId); + if (!room) { + return; + } + + room.members.delete(getSocketId(ws)); + room.updatedAt = Date.now(); + if (room.members.size === 0 || room.hostSocketId === getSocketId(ws)) { + rooms.delete(connectionId); + } +} + +function toRoomSnapshot(room: StoredRoom): RoomSnapshot { + const members = Array.from(room.members.values()); + return { + roomId: room.roomId, + connectionId: room.connectionId, + hostSocketId: room.hostSocketId, + createdAt: room.createdAt, + updatedAt: room.updatedAt, + members, + userCount: members.length + }; +} + /** * 获取或创建WebSocket会话的连接ID集合 * @param session WebSocket会话实例 @@ -140,6 +270,7 @@ function reset(mode: string): void { isPrivate = mode == "private"; clients.clear(); connectionGroup.clear(); + rooms.clear(); } /** @@ -201,12 +332,16 @@ function remove(ws: WebSocket): void { group.participants.forEach(participantWs => { safeSend(participantWs, { type: "disconnect", connectionId: connectionId, reason: "host-left" }); }); + rooms.delete(connectionId); connectionGroup.delete(connectionId); } else { group.participants.delete(ws); + removeRoomMember(ws, connectionId); // 包含participantId,让host能识别是哪个participant离开 safeSend(group.host, { type: "participant-left", connectionId: connectionId, participantId: getParticipantId(ws) }); } + } else { + removeRoomMember(ws, connectionId); } log(LogLevel.log, `Remove connectionId: ${connectionId}`); }); @@ -242,6 +377,7 @@ function onConnect(ws: WebSocket, connectionId: string): void { const connectionIds = getOrCreateConnectionIds(ws); connectionIds.add(connectionId); const role = polite ? 'participant' : 'host'; + saveRoomMember(ws, connectionId); safeSend(ws, { type: "connect", connectionId: connectionId, polite: polite, role: role, participantId: participantId }); } @@ -266,14 +402,18 @@ function onDisconnect(ws: WebSocket, connectionId: string): void { group.participants.forEach(participantWs => { safeSend(participantWs, { type: "disconnect", connectionId: connectionId, reason: "host-left" }); }); + rooms.delete(connectionId); connectionGroup.delete(connectionId); log(LogLevel.log, `Host disconnected, room ${connectionId} deleted, notified ${group.participants.size} participants`); } else { // participant断开连接,从组中移除并通知host(使用participant-left类型,host不会关闭房间) group.participants.delete(ws); + removeRoomMember(ws, connectionId); safeSend(group.host, { type: "participant-left", connectionId: connectionId, participantId: getParticipantId(ws) }); log(LogLevel.log, `Participant left connectionId: ${connectionId}, remaining participants: ${group.participants.size}`); } + } else { + removeRoomMember(ws, connectionId); } // 向当前连接发送断开连接消息 @@ -328,6 +468,7 @@ function onOffer(ws: WebSocket, message: any): void { if (!connectionGroup.has(connectionId)) { connectionGroup.set(connectionId, { host: ws, participants: new Set() }); } + saveRoomMember(ws, connectionId); // 向所有其他客户端广播offer clients.forEach((_v, k) => { if (k == ws) { @@ -570,8 +711,11 @@ function RemoveHeartbeat(ws: WebSocket) { */ function onGetAllConnectionIds(): string[] { // 获取所有connectionId - const connectionIds = Array.from(connectionGroup.keys()); - return connectionIds; + const connectionIds = new Set(Array.from(connectionGroup.keys())); + rooms.forEach((_room, connectionId) => { + connectionIds.add(connectionId); + }); + return Array.from(connectionIds); } /** @@ -629,6 +773,17 @@ function onGetOnlineUsers(connectionId?: string): OnlineUser[] { return onlineUsers; } +function onGetRooms(connectionId?: string): RoomSnapshot[] { + const roomSnapshots: RoomSnapshot[] = []; + rooms.forEach((room, roomConnectionId) => { + if (connectionId && roomConnectionId !== connectionId) { + return; + } + roomSnapshots.push(toRoomSnapshot(room)); + }); + return roomSnapshots; +} + /** * 处理chat-message信令(1对多模式) * host的消息转发给所有participants,participant的消息转发给host @@ -677,5 +832,5 @@ function onMessage(ws: WebSocket, message: any): void { * 导出WebSocket处理器函数 */ export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate, onCallConnectionId, - onBroadcast, onGetAllConnectionIds, onGetOnlineUsers, AddHeartbeat, RemoveHeartbeat, onMessage, isHost, + onBroadcast, onGetAllConnectionIds, onGetOnlineUsers, onGetRooms, AddHeartbeat, RemoveHeartbeat, onMessage, isHost, broadcastToGroup, connectionGroup, onHostUserInfo, onInviteCall }; diff --git a/src/signaling.ts b/src/signaling.ts index 6983daf..5671bab 100644 --- a/src/signaling.ts +++ b/src/signaling.ts @@ -6,6 +6,7 @@ const router: express.Router = express.Router(); // 不需要会话ID的路由 router.get('/connection-ids', handler.getAllConnectionIds); router.get('/users', handler.getOnlineUsers); +router.get('/rooms', handler.onGetConnections); // 需要会话ID的路由 router.use(handler.checkSessionId); diff --git a/test/websockethandler.test.ts b/test/websockethandler.test.ts index 53a6534..fd9ec31 100644 --- a/test/websockethandler.test.ts +++ b/test/websockethandler.test.ts @@ -161,6 +161,35 @@ describe('websocket signaling test in private mode', () => { }); }); + test('save room and member info', async () => { + wsHandler.onHostUserInfo(client, { id: 'host-user', name: 'Host User', avatar: '/host.png' }); + wsHandler.onHostUserInfo(client2, { id: 'guest-user', name: 'Guest User', avatar: '/guest.png' }); + + expect(wsHandler.onGetRooms()).toEqual([ + expect.objectContaining({ + roomId: connectionId, + connectionId: connectionId, + userCount: 2, + members: expect.arrayContaining([ + expect.objectContaining({ + connectionId: connectionId, + role: 'host', + userId: 'host-user', + name: 'Host User', + avatar: '/host.png' + }), + expect.objectContaining({ + connectionId: connectionId, + role: 'participant', + userId: 'guest-user', + name: 'Guest User', + avatar: '/guest.png' + }) + ]) + }) + ]); + }); + test('send offer from session1', async () => { await wsHandler.onOffer(client, { connectionId: connectionId, sdp: testsdp }); const receiveOffer = new Offer(testsdp, Date.now(), true);