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 = `
+
+