Files
video_socket-server/client/public/renderer.js

539 lines
21 KiB
JavaScript
Raw Normal View History

2026-04-29 15:18:30 +08:00
import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js';
import { mockCallSession } from './models.js';
import chatMessage from './chatmessage.js';
import store from './store.js';
2026-05-24 13:29:54 +08:00
import {
buildUserCountLabel,
createUserEntryElement,
getCallTitle,
getMediaStatusMeta,
getNetworkQualityDisplay,
getRemoteVideoPlaceholderText
} from './renderer-ui.js';
2026-05-24 13:56:53 +08:00
import { renderChatMessagesInto } from './renderer-chat.js';
import {
updateParticipantTileName as syncParticipantTileName,
updateParticipantTilePlaceholder
} from './renderer-participant-grid.js';
import {
adjustVideoSize,
clearParticipantGrid,
getVideoResolution,
removeParticipantTile,
renderParticipantStreamMedia,
renderSingleRemoteStreamMedia
} from './renderer-media.js';
2026-04-29 15:18:30 +08:00
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) {
2026-05-24 13:56:53 +08:00
adjustVideoSize(this.elements.remoteVideo, getVideoResolution(videoTracks[0]));
2026-04-29 15:18:30 +08:00
}
}
});
}
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() {
}
/**
2026-05-24 13:29:54 +08:00
* 婵炴挸寮堕悡瀣棘鐟欏嫮銆?- 闁哄秷顫夊畵渚懜纰樺亾娴g缍侀柛鏍ㄧ墬濞插潡寮惂鐕?
* @param {Object} state - 鐟滅増鎸告晶鐘虫償閺冨倹鏆忛柣妯垮煐閳?
* @param {Object} changes - 闁绘鍩栭埀顑跨瑜板宕犻弽褜鍤犻悹?
2026-04-29 15:18:30 +08:00
*/
render(state, changes) {
switch (changes.type) {
case 'INIT':
2026-05-24 14:05:51 +08:00
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);
2026-04-29 15:18:30 +08:00
if (this.elements.remoteVideoPlaceholder) {
if (state.remoteStream) {
2026-05-24 14:05:51 +08:00
this.elements.remoteVideoPlaceholder.classList.add('hidden');
2026-04-29 15:18:30 +08:00
} else {
2026-05-24 14:05:51 +08:00
this.elements.remoteVideoPlaceholder.classList.remove('hidden');
2026-04-29 15:18:30 +08:00
}
}
break;
case 'DURATION_UPDATE':
this.renderCallDuration(changes.duration);
break;
case 'LOCAL_MEDIA_CHANGE':
2026-05-24 14:05:51 +08:00
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);
2026-04-29 15:18:30 +08:00
break;
case 'LOCAL_STREAM_OBTAINED':
this.renderLocalStream(state.localStream);
this.renderLocalVideo(state.session.localUser, state.localStream);
break;
case 'REMOTE_STREAM_OBTAINED':
2026-05-24 14:05:51 +08:00
this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost);
2026-04-29 15:18:30 +08:00
if (this.elements.connectingOverlay) {
this.elements.connectingOverlay.classList.add('hidden');
}
break;
case 'REMOTE_MEDIA_CHANGE':
2026-05-24 14:05:51 +08:00
this.renderRemoteVideo(state.session.remoteUser);
this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants);
2026-04-29 15:18:30 +08:00
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() || '';
2026-05-24 13:29:54 +08:00
this.elements.headerTitle.textContent = getCallTitle(connectionId);
2026-04-29 15:18:30 +08:00
}
}
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) {
2026-05-24 13:29:54 +08:00
this.renderUserList(
this.stateManager.getState().session.localUser,
remoteUser,
this.stateManager.getState().participants
);
2026-04-29 15:18:30 +08:00
if (this.elements.remoteVideoPlaceholder) {
const shouldShowPlaceholder = !remoteUser.mediaState.video;
2026-05-24 13:29:54 +08:00
const placeholderText = getRemoteVideoPlaceholderText(!shouldShowPlaceholder);
2026-04-29 15:18:30 +08:00
toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder);
2026-05-16 20:11:36 +08:00
if (this.elements.remoteVideo) {
2026-05-24 13:29:54 +08:00
this.elements.remoteVideo.style.opacity = shouldShowPlaceholder ? '0' : '1';
2026-05-16 20:11:36 +08:00
}
2026-05-24 13:29:54 +08:00
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;
2026-04-29 15:18:30 +08:00
}
2026-05-24 13:29:54 +08:00
if (subtitleElement) {
subtitleElement.textContent = placeholderText.subtitle;
2026-04-29 15:18:30 +08:00
}
}
}
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) {
2026-05-24 13:29:54 +08:00
const display = getNetworkQualityDisplay(networkQuality);
textElement.textContent = display.label;
iconElement.className = display.headerIconClass;
2026-04-29 15:18:30 +08:00
}
}
}
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;
2026-05-24 14:05:51 +08:00
this.elements.localVideo.muted = true;
2026-04-29 15:18:30 +08:00
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, connectionId, isHost) {
if (isHost && connectionId) {
this.renderParticipantStream(stream, connectionId);
} else {
this.renderSingleRemoteStream(stream);
}
}
renderParticipantStream(stream, connectionId) {
2026-05-24 13:56:53 +08:00
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
});
2026-04-29 15:18:30 +08:00
}
renderParticipantVideoPlaceholder(participantId, showPlaceholder) {
const grid = this.elements.participantGrid;
if (!grid) return;
2026-05-24 13:56:53 +08:00
updateParticipantTilePlaceholder(grid, participantId, showPlaceholder);
console.log(`Updated placeholder for participant ${participantId}: ${showPlaceholder ? 'shown' : 'hidden'}`);
2026-04-29 15:18:30 +08:00
}
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;
2026-05-24 13:56:53 +08:00
syncParticipantTileName(grid, participantId, name);
if (name) {
console.log(`Updated tile name for participant ${participantId}: ${name}`);
2026-04-29 15:18:30 +08:00
}
}
renderSingleRemoteStream(stream) {
2026-05-24 13:56:53 +08:00
renderSingleRemoteStreamMedia({
remoteVideo: this.elements.remoteVideo,
stream,
disconnectedOverlay: this.elements.disconnectedOverlay,
remoteVideoPlaceholder: this.elements.remoteVideoPlaceholder,
connectingOverlay: this.elements.connectingOverlay
2026-04-29 15:18:30 +08:00
});
}
renderLocalUserStatus(localUser) {
2026-05-24 13:29:54 +08:00
const mediaMeta = getMediaStatusMeta(localUser.mediaState);
2026-04-29 15:18:30 +08:00
if (this.elements.localMediaStatus) {
2026-05-24 13:29:54 +08:00
this.elements.localMediaStatus.textContent = mediaMeta.text;
this.elements.localMediaStatus.className = mediaMeta.className;
2026-04-29 15:18:30 +08:00
}
if (this.elements.localMuteIcon) {
2026-05-24 13:29:54 +08:00
if (mediaMeta.showMuteIcon) {
2026-04-29 15:18:30 +08:00
this.elements.localMuteIcon.classList.remove('hidden');
2026-05-24 13:29:54 +08:00
this.elements.localMuteIcon.className = mediaMeta.muteIconClass;
2026-04-29 15:18:30 +08:00
} 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;
2026-05-24 13:29:54 +08:00
if (this.elements.userCountDisplay) {
this.elements.userCountDisplay.textContent = buildUserCountLabel(userCount);
2026-04-29 15:18:30 +08:00
}
this.elements.userList.innerHTML = '';
2026-05-24 13:29:54 +08:00
this.elements.userList.appendChild(createUserEntryElement({
2026-04-29 15:18:30 +08:00
user: localUser,
role: 'local'
}));
if (participantCount > 0) {
2026-05-24 13:29:54 +08:00
for (const [participantId, participant] of Object.entries(participantsMap)) {
this.elements.userList.appendChild(createUserEntryElement({
user: participant,
role: participant.role === 'host' ? 'host' : 'participant',
id: participantId
}));
2026-04-29 15:18:30 +08:00
}
} else if (remoteUser.status !== 'offline') {
2026-05-24 13:29:54 +08:00
this.elements.userList.appendChild(createUserEntryElement({
2026-04-29 15:18:30 +08:00
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) {
2026-05-24 13:56:53 +08:00
renderChatMessagesInto(this.elements.chatContent, messages, formatTimestamp);
2026-04-29 15:18:30 +08:00
}
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) {
2026-05-24 13:29:54 +08:00
const display = getNetworkQualityDisplay(quality);
2026-04-29 15:18:30 +08:00
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');
2026-05-24 13:29:54 +08:00
icon.className = display.statusIconClass;
networkStatusText.textContent = display.label;
networkStatusText.className = display.statusTextClass;
2026-04-29 15:18:30 +08:00
networkStatus.insertBefore(icon, networkStatusText);
}
if (this.elements.connectionQuality) {
2026-05-24 13:29:54 +08:00
this.elements.connectionQuality.textContent = `连接质量: ${display.label}`;
this.elements.connectionQuality.className = `text-xs ${display.connectionTextClass}`;
2026-04-29 15:18:30 +08:00
}
this.updateHeaderNetworkIndicator(quality);
this.renderHeaderNetworkStatus(quality);
}
updateHeaderNetworkIndicator(networkQuality) {
if (!this.elements.remoteNetworkIndicator) return;
2026-05-24 13:29:54 +08:00
this.elements.remoteNetworkIndicator.className = getNetworkQualityDisplay(networkQuality).indicatorClass;
2026-04-29 15:18:30 +08:00
}
renderCallEnded() {
console.log('Call ended');
2026-05-24 13:56:53 +08:00
clearParticipantGrid(this.elements.participantGrid);
2026-04-29 15:18:30 +08:00
window.location.href = './endcall/endcall.html';
}
renderParticipantLeft(connectionId) {
console.log(`Participant left: ${connectionId}, updating UI`);
2026-05-24 13:56:53 +08:00
removeParticipantTile({
grid: this.elements.participantGrid,
connectionId,
getGridTemplateColumns,
remoteVideo: this.elements.remoteVideo,
remoteVideoPlaceholder: this.elements.remoteVideoPlaceholder,
remoteNetworkIndicator: this.elements.remoteNetworkIndicator
});
2026-04-29 15:18:30 +08:00
}
destroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
if (this.messageUnsubscribe) {
this.messageUnsubscribe();
}
}
}
export default UIRenderer;