Files
webRtc/WebApp/client/public/onebyone/renderer.js
2026-03-05 11:30:27 +08:00

542 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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) {
this.elements.remoteVideo.srcObject = stream;
this.elements.remoteVideo.autoplay = true;
// 关键设置:启用硬件加速和最佳质量渲染
this.elements.remoteVideo.style.transform = 'translateZ(0)'; // 启用硬件加速
this.elements.remoteVideo.style.imageRendering = 'pixelated'; // 保持像素清晰
this.elements.remoteVideo.style.objectFit = 'contain'; // 保持比例
console.log('Remote stream set successfully:', this.elements.remoteVideo.srcObject);
// 隐藏断开连接覆盖层
if (this.elements.disconnectedOverlay) {
this.elements.disconnectedOverlay.classList.add('hidden');
}
// 隐藏占位背景
if (this.elements.remoteVideoPlaceholder) {
this.elements.remoteVideoPlaceholder.classList.add('hidden');
}
// 获取视频轨道并处理分辨率
const videoTracks = stream.getVideoTracks();
if (videoTracks.length > 0) {
const resolution = this.getVideoResolution(videoTracks[0]);
this.adjustVideoSize(this.elements.remoteVideo, resolution);
// 监听轨道变化,处理分辨率调整
videoTracks[0].addEventListener('resize', () => {
const newResolution = this.getVideoResolution(videoTracks[0]);
this.adjustVideoSize(this.elements.remoteVideo, newResolution);
});
}
} else {
console.error('Either remoteVideo element or stream is missing');
// 显示占位背景
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 = `
<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) {
const showNetworkStatus = quality !== 'excellent';
toggleElement(this.elements.networkStatus, showNetworkStatus);
if (showNetworkStatus) {
this.elements.networkStatusText.textContent = this.getNetworkQualityText(quality);
}
}
if (this.elements.connectionQuality) {
this.elements.connectionQuality.textContent = `连接质量: ${this.getNetworkQualityText(quality)}`;
}
}
// 渲染通话结束
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;