/** * UI渲染器 * 负责将状态映射到DOM,与状态管理解耦 */ import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js'; import { mockCallSession } from './models.js'; class UIRenderer { constructor(stateManager) { this.stateManager = stateManager; this.unsubscribe = stateManager.subscribe(this.render.bind(this)); // 缓存 DOM 元素 this.elements = { // 头部和底部 header: document.querySelector('header'), footer: document.querySelector('footer'), // 头部内容 headerTitle: document.getElementById('headerTitle'), callDuration: document.getElementById('callDuration'), encryptionBadge: document.getElementById('encryptionBadge'), unreadBadge: document.getElementById('unreadBadge'), // 远端视频 remoteVideo: document.getElementById('remoteVideo'), remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'), remoteAvatar: document.getElementById('remoteAvatar'), remoteName: document.getElementById('remoteName'), remoteStatus: document.getElementById('remoteStatus'), remoteSpeakingIndicator: document.getElementById('remoteSpeakingIndicator'), remoteAudioWave: document.getElementById('remoteAudioWave'), 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"]'), // 控制按钮 micBtn: document.getElementById('micBtn'), videoBtn: document.getElementById('videoBtn'), recordBtn: document.getElementById('recordBtn'), connectionQuality: document.getElementById('connectionQuality') }; // 绑定事件监听器 this.bindEventListeners(); // 订阅状态变化 this.unsubscribe = stateManager.subscribe(this.render.bind(this)); // 初始化渲染 this.render(this.stateManager.getState(), { type: 'INIT' }); window.addEventListener('resize', () => { if (this.elements.remoteVideo && this.elements.remoteVideo.srcObject) { const stream = this.elements.remoteVideo.srcObject; const videoTracks = stream.getVideoTracks(); if (videoTracks.length > 0) { const resolution = this.getVideoResolution(videoTracks[0]); this.adjustVideoSize(this.elements.remoteVideo, resolution); } } }); } // 绑定事件监听器 bindEventListeners() { // 事件监听器 } // 渲染状态变化 render(state, changes) { switch (changes.type) { case 'INIT': this.renderHeader(state.session); this.renderRemoteVideo(state.session.remoteUser); this.renderLocalVideo(state.session.localUser, state.localStream); this.renderControlButtons(state.session.localUser.mediaState); this.renderChatMessages(state.messages); this.renderUserList(state.session.localUser, state.session.remoteUser); // 初始化时检查远程流状态,显示或隐藏占位背景 if (this.elements.remoteVideoPlaceholder) { if (state.remoteStream) { this.elements.remoteVideoPlaceholder.classList.add('hidden'); } else { this.elements.remoteVideoPlaceholder.classList.remove('hidden'); } } break; case 'DURATION_UPDATE': this.renderCallDuration(changes.duration); break; case 'LOCAL_MEDIA_CHANGE': 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); break; case 'LOCAL_STREAM_OBTAINED': this.renderLocalStream(state.localStream); this.renderLocalVideo(state.session.localUser, state.localStream); break; case 'REMOTE_STREAM_OBTAINED': this.renderRemoteStream(state.remoteStream); // 当获取到远程流时,隐藏连接中提示 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); break; case 'NEW_MESSAGE': this.renderChatMessages(state.messages); this.renderUnreadCount(changes.unreadCount); break; case 'SIDEBAR_TOGGLE': this.renderSidebar(changes.isOpen); break; case 'NETWORK_CHANGE': this.renderNetworkStatus(changes.quality); break; case 'CALL_STATUS_CHANGE': this.renderCallStatus(changes.status); break; case 'CALL_ENDED': this.renderCallEnded(); break; } } // 渲染通话状态 renderCallStatus(status) { if (this.elements.connectingOverlay) { if (status === 'connecting') { this.elements.connectingOverlay.classList.remove('hidden'); } else { this.elements.connectingOverlay.classList.add('hidden'); } } } // 渲染头部 renderHeader(session) { if (this.elements.headerTitle) { this.elements.headerTitle.textContent = `与 ${session.remoteUser.name} 的通话`; } if (this.elements.encryptionBadge) { toggleElement(this.elements.encryptionBadge, session.isEncrypted); } this.renderCallDuration(session.duration); } // 渲染通话时长 renderCallDuration(duration) { if (this.elements.callDuration) { this.elements.callDuration.textContent = formatTime(duration); } } // 渲染远端视频 renderRemoteVideo(remoteUser) { if (this.elements.remoteName) { this.elements.remoteName.textContent = remoteUser.name; } if (this.elements.remoteAvatar) { this.elements.remoteAvatar.src = remoteUser.avatar; } if (this.elements.remoteStatus) { this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status); } // 渲染说话状态 if (this.elements.remoteSpeakingIndicator) { toggleElement(this.elements.remoteSpeakingIndicator, remoteUser.mediaState.isSpeaking); } if (this.elements.remoteAudioWave) { toggleElement(this.elements.remoteAudioWave, remoteUser.mediaState.isSpeaking && remoteUser.mediaState.audio); } // 渲染网络状态 this.renderNetworkStatus(remoteUser.networkQuality); } // 渲染本地视频 renderLocalVideo(localUser, localStream) { if (this.elements.localVideoPlaceholder) { // 当没有视频流或视频关闭时显示占位符 const shouldShowPlaceholder = !localStream || !localUser.mediaState.video; toggleElement(this.elements.localVideoPlaceholder, shouldShowPlaceholder); } if (this.elements.localAudioWave) { 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; // 本地视频静音,避免回声 console.log('srcObject set successfully:', this.elements.localVideo.srcObject); // 隐藏断开连接覆盖层 if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } } else { console.error('Either localVideo element or stream is missing'); } } // 渲染远程视频流 renderRemoteStream(stream) { if (this.elements.remoteVideo && stream) { console.log('Rendering remote stream:', stream); // 即使流对象相同,也要重新设置,确保视频元素能够识别轨道变化 this.elements.remoteVideo.srcObject = null; // 延迟设置srcObject,确保视频元素能够正确处理 setTimeout(() => { this.elements.remoteVideo.srcObject = stream; console.log('Remote stream reset successfully:', stream); // 确保视频元素的属性正确设置 this.elements.remoteVideo.autoplay = true; this.elements.remoteVideo.playsinline = true; this.elements.remoteVideo.muted = true; // 关键设置:启用硬件加速和最佳质量渲染 this.elements.remoteVideo.style.transform = 'translateZ(0)'; // 启用硬件加速 this.elements.remoteVideo.style.imageRendering = 'pixelated'; // 保持像素清晰 this.elements.remoteVideo.style.objectFit = 'contain'; // 保持比例 // 隐藏断开连接覆盖层 if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } // 隐藏占位背景 if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.add('hidden'); } // 获取视频轨道并处理分辨率 const videoTracks = stream.getVideoTracks(); console.log('Remote video tracks:', videoTracks); // 检查是否有有效的视频轨道 const hasValidVideoTrack = videoTracks.length > 0 && videoTracks.some(track => { // 检查轨道是否已停止或被禁用 return track.readyState === 'live'; }); console.log('Has valid video track:', hasValidVideoTrack); if (hasValidVideoTrack) { console.log('Found valid video tracks, updating resolution'); const activeVideoTrack = videoTracks.find(track => track.readyState === 'live'); if (activeVideoTrack) { const resolution = this.getVideoResolution(activeVideoTrack); this.adjustVideoSize(this.elements.remoteVideo, resolution); // 监听轨道变化,处理分辨率调整 activeVideoTrack.addEventListener('resize', () => { const newResolution = this.getVideoResolution(activeVideoTrack); this.adjustVideoSize(this.elements.remoteVideo, newResolution); }); } } else { console.log('No valid video tracks in remote stream'); // 清空视频元素的源 this.elements.remoteVideo.srcObject = null; // 显示占位背景 if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.remove('hidden'); } } }, 50); // 增加延迟时间,确保视频元素有足够的时间处理 } else { console.error('Either remoteVideo element or stream is missing'); // 清空视频元素的源 if (this.elements.remoteVideo) { this.elements.remoteVideo.srcObject = null; } // 显示占位背景 if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.remove('hidden'); } } } // 渲染本地用户状态 renderLocalUserStatus(localUser) { // 更新本地媒体状态文本 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'; } } // 更新静音图标 if (this.elements.localMuteIcon) { if (!localUser.mediaState.audio) { this.elements.localMuteIcon.classList.remove('hidden'); this.elements.localMuteIcon.className = 'fas fa-microphone-slash text-gray-500 text-xs'; } else { this.elements.localMuteIcon.classList.add('hidden'); } } } // 渲染侧边栏用户列表 renderUserList(localUser, remoteUser) { if (!this.elements.userList) return; // 渲染本地用户 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 remoteUserElement = this.elements.userList.querySelector('[data-user-id="remote"]'); if (remoteUserElement) { // 渲染远程用户头像 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; } } } // 在renderer.js中添加方法 // 获取视频流分辨率 getVideoResolution(track) { if (track && track.getSettings) { const settings = track.getSettings(); return { width: settings.width || 640, height: settings.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布局,使视频居中 container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'center'; // 优化图像渲染 videoElement.style.imageRendering = 'pixelated'; // 确保视频元素在容器内正确显示 videoElement.style.maxWidth = '100%'; videoElement.style.maxHeight = '100%'; videoElement.style.objectFit = 'contain'; } // 渲染控制按钮 renderControlButtons(mediaState) { if (this.elements.micBtn) { toggleButtonState(this.elements.micBtn, !mediaState.audio); } if (this.elements.videoBtn) { toggleButtonState(this.elements.videoBtn, !mediaState.video); } if (this.elements.recordBtn) { toggleButtonState(this.elements.recordBtn, mediaState.recording); } } // 渲染聊天消息 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)}`; 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'; } else if (message.isSelf) { messageClass += ' message-self'; } else { messageClass += ' message-other'; } messageDiv.className = messageClass; messageDiv.dataset.messageId = message.id; let contentHTML = ''; if (message.type === 'file' && message.content.startsWith('data:image/')) { // 图片消息 contentHTML = `
`; } else { // 文本消息 contentHTML = ` `; } messageDiv.innerHTML = ` `; return messageDiv; } // 渲染未读消息数 renderUnreadCount(count) { if (this.elements.unreadBadge) { if (count > 0) { this.elements.unreadBadge.textContent = count; this.elements.unreadBadge.classList.remove('hidden'); } else { this.elements.unreadBadge.classList.add('hidden'); } } } // 渲染侧边栏 renderSidebar(isOpen) { if (this.elements.sidebar) { if (isOpen) { this.elements.sidebar.classList.remove('hidden'); } else { this.elements.sidebar.classList.add('hidden'); } } } // 渲染网络状态 renderNetworkStatus(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; default: icon.className = 'fas fa-question-circle text-gray-400'; networkStatusText.textContent = '未知'; networkStatusText.className = 'text-gray-400'; } // 添加图标到网络状态元素 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; default: statusClass = 'text-gray-400'; } // 更新连接质量文本和样式 this.elements.connectionQuality.textContent = `连接质量: ${qualityText}`; this.elements.connectionQuality.className = `text-xs ${statusClass}`; } } // 渲染通话结束 renderCallEnded() { console.log('Call ended'); // 跳转到结束通话界面 window.location.href = './endcall/endcall.html'; } // 获取状态文本 getStatusText(status) { const statusMap = { 'online': '在线', 'offline': '离线', 'connecting': '连接中' }; return statusMap[status] || status; } // 获取网络质量文本 getNetworkQualityText(quality) { const qualityMap = { 'excellent': '优秀', 'good': '良好', 'fair': '一般', 'poor': '较差' }; return qualityMap[quality] || quality; } // 销毁 destroy() { if (this.unsubscribe) { this.unsubscribe(); } } } export default UIRenderer;