From c89b22d3206b4a953c650e8d8996d3913bd16657 Mon Sep 17 00:00:00 2001
From: stary <834207172@qq.com>
Date: Sun, 24 May 2026 13:29:54 +0800
Subject: [PATCH] ++
---
client/public/renderer-ui.js | 189 +++++++++++
client/public/renderer.js | 621 +++++++++++------------------------
2 files changed, 373 insertions(+), 437 deletions(-)
create mode 100644 client/public/renderer-ui.js
diff --git a/client/public/renderer-ui.js b/client/public/renderer-ui.js
new file mode 100644
index 0000000..6990cb6
--- /dev/null
+++ b/client/public/renderer-ui.js
@@ -0,0 +1,189 @@
+const DEFAULT_NETWORK_QUALITY = {
+ label: '\u672a\u77e5',
+ statusIconClass: 'fas fa-question-circle text-gray-400',
+ statusTextClass: 'text-gray-400',
+ headerIconClass: 'fas fa-signal text-gray-400',
+ indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse',
+ connectionTextClass: 'text-gray-400'
+};
+
+const NETWORK_QUALITY_DISPLAY = {
+ excellent: {
+ label: '\u4f18\u79c0',
+ statusIconClass: 'fas fa-check-circle text-green-400',
+ statusTextClass: 'text-green-400',
+ headerIconClass: 'fas fa-signal text-green-400',
+ indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse',
+ connectionTextClass: 'text-green-400'
+ },
+ good: {
+ label: '\u826f\u597d',
+ statusIconClass: 'fas fa-signal text-blue-400',
+ statusTextClass: 'text-blue-400',
+ headerIconClass: 'fas fa-signal text-green-500',
+ indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse',
+ connectionTextClass: 'text-blue-400'
+ },
+ fair: {
+ label: '\u4e00\u822c',
+ statusIconClass: 'fas fa-exclamation-circle text-yellow-500',
+ statusTextClass: 'text-yellow-500',
+ headerIconClass: 'fas fa-signal text-yellow-400',
+ indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse',
+ connectionTextClass: 'text-yellow-500'
+ },
+ poor: {
+ label: '\u8f83\u5dee',
+ statusIconClass: 'fas fa-exclamation-triangle text-red-500',
+ statusTextClass: 'text-red-500',
+ headerIconClass: 'fas fa-signal text-red-400',
+ indicatorClass: 'w-2 h-2 bg-green-500 rounded-full animate-pulse',
+ connectionTextClass: 'text-red-500'
+ },
+ no_signal: {
+ label: '\u65e0\u4fe1\u53f7',
+ statusIconClass: 'fas fa-times-circle text-gray-500',
+ statusTextClass: 'text-gray-500',
+ headerIconClass: 'fas fa-signal text-gray-400',
+ indicatorClass: 'w-2 h-2 bg-gray-500 rounded-full',
+ connectionTextClass: 'text-gray-500'
+ }
+};
+
+function getRoleTagMarkup(user, role) {
+ if (role === 'local') {
+ return user.isHost
+ ? '\u4e3b\u6301\u4eba'
+ : '\u53c2\u4e0e\u8005';
+ }
+
+ if (role === 'participant') {
+ return '\u53c2\u4e0e\u8005';
+ }
+
+ return '\u4e3b\u6301\u4eba';
+}
+
+function getDatasetUserId(role, id) {
+ switch (role) {
+ case 'local':
+ return 'local';
+ case 'remote':
+ return 'remote';
+ case 'host':
+ return `host_${id}`;
+ case 'participant':
+ return `participant_${id}`;
+ default:
+ return role;
+ }
+}
+
+function getAvatarMarkup(user, role) {
+ if (role === 'local') {
+ return `
`;
+ }
+
+ return `
+
+

+
+
+ `;
+}
+
+function getRightMarkup(mediaState, role, muteIconMarkup) {
+ if (role !== 'participant') {
+ return muteIconMarkup;
+ }
+
+ const speakingMarkup = (mediaState.isSpeaking && mediaState.audio)
+ ? '
'
+ : '';
+
+ return `
+
+ ${muteIconMarkup}
+ ${speakingMarkup}
+
+ `;
+}
+
+export function getCallTitle(connectionId) {
+ return `\u901a\u8bdd (${connectionId || ''})`;
+}
+
+export function getRemoteVideoPlaceholderText(isVideoEnabled) {
+ return isVideoEnabled
+ ? {
+ title: '\u7b49\u5f85\u5bf9\u65b9\u8fde\u63a5...',
+ subtitle: '\u8bf7\u786e\u8ba4\u5bf9\u65b9\u5df2\u52a0\u5165\u901a\u8bdd'
+ }
+ : {
+ title: '\u5bf9\u65b9\u6444\u50cf\u5934\u5df2\u5173\u95ed',
+ subtitle: '\u5bf9\u65b9\u6682\u65f6\u5173\u95ed\u4e86\u89c6\u9891'
+ };
+}
+
+export function getNetworkQualityDisplay(quality) {
+ return NETWORK_QUALITY_DISPLAY[quality] || DEFAULT_NETWORK_QUALITY;
+}
+
+export function getMediaStatusMeta(mediaState) {
+ if (!mediaState.audio) {
+ return {
+ text: '\u9759\u97f3\u4e2d',
+ className: 'text-xs text-gray-500',
+ muteIconClass: 'fas fa-microphone-slash text-gray-500 text-xs',
+ showMuteIcon: true
+ };
+ }
+
+ if (!mediaState.video) {
+ return {
+ text: '\u89c6\u9891\u5173\u95ed',
+ className: 'text-xs text-gray-500',
+ muteIconClass: 'fas fa-microphone-slash text-gray-500 text-xs',
+ showMuteIcon: false
+ };
+ }
+
+ return {
+ text: '\u5728\u7ebf',
+ className: 'text-xs text-green-400',
+ muteIconClass: 'fas fa-microphone-slash text-gray-500 text-xs',
+ showMuteIcon: false
+ };
+}
+
+export function buildUserCountLabel(userCount) {
+ return `\u901a\u8bdd\u6210\u5458 (${userCount})`;
+}
+
+export function createUserEntryElement({ user, role, id }) {
+ const entry = document.createElement('div');
+ const mediaMeta = getMediaStatusMeta(user.mediaState);
+ const muteIconMarkup = mediaMeta.showMuteIcon
+ ? ``
+ : '';
+ const baseClass = 'flex items-center gap-3 p-2 rounded-lg';
+ const dataFieldAttr = role === 'local' ? ' data-field="localUser.mediaStatus"' : '';
+
+ entry.className = role === 'local'
+ ? `${baseClass} hover:bg-white/5`
+ : `${baseClass} bg-white/5`;
+ entry.dataset.userId = getDatasetUserId(role, id);
+ entry.innerHTML = `
+ ${getAvatarMarkup(user, role)}
+
+
+ ${user.name}
+ ${getRoleTagMarkup(user, role)}
+
+
${mediaMeta.text}
+
+ ${getRightMarkup(user.mediaState, role, muteIconMarkup)}
+ `;
+
+ return entry;
+}
diff --git a/client/public/renderer.js b/client/public/renderer.js
index 9ff7af8..36f287f 100644
--- a/client/public/renderer.js
+++ b/client/public/renderer.js
@@ -1,11 +1,19 @@
/**
- * UI渲染器
- * 负责将状态映射到DOM,与状态管理解耦
+ * UI婵炴挸寮堕悡瀣闯?
+ * 閻犳劗鍠曢惌妤冧焊閸℃瑥笑闁诡兛鐒﹀Σ褏浜搁崟顐㈢厒DOM闁挎稑濂旂粭宀勬偐閼哥鍋撴担渚悁闁荤偛妫滆闁?
*/
import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js';
import { mockCallSession } from './models.js';
import chatMessage from './chatmessage.js';
import store from './store.js';
+import {
+ buildUserCountLabel,
+ createUserEntryElement,
+ getCallTitle,
+ getMediaStatusMeta,
+ getNetworkQualityDisplay,
+ getRemoteVideoPlaceholderText
+} from './renderer-ui.js';
const GRID_LAYOUT = {
maxColumns: 3,
@@ -30,14 +38,14 @@ class UIRenderer {
this.stateManager = stateManager;
this.unsubscribe = stateManager.subscribe(this.render.bind(this));
- // 缓存 DOM 元素
+ // 缂傚倹鎸搁悺?DOM 闁稿繐鍟扮粈?
this.elements = {
- // 头部和底部
+ // 濠㈣埖鎸抽崕鎾椽鐏炵晫淇洪梺?
header: document.querySelector('header'),
footer: document.querySelector('footer'),
- // 多Participant视频网格
+ // 濠㈣埖鐡峚rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹?
participantGrid: document.getElementById('participantGrid'),
- // 头部内容
+ // 濠㈣埖鎸抽崕鎾礃閸涱収鍟?
headerTitle: document.getElementById('headerTitle'),
callDuration: document.getElementById('callDuration'),
encryptionBadge: document.getElementById('encryptionBadge'),
@@ -45,41 +53,41 @@ class UIRenderer {
remoteNetworkIndicator: document.getElementById('remoteNetworkIndicator'),
remoteNetworkQuality: document.getElementById('remoteNetworkQuality'),
- // 远端视频
+ // 閺夆晜绮庨顒傛喆閸℃侗鏆?
remoteVideo: document.getElementById('remoteVideo'),
remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'),
networkStatus: document.getElementById('networkStatus'),
networkStatusText: document.getElementById('networkStatusText'),
connectingOverlay: document.getElementById('connectingOverlay'),
- // 本地视频
+ // 闁哄牜鍓欏﹢瀵告喆閸℃侗鏆?
localVideo: document.getElementById('localVideo'),
localVideoPlaceholder: document.getElementById('localVideoPlaceholder'),
localAudioWave: document.getElementById('localAudioWave'),
localInitials: document.getElementById('localInitials'),
- // 侧边栏
+ // 濞撴皜鍡欑彾闁?
sidebar: document.getElementById('sidebar'),
chatContent: document.getElementById('chatContent'),
userList: document.getElementById('userList'),
localMediaStatus: document.getElementById('localMediaStatus'),
localMuteIcon: document.querySelector('[data-field="localUser.muteIcon"]'),
userCountDisplay: document.getElementById('userCountDisplay'),
- // 控制按钮
+ // 闁硅矇鍐ㄧ厬闁圭顦甸幐?
micBtn: document.getElementById('micBtn'),
videoBtn: document.getElementById('videoBtn'),
recordBtn: document.getElementById('recordBtn'),
connectionQuality: document.getElementById('connectionQuality')
};
- // 绑定事件监听器
+ // 缂備焦鍨甸悾鐐鐎b晜顐介柣鈺傚灥閹宕?
this.bindEventListeners();
- // 订阅状态变化
+ // 閻犱降鍨藉Σ鍕偐閼哥鍋撴担绋跨秮闁?
this.unsubscribe = stateManager.subscribe(this.render.bind(this));
- // 订阅消息状态变化
+ // 閻犱降鍨藉Σ鍕槈閸喍绱栭柣妯垮煐閳ь兛绀佽ぐ澶愬礌?
this.messageUnsubscribe = chatMessage.subscribe(this.renderMessageState.bind(this));
- // 初始化渲染
+ // 闁告帗绻傞~鎰板礌閺嶃劏顩柡?
this.render(this.stateManager.getState(), { type: 'INIT' });
window.addEventListener('resize', () => {
@@ -94,7 +102,7 @@ class UIRenderer {
});
}
- // 渲染消息状态变化
+ // 婵炴挸寮堕悡瀣槈閸喍绱栭柣妯垮煐閳ь兛绀佽ぐ澶愬礌?
renderMessageState(messageState, changes) {
switch (changes.type) {
case 'NEW_MESSAGE':
@@ -103,7 +111,7 @@ class UIRenderer {
break;
case 'SIDEBAR_TOGGLE':
this.renderSidebar(changes.isOpen);
- // 当侧边栏打开时,重置未读消息计数
+ // 鐟滅増鎸烽弲鑸垫綇鐟欏嫮鍩夐柟鍨尭缁辨垿寮拋鍦闂佹彃绉堕悿鍡涘嫉椤忓浂鍤㈡繛鎴濈墛娴煎懐鎷嬮埄鍐╂
if (changes.isOpen) {
this.renderUnreadCount(0);
} else {
@@ -113,107 +121,107 @@ class UIRenderer {
}
}
- // 绑定事件监听器
+ // 缂備焦鍨甸悾鐐鐎b晜顐介柣鈺傚灥閹宕?
bindEventListeners() {
- // 事件监听器
+ // 濞存粌顑勫▎銏ゆ儎閹存繃鍎旈柛?
}
/**
- * 渲染方法 - 根据状态变化更新UI
- * @param {Object} state - 当前应用状态
- * @param {Object} changes - 状态变化对象
+ * 婵炴挸寮堕悡瀣棘鐟欏嫮銆?- 闁哄秷顫夊畵渚€鎮╅懜纰樺亾娴g缍侀柛鏍ㄧ墬濞插潡寮惂鐕?
+ * @param {Object} state - 鐟滅増鎸告晶鐘虫償閺冨倹鏆忛柣妯垮煐閳?
+ * @param {Object} changes - 闁绘鍩栭埀顑跨瑜板宕犻弽褜鍤犻悹?
*/
render(state, changes) {
- // 根据变化类型执行不同的渲染操作
+ // 闁哄秷顫夊畵渚€宕eΟ鍝勵嚙缂侇偉顕ч悗鐑藉箥瑜戦、鎴炵▔瀹ュ懏鍊遍柣銊ュ鐟曞棝寮婚幘瀛樻儥濞?
switch (changes.type) {
case 'INIT':
- // 初始化渲染 - 渲染所有UI元素
- this.renderRemoteVideo(state.session.remoteUser); // 渲染远程视频
- 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, state.participants); // 渲染用户列表
- this.renderHeader(state.session); // 渲染头部信息
- // 初始化时检查远程流状态,显示或隐藏占位背景
+ // 闁告帗绻傞~鎰板礌閺嶃劏顩柡?- 婵炴挸寮堕悡瀣箥閳ь剟寮垫繅姗ч柛蹇撳暟缁€?
+ this.renderRemoteVideo(state.session.remoteUser); // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤?
+ 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, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵?
+ this.renderHeader(state.session); // 婵炴挸寮堕悡瀣緞閹绢喖鍔ュǎ鍥e墲娴?
+ // 闁告帗绻傞~鎰板礌閺嶃劍顦ф俊顐熷亾闁哄被鍎寸换娆戠矙鐎n偆銈﹂柣妯垮煐閳ь兛绶ょ槐婵嬪及閸撗佷粵闁瑰瓨鐗犲▓锝夋寠韫囨挸绐楀ù锝呯Х閸庢寮?
if (this.elements.remoteVideoPlaceholder) {
if (state.remoteStream) {
- this.elements.remoteVideoPlaceholder.classList.add('hidden'); // 有远程流时隐藏占位背景
+ this.elements.remoteVideoPlaceholder.classList.add('hidden'); // 闁哄牆顦崇换娆戠矙鐎n偆銈﹂柡鍐ㄧ埣濞堬綁鎸婅箛鎾崇獥濞达絽绉烽崕妤呭疾?
} else {
- this.elements.remoteVideoPlaceholder.classList.remove('hidden'); // 无远程流时显示占位背景
+ this.elements.remoteVideoPlaceholder.classList.remove('hidden'); // 闁哄啰濮剧换娆戠矙鐎n偆銈﹂柡鍐煐濡绮堥崫鍕獥濞达絽绉烽崕妤呭疾?
}
}
break;
case 'DURATION_UPDATE':
- // 通话时长更新 - 渲染通话时长
+ // 闂侇偅淇洪惁浠嬪籍閸洘姣愰柡鍥х摠閺?- 婵炴挸寮堕悡瀣焻濮樺磭妯堥柡鍐ㄧ埣閺?
this.renderCallDuration(changes.duration);
break;
case 'LOCAL_MEDIA_CHANGE':
- // 本地媒体状态变化 - 更新相关UI
- 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, state.participants); // 渲染用户列表
+ // 闁哄牜鍓欏﹢瀛樺垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠囨儎缁嬪灝褰燯I
+ 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, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵?
break;
case 'LOCAL_STREAM_OBTAINED':
this.renderLocalStream(state.localStream);
this.renderLocalVideo(state.session.localUser, state.localStream);
break;
case 'REMOTE_STREAM_OBTAINED':
- // 远程流获取成功 - 更新远程视频显示
- this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 渲染远程流
- // 当获取到远程流时,隐藏连接中提示
+ // 閺夆晜绮庨埢鐓幟规担钘夌闁告瑦鐗楅崹姘跺礉?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤e爼寮伴崜褋浠?
+ this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 婵炴挸寮堕悡瀣交濠婂應鏌ゆ繛?
+ // 鐟滅増鎹侀獮蹇涘矗閺嵮冪厒閺夆晜绮庨埢鐓幟规担瑙勵槯闁挎稑鐭傚▓锝夋寠韫囨氨绠鹃柟鎭掑劙閼垫垿骞撻幇顔轰粵
if (this.elements.connectingOverlay) {
this.elements.connectingOverlay.classList.add('hidden');
}
break;
case 'REMOTE_MEDIA_CHANGE':
- // 远程媒体状态变化 - 更新远程视频和用户列表
- this.renderRemoteVideo(state.session.remoteUser); // 渲染远程视频
- this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 渲染用户列表
- // Host端:精准更新发送者participant tile的占位背景
- // 只有Host端需要处理participantId对应的tile占位符(Participant端没有其他Participant的视频流)
+ // 閺夆晜绮庨埢鍏煎垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤e爼宕畝鈧弫銈夊箣瀹勬澘鐏欓悶?
+ this.renderRemoteVideo(state.session.remoteUser); // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤?
+ this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵?
+ // Host缂佹棏鍨界槐鎵垝閹冩珯闁哄洤鐡ㄩ弻濠囧矗閹达腹鍋撴担瑙e亾閸楃瀽rticipant tile闁汇劌瀚畷鐗堟媴瀹ュ牆鍓归柡?
+ // 闁告瑯浜濆﹢涓爋st缂佹棏鍨跺〒鍓佹啺娴e壊妲遍柣鐐叉晙articipantId閻庣數鎳撶花鏌ユ儍閸撳檮le闁告濮崇紞鍛箔閿旇偐绀凱articipant缂佹棏鍨遍惀鍛村嫉婢跺﹤寰撳ù鐘虫耿articipant闁汇劌瀚~瀣紣閹寸偟銈﹂柨?
if (changes.participantId && state.session.localUser.isHost) {
- // 从participants Map中读取该participant的video状态,而非remoteUser(多Participant场景remoteUser不精确)
+ // 濞寸姴绐媋rticipants Map濞戞搩鍙€椤曚即宕i弽顒夊殙participant闁汇劌澧榠deo闁绘鍩栭埀顑跨筏缁辨繈鎳撳畝鍕remoteUser闁挎稑鐗嗛ˇ绺媋rticipant闁革妇鍎ゅ▍妾檈moteUser濞戞挸绉剁花璺ㄦ兜椤曞棛绀?
const pInfo = state.participants[changes.participantId];
const showPlaceholder = pInfo ? !pInfo.mediaState.video : true;
this.renderParticipantVideoPlaceholder(changes.participantId, showPlaceholder);
}
break;
case 'USER_LIST_UPDATE':
- // 用户列表更新 - 重新渲染用户列表
+ // 闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧啴寮寸€涙ɑ鐓€ - 闂佹彃绉甸弻濠傘€掗崣澶屽帬闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧?
this.renderUserList(changes.localUser, changes.remoteUser, state.participants);
break;
case 'PARTICIPANTS_UPDATE':
- // Participants信息变化 - 重新渲染用户列表并同步tile名称
+ // Participants濞e洠鍓濇导鍛村矗濡搫顕?- 闂佹彃绉甸弻濠傘€掗崣澶屽帬闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧啴鐛捄鐑樺€辨慨婵愭ile闁告艾绉惰ⅷ
this.renderUserList(state.session.localUser, state.session.remoteUser, changes.participants || state.participants);
- // 同步更新participant tile的名称标签
+ // 闁告艾鏈鐐哄即鐎涙ɑ鐓€participant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒?
this.syncParticipantTileNames(changes.participants || state.participants);
break;
case 'NETWORK_CHANGE':
- // 网络状态变化 - 渲染网络状态
+ // 缂傚啯鍨圭划鍫曟偐閼哥鍋撴担绋跨秮闁?- 婵炴挸寮堕悡瀣磾閹寸姷鎹曢柣妯垮煐閳?
this.renderNetworkStatus(changes.quality);
break;
case 'CALL_STATUS_CHANGE':
- // 通话状态变化 - 渲染通话状态
+ // 闂侇偅淇洪惁浠嬫偐閼哥鍋撴担绋跨秮闁?- 婵炴挸寮堕悡瀣焻濮樺磭妯堥柣妯垮煐閳?
this.renderCallStatus(changes.status);
break;
case 'CALL_ENDED':
- // 通话结束 - 渲染通话结束界面
+ // 闂侇偅淇洪惁鐣岀磼閹惧瓨灏?- 婵炴挸寮堕悡瀣焻濮樺磭妯堢紓浣规尰濞碱偊鎮惧畝鍕〃
this.renderCallEnded();
break;
case 'PARTICIPANT_LEFT':
- // participant离开 - 更新UI但房间仍然存在
+ // participant缂佸倽顕х槐?- 闁哄洤鐡ㄩ弻濂濞达絽妫欓崺褔姊荤紙鐘电煗闁绘帟娉涢悺銊╁捶?
this.renderParticipantLeft(changes.connectionId);
break;
case 'RESOLUTION_CHANGED':
- // 分辨率变化 - 更新UI中的分辨率选中状态
+ // 闁告帒妫滄ご鎼佹偝閸パ冪秮闁?- 闁哄洤鐡ㄩ弻濂濞戞搩鍘惧▓鎴﹀礆閸℃岸鍝洪柣婊冩喘閳ь剙顦懙鎴︽偐閼哥鍋?
this.renderResolutionChanged(changes.resolution);
break;
}
}
- // 渲染通话状态
+ // 婵炴挸寮堕悡瀣焻濮樺磭妯堥柣妯垮煐閳?
renderCallStatus(status) {
if (this.elements.connectingOverlay) {
if (status === 'connecting') {
@@ -224,7 +232,7 @@ class UIRenderer {
}
}
- // 渲染头部
+ // 婵炴挸寮堕悡瀣緞閹绢喖鍔?
renderHeader(session) {
this.renderHeaderTitle();
@@ -232,7 +240,7 @@ class UIRenderer {
toggleElement(this.elements.encryptionBadge, session.isEncrypted);
}
- // 始终显示网络状态指示器和质量
+ // 濠殿喖顑囩划鎾诲及閸撗佷粵缂傚啯鍨圭划鍫曟偐閼哥鍋撴担鐟扮樄缂佲偓閸濆嫭鐝ら柛婊冪焷瀹告繈鏌?
if (this.elements.remoteNetworkIndicator) {
this.elements.remoteNetworkIndicator.classList.remove('hidden');
}
@@ -245,24 +253,24 @@ class UIRenderer {
renderHeaderTitle() {
if (this.elements.headerTitle) {
const connectionId = store.getConnectionId() || '';
- // 未连接时不显示红框部分
- this.elements.headerTitle.textContent = `通话 (${connectionId})`;
+ // 闁哄牜浜g换娑㈠箳閵夛附顦у☉鎾崇У濡绮堥搹鐟邦劉婵℃妫濋崕鎾礆?
+ this.elements.headerTitle.textContent = getCallTitle(connectionId);
}
}
- // 渲染通话时长
+ // 婵炴挸寮堕悡瀣焻濮樺磭妯堥柡鍐ㄧ埣閺?
renderCallDuration(duration) {
if (this.elements.callDuration) {
this.elements.callDuration.textContent = formatTime(duration);
}
}
- // 渲染分辨率变化
+ // 婵炴挸寮堕悡瀣礆閸℃岸鍝洪柣婊冩搐瑜板宕?
renderResolutionChanged(resolution) {
if (!resolution) return;
- // 更新分辨率选项的选中状态
+ // 闁哄洤鐡ㄩ弻濠囧礆閸℃岸鍝洪柣婊冩喘閳ь剙顦甸妴宥夋儍閸曨垪鍋撴径澶庡幀闁绘鍩栭埀?
const options = document.querySelectorAll('.resolution-option');
options.forEach(btn => {
const btnRes = btn.dataset.resolution;
@@ -273,107 +281,64 @@ class UIRenderer {
btn.classList.toggle('active', isActive);
});
- // 更新当前分辨率文本
+ // 闁哄洤鐡ㄩ弻濠呫亹閹惧啿顤呴柛鎺戞妞存悂鎮抽崶銊︾€柡?
const currentResText = document.getElementById('currentResolutionText');
if (currentResText) {
currentResText.textContent = `当前: ${resolution.label}`;
}
}
- // 渲染远端视频
+ // 婵炴挸寮堕悡瀣交濠婂拋浼傞悷娆忔椤?
renderRemoteVideo(remoteUser) {
- // 同步更新侧边栏用户列表
- this.renderUserList(this.stateManager.getState().session.localUser, remoteUser, this.stateManager.getState().participants);
+ this.renderUserList(
+ this.stateManager.getState().session.localUser,
+ remoteUser,
+ this.stateManager.getState().participants
+ );
- // 当远程视频关闭时显示占位符
if (this.elements.remoteVideoPlaceholder) {
const shouldShowPlaceholder = !remoteUser.mediaState.video;
+ const placeholderText = getRemoteVideoPlaceholderText(!shouldShowPlaceholder);
toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder);
- // 当远程视频关闭时,隐藏视频元素本身,避免冻结画面透过占位符
if (this.elements.remoteVideo) {
- if (shouldShowPlaceholder) {
- this.elements.remoteVideo.style.opacity = '0';
- } else {
- this.elements.remoteVideo.style.opacity = '1';
- }
+ this.elements.remoteVideo.style.opacity = shouldShowPlaceholder ? '0' : '1';
}
- // 更新占位符文本内容
- if (shouldShowPlaceholder) {
- const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center');
- if (placeholderContent) {
- const titleElement = placeholderContent.querySelector('p.text-white.text-lg.font-medium');
- if (titleElement) {
- titleElement.textContent = '对方摄像头已关闭';
- }
- const subtitleElement = placeholderContent.querySelector('p.text-sm.text-gray-400');
- if (subtitleElement) {
- subtitleElement.textContent = '对方暂时关闭了视频';
- }
+ const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center');
+ if (placeholderContent) {
+ const titleElement = placeholderContent.querySelector('p.text-white.text-lg.font-medium');
+ const subtitleElement = placeholderContent.querySelector('p.text-sm.text-gray-400');
+
+ if (titleElement) {
+ titleElement.textContent = placeholderText.title;
}
- } else {
- // 恢复默认占位符文本
- const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center');
- if (placeholderContent) {
- const titleElement = placeholderContent.querySelector('p.text-white.text-lg.font-medium');
- if (titleElement) {
- titleElement.textContent = '等待对方连接...';
- }
- const subtitleElement = placeholderContent.querySelector('p.text-sm.text-gray-400');
- if (subtitleElement) {
- subtitleElement.textContent = '请确保对方已加入通话';
- }
+ if (subtitleElement) {
+ subtitleElement.textContent = placeholderText.subtitle;
}
}
}
- // 渲染网络状态
this.renderNetworkStatus(remoteUser.networkQuality);
-
- // 渲染header中的网络状态
this.renderHeaderNetworkStatus(remoteUser.networkQuality);
}
- // 渲染header中的网络状态
renderHeaderNetworkStatus(networkQuality) {
if (this.elements.remoteNetworkQuality) {
const textElement = this.elements.remoteNetworkQuality.querySelector('span');
const iconElement = this.elements.remoteNetworkQuality.querySelector('i');
if (textElement && iconElement) {
- let qualityText = '未知';
- let iconClass = 'fas fa-signal text-gray-400';
-
- switch (networkQuality) {
- case 'excellent':
- qualityText = '优秀';
- iconClass = 'fas fa-signal text-green-400';
- break;
- case 'good':
- qualityText = '良好';
- iconClass = 'fas fa-signal text-green-500';
- break;
- case 'fair':
- qualityText = '一般';
- iconClass = 'fas fa-signal text-yellow-400';
- break;
- case 'poor':
- qualityText = '较差';
- iconClass = 'fas fa-signal text-red-400';
- break;
- }
-
- textElement.textContent = qualityText;
- iconElement.className = iconClass;
+ const display = getNetworkQualityDisplay(networkQuality);
+ textElement.textContent = display.label;
+ iconElement.className = display.headerIconClass;
}
}
}
- // 渲染本地视频
renderLocalVideo(localUser, localStream) {
if (this.elements.localVideoPlaceholder) {
- // 当没有视频流或视频关闭时显示占位符
+ // 鐟滅増鎸婚惀鍛村嫉婢跺寒娼掑Λ鐗堝灦缁侊箓骞嬮弽顒夋綊濡増鍨甸崣褔姊婚鐔割槯闁哄嫬澧介妵姘跺础閻樿京绉寸紒?
const shouldShowPlaceholder = !localStream || !localUser.mediaState.video;
toggleElement(this.elements.localVideoPlaceholder, shouldShowPlaceholder);
}
@@ -382,19 +347,19 @@ class UIRenderer {
toggleElement(this.elements.localAudioWave, localUser.mediaState.isSpeaking);
}
- // 同时渲染本地用户状态
+ // 闁告艾鏈鍌氥€掗崣澶屽帬闁哄牜鍓欏﹢鎾偨閵婏箑鐓曢柣妯垮煐閳?
this.renderLocalUserStatus(localUser);
}
- // 渲染本地视频流
+ // 婵炴挸寮堕悡瀣嫉椤掆偓濠€瀵告喆閸℃侗鏆ユ繛?
renderLocalStream(stream) {
if (this.elements.localVideo && stream) {
this.elements.localVideo.srcObject = stream;
this.elements.localVideo.autoplay = true;
- this.elements.localVideo.muted = true; // 本地视频静音,避免回声
+ this.elements.localVideo.muted = true; // 闁哄牜鍓欏﹢瀵告喆閸℃侗鏆ラ梻鍫熺懇閻撳爼鏁嶅畝鍕級闁稿繐绉村ú鏍ㄧ珶?
console.log('srcObject set successfully:', this.elements.localVideo.srcObject);
- // 隐藏断开连接覆盖层
+ // 闂傚懏鍔樺Λ宀勫棘椤撶偟纾婚弶鈺冨仦鐢鎲伴崱娆愮0閻?
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
@@ -403,27 +368,27 @@ class UIRenderer {
}
}
- // 渲染远程视频流
+ // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤h泛霉?
renderRemoteStream(stream, connectionId, isHost) {
if (isHost && connectionId) {
- // Host端: 渲染到 participant 视频网格
+ // Host缂? 婵炴挸寮堕悡瀣礆?participant 閻熸瑥妫濋。鍓佺磾閹寸偟澹?
this.renderParticipantStream(stream, connectionId);
} else {
- // Participant端: 渲染到单一远端视频(Host的画面)
+ // Participant缂? 婵炴挸寮堕悡瀣礆閺夊灝绀嬪☉鎾亾閺夆晜绮庨顒傛喆閸℃侗鏆ラ柨娑樻箽ost闁汇劌瀚弫楣冩椤喚绀?
this.renderSingleRemoteStream(stream);
}
}
- // 渲染Host端的多Participant视频网格
- // 每个participant tile显示该participant实际的远端视频流
+ // 婵炴挸寮堕悡濠琽st缂佹棏鍨冲▓鎴炲緞濮濆崋rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹?
+ // 婵絽绻嬮柌娓沘rticipant tile闁哄嫬澧介妵姘辨嫚椤хΨrticipant閻庡湱鍋ゅ顖炴儍閸曨喚绠肩紒鏃戝灥椤锛愰幋鐐点偊
renderParticipantStream(stream, connectionId) {
const grid = this.elements.participantGrid;
if (!grid) return;
- // 显示网格,隐藏单路远端视频
+ // 闁哄嫬澧介妵姘辩磾閹寸偟澹愰柨娑樼焸濞堬綁鎸婅箛鎾崇閻犱警鍨电换娆戠博椤栨繍娼掑Λ?
grid.classList.remove('hidden');
- // 查找或创建该 connectionId 的视频格子
+ // 闁哄被鍎叉竟姗€骞嬮弽褍鐏$€点倝缂氶?connectionId 闁汇劌瀚~瀣紣閹寸偟澹愰悗?
let tile = grid.querySelector(`[data-participant-id="${connectionId}"]`);
if (!tile) {
tile = document.createElement('div');
@@ -434,11 +399,11 @@ class UIRenderer {
video.className = 'w-full h-full object-contain';
video.autoplay = true;
video.playsinline = true;
- video.muted = false; // 不静音,播放participant的音频
+ video.muted = false; // 濞戞挸绉瑰銈夋缁涘湱绀夐柟缁㈠幗閺備垢articipant闁汇劌瀚伴悡鑸碉紣?
video.id = `participantVideo_${connectionId}`;
tile.appendChild(video);
- // 参与者视频关闭时的占位背景(复用 remoteVideoPlaceholder 样式)
+ // 闁告瑥鍊风粭宀勬嚀閸涢偊娼掑Λ鐗堝灥閸櫻囨⒒椤撶喐顦ч柣銊ュ瀹曠増鎷呭鍫濆壒闁哄拋鍨界槐娆愬緞瀹ュ洦鏆?remoteVideoPlaceholder 闁哄秴鍢茬槐锟犳晬?
const placeholder = document.createElement('div');
placeholder.className = 'participant-video-placeholder absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900/80 to-purple-900/80 hidden';
placeholder.innerHTML = `
@@ -451,7 +416,7 @@ class UIRenderer {
`;
tile.appendChild(placeholder);
- // 参与者名称标签(优先使用participants中的真实姓名)
+ // 闁告瑥鍊风粭宀勬嚀閸涱厽鍊崇紒澶屽閻栵絿绮甸幘鍛濞村吋锚閸樻稒鎷呯捄銊︽殢participants濞戞搩鍘惧▓鎴︽儑閻旈鏉藉┑顔芥尭閹洟鏁?
const pInfo = this.stateManager.getState().participants[connectionId];
const displayName = pInfo?.name || '参与者';
const label = document.createElement('div');
@@ -459,7 +424,7 @@ class UIRenderer {
label.innerHTML = `${displayName}`;
tile.appendChild(label);
- // 在线标识
+ // 闁革负鍔庨崵搴ㄥ冀閸ヮ亞妲?
const liveTag = document.createElement('div');
liveTag.className = 'absolute top-3 right-3 bg-green-500/80 px-2 py-0.5 rounded-full text-xs flex items-center gap-1';
liveTag.innerHTML = `在线`;
@@ -470,10 +435,10 @@ class UIRenderer {
}
if (tile) {
- // 视频元素显示participant的远端视频流
+ // 閻熸瑥妫濋。鍫曞礂閸愵亞顦遍柡鍕⒔閵囨articipant闁汇劌瀚换娆戠博椤栨繍娼掑Λ鐗堝灦缁?
const video = tile.querySelector('video');
if (video && stream) {
- // 避免重复设置同一流对象(音频先到、视频后到时流对象相同)
+ // 闂侇剙鐏濋崢銈夋煂瀹ュ拋妲婚悹浣稿⒔閻ゅ棝宕ョ仦鑲╊伇婵炵繝绀侀顔炬寬閳藉懐绀勯梻濠冨▕椤e爼宕楅崼婵嗙厒闁靛棔娴囬~瀣紣閹存繃鍊甸柛鎺斿濡炲倸霉娴e壊鍤犻悹鐑囩磿濞村宕ュ畝瀣
if (video.srcObject === stream) {
console.log(`Same stream for participant ${connectionId}, ensuring playback`);
video.play().catch(e => console.log('Auto-play prevented:', e.message));
@@ -484,31 +449,31 @@ class UIRenderer {
}
}
- // 隐藏单路远端视频和占位符
+ // 闂傚懏鍔樺Λ宀勫础閺囷紕鐔呴弶鈺傜矌椤忣剛鎲撮崱娑辨殽闁告粌鑻畷鐗堟媴瀹ュ浂鍎?
const remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in');
if (remoteVideoDiv) {
remoteVideoDiv.classList.add('hidden');
}
}
- // 根据参与者数量调整网格列数
+ // 闁哄秷顫夊畵渚€宕i崒娆戠憿闁兼澘鎳忛弳鐔兼煂韫囨氨娈堕柡浣割嚟缂嶅寮介悡搴$仚闁?
const tileCount = grid.querySelectorAll('[data-participant-id]').length;
grid.style.gridTemplateColumns = getGridTemplateColumns(tileCount);
- // 隐藏连接中提示
+ // 闂傚懏鍔樺Λ灞炬交閻愭潙澶嶅☉鎿冨幗瑜颁胶绮?
if (this.elements.connectingOverlay) {
this.elements.connectingOverlay.classList.add('hidden');
}
- // 隐藏远端视频占位符
+ // 闂傚懏鍔樺Λ灞炬交濠婂拋浼傞悷娆忔椤e爼宕¢悩杈╃Т缂?
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.add('hidden');
}
}
- // 精准更新指定participant tile的占位背景
- // participantId: 发送media-state-changed的participant的连接ID
- // showPlaceholder: 是否显示占位背景(视频关闭时为true)
+ // 缂侇喗鍎抽崳顖炲即鐎涙ɑ鐓€闁圭娲ら悾绶媋rticipant tile闁汇劌瀚畷鐗堟媴瀹ュ牆鍓归柡?
+ // participantId: 闁告瑦鍨块埀顑挎矡edia-state-changed闁汇劌鍩rticipant闁汇劌瀚换娑㈠箳椤у粚
+ // showPlaceholder: 闁哄嫷鍨伴幆渚€寮伴崜褋浠涢柛妤冨С缂嶅懘鎳楃仦鐐彲闁挎稑鐗愰~瀣紣閹存繂褰犻梻鍌ゅ幗濡炲倹绋夌花宄硊e闁?
renderParticipantVideoPlaceholder(participantId, showPlaceholder) {
const grid = this.elements.participantGrid;
if (!grid) return;
@@ -521,7 +486,7 @@ class UIRenderer {
}
}
- // 同步更新所有participant tile的名称标签
+ // 闁告艾鏈鐐哄即鐎涙ɑ鐓€闁圭鍋撻柡鍫濄€宎rticipant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒?
syncParticipantTileNames(participants) {
if (!participants) return;
const grid = this.elements.participantGrid;
@@ -531,7 +496,7 @@ class UIRenderer {
}
}
- // 更新指定participant tile的名称标签
+ // 闁哄洤鐡ㄩ弻濠囧箰閸パ呮毎participant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒?
updateParticipantTileName(participantId, name) {
const grid = this.elements.participantGrid;
if (!grid) return;
@@ -547,7 +512,7 @@ class UIRenderer {
}
}
- // 渲染Participant端的单一远端视频(Host画面)
+ // 婵炴挸寮堕悡濠竌rticipant缂佹棏鍨冲▓鎴﹀础閺囨氨顏遍弶鈺傜矌椤忣剛鎲撮崱娑辨殽闁挎稑婀歰st闁汇垹顭峰浼存晬?
renderSingleRemoteStream(stream) {
if (!this.elements.remoteVideo || !stream) {
console.error('Either remoteVideo element or stream is missing');
@@ -556,38 +521,38 @@ class UIRenderer {
console.log('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(t => `${t.kind}(${t.readyState})`));
- // 关键修复:避免 srcObject = null 的重置模式
- // 如果 srcObject 已经是同一个 stream 对象,说明是同一流的轨道更新(如音频先到,视频后到)
- // 浏览器会自动识别新添加的轨道,无需重置 srcObject
+ // 闁稿繑濞婇弫顓熺┍椤旂⒈妲婚柨娑欏哺娴尖晠宕?srcObject = null 闁汇劌瀚伴崳鍝ョ磾椤斾茎浣割嚕?
+ // 濠碘€冲€归悘?srcObject 鐎规瓕灏欑划锟犲及椤栨碍鍊卞☉鎾亾濞?stream 閻庣數顢婇挅鍕晬瀹€鍐惧殯闁哄嫬瀛╁Σ鎼佸触鐏炶偐顏辨繛缈犺兌濞堟垶娼妸鈺€澹曢柡鍥х摠閺屽﹪鏁嶉崼婵愭搐闂傚﹥濞婇。鍫曞礂閸繂鐓傞柨娑樼焷椤锛愰幋婵囧€甸柛鎺戝簻缁?
+ // 婵炴潙绻楅~宥夊闯閵娿倗绐楅柤濂変簻婵晝鎷犻崱妤€鐒奸柡鍌滃閸у﹪宕濋悩鍨暠閺夌偑鍔戞禍楣冩晬鐏炵偓锟ラ梻鍥e亾闂佹彃绉堕悿?srcObject
if (this.elements.remoteVideo.srcObject === stream) {
console.log('Same stream object, track added - ensuring playback');
this.elements.remoteVideo.play().catch(e => console.log('Auto-play prevented:', e.message));
return;
}
- // 首次设置或流对象变化:直接设置 srcObject(不使用 null 重置模式)
+ // 濡絾鐗楅鑲╂媼閸撗呮瀭闁瑰瓨鐗楃粊锔锯偓鐢殿攰閽栧嫰宕eΟ鍝勵嚙闁挎稒姘ㄥú鍧楀箳閵夘煈鍟庣紓?srcObject闁挎稑鐗呯粭澶嬫媴鐠恒劍鏆?null 闂佹彃绉堕悿鍡椢熼垾宕囩闁?
this.elements.remoteVideo.srcObject = stream;
this.elements.remoteVideo.autoplay = true;
this.elements.remoteVideo.playsinline = true;
this.elements.remoteVideo.muted = false;
- // 确保视频开始播放
+ // 缁绢収鍠曠换姘辨喆閸℃侗鏆ョ€殿喒鍋撳┑顔碱儐閹搁亶寮?
this.elements.remoteVideo.play().catch(e => {
console.log('Auto-play prevented, will retry on interaction:', e.message);
});
- // 隐藏断开连接覆盖层
+ // 闂傚懏鍔樺Λ宀勫棘椤撶偟纾婚弶鈺冨仦鐢鎲伴崱娆愮0閻?
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
- // 监听视频轨道变化
+ // 闁烩晜鍨甸幆澶屾喆閸℃侗鏆ラ弶鐐姂娴滈箖宕eΟ鍝勵嚙
const videoTracks = stream.getVideoTracks();
const audioTracks = stream.getAudioTracks();
console.log(`Stream has ${videoTracks.length} video tracks, ${audioTracks.length} audio tracks`);
if (videoTracks.length > 0) {
- // 有视频轨道:隐藏占位符
+ // 闁哄牆顦抽~瀣紣閹达絽缂撻梺顒佹惈缁变即姊鹃幇顖涱棏闁告濮崇紞鍛箔?
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.add('hidden');
}
@@ -595,7 +560,7 @@ class UIRenderer {
this.elements.connectingOverlay.classList.add('hidden');
}
- // 监听视频轨道分辨率变化
+ // 闁烩晜鍨甸幆澶屾喆閸℃侗鏆ラ弶鐐姂娴滈箖宕氶崱姘跺摵闁绘粌娲よぐ澶愬礌?
const activeVideoTrack = videoTracks.find(track => track.readyState === 'live');
if (activeVideoTrack) {
const resolution = this.getVideoResolution(activeVideoTrack);
@@ -606,188 +571,66 @@ class UIRenderer {
});
}
} else {
- // 只有音频轨道(视频轨道尚未到达):不显示占位符,等待视频轨道到达
- // 不设置 srcObject = null,保持音频播放
+ // 闁告瑯浜濆﹢渚€妫呴幎绛嬫殽閺夌偑鍔戞禍楣冩晬閸綆娼掑Λ鐗堝灱瀵ゆ椽鏌嗛幘宕囨闁哄牜浜滈崺灞炬綇閹惧懐绀嗛柨娑欑煯缁楀寮伴崜褋浠涢柛妤冨С缂嶅懐绮敂鑲╃缂佹稑顦欢鐔烘喆閸℃侗鏆ラ弶鐐姂娴滈箖宕氶幏灞惧涧
+ // 濞戞挸绉烽鏇犵磾?srcObject = null闁挎稑濂旂换姘跺箰娓氣偓閻撹埖锛愰幋鐐村啊闁衡偓?
console.log('Audio-only stream, waiting for video track...');
}
}
- // 渲染本地用户状态
+ // 婵炴挸寮堕悡瀣嫉椤掆偓濠€鎾偨閵婏箑鐓曢柣妯垮煐閳?
renderLocalUserStatus(localUser) {
- // 更新本地媒体状态文本
+ const mediaMeta = getMediaStatusMeta(localUser.mediaState);
+
if (this.elements.localMediaStatus) {
- if (!localUser.mediaState.audio) {
- this.elements.localMediaStatus.textContent = '静音中';
- this.elements.localMediaStatus.className = 'text-xs text-gray-500';
- } else if (!localUser.mediaState.video) {
- this.elements.localMediaStatus.textContent = '视频关闭';
- this.elements.localMediaStatus.className = 'text-xs text-gray-500';
- } else {
- this.elements.localMediaStatus.textContent = '在线';
- this.elements.localMediaStatus.className = 'text-xs text-green-400';
- }
+ this.elements.localMediaStatus.textContent = mediaMeta.text;
+ this.elements.localMediaStatus.className = mediaMeta.className;
}
- // 更新静音图标
if (this.elements.localMuteIcon) {
- if (!localUser.mediaState.audio) {
+ if (mediaMeta.showMuteIcon) {
this.elements.localMuteIcon.classList.remove('hidden');
- this.elements.localMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs';
+ this.elements.localMuteIcon.className = mediaMeta.muteIconClass;
} else {
this.elements.localMuteIcon.classList.add('hidden');
}
}
}
- // 渲染侧边栏用户列表(支持多Participant动态渲染)
renderUserList(localUser, remoteUser, participants) {
if (!this.elements.userList) return;
const participantsMap = participants || {};
const participantCount = Object.keys(participantsMap).length;
-
- // 通话成员总数 = 本地用户(1) + participants中的条目数
- // Host端participants只含其他participant;Participant端participants含host+其他participant
const userCount = 1 + participantCount;
- // 更新通话成员总数显示
- const userCountElement = this.elements.userCountDisplay;
- if (userCountElement) {
- userCountElement.textContent = `通话成员 (${userCount})`;
+ if (this.elements.userCountDisplay) {
+ this.elements.userCountDisplay.textContent = buildUserCountLabel(userCount);
}
- // 清空列表并重新渲染
this.elements.userList.innerHTML = '';
-
- // 1. 渲染本地用户
- // 判断当前用户角色:Host端localUser是主持人;Participant端localUser是参与者
- this.elements.userList.appendChild(this.createUserEntry({
+ this.elements.userList.appendChild(createUserEntryElement({
user: localUser,
role: 'local'
}));
- // 2. 渲染远端成员
if (participantCount > 0) {
- // 有participants数据(Host端或Participant端收到participants-sync后)
- for (const [pid, p] of Object.entries(participantsMap)) {
- if (p.role === 'host') {
- this.elements.userList.appendChild(this.createUserEntry({
- user: p,
- role: 'host',
- id: pid
- }));
- } else {
- this.elements.userList.appendChild(this.createUserEntry({
- user: p,
- role: 'participant',
- id: pid
- }));
- }
+ for (const [participantId, participant] of Object.entries(participantsMap)) {
+ this.elements.userList.appendChild(createUserEntryElement({
+ user: participant,
+ role: participant.role === 'host' ? 'host' : 'participant',
+ id: participantId
+ }));
}
} else if (remoteUser.status !== 'offline') {
- // 兼容:Participant端未收到participants-sync时,使用remoteUser显示Host
- this.elements.userList.appendChild(this.createUserEntry({
+ this.elements.userList.appendChild(createUserEntryElement({
user: remoteUser,
role: 'remote'
}));
}
}
- // 创建通用用户条目
- createUserEntry(options) {
- const { user, role, id } = options;
-
- const div = document.createElement('div');
- const baseClass = 'flex items-center gap-3 p-2 rounded-lg';
- div.className = role === 'local'
- ? `${baseClass} hover:bg-white/5`
- : `${baseClass} bg-white/5`;
-
- // dataset.userId
- switch (role) {
- case 'local':
- div.dataset.userId = 'local';
- break;
- case 'remote':
- div.dataset.userId = 'remote';
- break;
- case 'host':
- div.dataset.userId = `host_${id}`;
- break;
- case 'participant':
- div.dataset.userId = `participant_${id}`;
- break;
- }
-
- const mediaState = user.mediaState;
- const mediaStatusText = !mediaState.audio ? '静音中' : (!mediaState.video ? '视频关闭' : '在线');
- const mediaStatusClass = (!mediaState.audio || !mediaState.video) ? 'text-xs text-gray-500' : 'text-xs text-green-400';
- const muteIconHtml = !mediaState.audio
- ? ''
- : '';
-
- // 头像区域
- let avatarHtml;
- if (role === 'local') {
- avatarHtml = `
`;
- } else {
- avatarHtml = `
-
-

-
-
- `;
- }
-
- // 角色标签
- let roleTag;
- if (role === 'local') {
- const isHost = user.isHost;
- roleTag = isHost
- ? '主持人'
- : '参与者';
- } else if (role === 'participant') {
- roleTag = '参与者';
- } else {
- // remote, host
- roleTag = '主持人';
- }
-
- // 媒体状态 data-field(仅local)
- const dataFieldAttr = role === 'local' ? ' data-field="localUser.mediaStatus"' : '';
-
- // 右侧内容
- let rightHtml;
- if (role === 'participant') {
- const speakingHtml = (mediaState.isSpeaking && mediaState.audio)
- ? '
'
- : '';
- rightHtml = `
-
- ${muteIconHtml}
- ${speakingHtml}
-
- `;
- } else {
- rightHtml = muteIconHtml;
- }
-
- div.innerHTML = `
- ${avatarHtml}
-
-
- ${user.name}
- ${roleTag}
-
-
${mediaStatusText}
-
- ${rightHtml}
- `;
-
- return div;
- }
- // 在renderer.js中添加方法
- // 获取视频流分辨率
+ // 闁革腹鏆渆nderer.js濞戞搩鍘介崸濠囧礉閻樿櫕鐓欐繛?
+ // 闁兼儳鍢茶ぐ鍥╂喆閸℃侗鏆ユ繛缈犵閸ㄥ孩娼忛妸褍鑺?
getVideoResolution(track) {
if (track && track.getSettings) {
const settings = track.getSettings();
@@ -796,35 +639,35 @@ class UIRenderer {
height: settings.height || 480
};
}
- return { width: 640, height: 480 }; // 默认值
+ return { width: 640, height: 480 }; // 濮掓稒顭堥濠氬磹?
}
- // 调整视频元素大小并居中显示
+ // 閻犲鍟弳锝囨喆閸℃侗鏆ラ柛蹇撳暟缁€灞惧緞瑜嶉惃顒勭嵁鐠鸿櫣婀峰☉鎿冨幗濡绮?
adjustVideoSize(videoElement, resolution) {
if (!videoElement) return;
const { width, height } = resolution;
const aspectRatio = width / height;
- // 根据容器大小和视频宽高比调整视频显示
+ // 闁哄秷顫夊畵浣衡偓鍦嚀濞呮帗寰勮閻剟宕畝鍐炬綊濡増鍨甸鏃€顨囧Ο鑽ゆХ閻犲鍟弳锝囨喆閸℃侗鏆ラ柡鍕⒔閵?
const container = videoElement.parentElement;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
- // 启用硬件加速
+ // 闁告凹鍨抽弫銈囨兜椤戞寧顐介柛鏃傚█閳?
videoElement.style.transform = 'translateZ(0)';
videoElement.style.willChange = 'transform';
- // 设置容器为flex布局,使视频居中
+ // 閻犱礁澧介悿鍡欌偓鍦嚀濞呮帗绋夌弧绨俥x閻㈩垰鍟惇顒勬晬鐏炵厧鈻忛悷娆忔椤e墎浠﹂崨顒冨幀
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'center';
- // 优化图像渲染
+ // 濞村吋锚鐎垫煡宕堕幆褍鍓兼繛鎾冲级閻?
videoElement.style.imageRendering = 'auto';
- // 确保视频元素在容器内正确显示
+ // 缁绢収鍠曠换姘辨喆閸℃侗鏆ラ柛蹇撳暟缁€宀勫捶閵娿儺鍟囬柛锝冨妼閸炴潙顫㈤敐鍥b偓姗€寮伴崜褋浠?
videoElement.style.maxWidth = '100%';
videoElement.style.maxHeight = '100%';
- videoElement.style.objectFit = 'contain'; // 保持原始比例,不裁剪
+ videoElement.style.objectFit = 'contain'; // 濞e洦绻冪€垫棃宕㈤悢濂夋綏婵絾鏌х欢銉╂晬鐏炶偐鐟濋悷浣风婢光偓
}
- // 渲染控制按钮
+ // 婵炴挸寮堕悡瀣箳瑜嶉崺妤呭箰婢舵劖灏?
renderControlButtons(mediaState) {
if (this.elements.micBtn) {
toggleButtonState(this.elements.micBtn, !mediaState.audio);
@@ -839,35 +682,35 @@ class UIRenderer {
}
}
- // 渲染聊天消息
+ // 婵炴挸寮堕悡瀣嚂婵犲倶浜繛鎴濈墛娴?
renderChatMessages(messages) {
if (!this.elements.chatContent) return;
- // 清空聊天内容
+ // 婵炴挸鎳愰埞鏍嚂婵犲倶浜柛鎰噹椤?
this.elements.chatContent.innerHTML = '';
- // 添加通话开始时间
+ // 婵烇綀顕ф慨鐐烘焻濮樺磭妯堢€殿喒鍋撳┑顔碱儐濡炲倿姊?
const startTimeElement = document.createElement('div');
startTimeElement.className = 'text-center text-xs text-gray-500 my-4';
const startTime = messages[0]?.timestamp || new Date().toISOString();
- startTimeElement.textContent = `通话开始 ${formatTimestamp(startTime)}`;
+ startTimeElement.textContent = `闂侇偅淇洪惁钘夘嚕閳ь剚鎱?${formatTimestamp(startTime)}`;
this.elements.chatContent.appendChild(startTimeElement);
- // 添加消息
+ // 婵烇綀顕ф慨鐐测槈閸喍绱?
messages.forEach(message => {
const messageElement = this.createMessageElement(message);
this.elements.chatContent.appendChild(messageElement);
});
- // 滚动到底部
+ // 婵犲﹥鑹炬慨鈺呭礆閺夎法淇洪梺?
this.elements.chatContent.scrollTop = this.elements.chatContent.scrollHeight;
}
- // 创建消息元素
+ // 闁告帗绋戠紓鎾斥槈閸喍绱栭柛蹇撳暟缁€?
createMessageElement(message) {
const messageDiv = document.createElement('div');
- // 根据消息类型设置不同的CSS类
+ // 闁哄秷顫夊畵浣糕槈閸喍绱栫紒顐ヮ嚙閻庨鎷嬮崜褏鏋傚☉鎾崇Т閹捇鎯冮崙娣猄缂?
let messageClass = 'chat-bubble';
if (message.type === 'system') {
messageClass += ' message-system';
@@ -882,7 +725,7 @@ class UIRenderer {
let contentHTML = '';
if (message.type === 'file' && message.content.startsWith('data:image/')) {
- // 图片消息
+ // 闁搞儱澧芥晶鏍р槈閸喍绱?
contentHTML = `

@@ -890,7 +733,7 @@ class UIRenderer {
`;
} else {
- // 文本消息
+ // 闁哄倸娲﹀﹢鏉库槈閸喍绱?
contentHTML = `
${message.content}
@@ -914,7 +757,7 @@ class UIRenderer {
return messageDiv;
}
- // 渲染未读消息数
+ // 婵炴挸寮堕悡瀣嫉椤忓浂鍤㈡繛鎴濈墛娴煎懘寮?
renderUnreadCount(count) {
if (this.elements.unreadBadge) {
if (count > 0) {
@@ -926,7 +769,7 @@ class UIRenderer {
}
}
- // 渲染侧边栏
+ // 婵炴挸寮堕悡瀣瑹瑜戠粩鐔煎冀?
renderSidebar(isOpen) {
if (this.elements.sidebar) {
if (isOpen) {
@@ -937,119 +780,45 @@ class UIRenderer {
}
}
- // 渲染网络状态
+ // 婵炴挸寮堕悡瀣磾閹寸姷鎹曢柣妯垮煐閳?
renderNetworkStatus(quality) {
+ const display = getNetworkQualityDisplay(quality);
+
if (this.elements.networkStatus && this.elements.networkStatusText) {
- // 始终显示网络状态
toggleElement(this.elements.networkStatus, true);
- // 根据网络质量设置不同的图标和颜色
const networkStatus = this.elements.networkStatus;
const networkStatusText = this.elements.networkStatusText;
-
- // 清除之前的图标
const existingIcon = networkStatus.querySelector('i');
if (existingIcon) {
existingIcon.remove();
}
- // 创建新的图标元素
const icon = document.createElement('i');
-
- // 根据网络质量设置图标和样式
- switch (quality) {
- case 'excellent':
- icon.className = 'fas fa-check-circle text-green-400';
- networkStatusText.textContent = this.getNetworkQualityText(quality);
- networkStatusText.className = 'text-green-400';
- break;
- case 'good':
- icon.className = 'fas fa-signal text-blue-400';
- networkStatusText.textContent = this.getNetworkQualityText(quality);
- networkStatusText.className = 'text-blue-400';
- break;
- case 'fair':
- icon.className = 'fas fa-exclamation-circle text-yellow-500';
- networkStatusText.textContent = this.getNetworkQualityText(quality);
- networkStatusText.className = 'text-yellow-500';
- break;
- case 'poor':
- icon.className = 'fas fa-exclamation-triangle text-red-500';
- networkStatusText.textContent = this.getNetworkQualityText(quality);
- networkStatusText.className = 'text-red-500';
- break;
- case 'no_signal':
- icon.className = 'fas fa-times-circle text-gray-500';
- networkStatusText.textContent = this.getNetworkQualityText(quality);
- networkStatusText.className = 'text-gray-500';
- break;
- default:
- icon.className = 'fas fa-question-circle text-gray-400';
- networkStatusText.textContent = '未知';
- networkStatusText.className = 'text-gray-400';
- }
-
- // 添加图标到网络状态元素
+ icon.className = display.statusIconClass;
+ networkStatusText.textContent = display.label;
+ networkStatusText.className = display.statusTextClass;
networkStatus.insertBefore(icon, networkStatusText);
}
if (this.elements.connectionQuality) {
- const qualityText = this.getNetworkQualityText(quality);
- let statusClass = '';
-
- // 根据网络质量设置文本颜色
- switch (quality) {
- case 'excellent':
- statusClass = 'text-green-400';
- break;
- case 'good':
- statusClass = 'text-blue-400';
- break;
- case 'fair':
- statusClass = 'text-yellow-500';
- break;
- case 'poor':
- statusClass = 'text-red-500';
- break;
- case 'no_signal':
- statusClass = 'text-gray-500';
- break;
- default:
- statusClass = 'text-gray-400';
- }
-
- // 更新连接质量文本和样式
- this.elements.connectionQuality.textContent = `连接质量: ${qualityText}`;
- this.elements.connectionQuality.className = `text-xs ${statusClass}`;
+ this.elements.connectionQuality.textContent = `连接质量: ${display.label}`;
+ this.elements.connectionQuality.className = `text-xs ${display.connectionTextClass}`;
}
- // 同步更新头部网络指示器
this.updateHeaderNetworkIndicator(quality);
-
- // 同步更新头部网络质量文本
this.renderHeaderNetworkStatus(quality);
}
- // 更新头部网络指示器
updateHeaderNetworkIndicator(networkQuality) {
if (!this.elements.remoteNetworkIndicator) return;
-
- // 根据网络质量设置指示器颜色
- if (networkQuality === 'no_signal') {
- // 无信号时显示灰色点,取消动画
- this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full';
- } else {
- // 有信号时显示绿色点,保持动画
- this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-green-500 rounded-full animate-pulse';
- }
+ this.elements.remoteNetworkIndicator.className = getNetworkQualityDisplay(networkQuality).indicatorClass;
}
-
- // 渲染通话结束
renderCallEnded() {
console.log('Call ended');
- // 清理participant网格
+ // 婵炴挸鎳愰幃濡廰rticipant缂傚啯鍨堕悧?
const grid = this.elements.participantGrid;
if (grid) {
grid.querySelectorAll('[data-participant-id]').forEach(tile => {
@@ -1060,11 +829,11 @@ class UIRenderer {
grid.classList.add('hidden');
}
- // 跳转到结束通话界面
+ // 閻犲搫鐤囧ù鍡涘礆閹殿喚娉㈤柡澶屽枛閳ь剚淇洪惁浠嬫偩瀹€鍕〃
window.location.href = './endcall/endcall.html';
}
- // 渲染participant离开(host端,房间仍然存在)
+ // 婵炴挸寮堕悡濯漚rticipant缂佸倽顕х槐鎴︽晬閸ф攼st缂佹棏鍨界槐婵嬪箣閸ф锛熷ù鐘茬Ф閸斞呪偓娑櫭﹢顏堟晬?
renderParticipantLeft(connectionId) {
console.log(`Participant left: ${connectionId}, updating UI`);
@@ -1098,29 +867,7 @@ class UIRenderer {
}
}
- // 获取状态文本
- getStatusText(status) {
- const statusMap = {
- 'online': '在线',
- 'offline': '离线',
- 'connecting': '连接中'
- };
- return statusMap[status] || status;
- }
-
- // 获取网络质量文本
- getNetworkQualityText(quality) {
- const qualityMap = {
- 'excellent': '优秀',
- 'good': '良好',
- 'fair': '一般',
- 'poor': '较差',
- 'no_signal': '无信号'
- };
- return qualityMap[quality] || quality;
- }
-
- // 销毁
+ // 闁兼儳鍢茶ぐ鍥偐閼哥鍋撴担瑙勭€柡?
destroy() {
if (this.unsubscribe) {
this.unsubscribe();