优化信息
This commit is contained in:
@@ -7,7 +7,7 @@ import Offer from './offer';
|
|||||||
import Answer from './answer';
|
import Answer from './answer';
|
||||||
import Candidate from './candidate';
|
import Candidate from './candidate';
|
||||||
import { v4 as uuid } from 'uuid';
|
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';
|
import { log, LogLevel } from '../log';
|
||||||
/**
|
/**
|
||||||
* 断开连接记录类
|
* 断开连接记录类
|
||||||
@@ -1039,40 +1039,13 @@ function postCandidate(req: Request, res: Response): void {
|
|||||||
* description: 总房间数
|
* description: 总房间数
|
||||||
*/
|
*/
|
||||||
function onGetConnections(req: Request, res: Response): void {
|
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和链接用户信息
|
res.json({ rooms: wsRooms, totalRooms: wsRooms.length });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -53,12 +53,37 @@ interface OnlineUser {
|
|||||||
avatar: string;
|
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<string, RoomMemberInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接组映射
|
* 连接组映射
|
||||||
* 键: connectionId
|
* 键: connectionId
|
||||||
* 值: ConnectionGroup(1个host + 多个participants)
|
* 值: ConnectionGroup(1个host + 多个participants)
|
||||||
*/
|
*/
|
||||||
const connectionGroup: Map<string, ConnectionGroup> = new Map<string, ConnectionGroup>();
|
const connectionGroup: Map<string, ConnectionGroup> = new Map<string, ConnectionGroup>();
|
||||||
|
const rooms: Map<string, StoredRoom> = new Map<string, StoredRoom>();
|
||||||
|
|
||||||
function asAppWebSocket(ws: WebSocket): AppWebSocket {
|
function asAppWebSocket(ws: WebSocket): AppWebSocket {
|
||||||
return ws as AppWebSocket;
|
return ws as AppWebSocket;
|
||||||
@@ -89,7 +114,12 @@ function getUserInfo(ws: WebSocket): UserInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setUserInfo(ws: WebSocket, userInfo: UserInfo): void {
|
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 {
|
function safeSend(ws: WebSocket, payload: unknown): boolean {
|
||||||
@@ -111,6 +141,106 @@ function findParticipantSocket(group: ConnectionGroup, participantId: string): W
|
|||||||
return null;
|
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<string, RoomMemberInfo>()
|
||||||
|
};
|
||||||
|
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集合
|
* 获取或创建WebSocket会话的连接ID集合
|
||||||
* @param session WebSocket会话实例
|
* @param session WebSocket会话实例
|
||||||
@@ -140,6 +270,7 @@ function reset(mode: string): void {
|
|||||||
isPrivate = mode == "private";
|
isPrivate = mode == "private";
|
||||||
clients.clear();
|
clients.clear();
|
||||||
connectionGroup.clear();
|
connectionGroup.clear();
|
||||||
|
rooms.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,12 +332,16 @@ function remove(ws: WebSocket): void {
|
|||||||
group.participants.forEach(participantWs => {
|
group.participants.forEach(participantWs => {
|
||||||
safeSend(participantWs, { type: "disconnect", connectionId: connectionId, reason: "host-left" });
|
safeSend(participantWs, { type: "disconnect", connectionId: connectionId, reason: "host-left" });
|
||||||
});
|
});
|
||||||
|
rooms.delete(connectionId);
|
||||||
connectionGroup.delete(connectionId);
|
connectionGroup.delete(connectionId);
|
||||||
} else {
|
} else {
|
||||||
group.participants.delete(ws);
|
group.participants.delete(ws);
|
||||||
|
removeRoomMember(ws, connectionId);
|
||||||
// 包含participantId,让host能识别是哪个participant离开
|
// 包含participantId,让host能识别是哪个participant离开
|
||||||
safeSend(group.host, { type: "participant-left", connectionId: connectionId, participantId: getParticipantId(ws) });
|
safeSend(group.host, { type: "participant-left", connectionId: connectionId, participantId: getParticipantId(ws) });
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
removeRoomMember(ws, connectionId);
|
||||||
}
|
}
|
||||||
log(LogLevel.log, `Remove connectionId: ${connectionId}`);
|
log(LogLevel.log, `Remove connectionId: ${connectionId}`);
|
||||||
});
|
});
|
||||||
@@ -242,6 +377,7 @@ function onConnect(ws: WebSocket, connectionId: string): void {
|
|||||||
const connectionIds = getOrCreateConnectionIds(ws);
|
const connectionIds = getOrCreateConnectionIds(ws);
|
||||||
connectionIds.add(connectionId);
|
connectionIds.add(connectionId);
|
||||||
const role = polite ? 'participant' : 'host';
|
const role = polite ? 'participant' : 'host';
|
||||||
|
saveRoomMember(ws, connectionId);
|
||||||
safeSend(ws, { type: "connect", connectionId: connectionId, polite: polite, role: role, participantId: participantId });
|
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 => {
|
group.participants.forEach(participantWs => {
|
||||||
safeSend(participantWs, { type: "disconnect", connectionId: connectionId, reason: "host-left" });
|
safeSend(participantWs, { type: "disconnect", connectionId: connectionId, reason: "host-left" });
|
||||||
});
|
});
|
||||||
|
rooms.delete(connectionId);
|
||||||
connectionGroup.delete(connectionId);
|
connectionGroup.delete(connectionId);
|
||||||
log(LogLevel.log, `Host disconnected, room ${connectionId} deleted, notified ${group.participants.size} participants`);
|
log(LogLevel.log, `Host disconnected, room ${connectionId} deleted, notified ${group.participants.size} participants`);
|
||||||
} else {
|
} else {
|
||||||
// participant断开连接,从组中移除并通知host(使用participant-left类型,host不会关闭房间)
|
// participant断开连接,从组中移除并通知host(使用participant-left类型,host不会关闭房间)
|
||||||
group.participants.delete(ws);
|
group.participants.delete(ws);
|
||||||
|
removeRoomMember(ws, connectionId);
|
||||||
safeSend(group.host, { type: "participant-left", connectionId: connectionId, participantId: getParticipantId(ws) });
|
safeSend(group.host, { type: "participant-left", connectionId: connectionId, participantId: getParticipantId(ws) });
|
||||||
log(LogLevel.log, `Participant left connectionId: ${connectionId}, remaining participants: ${group.participants.size}`);
|
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)) {
|
if (!connectionGroup.has(connectionId)) {
|
||||||
connectionGroup.set(connectionId, { host: ws, participants: new Set<WebSocket>() });
|
connectionGroup.set(connectionId, { host: ws, participants: new Set<WebSocket>() });
|
||||||
}
|
}
|
||||||
|
saveRoomMember(ws, connectionId);
|
||||||
// 向所有其他客户端广播offer
|
// 向所有其他客户端广播offer
|
||||||
clients.forEach((_v, k) => {
|
clients.forEach((_v, k) => {
|
||||||
if (k == ws) {
|
if (k == ws) {
|
||||||
@@ -570,8 +711,11 @@ function RemoveHeartbeat(ws: WebSocket) {
|
|||||||
*/
|
*/
|
||||||
function onGetAllConnectionIds(): string[] {
|
function onGetAllConnectionIds(): string[] {
|
||||||
// 获取所有connectionId
|
// 获取所有connectionId
|
||||||
const connectionIds = Array.from(connectionGroup.keys());
|
const connectionIds = new Set<string>(Array.from(connectionGroup.keys()));
|
||||||
return connectionIds;
|
rooms.forEach((_room, connectionId) => {
|
||||||
|
connectionIds.add(connectionId);
|
||||||
|
});
|
||||||
|
return Array.from(connectionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -629,6 +773,17 @@ function onGetOnlineUsers(connectionId?: string): OnlineUser[] {
|
|||||||
return onlineUsers;
|
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对多模式)
|
* 处理chat-message信令(1对多模式)
|
||||||
* host的消息转发给所有participants,participant的消息转发给host
|
* host的消息转发给所有participants,participant的消息转发给host
|
||||||
@@ -677,5 +832,5 @@ function onMessage(ws: WebSocket, message: any): void {
|
|||||||
* 导出WebSocket处理器函数
|
* 导出WebSocket处理器函数
|
||||||
*/
|
*/
|
||||||
export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate, onCallConnectionId,
|
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 };
|
broadcastToGroup, connectionGroup, onHostUserInfo, onInviteCall };
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const router: express.Router = express.Router();
|
|||||||
// 不需要会话ID的路由
|
// 不需要会话ID的路由
|
||||||
router.get('/connection-ids', handler.getAllConnectionIds);
|
router.get('/connection-ids', handler.getAllConnectionIds);
|
||||||
router.get('/users', handler.getOnlineUsers);
|
router.get('/users', handler.getOnlineUsers);
|
||||||
|
router.get('/rooms', handler.onGetConnections);
|
||||||
|
|
||||||
// 需要会话ID的路由
|
// 需要会话ID的路由
|
||||||
router.use(handler.checkSessionId);
|
router.use(handler.checkSessionId);
|
||||||
|
|||||||
@@ -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 () => {
|
test('send offer from session1', async () => {
|
||||||
await wsHandler.onOffer(client, { connectionId: connectionId, sdp: testsdp });
|
await wsHandler.onOffer(client, { connectionId: connectionId, sdp: testsdp });
|
||||||
const receiveOffer = new Offer(testsdp, Date.now(), true);
|
const receiveOffer = new Offer(testsdp, Date.now(), true);
|
||||||
|
|||||||
Reference in New Issue
Block a user