diff --git a/WebApp/client/public/onebyone/index.html b/WebApp/client/public/onebyone/index.html index 84dea38..f424e0a 100644 --- a/WebApp/client/public/onebyone/index.html +++ b/WebApp/client/public/onebyone/index.html @@ -112,7 +112,7 @@ 与 Sarah 的通话
- + 优秀 diff --git a/WebApp/client/public/onebyone/models.js b/WebApp/client/public/onebyone/models.js index 18e6b3f..c95adda 100644 --- a/WebApp/client/public/onebyone/models.js +++ b/WebApp/client/public/onebyone/models.js @@ -83,8 +83,8 @@ const mockCallSession = { id: "user-remote-002", name: "Unity", avatar: "/images/p2.png", - status: "online", // online | offline | connecting - networkQuality: "excellent", // excellent | good | fair | poor + status: "offline", // online | offline | connecting + networkQuality: "no_signal", // excellent | good | fair | poor | no_signal mediaState: { audio: true, video: true, diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index 98f58bd..09c2b01 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -20,6 +20,8 @@ class UIRenderer { callDuration: document.getElementById('callDuration'), encryptionBadge: document.getElementById('encryptionBadge'), unreadBadge: document.getElementById('unreadBadge'), + remoteNetworkIndicator: document.getElementById('remoteNetworkIndicator'), + remoteNetworkQuality: document.getElementById('remoteNetworkQuality'), // 远端视频 remoteVideo: document.getElementById('remoteVideo'), @@ -255,10 +257,9 @@ class UIRenderer { // 渲染header中的网络状态 renderHeaderNetworkStatus(networkQuality) { - const networkQualityElement = document.getElementById('remoteNetworkQuality'); - if (networkQualityElement) { - const textElement = networkQualityElement.querySelector('span'); - const iconElement = networkQualityElement.querySelector('i'); + if (this.elements.remoteNetworkQuality) { + const textElement = this.elements.remoteNetworkQuality.querySelector('span'); + const iconElement = this.elements.remoteNetworkQuality.querySelector('i'); if (textElement && iconElement) { let qualityText = '未知'; @@ -350,10 +351,7 @@ class UIRenderer { this.elements.disconnectedOverlay.classList.add('hidden'); } - // 隐藏占位背景 - if (this.elements.remoteVideoPlaceholder) { - this.elements.remoteVideoPlaceholder.classList.add('hidden'); - } + // 获取视频轨道并处理分辨率 const videoTracks = stream.getVideoTracks(); @@ -380,6 +378,15 @@ class UIRenderer { this.adjustVideoSize(this.elements.remoteVideo, newResolution); }); } + // 隐藏连接中提示 + if (this.elements.connectingOverlay) { + this.elements.connectingOverlay.classList.add('hidden'); + } + + // 隐藏占位背景 + if (this.elements.remoteVideoPlaceholder) { + this.elements.remoteVideoPlaceholder.classList.add('hidden'); + } } else { console.log('No valid video tracks in remote stream'); // 清空视频元素的源 @@ -507,8 +514,14 @@ class UIRenderer { const remoteStatusIndicator = remoteUserElement.querySelector('.absolute.-bottom-1.-right-1.w-3.h-3'); if (remoteStatusIndicator) { if (remoteUser.status === 'online') { - remoteStatusIndicator.classList.remove('hidden'); - remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900'; + // 根据网络质量设置状态指示器颜色 + if (remoteUser.networkQuality === 'no_signal') { + remoteStatusIndicator.classList.remove('hidden'); + remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-gray-500 rounded-full border-2 border-slate-900'; + } else { + remoteStatusIndicator.classList.remove('hidden'); + remoteStatusIndicator.className = 'absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900'; + } } else { remoteStatusIndicator.classList.add('hidden'); } @@ -726,6 +739,11 @@ class UIRenderer { networkStatusText.textContent = this.getNetworkQualityText(quality); networkStatusText.className = 'text-red-500'; break; + case 'no_signal': + icon.className = 'fas fa-times-circle text-gray-500'; + networkStatusText.textContent = this.getNetworkQualityText(quality); + networkStatusText.className = 'text-gray-500'; + break; default: icon.className = 'fas fa-question-circle text-gray-400'; networkStatusText.textContent = '未知'; @@ -754,6 +772,9 @@ class UIRenderer { case 'poor': statusClass = 'text-red-500'; break; + case 'no_signal': + statusClass = 'text-gray-500'; + break; default: statusClass = 'text-gray-400'; } @@ -762,8 +783,26 @@ class UIRenderer { this.elements.connectionQuality.textContent = `连接质量: ${qualityText}`; this.elements.connectionQuality.className = `text-xs ${statusClass}`; } + + // 同步更新头部网络指示器 + this.updateHeaderNetworkIndicator(quality); } + // 更新头部网络指示器 + updateHeaderNetworkIndicator(networkQuality) { + if (!this.elements.remoteNetworkIndicator) return; + + // 根据网络质量设置指示器颜色 + if (networkQuality === 'no_signal') { + // 无信号时显示灰色点,取消动画 + this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full'; + } else { + // 有信号时显示绿色点,保持动画 + this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-green-500 rounded-full animate-pulse'; + } + } + + // 渲染通话结束 renderCallEnded() { console.log('Call ended'); @@ -787,7 +826,8 @@ class UIRenderer { 'excellent': '优秀', 'good': '良好', 'fair': '一般', - 'poor': '较差' + 'poor': '较差', + 'no_signal': '无信号' }; return qualityMap[quality] || quality; } diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index 7d4b17f..2f4a793 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -47,11 +47,6 @@ class CallStateManager { // 初始化 async init() { - // 启动通话时长计时器 - this.durationInterval = setInterval(() => { - this.state.session.duration++; - this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration }); - }, 1000); // 初始化配置 await this.setupConfig(); // 获取本地摄像头视频流 @@ -356,7 +351,17 @@ class CallStateManager { // 通知UI远程流已更新 this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: this.state.remoteStream }); console.log('Notified UI about remote stream update'); - + // 只有当收到远程流时才更新远程用户状态为在线 + if (this.state.session.remoteUser.status !== 'online') { + this.updateRemoteUserStatus('online'); + // 更新远程用户网络质量为好 + this.updateRemoteUserNetworkQuality('good'); + // 启动通话时长计时器 + this.durationInterval = setInterval(() => { + this.state.session.duration++; + this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration }); + }, 1000); + } // 如果是音频轨道,启动远程音频活动检测 if (data.track.kind === 'audio') { this.startRemoteActivityDetection(); @@ -389,6 +394,7 @@ class CallStateManager { await this.renderstreaming.start(); await this.renderstreaming.createConnection(connectionId); + // 启动网络质量检测 this.startNetworkQualityDetection(); @@ -408,6 +414,11 @@ class CallStateManager { async hangUp() { this.clearStatsMessage(); // 清除统计信息 this.stopNetworkQualityDetection(); // 停止网络质量检测 + // 停止通话时长计时器 + if (this.durationInterval) { + clearInterval(this.durationInterval); + this.durationInterval = null; + } console.log(`Disconnect peer on ${this.connectionId}.`); // 删除连接并停止WebRTC @@ -421,6 +432,9 @@ class CallStateManager { this.renderstreaming = null; } + // 更新远程用户状态为离线 + this.updateRemoteUserStatus('offline'); + this.updateRemoteUserNetworkQuality('no_signal'); this.connectionId = null; this.state.session.status = 'ended'; this.notify({ type: 'CALL_ENDED' }); @@ -466,9 +480,22 @@ class CallStateManager { // 通知UI更新用户列表 this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); } + + // 更新远端用户状态 + updateRemoteUserStatus(status) { + this.state.session.remoteUser.status = status; + this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); + } + updateRemoteUserNetworkQuality(networkQuality) { + this.state.session.remoteUser.networkQuality = networkQuality; + this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); + } // 结束通话 endCall() { - clearInterval(this.durationInterval); + if (this.durationInterval) { + clearInterval(this.durationInterval); + this.durationInterval = null; + } this.state.session.status = 'ended'; this.notify({ type: 'CALL_ENDED' }); @@ -512,7 +539,7 @@ class CallStateManager { simulateNetworkChange() { // 模拟网络质量变化 - const qualities = ['good', 'fair', 'excellent', 'poor']; + const qualities = ['good', 'fair', 'excellent', 'poor', 'no_signal']; setInterval(() => { if (Math.random() > 0.8) { const quality = qualities[Math.floor(Math.random() * qualities.length)]; @@ -585,6 +612,9 @@ class CallStateManager { } else { quality = 'excellent'; } + } else { + // 没有收到任何RTP包,设置为无信号状态 + quality = 'no_signal'; } // 更新网络质量状态 diff --git a/WebApp/src/class/websockethandler.ts b/WebApp/src/class/websockethandler.ts index 585c47d..e0b23bf 100644 --- a/WebApp/src/class/websockethandler.ts +++ b/WebApp/src/class/websockethandler.ts @@ -336,7 +336,7 @@ function onCandidate(ws: WebSocket, message: any): void { onDisconnect(ws, connectionId); } else { // 发送ping消息 - ws.send(JSON.stringify({ type: "ping" })); + ws.send(JSON.stringify({ from: connectionId, to: "", type: "on-message", data: { type: "ping"} })); console.log('WebSocket connection heartbeat, lastActivity: ', (ws as any).lastActivity); } }, 3000);