优化目录结构
This commit is contained in:
189
client/public/call/renderers/renderer-ui.js
Normal file
189
client/public/call/renderers/renderer-ui.js
Normal file
@@ -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
|
||||
? '<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">\u4e3b\u6301\u4eba</span>'
|
||||
: '<span class="text-xs bg-purple-500 px-1.5 rounded ml-1">\u53c2\u4e0e\u8005</span>';
|
||||
}
|
||||
|
||||
if (role === 'participant') {
|
||||
return '<span class="text-xs bg-purple-500 px-1.5 rounded ml-1">\u53c2\u4e0e\u8005</span>';
|
||||
}
|
||||
|
||||
return '<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">\u4e3b\u6301\u4eba</span>';
|
||||
}
|
||||
|
||||
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 `<img src="${user.avatar}" class="w-10 h-10 rounded-full object-cover">`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="relative">
|
||||
<img src="${user.avatar}" class="w-10 h-10 rounded-full object-cover">
|
||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getRightMarkup(mediaState, role, muteIconMarkup) {
|
||||
if (role !== 'participant') {
|
||||
return muteIconMarkup;
|
||||
}
|
||||
|
||||
const speakingMarkup = (mediaState.isSpeaking && mediaState.audio)
|
||||
? '<div class="audio-wave w-6"><span></span><span></span><span></span><span></span><span></span></div>'
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="flex items-center gap-2">
|
||||
${muteIconMarkup}
|
||||
${speakingMarkup}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<i class="${mediaMeta.muteIconClass}"></i>`
|
||||
: '';
|
||||
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)}
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium">
|
||||
${user.name}
|
||||
${getRoleTagMarkup(user, role)}
|
||||
</div>
|
||||
<div class="${mediaMeta.className}"${dataFieldAttr}>${mediaMeta.text}</div>
|
||||
</div>
|
||||
${getRightMarkup(user.mediaState, role, muteIconMarkup)}
|
||||
`;
|
||||
|
||||
return entry;
|
||||
}
|
||||
541
client/public/call/renderers/renderer.js
Normal file
541
client/public/call/renderers/renderer.js
Normal file
@@ -0,0 +1,541 @@
|
||||
import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from '../../shared/utils.js';
|
||||
import { mockCallSession } from '../models.js';
|
||||
import chatMessage from '../chat/chatmessage.js';
|
||||
import store from '../store.js';
|
||||
import {
|
||||
buildUserCountLabel,
|
||||
createUserEntryElement,
|
||||
getCallTitle,
|
||||
getMediaStatusMeta,
|
||||
getNetworkQualityDisplay,
|
||||
getRemoteVideoPlaceholderText
|
||||
} from './renderer-ui.js';
|
||||
import { renderChatMessagesInto } from '../chat/renderer-chat.js';
|
||||
import {
|
||||
updateParticipantTileName as syncParticipantTileName,
|
||||
updateParticipantTilePlaceholder
|
||||
} from '../participants/renderer-participant-grid.js';
|
||||
import {
|
||||
adjustVideoSize,
|
||||
clearParticipantGrid,
|
||||
getVideoResolution,
|
||||
removeParticipantTile,
|
||||
renderParticipantStreamMedia,
|
||||
renderSingleRemoteStreamMedia
|
||||
} from '../media/renderer-media.js';
|
||||
import { createLogger } from '../../shared/logger.js';
|
||||
|
||||
const logger = createLogger('renderer');
|
||||
|
||||
const GRID_LAYOUT = {
|
||||
maxColumns: 3,
|
||||
breakpoints: [
|
||||
{ maxParticipants: 1, template: '1fr' },
|
||||
{ maxParticipants: 4, template: 'repeat(2, 1fr)' }
|
||||
],
|
||||
defaultTemplate: 'repeat(3, 1fr)'
|
||||
};
|
||||
|
||||
function getGridTemplateColumns(participantCount) {
|
||||
for (const bp of GRID_LAYOUT.breakpoints) {
|
||||
if (participantCount <= bp.maxParticipants) {
|
||||
return bp.template;
|
||||
}
|
||||
}
|
||||
return GRID_LAYOUT.defaultTemplate;
|
||||
}
|
||||
|
||||
class UIRenderer {
|
||||
constructor(stateManager) {
|
||||
this.stateManager = stateManager;
|
||||
this.unsubscribe = stateManager.subscribe(this.render.bind(this));
|
||||
|
||||
this.elements = {
|
||||
header: document.querySelector('header'),
|
||||
footer: document.querySelector('footer'),
|
||||
participantGrid: document.getElementById('participantGrid'),
|
||||
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'),
|
||||
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) {
|
||||
adjustVideoSize(this.elements.remoteVideo, getVideoResolution(videoTracks[0]));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 婵炴挸寮堕悡瀣棘鐟欏嫮銆?- 闁哄秷顫夊畵渚€鎮╅懜纰樺亾娴g缍侀柛鏍ㄧ墬濞插潡寮惂鐕?
|
||||
* @param {Object} state - 鐟滅増鎸告晶鐘虫償閺冨倹鏆忛柣妯垮煐閳?
|
||||
* @param {Object} changes - 闁绘鍩栭埀顑跨瑜板宕犻弽褜鍤犻悹?
|
||||
*/
|
||||
render(state, changes) {
|
||||
switch (changes.type) {
|
||||
case 'INIT':
|
||||
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);
|
||||
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, 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);
|
||||
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);
|
||||
if (changes.participantId && state.session.localUser.isHost) {
|
||||
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':
|
||||
this.renderUserList(state.session.localUser, state.session.remoteUser, changes.participants || state.participants);
|
||||
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':
|
||||
this.renderParticipantLeft(changes.connectionId);
|
||||
break;
|
||||
case 'RESOLUTION_CHANGED':
|
||||
this.renderResolutionChanged(changes.resolution);
|
||||
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) {
|
||||
|
||||
this.renderHeaderTitle();
|
||||
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);
|
||||
}
|
||||
renderHeaderTitle() {
|
||||
if (this.elements.headerTitle) {
|
||||
const connectionId = store.getConnectionId() || '';
|
||||
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;
|
||||
const isActive = (resolution.height >= 1440 && btnRes === '1440') ||
|
||||
(resolution.height >= 1080 && resolution.height < 1440 && btnRes === '1080') ||
|
||||
(resolution.height >= 720 && resolution.height < 1080 && btnRes === '720') ||
|
||||
(resolution.height < 720 && btnRes === '480');
|
||||
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
|
||||
);
|
||||
|
||||
if (this.elements.remoteVideoPlaceholder) {
|
||||
const shouldShowPlaceholder = !remoteUser.mediaState.video;
|
||||
const placeholderText = getRemoteVideoPlaceholderText(!shouldShowPlaceholder);
|
||||
toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder);
|
||||
|
||||
if (this.elements.remoteVideo) {
|
||||
this.elements.remoteVideo.style.opacity = shouldShowPlaceholder ? '0' : '1';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (subtitleElement) {
|
||||
subtitleElement.textContent = placeholderText.subtitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.renderNetworkStatus(remoteUser.networkQuality);
|
||||
this.renderHeaderNetworkStatus(remoteUser.networkQuality);
|
||||
}
|
||||
|
||||
renderHeaderNetworkStatus(networkQuality) {
|
||||
if (this.elements.remoteNetworkQuality) {
|
||||
const textElement = this.elements.remoteNetworkQuality.querySelector('span');
|
||||
const iconElement = this.elements.remoteNetworkQuality.querySelector('i');
|
||||
|
||||
if (textElement && iconElement) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
logger.debug('srcObject set successfully:', this.elements.localVideo.srcObject);
|
||||
|
||||
if (this.elements.disconnectedOverlay) {
|
||||
this.elements.disconnectedOverlay.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
logger.error('Either localVideo element or stream is missing');
|
||||
}
|
||||
}
|
||||
|
||||
renderRemoteStream(stream, connectionId, isHost) {
|
||||
if (isHost && connectionId) {
|
||||
this.renderParticipantStream(stream, connectionId);
|
||||
} else {
|
||||
this.renderSingleRemoteStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
renderParticipantStream(stream, connectionId) {
|
||||
const participantInfo = this.stateManager.getState().participants[connectionId];
|
||||
renderParticipantStreamMedia({
|
||||
grid: this.elements.participantGrid,
|
||||
stream,
|
||||
connectionId,
|
||||
displayName: participantInfo?.name,
|
||||
getGridTemplateColumns,
|
||||
remoteVideo: this.elements.remoteVideo,
|
||||
connectingOverlay: this.elements.connectingOverlay,
|
||||
remoteVideoPlaceholder: this.elements.remoteVideoPlaceholder
|
||||
});
|
||||
}
|
||||
renderParticipantVideoPlaceholder(participantId, showPlaceholder) {
|
||||
const grid = this.elements.participantGrid;
|
||||
if (!grid) return;
|
||||
updateParticipantTilePlaceholder(grid, participantId, showPlaceholder);
|
||||
logger.debug(`Updated placeholder for participant ${participantId}: ${showPlaceholder ? 'shown' : 'hidden'}`);
|
||||
}
|
||||
|
||||
syncParticipantTileNames(participants) {
|
||||
if (!participants) return;
|
||||
const grid = this.elements.participantGrid;
|
||||
if (!grid) return;
|
||||
for (const [participantId, pInfo] of Object.entries(participants)) {
|
||||
this.updateParticipantTileName(participantId, pInfo.name);
|
||||
}
|
||||
}
|
||||
|
||||
updateParticipantTileName(participantId, name) {
|
||||
const grid = this.elements.participantGrid;
|
||||
if (!grid) return;
|
||||
syncParticipantTileName(grid, participantId, name);
|
||||
if (name) {
|
||||
logger.debug(`Updated tile name for participant ${participantId}: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
renderSingleRemoteStream(stream) {
|
||||
renderSingleRemoteStreamMedia({
|
||||
remoteVideo: this.elements.remoteVideo,
|
||||
stream,
|
||||
disconnectedOverlay: this.elements.disconnectedOverlay,
|
||||
remoteVideoPlaceholder: this.elements.remoteVideoPlaceholder,
|
||||
connectingOverlay: this.elements.connectingOverlay
|
||||
});
|
||||
}
|
||||
|
||||
renderLocalUserStatus(localUser) {
|
||||
const mediaMeta = getMediaStatusMeta(localUser.mediaState);
|
||||
|
||||
if (this.elements.localMediaStatus) {
|
||||
this.elements.localMediaStatus.textContent = mediaMeta.text;
|
||||
this.elements.localMediaStatus.className = mediaMeta.className;
|
||||
}
|
||||
|
||||
if (this.elements.localMuteIcon) {
|
||||
if (mediaMeta.showMuteIcon) {
|
||||
this.elements.localMuteIcon.classList.remove('hidden');
|
||||
this.elements.localMuteIcon.className = mediaMeta.muteIconClass;
|
||||
} else {
|
||||
this.elements.localMuteIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderUserList(localUser, remoteUser, participants) {
|
||||
if (!this.elements.userList) return;
|
||||
|
||||
const participantsMap = participants || {};
|
||||
const participantCount = Object.keys(participantsMap).length;
|
||||
const userCount = 1 + participantCount;
|
||||
|
||||
if (this.elements.userCountDisplay) {
|
||||
this.elements.userCountDisplay.textContent = buildUserCountLabel(userCount);
|
||||
}
|
||||
|
||||
this.elements.userList.innerHTML = '';
|
||||
this.elements.userList.appendChild(createUserEntryElement({
|
||||
user: localUser,
|
||||
role: 'local'
|
||||
}));
|
||||
|
||||
if (participantCount > 0) {
|
||||
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') {
|
||||
this.elements.userList.appendChild(createUserEntryElement({
|
||||
user: remoteUser,
|
||||
role: 'remote'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
renderChatMessagesInto(this.elements.chatContent, messages, formatTimestamp);
|
||||
}
|
||||
|
||||
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) {
|
||||
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');
|
||||
icon.className = display.statusIconClass;
|
||||
networkStatusText.textContent = display.label;
|
||||
networkStatusText.className = display.statusTextClass;
|
||||
networkStatus.insertBefore(icon, networkStatusText);
|
||||
}
|
||||
|
||||
if (this.elements.connectionQuality) {
|
||||
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;
|
||||
this.elements.remoteNetworkIndicator.className = getNetworkQualityDisplay(networkQuality).indicatorClass;
|
||||
}
|
||||
|
||||
renderCallEnded() {
|
||||
logger.debug('Call ended');
|
||||
clearParticipantGrid(this.elements.participantGrid);
|
||||
window.location.href = '/endcall/';
|
||||
}
|
||||
|
||||
renderParticipantLeft(connectionId) {
|
||||
logger.debug(`Participant left: ${connectionId}, updating UI`);
|
||||
removeParticipantTile({
|
||||
grid: this.elements.participantGrid,
|
||||
connectionId,
|
||||
getGridTemplateColumns,
|
||||
remoteVideo: this.elements.remoteVideo,
|
||||
remoteVideoPlaceholder: this.elements.remoteVideoPlaceholder,
|
||||
remoteNetworkIndicator: this.elements.remoteNetworkIndicator
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
if (this.messageUnsubscribe) {
|
||||
this.messageUnsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UIRenderer;
|
||||
Reference in New Issue
Block a user