feat(websocket): 优化投屏状态管理并新增全员禁麦功能
- 重构 Redis 投屏状态存储,统一使用 screenShareOwnerUid 字段 - 移除冗余的 screenShareUid 字段,简化房间状态数据结构 - 新增 client_mute_all_students 消息类型支持全员禁麦功能 - 实现创建者权限验证的全员禁麦逻辑,排除老师角色用户 - 优化 Tauri 桌面应用悬浮窗创建接口,支持状态参数传递 - 添加悬浮窗关闭功能及教师端静音状态同步机制 - 修复学生端界面中投屏状态变量命名不一致问题 - 统一按钮样式规范,提升界面组件一致性
This commit is contained in:
@ -247,19 +247,14 @@ export class MeetingRedisService {
|
||||
/**
|
||||
* 设置投屏状态
|
||||
* @param roomId - 房间 ID
|
||||
* @param screenShareUid - 投屏人短 UID,null 表示停止投屏
|
||||
* @param screenShareOwnerUid - 投屏人短 UID,null 表示停止投屏
|
||||
*/
|
||||
async setScreenSharing(roomId: string, screenShareUid: number | null, screenShareOwnerUid?: number | null): Promise<void> {
|
||||
async setScreenSharing(roomId: string, screenShareOwnerUid: number | null): Promise<void> {
|
||||
const key = `${this.KEY_PREFIX.ROOM_STATE}${roomId}`;
|
||||
if (screenShareUid === null) {
|
||||
await this.getClient().hdel(key, 'screenShareUid');
|
||||
if (screenShareOwnerUid === null) {
|
||||
await this.getClient().hdel(key, 'screenShareOwnerUid');
|
||||
} else {
|
||||
const hm: Record<string, string> = { screenShareUid: String(screenShareUid) };
|
||||
if (typeof screenShareOwnerUid === 'number' && Number.isFinite(screenShareOwnerUid) && screenShareOwnerUid > 0) {
|
||||
hm.screenShareOwnerUid = String(screenShareOwnerUid);
|
||||
}
|
||||
await this.getClient().hset(key, hm);
|
||||
await this.getClient().hset(key, { screenShareOwnerUid: String(screenShareOwnerUid) });
|
||||
await this.getClient().expire(key, 24 * 60 * 60);
|
||||
}
|
||||
}
|
||||
@ -297,7 +292,6 @@ export class MeetingRedisService {
|
||||
async getRoomState(roomId: string): Promise<{
|
||||
classStatus: 'finished' | 'in_class' | 'not_started';
|
||||
speakerUid?: number;
|
||||
screenShareUid?: number;
|
||||
screenShareOwnerUid?: number;
|
||||
teacherUid?: number;
|
||||
endTimestamp?: number;
|
||||
@ -306,14 +300,12 @@ export class MeetingRedisService {
|
||||
const map = await this.getClient().hgetall(key);
|
||||
const status = (map?.classStatus as any) || 'not_started';
|
||||
const speakerUid = map?.speakerUid ? Number(map.speakerUid) : undefined;
|
||||
const screenShareUid = map?.screenShareUid ? Number(map.screenShareUid) : undefined;
|
||||
const screenShareOwnerUid = map?.screenShareOwnerUid ? Number(map.screenShareOwnerUid) : undefined;
|
||||
const teacherUid = map?.teacherUid ? Number(map.teacherUid) : undefined;
|
||||
const endTimestamp = map?.endTimestamp ? Number(map.endTimestamp) : undefined;
|
||||
return {
|
||||
classStatus: status,
|
||||
...(Number.isFinite(speakerUid) ? { speakerUid } : {}),
|
||||
...(Number.isFinite(screenShareUid) ? { screenShareUid } : {}),
|
||||
...(Number.isFinite(screenShareOwnerUid) ? { screenShareOwnerUid } : {}),
|
||||
...(Number.isFinite(teacherUid) ? { teacherUid } : {}),
|
||||
...(Number.isFinite(endTimestamp) ? { endTimestamp } : {}),
|
||||
|
||||
@ -250,7 +250,7 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
const shortUid = Number(socket.data.user?.shortUid || 0);
|
||||
const userName = socket.data.user?.userName || data.userName || '用户';
|
||||
const isHost = socket.data.user?.role !== 0;
|
||||
|
||||
|
||||
const joinMode: JoinMode =
|
||||
data?.joinMode === 'classroom' || data?.joinMode === 'monitor' || data?.joinMode === 'student' ? data.joinMode : isHost ? 'classroom' : 'student';
|
||||
|
||||
@ -363,7 +363,6 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
isAudioMuted: userState.isAudioMuted,
|
||||
isVideoMuted: userState.isVideoMuted,
|
||||
classStatus: roomState.classStatus,
|
||||
screenShareUid: roomState.screenShareUid,
|
||||
screenShareOwnerUid: roomState.screenShareOwnerUid,
|
||||
speakerUid,
|
||||
teacherUid,
|
||||
@ -501,6 +500,44 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
this.log(`用户 ${data.targetUid} 已被禁麦,房间 ${data.roomId}`);
|
||||
}
|
||||
|
||||
@SubscribeMessage<ClientToServerMessageType>('client_mute_all_students')
|
||||
public async handleMuteAllStudents(@MessageBody() data: { roomId: string }, @ConnectedSocket() socket: MeetingSocket): Promise<void> {
|
||||
// 基础参数校验
|
||||
if (!data?.roomId) {
|
||||
this.logger.warn({}, '[会议] mute_all_students 数据不完整', 'MeetingWebSocket');
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限校验:仅创建者/管理员可全员禁麦
|
||||
const isHost = socket.data.user?.role !== 0;
|
||||
if (!isHost) {
|
||||
this.logger.warn({}, `[会议] 非创建者尝试全员禁麦:userId=${socket.data.user?.userId}`, 'MeetingWebSocket');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取房间内所有用户的 socketIds
|
||||
const sockets = await this.server?.in(data.roomId).fetchSockets();
|
||||
if (!sockets || sockets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历所有用户,排除老师(创建者),并行执行所有操作
|
||||
const promises = sockets.map(async (s) => {
|
||||
const targetUid = s.data.user?.shortUid;
|
||||
// 排除老师自己(role === 0 表示学生)
|
||||
if (s.data.user?.role === 0 && Number.isFinite(targetUid)) {
|
||||
// 下发禁麦通知
|
||||
this.server?.to(s.id).emit('message', { type: 'sev_mute_audio', data: { fromRoomId: data.roomId, targetUid: targetUid! } });
|
||||
// 写入 Redis:下次加入/重连时恢复禁麦状态
|
||||
return this.redisService.setUserState(data.roomId, targetUid!, { isAudioMuted: true }).catch(() => {});
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
this.log(`全员禁麦成功:roomId=${data.roomId}`);
|
||||
}
|
||||
|
||||
@SubscribeMessage<ClientToServerMessageType>('client_unmute_audio')
|
||||
public async handleUnmuteAudio(@MessageBody() data: MuteUserData, @ConnectedSocket() socket: MeetingSocket): Promise<void> {
|
||||
// 基础参数校验
|
||||
@ -679,7 +716,7 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
return;
|
||||
}
|
||||
// 保存投屏状态到 Redis
|
||||
await this.redisService.setScreenSharing(data.roomId, screenUid, targetUid);
|
||||
await this.redisService.setScreenSharing(data.roomId, targetUid);
|
||||
// 广播给整个房间:有人开始投屏
|
||||
this.server?.to(data.roomId).emit('message', { type: 'sev_start_screen_share', data: { fromRoomId: data.roomId, targetUid, screenUid } });
|
||||
this.log(`创建者开始投屏:roomId=${data.roomId}, shortUid=${targetUid}, screenUid=${screenUid}`);
|
||||
@ -733,7 +770,10 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
|
||||
if (monitorSocketIds.length > 0) {
|
||||
// 发送给所有监控端的 socket
|
||||
ns.to(monitorSocketIds).emit('message', { type: 'sev_student_order_changed', data: { fromRoomId: data.roomId, studentShortIds: data.studentShortIds } });
|
||||
ns.to(monitorSocketIds).emit('message', {
|
||||
type: 'sev_student_order_changed',
|
||||
data: { fromRoomId: data.roomId, studentShortIds: data.studentShortIds },
|
||||
});
|
||||
}
|
||||
}
|
||||
this.log(`创建者更新了学生排序:roomId=${data.roomId}, 学生数量=${data.studentShortIds.length}`);
|
||||
|
||||
@ -113,6 +113,8 @@ export interface MeetingClientToServerEvents {
|
||||
client_mute_audio: (data: MeetingMuteUserData) => void;
|
||||
/** 创建者解除禁麦 */
|
||||
client_unmute_audio: (data: MeetingMuteUserData) => void;
|
||||
/** 创建者全员禁麦(不包括老师) */
|
||||
client_mute_all_students: (data: { roomId: string }) => void;
|
||||
/** 创建者禁视频 */
|
||||
client_mute_video: (data: MeetingMuteUserData) => void;
|
||||
/** 创建者解除禁视频 */
|
||||
@ -159,12 +161,11 @@ export interface MeetingJoinRoomSuccessData {
|
||||
isVideoMuted: boolean;
|
||||
/** 课堂状态:not_started(未开始)/ in_class(上课中)/ finished(已下课) */
|
||||
classStatus: 'finished' | 'in_class' | 'not_started';
|
||||
/** 投屏状态:正在投屏的用户短 UID,undefined 表示无人投屏 */
|
||||
screenShareUid?: number;
|
||||
/** 投屏状态:正在投屏的用户短 UID,undefined表示无人投屏 */
|
||||
screenShareOwnerUid?: number;
|
||||
/** 上台的短uid,默认为老师的uid */
|
||||
/** 上台的短 uid,默认为老师的 uid */
|
||||
speakerUid: number;
|
||||
/** 老师的短uid */
|
||||
/** 老师的短 uid */
|
||||
teacherUid: number;
|
||||
/** 房间结束时间(毫秒时间戳) */
|
||||
endTimestamp?: number;
|
||||
@ -268,6 +269,7 @@ export type ClientToServerMessageType =
|
||||
| 'client_join_room' // 加入会议房间
|
||||
| 'client_kick_user' // 踢出指定用户(仅创建者)
|
||||
| 'client_leave_room' // 离开会议房间
|
||||
| 'client_mute_all_students' // 全员禁麦(不包括老师,仅创建者)
|
||||
| 'client_mute_audio' // 禁麦指定用户(仅创建者)
|
||||
| 'client_mute_video' // 禁视频指定用户(仅创建者)
|
||||
| 'client_set_main_video' // 设置主屏用户
|
||||
|
||||
Reference in New Issue
Block a user