成员列表
This commit is contained in:
@@ -663,12 +663,13 @@ class UIRenderer {
|
|||||||
|
|
||||||
const participantsMap = participants || {};
|
const participantsMap = participants || {};
|
||||||
const participantCount = Object.keys(participantsMap).length;
|
const participantCount = Object.keys(participantsMap).length;
|
||||||
// Host端使用participants计数,Participant端使用remoteUser状态
|
|
||||||
const isHost = participantCount > 0;
|
// 通话成员总数 = 本地用户(1) + participants中的条目数
|
||||||
const userCount = isHost ? (1 + participantCount) : (remoteUser.status !== 'offline' ? 2 : 1);
|
// 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) {
|
if (userCountElement) {
|
||||||
userCountElement.textContent = `通话成员 (${userCount})`;
|
userCountElement.textContent = `通话成员 (${userCount})`;
|
||||||
}
|
}
|
||||||
@@ -676,20 +677,27 @@ class UIRenderer {
|
|||||||
// 清空列表并重新渲染
|
// 清空列表并重新渲染
|
||||||
this.elements.userList.innerHTML = '';
|
this.elements.userList.innerHTML = '';
|
||||||
|
|
||||||
// 1. 渲染本地用户(主持人)
|
// 1. 渲染本地用户
|
||||||
|
// 判断当前用户角色:Host端localUser是主持人;Participant端localUser是参与者
|
||||||
this.elements.userList.appendChild(this.createLocalUserEntry(localUser));
|
this.elements.userList.appendChild(this.createLocalUserEntry(localUser));
|
||||||
|
|
||||||
// 2. Host端:渲染所有participants;Participant端:渲染单一remoteUser
|
// 2. 渲染远端成员
|
||||||
if (isHost) {
|
if (participantCount > 0) {
|
||||||
|
// 有participants数据(Host端或Participant端收到participants-sync后)
|
||||||
for (const [pid, p] of Object.entries(participantsMap)) {
|
for (const [pid, p] of Object.entries(participantsMap)) {
|
||||||
|
if (p.role === 'host') {
|
||||||
|
this.elements.userList.appendChild(this.createHostEntry(pid, p));
|
||||||
|
} else {
|
||||||
this.elements.userList.appendChild(this.createParticipantEntry(pid, p));
|
this.elements.userList.appendChild(this.createParticipantEntry(pid, p));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (remoteUser.status !== 'offline') {
|
} else if (remoteUser.status !== 'offline') {
|
||||||
|
// 兼容:Participant端未收到participants-sync时,使用remoteUser显示Host
|
||||||
this.elements.userList.appendChild(this.createRemoteUserEntry(remoteUser));
|
this.elements.userList.appendChild(this.createRemoteUserEntry(remoteUser));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建本地用户条目(主持人)
|
// 创建本地用户条目
|
||||||
createLocalUserEntry(localUser) {
|
createLocalUserEntry(localUser) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'flex items-center gap-3 p-2 rounded-lg hover:bg-white/5';
|
div.className = 'flex items-center gap-3 p-2 rounded-lg hover:bg-white/5';
|
||||||
@@ -701,12 +709,18 @@ class UIRenderer {
|
|||||||
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
// 根据是否为Host显示不同角色标签
|
||||||
|
const isHost = localUser.isHost;
|
||||||
|
const roleTag = isHost
|
||||||
|
? '<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>'
|
||||||
|
: '<span class="text-xs bg-purple-500 px-1.5 rounded ml-1">参与者</span>';
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<img src="${localUser.avatar}" class="w-10 h-10 rounded-full object-cover">
|
<img src="${localUser.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-sm font-medium">
|
<div class="text-sm font-medium">
|
||||||
${localUser.name}
|
${localUser.name}
|
||||||
<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>
|
${roleTag}
|
||||||
</div>
|
</div>
|
||||||
<div class="${mediaStatusClass}" data-field="localUser.mediaStatus">${mediaStatusText}</div>
|
<div class="${mediaStatusClass}" data-field="localUser.mediaStatus">${mediaStatusText}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -715,7 +729,36 @@ class UIRenderer {
|
|||||||
return div;
|
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
|
||||||
|
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="relative">
|
||||||
|
<img src="${hostInfo.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||||
|
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="text-sm font-medium">
|
||||||
|
${hostInfo.name}
|
||||||
|
<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>
|
||||||
|
</div>
|
||||||
|
<div class="${mediaStatusClass}">${mediaStatusText}</div>
|
||||||
|
</div>
|
||||||
|
${muteIconHtml}
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建远程用户条目(兼容回退:Participant端未收到participants-sync时显示Host)
|
||||||
createRemoteUserEntry(remoteUser) {
|
createRemoteUserEntry(remoteUser) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'flex items-center gap-3 p-2 rounded-lg bg-white/5';
|
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 muteIconHtml = !remoteUser.mediaState.audio
|
||||||
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
? '<i class="fas fa-microphone-slash text-gray-500 text-xs"></i>'
|
||||||
: '';
|
: '';
|
||||||
const speakingHtml = (remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio)
|
|
||||||
? '<div class="audio-wave w-6"><span></span><span></span><span></span><span></span><span></span></div>'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -736,13 +776,13 @@ class UIRenderer {
|
|||||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-sm font-medium">${remoteUser.name}</div>
|
<div class="text-sm font-medium">
|
||||||
|
${remoteUser.name}
|
||||||
|
<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>
|
||||||
|
</div>
|
||||||
<div class="${mediaStatusClass}">${mediaStatusText}</div>
|
<div class="${mediaStatusClass}">${mediaStatusText}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
${muteIconHtml}
|
${muteIconHtml}
|
||||||
${speakingHtml}
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,7 +341,13 @@ class CallStateManager {
|
|||||||
// 保存角色信息(host/participant)
|
// 保存角色信息(host/participant)
|
||||||
if (data && data.role) {
|
if (data && data.role) {
|
||||||
this.role = 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
|
// 连接建立后,更新状态为ongoing
|
||||||
this.state.session.status = 'ongoing';
|
this.state.session.status = 'ongoing';
|
||||||
@@ -401,6 +407,7 @@ class CallStateManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
||||||
|
this.broadcastParticipantsList();
|
||||||
};
|
};
|
||||||
|
|
||||||
// participant离开回调(host收到,房间仍然存在)
|
// participant离开回调(host收到,房间仍然存在)
|
||||||
@@ -423,6 +430,7 @@ class CallStateManager {
|
|||||||
// 通知UI更新,用participantId作为connectionId传给renderer
|
// 通知UI更新,用participantId作为connectionId传给renderer
|
||||||
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
|
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
|
||||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
||||||
|
this.broadcastParticipantsList();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 轨道事件回调
|
// 轨道事件回调
|
||||||
@@ -468,6 +476,7 @@ class CallStateManager {
|
|||||||
status: 'online'
|
status: 'online'
|
||||||
};
|
};
|
||||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
||||||
|
this.broadcastParticipantsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知UI远程流已更新
|
// 通知UI远程流已更新
|
||||||
@@ -523,10 +532,21 @@ class CallStateManager {
|
|||||||
this.state.participants[data.participantId].avatar = data.message.senderAvatar;
|
this.state.participants[data.participantId].avatar = data.message.senderAvatar;
|
||||||
}
|
}
|
||||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
||||||
|
this.broadcastParticipantsList();
|
||||||
}
|
}
|
||||||
// Participant端:从消息中提取Host用户信息并更新remoteUser
|
// Participant端:根据消息来源更新对应用户信息
|
||||||
if (!this.role || this.role !== 'host') {
|
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 = {
|
||||||
...this.state.session.remoteUser,
|
...this.state.session.remoteUser,
|
||||||
id: data.message.senderId,
|
id: data.message.senderId,
|
||||||
@@ -550,6 +570,8 @@ class CallStateManager {
|
|||||||
this.updateRemoteMedia(data.data, data.participantId);
|
this.updateRemoteMedia(data.data, data.participantId);
|
||||||
// 通知UI更新participants
|
// 通知UI更新participants
|
||||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
||||||
|
// Host端广播最新成员列表(含媒体状态)给所有Participant
|
||||||
|
this.broadcastParticipantsList();
|
||||||
} else if (data.type === 'user-info') {
|
} else if (data.type === 'user-info') {
|
||||||
// 处理用户信息更新
|
// 处理用户信息更新
|
||||||
console.log('收到用户信息:', data.data, 'from participant:', data.participantId);
|
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].name = data.data.name || '参与者';
|
||||||
this.state.participants[data.participantId].avatar = data.data.avatar || '/images/p2.png';
|
this.state.participants[data.participantId].avatar = data.data.avatar || '/images/p2.png';
|
||||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
||||||
|
this.broadcastParticipantsList();
|
||||||
} else {
|
} else {
|
||||||
// Participant端:更新单一remoteUser(Host的信息)
|
// Participant端:更新单一remoteUser(Host的信息)
|
||||||
this.state.session.remoteUser = {
|
this.state.session.remoteUser = {
|
||||||
@@ -580,6 +603,20 @@ class CallStateManager {
|
|||||||
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState });
|
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');
|
this.updateRemoteUserNetworkQuality('no_signal');
|
||||||
// 清理participants
|
// 清理participants
|
||||||
this.state.participants = {};
|
this.state.participants = {};
|
||||||
|
this.selfParticipantId = null;
|
||||||
this.connectionId = null;
|
this.connectionId = null;
|
||||||
this.role = null;
|
this.role = null;
|
||||||
this.state.session.status = 'ended';
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置编解码器偏好
|
* 设置编解码器偏好
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -457,8 +457,14 @@ function onMessage(ws: WebSocket, message: any): void {
|
|||||||
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage }));
|
participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage }));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// participant发送消息,转发给host,附带participantId以便host识别发送者
|
// participant发送消息,转发给host(附带participantId)和其他participants
|
||||||
group.host.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: chatMessage, participantId: senderParticipantId }));
|
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 }));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user