diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index 34a319d..1bcbdac 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -120,7 +120,7 @@ class UIRenderer { this.renderLocalVideo(state.session.localUser, state.localStream); // 渲染本地视频 this.renderControlButtons(state.session.localUser.mediaState); // 渲染控制按钮 this.renderChatMessages(chatMessage.getMessageState().messages); // 渲染聊天消息 - this.renderUserList(state.session.localUser, state.session.remoteUser); // 渲染用户列表 + this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 渲染用户列表 this.renderHeader(state.session); // 渲染头部信息 // 初始化时检查远程流状态,显示或隐藏占位背景 if (this.elements.remoteVideoPlaceholder) { @@ -140,7 +140,7 @@ class UIRenderer { this.renderControlButtons(state.session.localUser.mediaState); // 渲染控制按钮 this.renderLocalVideo(state.session.localUser, state.localStream); // 渲染本地视频 this.renderLocalUserStatus(state.session.localUser); // 渲染本地用户状态 - this.renderUserList(state.session.localUser, state.session.remoteUser); // 渲染用户列表 + this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 渲染用户列表 break; case 'LOCAL_STREAM_OBTAINED': this.renderLocalStream(state.localStream); @@ -157,7 +157,7 @@ class UIRenderer { case 'REMOTE_MEDIA_CHANGE': // 远程媒体状态变化 - 更新远程视频和用户列表 this.renderRemoteVideo(state.session.remoteUser); // 渲染远程视频 - this.renderUserList(state.session.localUser, state.session.remoteUser); // 渲染用户列表 + this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 渲染用户列表 // Host端:精准更新发送者participant tile的占位背景 if (changes.participantId) { this.renderParticipantVideoPlaceholder(changes.participantId, !state.session.remoteUser.mediaState.video); @@ -165,7 +165,13 @@ class UIRenderer { break; case 'USER_LIST_UPDATE': // 用户列表更新 - 重新渲染用户列表 - this.renderUserList(changes.localUser, changes.remoteUser); + this.renderUserList(changes.localUser, changes.remoteUser, state.participants); + break; + case 'PARTICIPANTS_UPDATE': + // Participants信息变化 - 重新渲染用户列表并同步tile名称 + this.renderUserList(state.session.localUser, state.session.remoteUser, changes.participants || state.participants); + // 同步更新participant tile的名称标签 + this.syncParticipantTileNames(changes.participants || state.participants); break; case 'NETWORK_CHANGE': // 网络状态变化 - 渲染网络状态 @@ -271,7 +277,7 @@ class UIRenderer { } // 同步更新侧边栏用户列表 - this.renderUserList(this.stateManager.getState().session.localUser, remoteUser); + this.renderUserList(this.stateManager.getState().session.localUser, remoteUser, this.stateManager.getState().participants); // 当远程视频关闭时显示占位符 if (this.elements.remoteVideoPlaceholder) { @@ -439,10 +445,12 @@ class UIRenderer { `; tile.appendChild(placeholder); - // 参与者名称标签 + // 参与者名称标签(优先使用participants中的真实姓名) + const pInfo = this.stateManager.getState().participants[connectionId]; + const displayName = pInfo?.name || '参与者'; const label = document.createElement('div'); label.className = 'absolute bottom-3 left-3 glass px-3 py-1 rounded-full text-xs flex items-center gap-2'; - label.innerHTML = `Participant ${connectionId.slice(-4)}`; + label.innerHTML = `${displayName}`; tile.appendChild(label); // 在线标识 @@ -506,6 +514,32 @@ class UIRenderer { } } + // 同步更新所有participant tile的名称标签 + syncParticipantTileNames(participants) { + if (!participants) return; + const grid = this.elements.participantGrid; + if (!grid) return; + for (const [participantId, pInfo] of Object.entries(participants)) { + this.updateParticipantTileName(participantId, pInfo.name); + } + } + + // 更新指定participant tile的名称标签 + updateParticipantTileName(participantId, name) { + const grid = this.elements.participantGrid; + if (!grid) return; + const tile = grid.querySelector(`[data-participant-id="${participantId}"]`); + if (!tile) return; + const label = tile.querySelector('.absolute.bottom-3'); + if (label) { + const nameSpan = label.querySelector('span'); + if (nameSpan && name) { + nameSpan.textContent = name; + console.log(`Updated tile name for participant ${participantId}: ${name}`); + } + } + } + // 渲染Participant端的单一远端视频(Host画面) renderSingleRemoteStream(stream) { if (this.elements.remoteVideo && stream) { @@ -623,15 +657,15 @@ class UIRenderer { } } - // 渲染侧边栏用户列表 - renderUserList(localUser, remoteUser) { + // 渲染侧边栏用户列表(支持多Participant动态渲染) + renderUserList(localUser, remoteUser, participants) { if (!this.elements.userList) return; - // 计算通话成员总数 - let userCount = 1; // 至少有本地用户 - if (remoteUser.status !== 'offline') { - userCount++; // 如果远程用户在线,增加计数 - } + 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); // 更新通话成员总数显示 const userCountElement = this.elements.userList.closest('div').querySelector('h3.text-sm.font-medium.text-gray-400'); @@ -639,117 +673,114 @@ class UIRenderer { userCountElement.textContent = `通话成员 (${userCount})`; } - // 渲染本地用户 - const localUserElement = this.elements.userList.querySelector('[data-user-id="local"]'); - if (localUserElement) { - // 渲染本地用户头像 - const localAvatar = localUserElement.querySelector('img[data-field="localUser.avatar"]'); - if (localAvatar) { - localAvatar.src = localUser.avatar; - } - // 渲染本地用户名字 - const localName = localUserElement.querySelector('[data-field="localUser.name"]'); - if (localName) { - localName.textContent = localUser.name; - } - // 渲染本地用户媒体状态 - const localMediaStatus = localUserElement.querySelector('[data-field="localUser.mediaStatus"]'); - if (localMediaStatus) { - if (!localUser.mediaState.audio) { - localMediaStatus.textContent = '静音中'; - localMediaStatus.className = 'text-xs text-gray-500'; - } else if (!localUser.mediaState.video) { - localMediaStatus.textContent = '视频关闭'; - localMediaStatus.className = 'text-xs text-gray-500'; - } else { - localMediaStatus.textContent = '在线'; - localMediaStatus.className = 'text-xs text-green-400'; - } - } - // 渲染本地用户静音图标 - const localMuteIcon = localUserElement.querySelector('[data-field="localUser.muteIcon"]'); - if (localMuteIcon) { - if (!localUser.mediaState.audio) { - localMuteIcon.classList.remove('hidden'); - localMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs'; - } else { - localMuteIcon.classList.add('hidden'); - } - } - } + // 清空列表并重新渲染 + this.elements.userList.innerHTML = ''; - // 渲染远程用户 - const remoteUserElement = this.elements.userList.querySelector('[data-user-id="remote"]'); - if (remoteUserElement) { - // 未连接时不显示远程用户信息 - if (remoteUser.status === 'offline') { - remoteUserElement.classList.add('hidden'); - } else { - // 连接后显示远程用户信息并更新数据 - remoteUserElement.classList.remove('hidden'); + // 1. 渲染本地用户(主持人) + this.elements.userList.appendChild(this.createLocalUserEntry(localUser)); - // 渲染远程用户头像 - const remoteAvatar = remoteUserElement.querySelector('img[data-field="remoteUser.avatar"]'); - if (remoteAvatar) { - remoteAvatar.src = remoteUser.avatar; - } - // 渲染远程用户名字 - const remoteName = remoteUserElement.querySelector('[data-field="remoteUser.name"]'); - if (remoteName) { - remoteName.textContent = remoteUser.name; - } - // 渲染远程用户媒体状态 - const remoteMediaStatus = remoteUserElement.querySelector('[data-field="remoteUser.mediaStatus"]'); - if (remoteMediaStatus) { - if (!remoteUser.mediaState.audio) { - remoteMediaStatus.textContent = '静音中'; - remoteMediaStatus.className = 'text-xs text-gray-500'; - } else if (!remoteUser.mediaState.video) { - remoteMediaStatus.textContent = '视频关闭'; - remoteMediaStatus.className = 'text-xs text-gray-500'; - } else { - remoteMediaStatus.textContent = '在线'; - remoteMediaStatus.className = 'text-xs text-green-400'; - } - } - // 渲染远程用户在线状态指示器 - const remoteStatusIndicator = remoteUserElement.querySelector('.absolute.-bottom-1.-right-1.w-3.h-3'); - if (remoteStatusIndicator) { - if (remoteUser.status === 'online') { - // 根据网络质量设置状态指示器颜色 - if (remoteUser.networkQuality === 'no_signal') { - remoteStatusIndicator.classList.remove('hidden'); - remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-gray-500 rounded-full border-2 border-slate-900'; - } else { - remoteStatusIndicator.classList.remove('hidden'); - remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900'; - } - } else { - remoteStatusIndicator.classList.add('hidden'); - } - } - // 渲染远程用户静音图标 - const remoteMuteIcon = remoteUserElement.querySelector('[data-field="remoteUser.muteIcon"]'); - if (remoteMuteIcon) { - if (!remoteUser.mediaState.audio) { - remoteMuteIcon.classList.remove('hidden'); - remoteMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs'; - } else { - remoteMuteIcon.classList.add('hidden'); - } - } - // 渲染远程用户说话状态指示器 - const remoteSpeakingIndicator = remoteUserElement.querySelector('[data-field="remoteUser.speakingIndicator"]'); - if (remoteSpeakingIndicator) { - if (remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio) { - remoteSpeakingIndicator.classList.remove('hidden'); - } else { - remoteSpeakingIndicator.classList.add('hidden'); - } - } + // 2. Host端:渲染所有participants;Participant端:渲染单一remoteUser + if (isHost) { + for (const [pid, p] of Object.entries(participantsMap)) { + this.elements.userList.appendChild(this.createParticipantEntry(pid, p)); } + } else if (remoteUser.status !== 'offline') { + 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'; + div.dataset.userId = 'local'; + + const mediaStatusText = !localUser.mediaState.audio ? '静音中' : (!localUser.mediaState.video ? '视频关闭' : '在线'); + const mediaStatusClass = (!localUser.mediaState.audio || !localUser.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400'; + const muteIconHtml = !localUser.mediaState.audio + ? '' + : ''; + + div.innerHTML = ` + +
+
+ ${localUser.name} + 主持人 +
+
${mediaStatusText}
+
+ ${muteIconHtml} + `; + return div; + } + + // 创建远程用户条目(Participant端显示Host用) + createRemoteUserEntry(remoteUser) { + const div = document.createElement('div'); + div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5'; + div.dataset.userId = 'remote'; + + const mediaStatusText = !remoteUser.mediaState.audio ? '静音中' : (!remoteUser.mediaState.video ? '视频关闭' : '在线'); + const mediaStatusClass = (!remoteUser.mediaState.audio || !remoteUser.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400'; + const muteIconHtml = !remoteUser.mediaState.audio + ? '' + : ''; + const speakingHtml = (remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio) + ? '
' + : ''; + + div.innerHTML = ` +
+ +
+
+
+
${remoteUser.name}
+
${mediaStatusText}
+
+
+ ${muteIconHtml} + ${speakingHtml} +
+ `; + return div; + } + + // 创建Participant条目(Host端显示每个Participant) + createParticipantEntry(participantId, participant) { + const div = document.createElement('div'); + div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5'; + div.dataset.userId = `participant_${participantId}`; + + const mediaStatusText = !participant.mediaState.audio ? '静音中' : (!participant.mediaState.video ? '视频关闭' : '在线'); + const mediaStatusClass = (!participant.mediaState.audio || !participant.mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400'; + const muteIconHtml = !participant.mediaState.audio + ? '' + : ''; + const speakingHtml = (participant.mediaState.isSpeaking && participant.mediaState.audio) + ? '
' + : ''; + + div.innerHTML = ` +
+ +
+
+
+
+ ${participant.name} + 参与者 +
+
${mediaStatusText}
+
+
+ ${muteIconHtml} + ${speakingHtml} +
+ `; + return div; + } // 在renderer.js中添加方法 // 获取视频流分辨率 getVideoResolution(track) { diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index 41e696b..84ccf13 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -20,7 +20,8 @@ class CallStateManager { }, localStream: null, // MediaStream 对象 remoteStream: null, // 单路远端流(兼容旧逻辑,participant端使用) - remoteStreams: {} // 多路远端流 Map: { connectionId: MediaStream }(host端使用) + remoteStreams: {}, // 多路远端流 Map: { connectionId: MediaStream }(host端使用) + participants: {} // 多Participant用户信息 Map: { participantId: { id, name, avatar, mediaState, status } }(host端使用) }; // 监听器数组 @@ -372,6 +373,21 @@ class CallStateManager { this.hangUp(); // 房间已关闭,挂断连接 }; + // participant加入回调(host收到,新participant加入房间) + this.renderstreaming.onParticipantJoined = (participantId) => { + console.log(`Participant joined: ${participantId}`); + if (!this.state.participants[participantId]) { + this.state.participants[participantId] = { + id: '', + name: '参与者', + avatar: '/images/p2.png', + mediaState: { audio: false, video: false, isSpeaking: false }, + status: 'online' + }; + } + this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + }; + // participant离开回调(host收到,房间仍然存在) this.renderstreaming.onParticipantLeft = (participantId) => { console.log(`Participant left: ${participantId}, room still active`); @@ -387,8 +403,11 @@ class CallStateManager { this.state.remoteStream.getTracks().forEach(track => track.stop()); this.state.remoteStream = null; } + // 清理该 participant 的用户信息 + delete this.state.participants[participantId]; // 通知UI更新,用participantId作为connectionId传给renderer this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId }); + this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); }; // 轨道事件回调 @@ -424,6 +443,18 @@ class CallStateManager { targetStream.addTrack(data.track); console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId); + // Host端兜底:确保participants中有该participant条目 + if (isHost && !this.state.participants[trackParticipantId]) { + this.state.participants[trackParticipantId] = { + id: '', + name: '参与者', + avatar: '/images/p2.png', + mediaState: { audio: false, video: false, isSpeaking: false }, + status: 'online' + }; + this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); + } + // 通知UI远程流已更新 this.notify({ type: 'REMOTE_STREAM_OBTAINED', @@ -467,32 +498,72 @@ class CallStateManager { // 处理聊天 // 添加到列表并更新UI chatMessage.handleChatMessage(data.message); - // 从消息中提取用户信息并更新remoteUser - if (data.message && data.message.senderId !== this.state.session.localUser.id) { - this.state.session.remoteUser = { - ...this.state.session.remoteUser, - id: data.message.senderId, - name: data.message.senderName, - avatar: data.message.senderAvatar - }; - this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); + // Host端:按participantId更新对应用户信息 + if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) { + this.state.participants[data.participantId].id = data.message.senderId; + 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 }); + } + // Participant端:从消息中提取Host用户信息并更新remoteUser + if (!this.role || this.role !== 'host') { + if (data.message && data.message.senderId !== this.state.session.localUser.id) { + this.state.session.remoteUser = { + ...this.state.session.remoteUser, + id: data.message.senderId, + name: data.message.senderName, + avatar: data.message.senderAvatar + }; + this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); + } } } else if (data.type === 'media-state-changed') { // 处理媒体状态变化 console.log('收到媒体状态变化:', data.data, 'from participant:', data.participantId); - // 更新远程用户的媒体状态,传递participantId以便精准定位 + // Host端:同步更新participants中对应participant的mediaState + if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) { + this.state.participants[data.participantId].mediaState = { + ...this.state.participants[data.participantId].mediaState, + ...data.data + }; + } + // 更新远端媒体状态(兼容Participant端) this.updateRemoteMedia(data.data, data.participantId); + // 通知UI更新participants + this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); } else if (data.type === 'user-info') { // 处理用户信息更新 - console.log('收到用户信息:', data.data); + console.log('收到用户信息:', data.data, 'from participant:', data.participantId); if (data.data) { - this.state.session.remoteUser = { - ...this.state.session.remoteUser, - id: data.data.id || this.state.session.remoteUser.id, - name: data.data.name || this.state.session.remoteUser.name, - avatar: data.data.avatar || this.state.session.remoteUser.avatar - }; - this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); + if (data.participantId && this.role === 'host') { + // Host端:按participantId存储到participants Map + if (!this.state.participants[data.participantId]) { + this.state.participants[data.participantId] = { + id: '', + name: '参与者', + avatar: '/images/p2.png', + mediaState: { audio: false, video: false, isSpeaking: false }, + status: 'online' + }; + } + this.state.participants[data.participantId].id = data.data.id || ''; + 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 }); + } else { + // Participant端:更新单一remoteUser(Host的信息) + this.state.session.remoteUser = { + ...this.state.session.remoteUser, + id: data.data.id || this.state.session.remoteUser.id, + name: data.data.name || this.state.session.remoteUser.name, + avatar: data.data.avatar || this.state.session.remoteUser.avatar + }; + this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); + } } } @@ -552,6 +623,8 @@ class CallStateManager { // 更新远程用户状态为离线 this.updateRemoteUserStatus('offline'); this.updateRemoteUserNetworkQuality('no_signal'); + // 清理participants + this.state.participants = {}; this.connectionId = null; this.role = null; this.state.session.status = 'ended';