diff --git a/client/public/chatmessage.js b/client/public/chatmessage.js index 4f87867..95750b4 100644 --- a/client/public/chatmessage.js +++ b/client/public/chatmessage.js @@ -1,3 +1,6 @@ +import { createLogger } from './logger.js'; + +const logger = createLogger('chat'); /** * 消息模块 * 处理聊天消息的发送、接收和显示 @@ -72,7 +75,7 @@ function sendChatMessage(message) { * @param {Object} data - 消息数据 */ function handleChatMessage(data) { - console.log('处理聊天:', data); + logger.debug('处理聊天:', data); addMessage(data); const isImage = data.content && data.content.startsWith('data:image/'); diff --git a/client/public/connect/connect.js b/client/public/connect/connect.js index 0ace7b9..8fc69bf 100644 --- a/client/public/connect/connect.js +++ b/client/public/connect/connect.js @@ -1,3 +1,6 @@ +import { createLogger } from '../logger.js'; + +const logger = createLogger('legacy-connect'); /** * 连接界面逻辑 * 处理初始连接、创建通话和加入通话的功能 @@ -51,7 +54,7 @@ async function getAllConnectionIds() { const data = await response.json(); displayConnectionIds(data.connectionIds); } catch (error) { - console.error('Error fetching connection IDs:', error); + logger.error('Error fetching connection IDs:', error); showNotification('获取连接ID失败', 'error'); } } @@ -164,7 +167,7 @@ function loadUserSettings() { document.getElementById('userAvatar').src = avatar; document.getElementById('avatarPreview').src = avatar; } catch (error) { - console.error('Error loading user settings:', error); + logger.error('Error loading user settings:', error); // 加载失败时使用默认头像 const defaultAvatar = '/images/p1.png'; document.getElementById('userAvatar').src = defaultAvatar; @@ -254,7 +257,7 @@ function handleAvatarUpload(event) { } }) .catch(error => { - console.error('Error uploading avatar:', error); + logger.error('Error uploading avatar:', error); showNotification('头像上传失败,请重试', 'error'); // 上传失败时,使用默认头像 diff --git a/client/public/connectview.js b/client/public/connectview.js index a016b79..176a086 100644 --- a/client/public/connectview.js +++ b/client/public/connectview.js @@ -7,6 +7,9 @@ import { renderOnlineUsers } from './connect-directory.js'; import { createProfileSettingsController } from './profile-settings.js'; +import { createLogger } from './logger.js'; + +const logger = createLogger('connectview'); let onWsStatusChange = null; let cachedOnlineUsers = []; @@ -45,9 +48,9 @@ export async function initWebSocket() { store.syncSocketUserInfo(); updateWsStatus(true); await refreshOnlineUsers(); - console.log('WebSocket initialized from connectview'); + logger.debug('WebSocket initialized from connectview'); } catch (error) { - console.error('Failed to initialize WebSocket:', error); + logger.error('Failed to initialize WebSocket:', error); updateWsStatus(false); showNotification('WebSocket连接失败,请刷新页面重试', 'error'); } @@ -62,7 +65,7 @@ async function refreshOnlineUsers(silent = true) { showNotification(`当前共有 ${cachedOnlineUsers.length} 个WebSocket用户在线`); } } catch (error) { - console.error('Error fetching online users:', error); + logger.error('Error fetching online users:', error); if (!silent) { showNotification('获取在线用户失败', 'error'); } @@ -78,7 +81,7 @@ async function getAllConnectionIds() { updateConnectionIdList(connectionIds); updateOnlineUsersList(cachedOnlineUsers); } catch (error) { - console.error('Error fetching connection IDs:', error); + logger.error('Error fetching connection IDs:', error); showNotification('获取连接信息失败', 'error'); } } @@ -104,7 +107,7 @@ function getCurrentUserId() { const settings = JSON.parse(localStorage.getItem('userSettings') || '{}'); return settings.userId || settings.id || ''; } catch (error) { - console.error('Error parsing current user settings:', error); + logger.error('Error parsing current user settings:', error); return ''; } } diff --git a/client/public/invite-controller.js b/client/public/invite-controller.js index 3e8f4f5..45a80b4 100644 --- a/client/public/invite-controller.js +++ b/client/public/invite-controller.js @@ -1,3 +1,6 @@ +import { createLogger } from './logger.js'; + +const logger = createLogger('invite'); const DEFAULT_CALLER_NAME = '\u9080\u8bf7\u65b9'; const DEFAULT_CALLER_AVATAR = '/images/p2.png'; const DEFAULT_APPLY_REASON = '\u672a\u586b\u5199'; @@ -127,7 +130,7 @@ export function createInviteController({ targetUserId: pendingInvite.inviterUserId }); } catch (error) { - console.error('Error accepting invite:', error); + logger.error('Error accepting invite:', error); notify('\u63a5\u53d7\u9080\u8bf7\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5', 'error'); return; } @@ -152,7 +155,7 @@ export function createInviteController({ targetUserId: pendingInvite.inviterUserId }); } catch (error) { - console.error('Error rejecting invite:', error); + logger.error('Error rejecting invite:', error); } } diff --git a/client/public/logger.js b/client/public/logger.js new file mode 100644 index 0000000..9cca134 --- /dev/null +++ b/client/public/logger.js @@ -0,0 +1,84 @@ +const LOG_LEVELS = { + debug: 10, + info: 20, + warn: 30, + error: 40, + silent: 50 +}; + +const STORAGE_KEY = 'video_socket_log_level'; +const DEFAULT_LEVEL = 'warn'; + +function normalizeLevel(level) { + if (!level) { + return DEFAULT_LEVEL; + } + + const normalized = String(level).toLowerCase(); + return Object.prototype.hasOwnProperty.call(LOG_LEVELS, normalized) + ? normalized + : DEFAULT_LEVEL; +} + +function getConfiguredLevel() { + try { + const queryLevel = new URLSearchParams(window.location.search).get('logLevel'); + if (queryLevel) { + return normalizeLevel(queryLevel); + } + } catch (_error) { + } + + try { + const storageLevel = localStorage.getItem(STORAGE_KEY); + if (storageLevel) { + return normalizeLevel(storageLevel); + } + } catch (_error) { + } + + return DEFAULT_LEVEL; +} + +function shouldLog(level) { + return LOG_LEVELS[level] >= LOG_LEVELS[getConfiguredLevel()]; +} + +function getConsoleMethod(level) { + switch (level) { + case 'debug': + return console.debug; + case 'info': + return console.info; + case 'warn': + return console.warn; + case 'error': + return console.error; + default: + return console.log; + } +} + +function emit(level, scope, args) { + if (!shouldLog(level)) { + return; + } + + const prefix = scope ? `[${scope}]` : '[app]'; + getConsoleMethod(level)(prefix, ...args); +} + +export function createLogger(scope) { + return { + debug: (...args) => emit('debug', scope, args), + info: (...args) => emit('info', scope, args), + warn: (...args) => emit('warn', scope, args), + error: (...args) => emit('error', scope, args) + }; +} + +export function setBrowserLogLevel(level) { + const normalized = normalizeLevel(level); + localStorage.setItem(STORAGE_KEY, normalized); + return normalized; +} diff --git a/client/public/main.js b/client/public/main.js index 944cc63..9f3bf6c 100644 --- a/client/public/main.js +++ b/client/public/main.js @@ -9,6 +9,9 @@ import { loadUserSettings } from './connectview.js'; import { createInviteController } from './invite-controller.js'; +import { createLogger } from './logger.js'; + +const logger = createLogger('main'); let connectionId = ''; let currentView = 'connect'; @@ -43,10 +46,10 @@ async function switchToCallView(targetConnectionId) { renderer.renderHeaderTitle(); callViewController.bindDomEvents(); - console.log('Video call app initialized successfully'); + logger.debug('Video call app initialized successfully'); return true; } catch (error) { - console.error('Error initializing app:', error); + logger.error('Error initializing app:', error); showNotification('初始化失败,请刷新页面重试', 'error'); return false; } @@ -116,9 +119,9 @@ window.addEventListener('DOMContentLoaded', async () => { inviteController.showCallRequestDialog(invitePayload); } - console.log('SPA initialized, showing connect view'); + logger.debug('SPA initialized, showing connect view'); } catch (error) { - console.error('Error initializing SPA:', error); + logger.error('Error initializing SPA:', error); showNotification('初始化失败,请刷新页面重试', 'error'); } }); diff --git a/client/public/profile-settings.js b/client/public/profile-settings.js index 6af0c36..845f86b 100644 --- a/client/public/profile-settings.js +++ b/client/public/profile-settings.js @@ -1,3 +1,6 @@ +import { createLogger } from './logger.js'; + +const logger = createLogger('profile'); const DEFAULT_AVATAR = '/images/p1.png'; const MAX_AVATAR_SIZE = 2 * 1024 * 1024; const USER_ID_PREFIX = 'user_'; @@ -100,7 +103,7 @@ export function createProfileSettingsController({ store, notify }) { updateUserName(settings.name || '\u6211'); setAvatarPreview(settings.avatar || DEFAULT_AVATAR); } catch (error) { - console.error('Error loading user settings:', error); + logger.error('Error loading user settings:', error); setAvatarPreview(DEFAULT_AVATAR); } } @@ -152,7 +155,7 @@ export function createProfileSettingsController({ store, notify }) { saveSettings(); notify('\u5934\u50cf\u4e0a\u4f20\u6210\u529f', 'success'); } catch (error) { - console.error('Error uploading avatar:', error); + logger.error('Error uploading avatar:', error); setAvatarPreview(DEFAULT_AVATAR); notify('\u5934\u50cf\u4e0a\u4f20\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5', 'error'); } diff --git a/client/public/renderer-media.js b/client/public/renderer-media.js index ca3c43a..906bcdc 100644 --- a/client/public/renderer-media.js +++ b/client/public/renderer-media.js @@ -1,4 +1,7 @@ import { createParticipantTile, getParticipantTile } from './renderer-participant-grid.js'; +import { createLogger } from './logger.js'; + +const logger = createLogger('renderer-media'); export function getVideoResolution(track) { if (track && track.getSettings) { @@ -47,18 +50,18 @@ export function renderParticipantStreamMedia({ if (!tile) { tile = createParticipantTile(connectionId, displayName); grid.appendChild(tile); - console.log(`Created participant video tile for ${connectionId}`); + logger.debug(`Created participant video tile for ${connectionId}`); } const video = tile.querySelector('video'); if (video && stream) { if (video.srcObject === stream) { - console.log(`Same stream for participant ${connectionId}, ensuring playback`); - video.play().catch(error => console.log('Auto-play prevented:', error.message)); + logger.debug(`Same stream for participant ${connectionId}, ensuring playback`); + video.play().catch(error => logger.debug('Auto-play prevented:', error.message)); } else { video.srcObject = stream; - video.play().catch(error => console.log('Auto-play prevented:', error.message)); - console.log(`Set remote stream for participant tile ${connectionId}`); + video.play().catch(error => logger.debug('Auto-play prevented:', error.message)); + logger.debug(`Set remote stream for participant tile ${connectionId}`); } } @@ -86,15 +89,15 @@ export function renderSingleRemoteStreamMedia({ connectingOverlay }) { if (!remoteVideo || !stream) { - console.error('Either remoteVideo element or stream is missing'); + logger.error('Either remoteVideo element or stream is missing'); return; } - console.log('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(track => `${track.kind}(${track.readyState})`)); + logger.debug('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(track => `${track.kind}(${track.readyState})`)); if (remoteVideo.srcObject === stream) { - console.log('Same stream object, track added - ensuring playback'); - remoteVideo.play().catch(error => console.log('Auto-play prevented:', error.message)); + logger.debug('Same stream object, track added - ensuring playback'); + remoteVideo.play().catch(error => logger.debug('Auto-play prevented:', error.message)); return; } @@ -103,7 +106,7 @@ export function renderSingleRemoteStreamMedia({ remoteVideo.playsinline = true; remoteVideo.muted = false; remoteVideo.play().catch(error => { - console.log('Auto-play prevented, will retry on interaction:', error.message); + logger.debug('Auto-play prevented, will retry on interaction:', error.message); }); if (disconnectedOverlay) { @@ -112,10 +115,10 @@ export function renderSingleRemoteStreamMedia({ const videoTracks = stream.getVideoTracks(); const audioTracks = stream.getAudioTracks(); - console.log(`Stream has ${videoTracks.length} video tracks, ${audioTracks.length} audio tracks`); + logger.debug(`Stream has ${videoTracks.length} video tracks, ${audioTracks.length} audio tracks`); if (videoTracks.length === 0) { - console.log('Audio-only stream, waiting for video track...'); + logger.debug('Audio-only stream, waiting for video track...'); return; } @@ -165,7 +168,7 @@ export function removeParticipantTile({ video.srcObject = null; } tile.remove(); - console.log(`Removed participant video tile for ${connectionId}`); + logger.debug(`Removed participant video tile for ${connectionId}`); } const remainingTiles = grid.querySelectorAll('[data-participant-id]'); diff --git a/client/public/renderer.js b/client/public/renderer.js index 0d474e1..c9ebe2e 100644 --- a/client/public/renderer.js +++ b/client/public/renderer.js @@ -23,6 +23,9 @@ import { renderParticipantStreamMedia, renderSingleRemoteStreamMedia } from './renderer-media.js'; +import { createLogger } from './logger.js'; + +const logger = createLogger('renderer'); const GRID_LAYOUT = { maxColumns: 3, @@ -317,13 +320,13 @@ class UIRenderer { this.elements.localVideo.srcObject = stream; this.elements.localVideo.autoplay = true; this.elements.localVideo.muted = true; - console.log('srcObject set successfully:', this.elements.localVideo.srcObject); + logger.debug('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'); + logger.error('Either localVideo element or stream is missing'); } } @@ -352,7 +355,7 @@ class UIRenderer { const grid = this.elements.participantGrid; if (!grid) return; updateParticipantTilePlaceholder(grid, participantId, showPlaceholder); - console.log(`Updated placeholder for participant ${participantId}: ${showPlaceholder ? 'shown' : 'hidden'}`); + logger.debug(`Updated placeholder for participant ${participantId}: ${showPlaceholder ? 'shown' : 'hidden'}`); } syncParticipantTileNames(participants) { @@ -369,7 +372,7 @@ class UIRenderer { if (!grid) return; syncParticipantTileName(grid, participantId, name); if (name) { - console.log(`Updated tile name for participant ${participantId}: ${name}`); + logger.debug(`Updated tile name for participant ${participantId}: ${name}`); } } @@ -508,13 +511,13 @@ class UIRenderer { } renderCallEnded() { - console.log('Call ended'); + logger.debug('Call ended'); clearParticipantGrid(this.elements.participantGrid); window.location.href = './endcall/endcall.html'; } renderParticipantLeft(connectionId) { - console.log(`Participant left: ${connectionId}, updating UI`); + logger.debug(`Participant left: ${connectionId}, updating UI`); removeParticipantTile({ grid: this.elements.participantGrid, connectionId, diff --git a/client/public/signaling-session.js b/client/public/signaling-session.js index 9b051d5..737d3ae 100644 --- a/client/public/signaling-session.js +++ b/client/public/signaling-session.js @@ -1,4 +1,7 @@ import { Signaling, WebSocketSignaling } from "../../module/signaling.js"; +import { createLogger } from './logger.js'; + +const logger = createLogger('signaling'); const INVITE_EVENT_NAMES = Object.freeze([ 'invite-call', @@ -72,7 +75,7 @@ function readStoredSocketUserInfo() { try { return JSON.parse(localStorage.getItem('userSettings') || '{}'); } catch (error) { - console.error('Error parsing user settings:', error); + logger.error('Error parsing user settings:', error); return {}; } } diff --git a/client/public/store.js b/client/public/store.js index d478454..c78f105 100644 --- a/client/public/store.js +++ b/client/public/store.js @@ -8,6 +8,9 @@ import { AUDIO_CONFIG, VAD_CONFIG, VIDEO_ONLY_CONSTRAINT, buildVideoConstraints, import { buildStatsLogPayload, createAudioAnalyser, getAudioLevel } from './media-monitoring.js'; import { bindInviteSocketEvents, buildSocketUserInfoPayload, createSignalingInstance, ensureSignalingStarted, getActiveSignalingInstance, sendInviteSignal, sendSocketUserInfo } from './signaling-session.js'; import { getNetworkQualityFromSummary, summarizeInboundStats } from './webrtc-stats.js'; +import { createLogger } from './logger.js'; + +const logger = createLogger('store'); class CallStateManager { constructor() { this.state = { @@ -55,11 +58,11 @@ class CallStateManager { } if (settings.resolution) { this._savedResolution = settings.resolution; - console.log(`已恢复分辨率设置: ${settings.resolution.width}x${settings.resolution.height}`); + logger.debug(`已恢复分辨率设置: ${settings.resolution.width}x${settings.resolution.height}`); } } catch (error) { - console.error('Error loading user settings:', error); + logger.error('Error loading user settings:', error); } } } @@ -69,9 +72,9 @@ class CallStateManager { } async getLocalStream() { try { - console.log('Requesting camera permission...'); + logger.debug('Requesting camera permission...'); if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - console.error('getUserMedia is not supported'); + logger.error('getUserMedia is not supported'); throw new Error('getUserMedia is not supported'); } const videoConstraints = buildVideoConstraints(this._savedResolution); @@ -79,13 +82,13 @@ class CallStateManager { video: videoConstraints, audio: AUDIO_CONFIG }); - console.log('Stream obtained successfully:', stream); - console.log('Video tracks:', stream.getVideoTracks()); - console.log('Audio tracks:', stream.getAudioTracks()); + logger.debug('Stream obtained successfully:', stream); + logger.debug('Video tracks:', stream.getVideoTracks()); + logger.debug('Audio tracks:', stream.getAudioTracks()); this.state.localStream = stream; this.state.session.localUser.mediaState.video = true; this.state.session.localUser.mediaState.audio = true; - console.log('Local stream stored, notifying UI...'); + logger.debug('Local stream stored, notifying UI...'); this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream }); this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true }); this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: true }); @@ -93,7 +96,7 @@ class CallStateManager { this.startActivityDetection(this.state.localStream, { isLocal: true }); } catch (error) { - console.error('Error getting local stream:', error); + logger.error('Error getting local stream:', error); this.state.session.localUser.mediaState.video = false; this.state.session.localUser.mediaState.audio = false; this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: false }); @@ -133,7 +136,7 @@ class CallStateManager { this.startActivityDetection(this.state.localStream, { isLocal: true }); } catch (error) { - console.error('Error reopening video:', error); + logger.error('Error reopening video:', error); this.state.session.localUser.mediaState.video = false; this._notifyLocalMediaChange('video', false); } @@ -162,7 +165,7 @@ class CallStateManager { if (!this.renderstreaming) { return; } - console.log('Updating video track in WebRTC connection'); + logger.debug('Updating video track in WebRTC connection'); if (this.role === 'host') { const participantIds = Object.keys(this.state.remoteStreams); for (const participantId of participantIds) { @@ -190,12 +193,12 @@ class CallStateManager { for (const transceiver of videoTransceivers) { try { await transceiver.sender.replaceTrack(newVideoTrack); - console.log(participantId + logger.debug(participantId ? `Replaced video track for participant ${participantId}` : 'Successfully replaced video track'); } catch (error) { - console.error(participantId + logger.error(participantId ? `Error replacing video track for ${participantId}:` : 'Error replacing video track:', error); } @@ -205,14 +208,14 @@ class CallStateManager { try { if (participantId) { this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }, participantId); - console.log(`Added new video transceiver for participant ${participantId}`); + logger.debug(`Added new video transceiver for participant ${participantId}`); return; } this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }); - console.log('Added new video transceiver'); + logger.debug('Added new video transceiver'); } catch (error) { - console.error(participantId + logger.error(participantId ? `Error adding video transceiver for ${participantId}:` : 'Error adding video transceiver:', error); } @@ -262,10 +265,10 @@ class CallStateManager { this._signaling = signaling; this._inviteEventSignaling = bindInviteSocketEvents(this._signaling, this.socketEventHandlers, this._inviteEventSignaling); if (reused) { - console.log('Signaling already connected, reusing existing instance'); + logger.debug('Signaling already connected, reusing existing instance'); return this._signaling; } - console.log('Signaling connected (WebSocket only, no room yet)'); + logger.debug('Signaling connected (WebSocket only, no room yet)'); return this._signaling; } getActiveSignaling() { @@ -295,7 +298,7 @@ class CallStateManager { this.state.session.status = 'connecting'; this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); if (!this.state.localStream) { - console.log('Local stream not available, waiting for initialization...'); + logger.debug('Local stream not available, waiting for initialization...'); await new Promise((resolve) => { const checkStream = () => { if (this.state.localStream) { @@ -320,7 +323,7 @@ class CallStateManager { } _registerCallbacks() { this.renderstreaming.onNewPeer = (participantId) => { - console.log(`New peer created for ${participantId}, adding local tracks`); + logger.debug(`New peer created for ${participantId}, adding local tracks`); if (this.state.localStream) { const tracks = this.state.localStream.getTracks(); for (const track of tracks) { @@ -337,7 +340,7 @@ class CallStateManager { if (data.participantId) { this.selfParticipantId = data.participantId; } - console.log(`Connected as ${this.role}, participantId: ${this.selfParticipantId}`); + logger.debug(`Connected as ${this.role}, participantId: ${this.selfParticipantId}`); } this.state.session.status = 'ongoing'; this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' }); @@ -349,7 +352,7 @@ class CallStateManager { } this.state.session.localUser.mediaState.audio = false; this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false }); - console.log('Participant joined with audio muted by default'); + logger.debug('Participant joined with audio muted by default'); } this.sendMessage('user-info', { id: this.state.session.localUser.id, @@ -361,16 +364,16 @@ class CallStateManager { this.showStatsMessage(); } else { - console.error('Local stream is not available'); + logger.error('Local stream is not available'); showNotification('本地视频流不可用', 'error'); } }; this.renderstreaming.onDisconnect = () => { - console.log('Received disconnect from server, host left or room closed'); + logger.debug('Received disconnect from server, host left or room closed'); this.hangUp(); }; this.renderstreaming.onGotAnswer = (connectionId) => { - console.log('SDP Answer received, resetting encoding parameters for connectionId:', connectionId); + logger.debug('SDP Answer received, resetting encoding parameters for connectionId:', connectionId); if (this.role === 'host') { const allParticipantIds = Object.keys(this.state.remoteStreams || {}); for (const pid of allParticipantIds) { @@ -382,13 +385,13 @@ class CallStateManager { } }; this.renderstreaming.onParticipantJoined = (participantId) => { - console.log(`Participant joined: ${participantId}`); + logger.debug(`Participant joined: ${participantId}`); this._upsertParticipant(participantId); this._notifyParticipantsUpdate(); this.broadcastParticipantsList(); }; this.renderstreaming.onParticipantLeft = (participantId) => { - console.log(`Participant left: ${participantId}, room still active`); + logger.debug(`Participant left: ${participantId}, room still active`); this.updateRemoteUserStatus('offline'); this.updateRemoteUserNetworkQuality('no_signal'); showNotification('对方已离开通话', 'warning'); @@ -428,14 +431,14 @@ class CallStateManager { } this.durationSynced = false; const isHost = this.role === 'host'; - console.log(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`); + logger.debug(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`); if (this.renderstreaming) { try { await this.renderstreaming.deleteConnection(); await this.renderstreaming.stop(); } catch (error) { - console.error('Error during hangUp:', error); + logger.error('Error during hangUp:', error); } this.renderstreaming = null; } @@ -463,7 +466,7 @@ class CallStateManager { const isHost = this.role === 'host'; const targetStream = this._getOrCreateRemoteStream(trackParticipantId, isHost); this._replaceTrackOfSameKind(targetStream, data.track); - console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId); + logger.debug('Added new track:', data.track.kind, 'for participant:', trackParticipantId); if (isHost && !this.state.participants[trackParticipantId]) { this._upsertParticipant(trackParticipantId); this._notifyParticipantsUpdate(); @@ -500,7 +503,7 @@ class CallStateManager { const existingTracks = targetStream.getTracks().filter(existingTrack => existingTrack.kind === track.kind); existingTracks.forEach(existingTrack => { targetStream.removeTrack(existingTrack); - console.log('Removed old track:', existingTrack.kind); + logger.debug('Removed old track:', existingTrack.kind); }); targetStream.addTrack(track); } @@ -512,13 +515,13 @@ class CallStateManager { connectionId: trackParticipantId, isHost }); - console.log('Notified UI about remote stream update'); + logger.debug('Notified UI about remote stream update'); }; if (trackKind === 'audio' && targetStream.getVideoTracks().length === 0) { - console.log('Audio track arrived first, delaying stream notification for video track...'); + logger.debug('Audio track arrived first, delaying stream notification for video track...'); setTimeout(() => { const nowHasVideo = targetStream.getVideoTracks().length > 0; - console.log(`After delay, stream has video: ${nowHasVideo}`); + logger.debug(`After delay, stream has video: ${nowHasVideo}`); notifyStreamUpdate(); }, 200); return; @@ -526,7 +529,7 @@ class CallStateManager { notifyStreamUpdate(); } _handleRenderStreamingMessage(data) { - console.log('收到信令消息:', data); + logger.debug('收到信令消息:', data); switch (data.type) { case 'chat-message': this._handleChatMessage(data); @@ -578,7 +581,7 @@ class CallStateManager { } } _handleMediaStateChangedMessage(data) { - console.log('收到媒体状态更新:', data.data, 'from participant:', data.participantId); + logger.debug('收到媒体状态更新:', data.data, 'from participant:', data.participantId); if (this.role === 'host') { if (data.participantId && this.state.participants[data.participantId]) { this._upsertParticipant(data.participantId, { @@ -600,12 +603,12 @@ class CallStateManager { if (data.participantId === this.selfParticipantId) { return; } - console.log('Received media-state-changed from Host, updating remoteUser:', data.data); + logger.debug('Received media-state-changed from Host, updating remoteUser:', data.data); this.updateRemoteMedia(data.data, data.participantId); this._notifyParticipantsUpdate(); } _handleUserInfoMessage(data) { - console.log('收到用户信息:', data.data, 'from participant:', data.participantId); + logger.debug('收到用户信息:', data.data, 'from participant:', data.participantId); if (!data.data) { return; } @@ -629,7 +632,7 @@ class CallStateManager { if (this.role === 'host' || !data.data) { return; } - console.log('收到成员同步列表:', data.data); + logger.debug('收到成员同步列表:', data.data); this.state.participants = omitParticipant(data.data, this.selfParticipantId); this._notifyParticipantsUpdate(); this._syncCallDuration(data.callDuration); @@ -646,7 +649,7 @@ class CallStateManager { this.durationSynced = true; this._startDurationTimer(); this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration }); - console.log(`Call duration synced: ${callDuration} seconds`); + logger.debug(`Call duration synced: ${callDuration} seconds`); } _startDurationTimer() { if (this.durationInterval) { @@ -706,7 +709,7 @@ class CallStateManager { data: memberList, callDuration: this.state.session.duration }); - console.log('Broadcast participants list:', Object.keys(memberList)); + logger.debug('Broadcast participants list:', Object.keys(memberList)); } setCodecPreferences(participantId) { const capabilities = RTCRtpSender.getCapabilities('video'); @@ -744,10 +747,10 @@ class CallStateManager { t.setCodecPreferences(selectedCodecs); } catch (e) { - console.error('Error setting codec preferences:', e); + logger.error('Error setting codec preferences:', e); } }); - console.log(`Codec preferences set: ${selectedCodecs.map(c => c.mimeType).join(' > ')}`); + logger.debug(`Codec preferences set: ${selectedCodecs.map(c => c.mimeType).join(' > ')}`); } } } @@ -777,10 +780,10 @@ class CallStateManager { params.degradationPreference = 'maintain-resolution'; } sender.setParameters(params); - console.log(`Set video encoding: maxBitrate=${maxBitrate / 1000000}Mbps, scaleResolutionDownBy=1.0, xGoogleMinBitrate=${Math.floor(maxBitrate * 0.5)}${participantId ? ` for ${participantId}` : ''}`); + logger.debug(`Set video encoding: maxBitrate=${maxBitrate / 1000000}Mbps, scaleResolutionDownBy=1.0, xGoogleMinBitrate=${Math.floor(maxBitrate * 0.5)}${participantId ? ` for ${participantId}` : ''}`); } catch (error) { - console.error('Error setting video encoding parameters:', error); + logger.error('Error setting video encoding parameters:', error); } } } @@ -802,7 +805,7 @@ class CallStateManager { height: { ideal: height, max: height }, frameRate: { ideal: 30, max: 30 } }); - console.log(`分辨率已切换为 ${width}x${height}`); + logger.debug(`分辨率已切换为 ${width}x${height}`); const maxBitrate = getTargetResolutionBitrate(height); this._applyMaxBitrate(maxBitrate); const userSettings = JSON.parse(localStorage.getItem('userSettings') || '{}'); @@ -812,7 +815,7 @@ class CallStateManager { showNotification('已切换为 ' + label, 'success'); } catch (error) { - console.error('切换分辨率失败:', error); + logger.error('切换分辨率失败:', error); showNotification('切换分辨率失败,摄像头可能不支持该分辨率', 'error'); } } @@ -835,10 +838,10 @@ class CallStateManager { } params.encodings[0].maxBitrate = maxBitrate; sender.setParameters(params); - console.log(`Updated maxBitrate to ${maxBitrate} for ${pid || 'self'}`); + logger.debug(`Updated maxBitrate to ${maxBitrate} for ${pid || 'self'}`); } catch (error) { - console.error('Error updating maxBitrate:', error); + logger.error('Error updating maxBitrate:', error); } } } @@ -865,7 +868,7 @@ class CallStateManager { this.updateRemoteMedia({ isSpeaking }); } async endCall() { - console.log(`endCall called. Role: ${this.role}`); + logger.debug(`endCall called. Role: ${this.role}`); await this.hangUp(); } async joinCall(connectionId) { @@ -898,7 +901,7 @@ class CallStateManager { } } catch (error) { - console.error('Error detecting network quality:', error); + logger.error('Error detecting network quality:', error); } } startActivityDetection(stream, { isLocal = false } = {}) { @@ -936,10 +939,10 @@ class CallStateManager { } }; detectActivity(); - console.log(`${isLocal ? 'Local' : 'Remote'} activity detection started`); + logger.debug(`${isLocal ? 'Local' : 'Remote'} activity detection started`); } catch (error) { - console.error(`Error starting ${isLocal ? 'local' : 'remote'} activity detection:`, error); + logger.error(`Error starting ${isLocal ? 'local' : 'remote'} activity detection:`, error); } } startNetworkQualityDetection() { @@ -958,7 +961,7 @@ class CallStateManager { userId: this.state.session.localUser.id, ...this.state.session.localUser.mediaState }; - console.log('[WebSocket Emit] media-state-changed:', payload); + logger.debug('[WebSocket Emit] media-state-changed:', payload); if (this.renderstreaming) { this.renderstreaming.sendMessage({ type: 'media-state-changed', @@ -967,7 +970,7 @@ class CallStateManager { } } async showStatsMessage() { - console.log('Showing stats message'); + logger.debug('Showing stats message'); await this.detectNetworkQuality(); this.statsInterval = setInterval(async () => { if (!this.renderstreaming) { @@ -980,19 +983,19 @@ class CallStateManager { } const statsSummary = summarizeInboundStats(stats); const statsLog = buildStatsLogPayload(this.state.session.remoteUser.networkQuality, statsSummary); - console.log('=== WebRTC Statistics ==='); - console.log(`Network Quality: ${statsLog.networkQuality}`); - console.log('Video Stats:', statsLog.video); - console.log('Audio Stats:', statsLog.audio); - console.log('========================'); + logger.debug('=== WebRTC Statistics ==='); + logger.debug(`Network Quality: ${statsLog.networkQuality}`); + logger.debug('Video Stats:', statsLog.video); + logger.debug('Audio Stats:', statsLog.audio); + logger.debug('========================'); } catch (error) { - console.error('Error showing stats message:', error); + logger.error('Error showing stats message:', error); } }, 5000); } clearStatsMessage() { - console.log('Clearing stats message'); + logger.debug('Clearing stats message'); if (this.statsInterval) { clearInterval(this.statsInterval); this.statsInterval = null; diff --git a/client/public/store.js.tmp b/client/public/store.js.tmp deleted file mode 100644 index dccd7e8..0000000 --- a/client/public/store.js.tmp +++ /dev/null @@ -1,1013 +0,0 @@ -import { mockCallSession } from './models.js'; -import { RenderStreaming } from "../../module/renderstreaming.js"; -import { getServerConfig, getRTCConfiguration } from "../js/config.js"; -import { showNotification, generateId } from './utils.js'; -import chatMessage from './chatmessage.js'; -import { DEFAULT_PARTICIPANT_AVATAR, DEFAULT_PARTICIPANT_NAME, buildParticipantsSyncData, omitParticipant, removeParticipant, upsertParticipant } from './participants.js'; -import { AUDIO_CONFIG, VAD_CONFIG, VIDEO_ONLY_CONSTRAINT, buildVideoConstraints, getAdaptiveVideoBitrate, getResolutionLabel, getTargetResolutionBitrate } from './media-config.js'; -import { buildStatsLogPayload, createAudioAnalyser, getAudioLevel } from './media-monitoring.js'; -import { bindInviteSocketEvents, buildSocketUserInfoPayload, createSignalingInstance, ensureSignalingStarted, getActiveSignalingInstance, sendInviteSignal, sendSocketUserInfo } from './signaling-session.js'; -import { getNetworkQualityFromSummary, summarizeInboundStats } from './webrtc-stats.js'; -class CallStateManager { - constructor() { - this.state = { - id: generateId(), - session: { - ...mockCallSession, - status: 'idle' - }, - localStream: null, - remoteStream: null, - remoteStreams: {}, - participants: {} - }; - this.listeners = []; - this.socketEventHandlers = {}; - this._inviteEventSignaling = null; - } - subscribe(callback) { - this.listeners.push(callback); - return () => { - this.listeners = this.listeners.filter(cb => cb !== callback); - }; - } - notify(changes) { - this.listeners.forEach(cb => cb(this.state, changes)); - } - async init() { - await this.setupConfig(); - this.loadUserSettings(); - await this.getLocalStream(); - } - loadUserSettings() { - const userSettings = localStorage.getItem('userSettings'); - if (userSettings) { - try { - const settings = JSON.parse(userSettings); - if (settings.name || settings.avatar) { - this.state.session.localUser = { - ...this.state.session.localUser, - id: settings.userId || this.state.session.localUser.id, - name: settings.name || this.state.session.localUser.name, - avatar: settings.avatar || this.state.session.localUser.avatar - }; - this.notify({ type: 'USER_SETTINGS_UPDATED', user: this.state.session.localUser }); - } - if (settings.resolution) { - this._savedResolution = settings.resolution; - console.log(`闂傚倷娴囬褎顨ョ粙鍖¤€块梺顒€绉埀顒婄畵瀹曠厧鈹戦幇顒侇吙闂備胶鍘ч幗婊堝极閹间降鈧懘鏌ㄧ€c劋绨婚梺鍝勫暙濞诧箓藟婢跺瞼纾奸柛鎾茬娴犙囨煃瑜滈崜娆戠不瀹ュ纾块梺顒€绉寸粻鐘诲箹濞n剙濡介柛濠囨涧閳规垿鎮╃€圭姴顥濈紓浣哄珡閸ャ劎鍘卞銈嗗姧缁插潡鍩ユ径濞炬斀闂勫洤鈻旈弴銏犵劦妞ゆ帒鍠氬鎰版煟閳╁啯绀嬬€规洘鍨块獮鍥级鐠侯煈鍞甸梺璇插嚱缂嶅棝宕伴弽顐ょ焼闁割偁鍨洪崰鎰扮叓閸ャ劎鈽夐柛? ${settings.resolution.width}x${settings.resolution.height}`); - } - } - catch (error) { - console.error('Error loading user settings:', error); - } - } - } - async setupConfig() { - const res = await getServerConfig(); - this.useWebSocket = res.useWebSocket; - } - async getLocalStream() { - try { - console.log('Requesting camera permission...'); - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - console.error('getUserMedia is not supported'); - throw new Error('getUserMedia is not supported'); - } - const videoConstraints = buildVideoConstraints(this._savedResolution); - const stream = await navigator.mediaDevices.getUserMedia({ - video: videoConstraints, - audio: AUDIO_CONFIG - }); - console.log('Stream obtained successfully:', stream); - console.log('Video tracks:', stream.getVideoTracks()); - console.log('Audio tracks:', stream.getAudioTracks()); - this.state.localStream = stream; - this.state.session.localUser.mediaState.video = true; - this.state.session.localUser.mediaState.audio = true; - console.log('Local stream stored, notifying UI...'); - this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream }); - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true }); - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: true }); - this.emitMediaStateChange(); - this.startActivityDetection(this.state.localStream, { isLocal: true }); - } - catch (error) { - console.error('Error getting local stream:', error); - this.state.session.localUser.mediaState.video = false; - this.state.session.localUser.mediaState.audio = false; - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: false }); - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false }); - } - } - async updateLocalMedia(mediaType, value) { - await this._updateLocalMediaRefactored(mediaType, value); - return; - } - async _updateLocalMediaRefactored(mediaType, value) { - if (mediaType === 'video' && value) { - await this._enableLocalVideo(); - this._notifyUserListUpdate(); - return; - } - this.state.session.localUser.mediaState[mediaType] = value; - this._notifyLocalMediaChange(mediaType, value); - this.emitMediaStateChange(); - if (mediaType === 'video' && !value) { - this._disableLocalVideoTracks(); - } - if (mediaType === 'audio') { - this._setLocalAudioTrackEnabled(value); - } - this._notifyUserListUpdate(); - } - async _enableLocalVideo() { - try { - const newVideoTrack = await this._requestNewVideoTrack(); - this._replaceLocalVideoTrack(newVideoTrack); - await this._updateOutgoingVideoTrack(newVideoTrack); - this.state.session.localUser.mediaState.video = true; - this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream: this.state.localStream }); - this._notifyLocalMediaChange('video', true); - this.emitMediaStateChange(); - this.startActivityDetection(this.state.localStream, { isLocal: true }); - } - catch (error) { - console.error('Error reopening video:', error); - this.state.session.localUser.mediaState.video = false; - this._notifyLocalMediaChange('video', false); - } - } - async _requestNewVideoTrack() { - const newVideoStream = await navigator.mediaDevices.getUserMedia(VIDEO_ONLY_CONSTRAINT); - const newVideoTrack = newVideoStream.getVideoTracks()[0]; - if (!newVideoTrack) { - throw new Error('Failed to get video track'); - } - return newVideoTrack; - } - _replaceLocalVideoTrack(newVideoTrack) { - if (this.state.localStream) { - const oldVideoTracks = this.state.localStream.getVideoTracks(); - oldVideoTracks.forEach(track => { - track.stop(); - this.state.localStream.removeTrack(track); - }); - this.state.localStream.addTrack(newVideoTrack); - return; - } - this.state.localStream = new MediaStream([newVideoTrack]); - } - async _updateOutgoingVideoTrack(newVideoTrack) { - if (!this.renderstreaming) { - return; - } - console.log('Updating video track in WebRTC connection'); - if (this.role === 'host') { - const participantIds = Object.keys(this.state.remoteStreams); - for (const participantId of participantIds) { - await this._updateVideoTrackForPeer(newVideoTrack, participantId); - } - return; - } - await this._updateVideoTrackForPeer(newVideoTrack); - } - async _updateVideoTrackForPeer(newVideoTrack, participantId = undefined) { - const transceivers = this.renderstreaming.getTransceivers(participantId); - if (!transceivers) { - return; - } - const videoTransceivers = transceivers.filter(transceiver => transceiver.sender && transceiver.sender.track && transceiver.sender.track.kind === 'video'); - if (videoTransceivers.length > 0) { - await this._replaceVideoTrackOnTransceivers(videoTransceivers, newVideoTrack, participantId); - } - else { - this._addVideoTransceiver(newVideoTrack, participantId); - } - this._scheduleVideoSenderUpdate(participantId); - } - async _replaceVideoTrackOnTransceivers(videoTransceivers, newVideoTrack, participantId) { - for (const transceiver of videoTransceivers) { - try { - await transceiver.sender.replaceTrack(newVideoTrack); - console.log(participantId - ? `Replaced video track for participant ${participantId}` - : 'Successfully replaced video track'); - } - catch (error) { - console.error(participantId - ? `Error replacing video track for ${participantId}:` - : 'Error replacing video track:', error); - } - } - } - _addVideoTransceiver(newVideoTrack, participantId) { - try { - if (participantId) { - this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }, participantId); - console.log(`Added new video transceiver for participant ${participantId}`); - return; - } - this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }); - console.log('Added new video transceiver'); - } - catch (error) { - console.error(participantId - ? `Error adding video transceiver for ${participantId}:` - : 'Error adding video transceiver:', error); - } - } - _scheduleVideoSenderUpdate(participantId) { - setTimeout(() => { this.setCodecPreferences(participantId); }, 100); - setTimeout(() => { this.setVideoEncodingParameters(participantId); }, 200); - } - _disableLocalVideoTracks() { - if (!this.state.localStream) { - return; - } - this.state.session.localUser.mediaState.video = false; - this.state.localStream.getTracks().forEach(track => { - if (track.kind === 'video') { - track.stop(); - } - }); - } - _setLocalAudioTrackEnabled(value) { - if (!this.state.localStream) { - return; - } - this.state.session.localUser.mediaState.audio = value; - this.state.localStream.getTracks().forEach(track => { - if (track.kind === 'audio') { - track.enabled = value; - } - }); - } - _notifyLocalMediaChange(mediaType, value) { - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType, value }); - } - _notifyUserListUpdate() { - this.notify({ - type: 'USER_LIST_UPDATE', - localUser: this.state.session.localUser, - remoteUser: this.state.session.remoteUser - }); - } - onSocketEvent(eventName, handler) { - this.socketEventHandlers[eventName] = handler; - } - async connectSignaling() { - await this.setupConfig(); - const { signaling, reused } = await ensureSignalingStarted(this._signaling, this.useWebSocket); - this._signaling = signaling; - this._inviteEventSignaling = bindInviteSocketEvents(this._signaling, this.socketEventHandlers, this._inviteEventSignaling); - if (reused) { - console.log('Signaling already connected, reusing existing instance'); - return this._signaling; - } - console.log('Signaling connected (WebSocket only, no room yet)'); - return this._signaling; - } - getActiveSignaling() { - return getActiveSignalingInstance(this._signaling, this.renderstreaming); - } - sendInviteCall(payload) { - sendInviteSignal(this.getActiveSignaling(), 'sendInviteCall', payload); - } - sendInviteAccepted(payload) { - sendInviteSignal(this.getActiveSignaling(), 'sendInviteAccepted', payload); - } - sendInviteRejected(payload) { - sendInviteSignal(this.getActiveSignaling(), 'sendInviteRejected', payload); - } - syncSocketUserInfo(userInfo = null) { - const payload = buildSocketUserInfoPayload(userInfo, this.state.session.localUser); - this.state.session.localUser = { - ...this.state.session.localUser, - id: payload.id, - name: payload.name, - avatar: payload.avatar - }; - sendSocketUserInfo(this.getActiveSignaling(), payload); - } - async _createSignalingAndRTC(connectionId) { - this.connectionId = connectionId; - this.state.session.status = 'connecting'; - this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); - if (!this.state.localStream) { - console.log('Local stream not available, waiting for initialization...'); - await new Promise((resolve) => { - const checkStream = () => { - if (this.state.localStream) { - resolve(); - } - else { - setTimeout(checkStream, 100); - } - }; - checkStream(); - }); - } - const signaling = this._signaling || createSignalingInstance(this.useWebSocket); - const config = getRTCConfiguration(); - this.renderstreaming = new RenderStreaming(signaling, config); - this._signaling = null; - } - async setUp(connectionId) { - await this._createSignalingAndRTC(connectionId); - this._registerCallbacks(); - await this._startConnection(connectionId); - } - _registerCallbacks() { - this.renderstreaming.onNewPeer = (participantId) => { - console.log(`New peer created for ${participantId}, adding local tracks`); - if (this.state.localStream) { - const tracks = this.state.localStream.getTracks(); - for (const track of tracks) { - this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }, participantId); - } - this.setCodecPreferences(participantId); - this.setVideoEncodingParameters(participantId); - } - }; - this.renderstreaming.onConnect = (connectionId, data) => { - if (data && data.role) { - this.role = data.role; - this.state.session.localUser.isHost = (this.role === 'host'); - if (data.participantId) { - this.selfParticipantId = data.participantId; - } - console.log(`Connected as ${this.role}, participantId: ${this.selfParticipantId}`); - } - this.state.session.status = 'ongoing'; - this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' }); - if (this.role === 'participant') { - if (this.state.localStream) { - this.state.localStream.getAudioTracks().forEach(track => { - track.enabled = false; - }); - } - this.state.session.localUser.mediaState.audio = false; - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false }); - console.log('Participant joined with audio muted by default'); - } - this.sendMessage('user-info', { - id: this.state.session.localUser.id, - name: this.state.session.localUser.name, - avatar: this.state.session.localUser.avatar - }); - this.emitMediaStateChange(); - if (this.state.localStream) { - this.showStatsMessage(); - } - else { - console.error('Local stream is not available'); - showNotification('Local video stream is not available', 'error'); - } - }; - this.renderstreaming.onDisconnect = () => { - console.log('Received disconnect from server, host left or room closed'); - this.hangUp(); - }; - this.renderstreaming.onGotAnswer = (connectionId) => { - console.log('SDP Answer received, resetting encoding parameters for connectionId:', connectionId); - if (this.role === 'host') { - const allParticipantIds = Object.keys(this.state.remoteStreams || {}); - for (const pid of allParticipantIds) { - setTimeout(() => { this.setVideoEncodingParameters(pid); }, 50); - } - } - else { - setTimeout(() => { this.setVideoEncodingParameters(); }, 50); - } - }; - this.renderstreaming.onParticipantJoined = (participantId) => { - console.log(`Participant joined: ${participantId}`); - this._upsertParticipant(participantId); - this._notifyParticipantsUpdate(); - this.broadcastParticipantsList(); - }; - this.renderstreaming.onParticipantLeft = (participantId) => { - console.log(`Participant left: ${participantId}, room still active`); - this.updateRemoteUserStatus('offline'); - this.updateRemoteUserNetworkQuality('no_signal'); - showNotification('The other participant left the call', 'warning'); - if (this.state.remoteStreams[participantId]) { - this.state.remoteStreams[participantId].getTracks().forEach(track => track.stop()); - delete this.state.remoteStreams[participantId]; - } - if (this.state.remoteStream) { - this.state.remoteStream.getTracks().forEach(track => track.stop()); - this.state.remoteStream = null; - } - this._removeParticipant(participantId); - this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId }); - this._notifyParticipantsUpdate(); - this.broadcastParticipantsList(); - }; - this.renderstreaming.onTrackEvent = (data) => { - this._handleTrackEvent(data); - }; - this.renderstreaming.onMessage = (data) => { - this._handleRenderStreamingMessage(data); - }; - } - async _startConnection(connectionId) { - await this.renderstreaming.start(); - await this.renderstreaming.createConnection(connectionId); - this.startNetworkQualityDetection(); - this.startActivityDetection(this.state.localStream, { isLocal: true }); - this.startActivityDetection(this.state.remoteStream, { isLocal: false }); - } - async hangUp() { - this.clearStatsMessage(); - this.stopNetworkQualityDetection(); - if (this.durationInterval) { - clearInterval(this.durationInterval); - this.durationInterval = null; - } - this.durationSynced = false; - const isHost = this.role === 'host'; - console.log(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`); - if (this.renderstreaming) { - try { - await this.renderstreaming.deleteConnection(); - await this.renderstreaming.stop(); - } - catch (error) { - console.error('Error during hangUp:', error); - } - this.renderstreaming = null; - } - this.updateRemoteUserStatus('offline'); - this.updateRemoteUserNetworkQuality('no_signal'); - this.state.participants = {}; - this.selfParticipantId = null; - this.connectionId = null; - this.role = null; - this.state.session.status = 'ended'; - this.notify({ type: 'CALL_ENDED', reason: isHost ? 'host_hangup' : 'participant_hangup' }); - } - _handleTrackEvent(data) { - const direction = data.transceiver.direction; - if (direction === 'sendrecv' || direction === 'recvonly') { - this._handleIncomingTrack(data); - return; - } - if (direction === 'sendonly' && data.track.kind === 'audio') { - this.startActivityDetection(this.state.localStream, { isLocal: true }); - } - } - _handleIncomingTrack(data) { - const trackParticipantId = data.participantId || this.connectionId; - const isHost = this.role === 'host'; - const targetStream = this._getOrCreateRemoteStream(trackParticipantId, isHost); - this._replaceTrackOfSameKind(targetStream, data.track); - console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId); - if (isHost && !this.state.participants[trackParticipantId]) { - this._upsertParticipant(trackParticipantId); - this._notifyParticipantsUpdate(); - this.broadcastParticipantsList(); - } - this._notifyRemoteStreamUpdate(targetStream, trackParticipantId, isHost, data.track.kind); - if (this.state.session.remoteUser.status !== 'online') { - this.updateRemoteUserStatus('online'); - this.updateRemoteUserNetworkQuality('good'); - this.sendMessage('user-info', { - id: this.state.session.localUser.id, - name: this.state.session.localUser.name, - avatar: this.state.session.localUser.avatar - }); - this._startDurationTimer(); - } - if (data.track.kind === 'audio') { - this.startActivityDetection(this.state.remoteStream, { isLocal: false }); - } - } - _getOrCreateRemoteStream(trackParticipantId, isHost) { - if (isHost) { - if (!this.state.remoteStreams[trackParticipantId]) { - this.state.remoteStreams[trackParticipantId] = new MediaStream(); - } - return this.state.remoteStreams[trackParticipantId]; - } - if (this.state.remoteStream == null) { - this.state.remoteStream = new MediaStream(); - } - return this.state.remoteStream; - } - _replaceTrackOfSameKind(targetStream, track) { - const existingTracks = targetStream.getTracks().filter(existingTrack => existingTrack.kind === track.kind); - existingTracks.forEach(existingTrack => { - targetStream.removeTrack(existingTrack); - console.log('Removed old track:', existingTrack.kind); - }); - targetStream.addTrack(track); - } - _notifyRemoteStreamUpdate(targetStream, trackParticipantId, isHost, trackKind) { - const notifyStreamUpdate = () => { - this.notify({ - type: 'REMOTE_STREAM_OBTAINED', - stream: targetStream, - connectionId: trackParticipantId, - isHost - }); - console.log('Notified UI about remote stream update'); - }; - if (trackKind === 'audio' && targetStream.getVideoTracks().length === 0) { - console.log('Audio track arrived first, delaying stream notification for video track...'); - setTimeout(() => { - const nowHasVideo = targetStream.getVideoTracks().length > 0; - console.log(`After delay, stream has video: ${nowHasVideo}`); - notifyStreamUpdate(); - }, 200); - return; - } - notifyStreamUpdate(); - } - _handleRenderStreamingMessage(data) { - console.log('闂傚倸鍊搁崐宄懊归崶顒€违闁逞屽墴閺屾稓鈧綆鍋呭畷宀勬煙椤旇偐绉虹€规洦鍋婂畷鐔碱敆娴g澹嶉梻鍌欒兌缁垶骞愰幖浣哥9闁革富鍘藉畷鍙夌箾閹存瑥鐏╃紒鐙呯稻缁绘繈妫冨☉姘暫濡炪們鍊愰崑鎾寸節?', data); - switch (data.type) { - case 'chat-message': - this._handleChatMessage(data); - break; - case 'media-state-changed': - this._handleMediaStateChangedMessage(data); - break; - case 'user-info': - this._handleUserInfoMessage(data); - break; - case 'participants-sync': - this._handleParticipantsSyncMessage(data); - break; - default: - break; - } - } - _handleChatMessage(data) { - const chatPayload = data.data || data.message; - if (!chatPayload) { - return; - } - chatMessage.handleChatMessage(chatPayload); - if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) { - this._upsertParticipant(data.participantId, { - id: chatPayload.senderId, - ...(chatPayload.senderName ? { name: chatPayload.senderName } : {}), - ...(chatPayload.senderAvatar ? { avatar: chatPayload.senderAvatar } : {}) - }); - this._notifyParticipantsUpdate(); - this.broadcastParticipantsList(); - return; - } - if (!this.role || this.role !== 'host') { - if (data.participantId && this.state.participants[data.participantId]) { - this._upsertParticipant(data.participantId, { - ...(chatPayload.senderName ? { name: chatPayload.senderName } : {}), - ...(chatPayload.senderAvatar ? { avatar: chatPayload.senderAvatar } : {}) - }); - this._notifyParticipantsUpdate(); - } - else if (chatPayload.senderId !== this.state.session.localUser.id) { - this._updateRemoteUserProfile({ - id: chatPayload.senderId, - name: chatPayload.senderName, - avatar: chatPayload.senderAvatar - }); - } - } - } - _handleMediaStateChangedMessage(data) { - console.log('闂傚倸鍊搁崐宄懊归崶顒€违闁逞屽墴閺屾稓鈧綆鍋呭畷宀勬煙椤旇偐绉虹€规洦鍋婂畷鐔碱敆娴g澹嶉梻鍌欒兌缁垶骞愰幖浣哥9闁归棿绀侀悡姗€鏌熸潏鎯х槣闁轰礁绉电换娑㈠箣閻戝棛鍔烽梺鑽ゅ枑閸旀妲愰幘璇茬<婵ɑ鐦烽姀掳浜滈柟瀛樼箥濡偓濡ょ姷鍋涢崯鎾春閿熺姴宸濇い鏂垮悑閻ゅ倻绱撴担绋库挃濠⒀勵殜閺佸绻涚€涙鐭嬮柛搴㈠▕濠€渚€姊洪幐搴g畵婵炴潙鍊块幃鐐哄礈瑜忕壕鐣屸偓骞垮劚鐎氼喚绮i弮鍫熺厸?', data.data, 'from participant:', data.participantId); - if (this.role === 'host') { - if (data.participantId && this.state.participants[data.participantId]) { - this._upsertParticipant(data.participantId, { - mediaState: data.data - }); - } - this.updateRemoteMedia(data.data, data.participantId); - this._notifyParticipantsUpdate(); - this.broadcastParticipantsList(); - return; - } - if (data.participantId && data.participantId !== this.selfParticipantId && this.state.participants[data.participantId]) { - this._upsertParticipant(data.participantId, { - mediaState: data.data - }); - this._notifyParticipantsUpdate(); - return; - } - if (data.participantId === this.selfParticipantId) { - return; - } - console.log('Received media-state-changed from Host, updating remoteUser:', data.data); - this.updateRemoteMedia(data.data, data.participantId); - this._notifyParticipantsUpdate(); - } - _handleUserInfoMessage(data) { - console.log('闂傚倸鍊搁崐宄懊归崶顒€违闁逞屽墴閺屾稓鈧綆鍋呭畷宀勬煙椤旇偐绉虹€规洦鍋婂畷鐔碱敆娴g澹嶉梻鍌欒兌缁垶骞愰幖浣哥9闁秆勵殔閽冪喖鏌ㄥ┑鍡╂缂傚秵鐗楅妵鍕箳閸℃ぞ澹曞┑鐘殿暯閳ь剝灏欓惌娆撴煛鐏炲墽娲撮柛鈺嬬節瀹曟帒鈹冮幆褜娼撶紓鍌氬€风粈渚€鎯岄崒娑氼洸闁割偅娲栭弰銉╂煕閺囥劌鐏犵紒鈧崘顏呭枑闊洦娲滈惌鍡涙煃?', data.data, 'from participant:', data.participantId); - if (!data.data) { - return; - } - if (data.participantId && this.role === 'host') { - this._upsertParticipant(data.participantId, { - id: data.data.id || '', - name: data.data.name || DEFAULT_PARTICIPANT_NAME, - avatar: data.data.avatar || DEFAULT_PARTICIPANT_AVATAR - }); - this._notifyParticipantsUpdate(); - this.broadcastParticipantsList(); - return; - } - this._updateRemoteUserProfile({ - id: data.data.id || this.state.session.remoteUser.id, - name: data.data.name || this.state.session.remoteUser.name, - avatar: data.data.avatar || this.state.session.remoteUser.avatar - }); - } - _handleParticipantsSyncMessage(data) { - if (this.role === 'host' || !data.data) { - return; - } - console.log('闂傚倸鍊搁崐宄懊归崶顒€违闁逞屽墴閺屾稓鈧綆鍋呭畷宀勬煙椤旇偐绉虹€规洦鍋婂畷鐔碱敆娴g澹嶉梻鍌欒兌缁垶骞愰幖浣哥9闁秆勵殔閽冪喖鏌i弮鍥モ偓鈧柛瀣尭閳藉鈻嶉褌绨奸柟渚垮姂瀹曞爼顢楁担鍝勫箥闂備礁鎲¢悷銉┧囬鐐茬厺闁哄洨濮崑鎾舵喆閸曨剛顦ㄩ梺鑹邦潐瀹曟﹢鎮橀崘顔解拺闁告稑锕ョ壕鐢告煛閸屾瑧绐旂€规洘鍨块獮妯兼嫚閼碱剦妲版俊鐐€栧Λ浣圭珶閸綆鏉洪梻鍌欐祰椤曆呮崲閹烘纾婚柣鏂垮悑閹偤骞栧ǎ顒€濡肩紒鈧?', data.data); - this.state.participants = omitParticipant(data.data, this.selfParticipantId); - this._notifyParticipantsUpdate(); - this._syncCallDuration(data.callDuration); - } - _updateRemoteUserProfile(profile) { - this._setRemoteUserState(profile); - this._notifyRemoteUserChange({ mediaState: this.state.session.remoteUser.mediaState }); - } - _syncCallDuration(callDuration) { - if (this.durationSynced || typeof callDuration !== 'number') { - return; - } - this.state.session.duration = callDuration; - this.durationSynced = true; - this._startDurationTimer(); - this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration }); - console.log(`Call duration synced: ${callDuration} seconds`); - } - _startDurationTimer() { - if (this.durationInterval) { - return; - } - this.durationInterval = setInterval(() => { - this.state.session.duration++; - this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration }); - }, 1000); - } - _setRemoteUserState(patch) { - this.state.session.remoteUser = { - ...this.state.session.remoteUser, - ...patch - }; - } - _setRemoteUserMediaState(mediaState) { - this._setRemoteUserState({ - mediaState: { - ...this.state.session.remoteUser.mediaState, - ...mediaState - } - }); - } - _notifyRemoteUserChange(changes = {}) { - this.notify({ - type: 'REMOTE_MEDIA_CHANGE', - ...changes, - localUser: this.state.session.localUser, - remoteUser: this.state.session.remoteUser - }); - this._notifyUserListUpdate(); - } - sendMessage(type, data) { - if (this.renderstreaming) { - this.renderstreaming.sendMessage({ - type: type, - data: data - }); - } - } - _notifyParticipantsUpdate() { - this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); - } - _upsertParticipant(participantId, patch = {}) { - return upsertParticipant(this.state.participants, participantId, patch); - } - _removeParticipant(participantId) { - return removeParticipant(this.state.participants, participantId); - } - broadcastParticipantsList() { - if (this.role !== 'host' || !this.renderstreaming) - return; - const memberList = buildParticipantsSyncData(this.state.session.localUser, this.state.participants); - this.renderstreaming.sendMessage({ - type: 'participants-sync', - data: memberList, - callDuration: this.state.session.duration - }); - console.log('Broadcast participants list:', Object.keys(memberList)); - } - setCodecPreferences(participantId) { - const capabilities = RTCRtpSender.getCapabilities('video'); - if (!capabilities || !capabilities.codecs || capabilities.codecs.length === 0) - return; - const { codecs } = capabilities; - let selectedCodecs = []; - const av1Codec = codecs.find(c => c.mimeType === 'video/AV1'); - const vp9Codec = codecs.find(c => c.mimeType === 'video/VP9'); - const h264HighCodec = codecs.find(c => c.mimeType === 'video/H264' && - c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=6400')); - const h264Codec = codecs.find(c => c.mimeType === 'video/H264'); - if (av1Codec) - selectedCodecs.push(av1Codec); - if (vp9Codec) - selectedCodecs.push(vp9Codec); - if (h264HighCodec) - selectedCodecs.push(h264HighCodec); - if (h264Codec && (!h264HighCodec || h264Codec !== h264HighCodec)) - selectedCodecs.push(h264Codec); - if (selectedCodecs.length === 0) - return; - if (this.renderstreaming) { - const transceivers = this.renderstreaming.getTransceivers(participantId); - if (transceivers && transceivers.length > 0) { - const videoTransceivers = transceivers.filter(t => { - if (t.sender && t.sender.track) { - return t.sender.track.kind === 'video'; - } - return t.mid !== null && t.receiver && t.receiver.track && t.receiver.track.kind === 'video'; - }); - if (videoTransceivers && videoTransceivers.length > 0) { - videoTransceivers.forEach(t => { - try { - t.setCodecPreferences(selectedCodecs); - } - catch (e) { - console.error('Error setting codec preferences:', e); - } - }); - console.log(`Codec preferences set: ${selectedCodecs.map(c => c.mimeType).join(' > ')}`); - } - } - } - } - setVideoEncodingParameters(participantId) { - if (!this.renderstreaming) - return; - const transceivers = this.renderstreaming.getTransceivers(participantId); - if (!transceivers || transceivers.length === 0) - return; - const videoTransceivers = transceivers.filter(t => t.sender && t.sender.track && t.sender.track.kind === 'video'); - for (const transceiver of videoTransceivers) { - try { - const sender = transceiver.sender; - const params = sender.getParameters(); - if (!params.encodings || params.encodings.length === 0) { - params.encodings = [{}]; - } - const videoTrack = sender.track; - const settings = videoTrack ? videoTrack.getSettings() : {}; - const height = settings.height || 1080; - const maxBitrate = getAdaptiveVideoBitrate(height); - params.encodings[0].maxBitrate = maxBitrate; - params.encodings[0].scaleResolutionDownBy = 1.0; - params.encodings[0].xGoogleMinBitrate = Math.floor(maxBitrate * 0.5); - if (params.degradationPreference !== undefined) { - params.degradationPreference = 'maintain-resolution'; - } - sender.setParameters(params); - console.log(`Set video encoding: maxBitrate=${maxBitrate / 1000000}Mbps, scaleResolutionDownBy=1.0, xGoogleMinBitrate=${Math.floor(maxBitrate * 0.5)}${participantId ? ` for ${participantId}` : ''}`); - } - catch (error) { - console.error('Error setting video encoding parameters:', error); - } - } - } - async changeResolution(width, height) { - if (!this.state.localStream) { - showNotification('Local video stream is not available', 'error'); - return; - } - const videoTracks = this.state.localStream.getVideoTracks(); - if (videoTracks.length === 0) { - showNotification('Failed to switch resolution', 'error'); - return; - } - const track = videoTracks[0]; - const label = getResolutionLabel(height); - try { - await track.applyConstraints({ - width: { ideal: width, max: width }, - height: { ideal: height, max: height }, - frameRate: { ideal: 30, max: 30 } - }); - console.log(`闂傚倸鍊搁崐椋庣矆娓氣偓楠炲鏁嶉崟顒佹闂佸湱鍎ら崵锕€鈽夊鍡欏弳闂佸憡娲嶉弲娆戝垝閹剧粯鈷戠憸鐗堝笚閿涚喖鏌i幒鐐电暤鐎规洘鍨甸埞鎴犫偓锝庡亞閸橀亶姊洪棃娑辨Ч闁搞劎鏁诲畷顖烆敃閳垛晜鐏侀梺闈涱槴閺呮粓鍩涢幋锔界厱婵炴垶锕銉х磼濡や礁绗氱紒缁樼〒閹风姾顦撮柣锝変憾閺岋綁鏁愰崶褍骞嬮悗瑙勬穿缁叉儳顕ラ崟顓濇勃闁告稑锕︽禍?${width}x${height}`); - const maxBitrate = getTargetResolutionBitrate(height); - this._applyMaxBitrate(maxBitrate); - const userSettings = JSON.parse(localStorage.getItem('userSettings') || '{}'); - userSettings.resolution = { width, height }; - localStorage.setItem('userSettings', JSON.stringify(userSettings)); - this.notify({ type: 'RESOLUTION_CHANGED', resolution: { width, height, label } }); - showNotification('Switched to ' + label, 'success'); - } - catch (error) { - console.error('闂傚倸鍊搁崐椋庣矆娓氣偓楠炲鏁嶉崟顒佹闂佸湱鍎ら崵姘洪鍛珖闂侀€炲苯澧撮柟顕€绠栭幃婊堟寠婢跺孩鎲伴梻渚€娼чˇ顓㈠磿閹跺壙鍥敃閿旇В鎷虹紓渚囧灡濞叉牗鏅堕懠顑藉亾閸忓浜鹃梺褰掓?缁€浣瑰閻樺磭绠剧€瑰壊鍠曠花濂告煟閹捐泛鏋戠紒缁樼箖缁绘繈宕掑顓燁唹闂備胶鎳撻崥瀣礉濞嗘挸钃熼柡鍥ュ灩閻愬﹪鏌曟繛鍨姢濞寸姴銈稿?', error); - showNotification('Failed to switch resolution. The camera may not support it.', 'error'); - } - } - _applyMaxBitrate(maxBitrate) { - if (!this.renderstreaming) - return; - const isHost = this.role === 'host'; - const participantIds = isHost ? Object.keys(this.state.participants) : [null]; - for (const pid of participantIds) { - const transceivers = this.renderstreaming.getTransceivers(pid); - if (!transceivers) - continue; - const videoTransceivers = transceivers.filter(t => t.sender && t.sender.track && t.sender.track.kind === 'video'); - for (const transceiver of videoTransceivers) { - try { - const sender = transceiver.sender; - const params = sender.getParameters(); - if (!params.encodings || params.encodings.length === 0) { - params.encodings = [{}]; - } - params.encodings[0].maxBitrate = maxBitrate; - sender.setParameters(params); - console.log(`Updated maxBitrate to ${maxBitrate} for ${pid || 'self'}`); - } - catch (error) { - console.error('Error updating maxBitrate:', error); - } - } - } - } - updateRemoteMedia(mediaState, participantId) { - this._setRemoteUserMediaState(mediaState); - this._notifyRemoteUserChange({ mediaState, participantId }); - } - updateRemoteUserStatus(status) { - this._setRemoteUserState({ status }); - this._notifyRemoteUserChange(); - } - updateRemoteUserNetworkQuality(networkQuality) { - this._setRemoteUserState({ networkQuality }); - this._notifyRemoteUserChange(); - } - _setSpeakingState(isLocal, isSpeaking) { - if (isLocal) { - this.state.session.localUser.mediaState.isSpeaking = isSpeaking; - this._notifyLocalMediaChange('isSpeaking', isSpeaking); - this.emitMediaStateChange(); - return; - } - this.updateRemoteMedia({ isSpeaking }); - } - async endCall() { - console.log(`endCall called. Role: ${this.role}`); - await this.hangUp(); - } - async joinCall(connectionId) { - this.state.session.status = 'connecting'; - this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); - showNotification('Joining call (' + connectionId + ')'); - await this.init(); - this.connectionId = connectionId; - } - async createCall() { - this.state.session.status = 'connecting'; - this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); - showNotification('Creating call...'); - await this.init(); - } - async detectNetworkQuality() { - if (!this.renderstreaming) { - return; - } - try { - const stats = await this.renderstreaming.getStats(); - if (!stats) { - return; - } - const summary = summarizeInboundStats(stats); - const quality = getNetworkQualityFromSummary(summary); - if (this.state.session.remoteUser.networkQuality !== quality) { - this.updateRemoteUserNetworkQuality(quality); - this.notify({ type: 'NETWORK_CHANGE', quality }); - } - } - catch (error) { - console.error('Error detecting network quality:', error); - } - } - startActivityDetection(stream, { isLocal = false } = {}) { - if (!stream) { - return; - } - const audioTracks = stream.getAudioTracks(); - if (audioTracks.length === 0) { - return; - } - try { - const { threshold, debounceTime, fftSize } = VAD_CONFIG; - const { analyser, dataArray } = createAudioAnalyser(stream, fftSize); - let isSpeaking = false; - let lastActivityTime = 0; - const detectActivity = () => { - if (!stream || !this.renderstreaming) { - return; - } - const level = getAudioLevel(analyser, dataArray); - const currentTime = Date.now(); - if (level > threshold / 100) { - lastActivityTime = currentTime; - if (!isSpeaking) { - isSpeaking = true; - this._setSpeakingState(isLocal, true); - } - } - else if (isSpeaking && currentTime - lastActivityTime > debounceTime) { - isSpeaking = false; - this._setSpeakingState(isLocal, false); - } - if (this.state.session.status === 'ongoing') { - requestAnimationFrame(detectActivity); - } - }; - detectActivity(); - console.log(`${isLocal ? 'Local' : 'Remote'} activity detection started`); - } - catch (error) { - console.error(`Error starting ${isLocal ? 'local' : 'remote'} activity detection:`, error); - } - } - startNetworkQualityDetection() { - this.networkQualityInterval = setInterval(() => { - this.detectNetworkQuality(); - }, 3000); - } - stopNetworkQualityDetection() { - if (this.networkQualityInterval) { - clearInterval(this.networkQualityInterval); - this.networkQualityInterval = null; - } - } - emitMediaStateChange() { - const payload = { - userId: this.state.session.localUser.id, - ...this.state.session.localUser.mediaState - }; - console.log('[WebSocket Emit] media-state-changed:', payload); - if (this.renderstreaming) { - this.renderstreaming.sendMessage({ - type: 'media-state-changed', - data: payload - }); - } - } - async showStatsMessage() { - console.log('Showing stats message'); - await this.detectNetworkQuality(); - this.statsInterval = setInterval(async () => { - if (!this.renderstreaming) { - return; - } - try { - const stats = await this.renderstreaming.getStats(); - if (!stats) { - return; - } - const statsSummary = summarizeInboundStats(stats); - const statsLog = buildStatsLogPayload(this.state.session.remoteUser.networkQuality, statsSummary); - console.log('=== WebRTC Statistics ==='); - console.log(`Network Quality: ${statsLog.networkQuality}`); - console.log('Video Stats:', statsLog.video); - console.log('Audio Stats:', statsLog.audio); - console.log('========================'); - } - catch (error) { - console.error('Error showing stats message:', error); - } - }, 5000); - } - clearStatsMessage() { - console.log('Clearing stats message'); - if (this.statsInterval) { - clearInterval(this.statsInterval); - this.statsInterval = null; - } - } - getState() { return this.state; } - getLocalUser() { return this.state.session.localUser; } - getRemoteUser() { return this.state.session.remoteUser; } - getConnectionId() { return this.connectionId; } - getRenderStreaming() { return this.renderstreaming; } -} -const store = new CallStateManager(); -window.addEventListener('beforeunload', async () => { - if (!store.renderstreaming) - return; - await store.renderstreaming.stop(); -}, true); -export default store;