diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index 1bcbdac..afb9e73 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -663,12 +663,13 @@ class UIRenderer { const participantsMap = participants || {}; const participantCount = Object.keys(participantsMap).length; - // Host端使用participants计数,Participant端使用remoteUser状态 - const isHost = participantCount > 0; - const userCount = isHost ? (1 + participantCount) : (remoteUser.status !== 'offline' ? 2 : 1); + + // 通话成员总数 = 本地用户(1) + participants中的条目数 + // Host端participants只含其他participant;Participant端participants含host+其他participant + const userCount = 1 + participantCount; // 更新通话成员总数显示 - const userCountElement = this.elements.userList.closest('div').querySelector('h3.text-sm.font-medium.text-gray-400'); + const userCountElement = this.elements.userCountDisplay; if (userCountElement) { userCountElement.textContent = `通话成员 (${userCount})`; } @@ -676,20 +677,27 @@ class UIRenderer { // 清空列表并重新渲染 this.elements.userList.innerHTML = ''; - // 1. 渲染本地用户(主持人) + // 1. 渲染本地用户 + // 判断当前用户角色:Host端localUser是主持人;Participant端localUser是参与者 this.elements.userList.appendChild(this.createLocalUserEntry(localUser)); - // 2. Host端:渲染所有participants;Participant端:渲染单一remoteUser - if (isHost) { + // 2. 渲染远端成员 + if (participantCount > 0) { + // 有participants数据(Host端或Participant端收到participants-sync后) for (const [pid, p] of Object.entries(participantsMap)) { - this.elements.userList.appendChild(this.createParticipantEntry(pid, p)); + if (p.role === 'host') { + this.elements.userList.appendChild(this.createHostEntry(pid, p)); + } else { + this.elements.userList.appendChild(this.createParticipantEntry(pid, p)); + } } } else if (remoteUser.status !== 'offline') { + // 兼容:Participant端未收到participants-sync时,使用remoteUser显示Host this.elements.userList.appendChild(this.createRemoteUserEntry(remoteUser)); } } - // 创建本地用户条目(主持人) + // 创建本地用户条目 createLocalUserEntry(localUser) { const div = document.createElement('div'); div.className = 'flex items-center gap-3 p-2 rounded-lg hover:bg-white/5'; @@ -701,12 +709,18 @@ class UIRenderer { ? '' : ''; + // 根据是否为Host显示不同角色标签 + const isHost = localUser.isHost; + const roleTag = isHost + ? '主持人' + : '参与者'; + div.innerHTML = `
${localUser.name} - 主持人 + ${roleTag}
${mediaStatusText}
@@ -715,7 +729,36 @@ class UIRenderer { return div; } - // 创建远程用户条目(Participant端显示Host用) + // 创建Host条目(Participant端显示Host用) + createHostEntry(hostId, hostInfo) { + const div = document.createElement('div'); + div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5'; + div.dataset.userId = `host_${hostId}`; + + const mediaStatusText = !hostInfo.mediaState.audio ? '静音中' : (!hostInfo.mediaState.video ? '视频关闭' : '在线'); + const mediaStatusClass = (!hostInfo.mediaState.audio || !hostInfo.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400'; + const muteIconHtml = !hostInfo.mediaState.audio + ? '' + : ''; + + div.innerHTML = ` +
+ +
+
+
+
+ ${hostInfo.name} + 主持人 +
+
${mediaStatusText}
+
+ ${muteIconHtml} + `; + return div; + } + + // 创建远程用户条目(兼容回退:Participant端未收到participants-sync时显示Host) createRemoteUserEntry(remoteUser) { const div = document.createElement('div'); div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5'; @@ -726,9 +769,6 @@ class UIRenderer { const muteIconHtml = !remoteUser.mediaState.audio ? '' : ''; - const speakingHtml = (remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio) - ? '
' - : ''; div.innerHTML = `
@@ -736,13 +776,13 @@ class UIRenderer {
-
${remoteUser.name}
+
+ ${remoteUser.name} + 主持人 +
${mediaStatusText}
-
- ${muteIconHtml} - ${speakingHtml} -
+ ${muteIconHtml} `; return div; } diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index b995968..8aabe88 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -341,7 +341,13 @@ class CallStateManager { // 保存角色信息(host/participant) if (data && data.role) { this.role = data.role; - console.log(`Connected as ${this.role}`); + // 更新localUser的isHost标志 + this.state.session.localUser.isHost = (this.role === 'host'); + // 保存自身的participantId,用于从participants-sync中过滤自身 + if (data.participantId) { + this.selfParticipantId = data.participantId; + } + console.log(`Connected as ${this.role}, participantId: ${this.selfParticipantId}`); } // 连接建立后,更新状态为ongoing this.state.session.status = 'ongoing'; @@ -401,6 +407,7 @@ class CallStateManager { }; } this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + this.broadcastParticipantsList(); }; // participant离开回调(host收到,房间仍然存在) @@ -423,6 +430,7 @@ class CallStateManager { // 通知UI更新,用participantId作为connectionId传给renderer this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId }); this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + this.broadcastParticipantsList(); }; // 轨道事件回调 @@ -468,6 +476,7 @@ class CallStateManager { status: 'online' }; this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + this.broadcastParticipantsList(); } // 通知UI远程流已更新 @@ -523,10 +532,21 @@ class CallStateManager { this.state.participants[data.participantId].avatar = data.message.senderAvatar; } this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + this.broadcastParticipantsList(); } - // Participant端:从消息中提取Host用户信息并更新remoteUser + // Participant端:根据消息来源更新对应用户信息 if (!this.role || this.role !== 'host') { - if (data.message && data.message.senderId !== this.state.session.localUser.id) { + if (data.participantId && this.state.participants[data.participantId]) { + // 来自其他Participant的消息:更新participants中对应条目 + if (data.message.senderName) { + this.state.participants[data.participantId].name = data.message.senderName; + } + if (data.message.senderAvatar) { + this.state.participants[data.participantId].avatar = data.message.senderAvatar; + } + this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + } else if (data.message && data.message.senderId !== this.state.session.localUser.id) { + // 来自Host的消息:更新remoteUser this.state.session.remoteUser = { ...this.state.session.remoteUser, id: data.message.senderId, @@ -550,6 +570,8 @@ class CallStateManager { this.updateRemoteMedia(data.data, data.participantId); // 通知UI更新participants this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + // Host端广播最新成员列表(含媒体状态)给所有Participant + this.broadcastParticipantsList(); } else if (data.type === 'user-info') { // 处理用户信息更新 console.log('收到用户信息:', data.data, 'from participant:', data.participantId); @@ -569,6 +591,7 @@ class CallStateManager { this.state.participants[data.participantId].name = data.data.name || '参与者'; this.state.participants[data.participantId].avatar = data.data.avatar || '/images/p2.png'; this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + this.broadcastParticipantsList(); } else { // Participant端:更新单一remoteUser(Host的信息) this.state.session.remoteUser = { @@ -580,6 +603,20 @@ class CallStateManager { this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); } } + } else if (data.type === 'participants-sync') { + // Participant端:接收Host广播的完整成员列表 + if (this.role !== 'host' && data.data) { + console.log('收到成员列表同步:', data.data); + // 过滤掉自身条目,避免在列表中重复显示(自身已作为localUser显示) + const filtered = {}; + for (const [pid, pInfo] of Object.entries(data.data)) { + if (pid !== this.selfParticipantId) { + filtered[pid] = pInfo; + } + } + this.state.participants = filtered; + this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + } } }; @@ -640,6 +677,7 @@ class CallStateManager { this.updateRemoteUserNetworkQuality('no_signal'); // 清理participants this.state.participants = {}; + this.selfParticipantId = null; this.connectionId = null; this.role = null; this.state.session.status = 'ended'; @@ -660,6 +698,41 @@ class CallStateManager { } } + /** + * Host端广播完整成员列表给所有Participant + * 包含Host自身信息 + 所有Participant信息 + * Participant收到后可展示完整通话成员列表 + */ + broadcastParticipantsList() { + if (this.role !== 'host' || !this.renderstreaming) return; + + const memberList = {}; + + // 添加Host自身信息 + memberList['host'] = { + id: this.state.session.localUser.id, + name: this.state.session.localUser.name, + avatar: this.state.session.localUser.avatar, + mediaState: { ...this.state.session.localUser.mediaState }, + status: 'online', + role: 'host' + }; + + // 添加所有Participant信息 + for (const [pid, pInfo] of Object.entries(this.state.participants)) { + memberList[pid] = { + ...pInfo, + role: 'participant' + }; + } + + this.renderstreaming.sendMessage({ + type: 'participants-sync', + data: memberList + }); + console.log('Broadcast participants list:', Object.keys(memberList)); + } + /** * 设置编解码器偏好 */ diff --git a/WebApp/src/class/websockethandler.ts b/WebApp/src/class/websockethandler.ts index d235e2d..4acb86a 100644 --- a/WebApp/src/class/websockethandler.ts +++ b/WebApp/src/class/websockethandler.ts @@ -457,8 +457,14 @@ function onMessage(ws: WebSocket, message: any): void { participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage })); }); } else { - // participant发送消息,转发给host,附带participantId以便host识别发送者 + // participant发送消息,转发给host(附带participantId)和其他participants group.host.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage, participantId: senderParticipantId })); + // 同时转发给其他participants(排除发送者自身) + group.participants.forEach(participantWs => { + if (participantWs !== ws) { + participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage, participantId: senderParticipantId })); + } + }); } } }