913 lines
38 KiB
JavaScript
913 lines
38 KiB
JavaScript
/**
|
||
* UI渲染器
|
||
* 负责将状态映射到DOM,与状态管理解耦
|
||
*/
|
||
import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js';
|
||
import { mockCallSession } from './models.js';
|
||
import chatMessage from './chatmessage.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'),
|
||
remoteNetworkIndicator: document.getElementById('remoteNetworkIndicator'),
|
||
remoteNetworkQuality: document.getElementById('remoteNetworkQuality'),
|
||
|
||
// 远端视频
|
||
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'),
|
||
remoteInfoOverlay: document.querySelector('.absolute.top-6.left-6.glass'),
|
||
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')
|
||
};
|
||
|
||
// 绑定事件监听器
|
||
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', () => {
|
||
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);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 渲染消息状态变化
|
||
renderMessageState(messageState, changes) {
|
||
switch (changes.type) {
|
||
case 'NEW_MESSAGE':
|
||
this.renderChatMessages(messageState.messages);
|
||
this.renderUnreadCount(changes.unreadCount);
|
||
break;
|
||
case 'SIDEBAR_TOGGLE':
|
||
this.renderSidebar(changes.isOpen);
|
||
// 当侧边栏打开时,重置未读消息计数
|
||
if (changes.isOpen) {
|
||
this.renderUnreadCount(0);
|
||
} else {
|
||
this.renderUnreadCount(changes.unreadCount);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 绑定事件监听器
|
||
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(chatMessage.getMessageState().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 'USER_LIST_UPDATE':
|
||
this.renderUserList(changes.localUser, changes.remoteUser);
|
||
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) {
|
||
// 未连接时不显示红框部分
|
||
if (session.status === 'idle' || session.status === 'connecting') {
|
||
this.elements.headerTitle.textContent = '通话';
|
||
} else {
|
||
this.elements.headerTitle.textContent = `与 ${session.remoteUser.name} 的通话`;
|
||
}
|
||
}
|
||
|
||
if (this.elements.encryptionBadge) {
|
||
toggleElement(this.elements.encryptionBadge, session.isEncrypted);
|
||
}
|
||
|
||
// 始终显示网络状态指示器和质量
|
||
if (this.elements.remoteNetworkIndicator) {
|
||
this.elements.remoteNetworkIndicator.classList.remove('hidden');
|
||
}
|
||
if (this.elements.remoteNetworkQuality) {
|
||
this.elements.remoteNetworkQuality.classList.remove('hidden');
|
||
}
|
||
|
||
this.renderCallDuration(session.duration);
|
||
}
|
||
|
||
// 渲染通话时长
|
||
renderCallDuration(duration) {
|
||
if (this.elements.callDuration) {
|
||
this.elements.callDuration.textContent = formatTime(duration);
|
||
}
|
||
}
|
||
|
||
// 渲染远端视频
|
||
renderRemoteVideo(remoteUser) {
|
||
// 找到远端信息覆盖层元素
|
||
const remoteInfoOverlay = this.elements.remoteName?.closest('.absolute.top-6.left-6.glass');
|
||
|
||
// 未连接时不显示红框部分
|
||
if (remoteUser.status === 'offline') {
|
||
// 隐藏整个远端信息覆盖层
|
||
if (remoteInfoOverlay) {
|
||
remoteInfoOverlay.classList.add('hidden');
|
||
}
|
||
} else {
|
||
// 显示远端信息覆盖层
|
||
if (remoteInfoOverlay) {
|
||
remoteInfoOverlay.classList.remove('hidden');
|
||
}
|
||
|
||
// 连接后更新头像和名称数据
|
||
if (this.elements.remoteName) {
|
||
this.elements.remoteName.textContent = remoteUser.name;
|
||
}
|
||
|
||
if (this.elements.remoteAvatar) {
|
||
this.elements.remoteAvatar.src = remoteUser.avatar;
|
||
this.elements.remoteAvatar.classList.remove('hidden');
|
||
}
|
||
|
||
if (this.elements.remoteStatus) {
|
||
// 显示与黄框中一致的状态
|
||
if (!remoteUser.mediaState.audio) {
|
||
this.elements.remoteStatus.textContent = '静音中';
|
||
} else if (!remoteUser.mediaState.video) {
|
||
this.elements.remoteStatus.textContent = '视频关闭';
|
||
} else {
|
||
this.elements.remoteStatus.textContent = this.getStatusText(remoteUser.status);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 同步更新侧边栏用户列表
|
||
this.renderUserList(this.stateManager.getState().session.localUser, remoteUser);
|
||
|
||
// 当远程视频关闭时显示占位符
|
||
if (this.elements.remoteVideoPlaceholder) {
|
||
const shouldShowPlaceholder = !remoteUser.mediaState.video;
|
||
toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder);
|
||
|
||
// 更新占位符文本内容
|
||
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 = '对方暂时关闭了视频';
|
||
}
|
||
}
|
||
} 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 (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);
|
||
|
||
// 渲染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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 渲染本地视频
|
||
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 = false; // 不要静音远程视频,否则听不到对方的声音
|
||
|
||
// 关键设置:启用硬件加速和最佳质量渲染
|
||
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');
|
||
}
|
||
|
||
|
||
|
||
// 获取视频轨道并处理分辨率
|
||
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);
|
||
});
|
||
}
|
||
// 隐藏连接中提示
|
||
if (this.elements.connectingOverlay) {
|
||
this.elements.connectingOverlay.classList.add('hidden');
|
||
}
|
||
|
||
// 隐藏占位背景
|
||
if (this.elements.remoteVideoPlaceholder) {
|
||
this.elements.remoteVideoPlaceholder.classList.add('hidden');
|
||
}
|
||
} 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;
|
||
|
||
// 计算通话成员总数
|
||
let userCount = 1; // 至少有本地用户
|
||
if (remoteUser.status !== 'offline') {
|
||
userCount++; // 如果远程用户在线,增加计数
|
||
}
|
||
|
||
// 更新通话成员总数显示
|
||
const userCountElement = this.elements.userList.closest('div').querySelector('h3.text-sm.font-medium.text-gray-400');
|
||
if (userCountElement) {
|
||
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');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 渲染远程用户
|
||
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');
|
||
|
||
// 渲染远程用户头像
|
||
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');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 在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 = `
|
||
<div class="message-image-container">
|
||
<img src="${message.content}" class="message-image" alt="${message.fileName || '图片'}">
|
||
${message.fileName ? `<div class="message-image-name">${message.fileName}</div>` : ''}
|
||
</div>
|
||
`;
|
||
} else {
|
||
// 文本消息
|
||
contentHTML = `
|
||
<div class="message-text">
|
||
${message.content}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
messageDiv.innerHTML = `
|
||
<div class="message-header">
|
||
<img src="${message.senderAvatar}" class="message-avatar">
|
||
<div>
|
||
<span class="message-sender">${message.senderName}</span>
|
||
<span class="message-time">${formatTimestamp(message.timestamp)}</span>
|
||
</div>
|
||
</div>
|
||
<div class="message-content">
|
||
${contentHTML}
|
||
</div>
|
||
`;
|
||
|
||
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;
|
||
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';
|
||
}
|
||
|
||
// 添加图标到网络状态元素
|
||
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.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';
|
||
}
|
||
}
|
||
|
||
|
||
// 渲染通话结束
|
||
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': '较差',
|
||
'no_signal': '无信号'
|
||
};
|
||
return qualityMap[quality] || quality;
|
||
}
|
||
|
||
// 销毁
|
||
destroy() {
|
||
if (this.unsubscribe) {
|
||
this.unsubscribe();
|
||
}
|
||
if (this.messageUnsubscribe) {
|
||
this.messageUnsubscribe();
|
||
}
|
||
}
|
||
}
|
||
|
||
export default UIRenderer;
|