diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index 917a70a..6dddf3a 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -59,12 +59,6 @@ class CallStateManager { await this.setupConfig(); // 获取本地摄像头视频流 await this.getLocalStream(); - - // 模拟远端音频活动 (实际应由 WebRTC VAD 检测触发) - //this.simulateRemoteActivity(); - - // 模拟网络质量变化 - this.simulateNetworkChange(); } async setupConfig() { const res = await getServerConfig(); @@ -237,6 +231,11 @@ class CallStateManager { this.state.remoteStream.addTrack(data.track); // 通知UI远程流已更新 this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: this.state.remoteStream }); + + // 如果是音频轨道,启动音频活动检测 + if (data.track.kind === 'audio') { + this.startRemoteActivityDetection(); + } } }; @@ -244,6 +243,12 @@ class CallStateManager { await this.renderstreaming.start(); await this.renderstreaming.createConnection(connectionId); + // 启动网络质量检测 + this.startNetworkQualityDetection(); + // 启动远端音频活动检测 + this.startRemoteActivityDetection(); + //模拟远端活动 (开发测试用) + //this.simulateRemoteActivity(); } /** @@ -253,6 +258,7 @@ class CallStateManager { */ async hangUp() { this.clearStatsMessage(); // 清除统计信息 + this.stopNetworkQualityDetection(); // 停止网络质量检测 console.log(`Disconnect peer on ${this.connectionId}.`); // 删除连接并停止WebRTC @@ -374,18 +380,195 @@ class CallStateManager { } }, 800); } - - // 模拟网络质量变化 (开发测试用) simulateNetworkChange() { - const qualities = ['excellent', 'good', 'fair', 'poor']; + // 模拟网络质量变化 + + const qualities = ['good', 'fair', 'excellent', 'poor']; setInterval(() => { if (Math.random() > 0.8) { - const quality = qualities[Math.floor(Math.random() * qualities.length)]; - this.state.session.remoteUser.networkQuality = quality; + const networkQuality = qualities[Math.floor(Math.random() * qualities.length)]; + this.state.session.remoteUser.networkQuality = networkQuality; this.notify({ type: 'NETWORK_CHANGE', quality }); } }, 5000); } + // 真实网络质量检测 + async detectNetworkQuality() { + if (!this.renderstreaming) { + return; + } + + try { + const stats = await this.renderstreaming.getStats(); + if (!stats) { + return; + } + + let totalPacketsLost = 0; + let totalPacketsReceived = 0; + let inboundRTPCount = 0; + let jitter = 0; + let roundTripTime = 0; + + // 分析统计信息 + stats.forEach(report => { + if (report.type === 'inbound-rtp' && report.mediaType === 'video') { + inboundRTPCount++; + + // 计算丢包率 + if (report.packetsLost !== undefined && report.packetsReceived !== undefined) { + totalPacketsLost += report.packetsLost; + totalPacketsReceived += report.packetsReceived; + } + + // 获取抖动 + if (report.jitter !== undefined) { + jitter = Math.max(jitter, report.jitter); + } + + // 获取往返时间 + if (report.roundTripTime !== undefined) { + roundTripTime = Math.max(roundTripTime, report.roundTripTime); + } + } + }); + + // 计算网络质量指标 + let quality = 'excellent'; + + if (inboundRTPCount > 0) { + // 基于丢包率判断 + const packetLossRate = totalPacketsReceived > 0 ? (totalPacketsLost / (totalPacketsLost + totalPacketsReceived)) : 0; + + // 基于抖动判断 + const jitterMs = jitter * 1000; + + // 基于往返时间判断 + const rttMs = roundTripTime * 1000; + + // 综合评估网络质量 + if (packetLossRate > 0.05 || jitterMs > 100 || rttMs > 300) { + quality = 'poor'; + } else if (packetLossRate > 0.02 || jitterMs > 50 || rttMs > 150) { + quality = 'fair'; + } else if (packetLossRate > 0.01 || jitterMs > 30 || rttMs > 100) { + quality = 'good'; + } else { + quality = 'excellent'; + } + } + + // 更新网络质量状态 + if (this.state.session.remoteUser.networkQuality !== quality) { + this.state.session.remoteUser.networkQuality = quality; + this.notify({ type: 'NETWORK_CHANGE', quality }); + } + + } catch (error) { + console.error('Error detecting network quality:', error); + } + } + // 真实音频活动检测 + startRemoteActivityDetection() { + // 检查是否有远端音频流 + if (!this.state.remoteStream) { + return; + } + + // 获取音频轨道 + const audioTracks = this.state.remoteStream.getAudioTracks(); + if (audioTracks.length === 0) { + return; + } + + try { + // 创建音频上下文 + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + + // 创建媒体流源 + const source = audioContext.createMediaStreamSource(this.state.remoteStream); + + // 创建音频分析器 + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; + + // 连接音频节点 + source.connect(analyser); + + // 创建数据缓冲区 + const dataArray = new Uint8Array(analyser.frequencyBinCount); + + // 检测参数 + const threshold = 15; // 音频电平阈值 + const debounceTime = 500; // 防抖时间 + let isSpeaking = false; + let lastActivityTime = 0; + + // 音频活动检测循环 + const detectActivity = () => { + if (!this.state.remoteStream || !this.renderstreaming) { + return; + } + + // 获取时域数据 + analyser.getByteTimeDomainData(dataArray); + + // 计算音频电平 + let sum = 0; + for (let i = 0; i < dataArray.length; i++) { + // 转换为振幅 (0-255 → -128-127) + const amplitude = dataArray[i] - 128; + sum += amplitude * amplitude; + } + const rms = Math.sqrt(sum / dataArray.length); + const level = rms / 128; // 归一化到 0-1 + + // 检测说话状态 + const currentTime = Date.now(); + if (level > threshold / 100) { + // 检测到说话 + lastActivityTime = currentTime; + if (!isSpeaking) { + isSpeaking = true; + this.updateRemoteMedia({ isSpeaking: true }); + } + } else if (isSpeaking && currentTime - lastActivityTime > debounceTime) { + // 停止说话 + isSpeaking = false; + this.updateRemoteMedia({ isSpeaking: false }); + } + + // 继续检测 + if (this.state.session.status === 'ongoing') { + requestAnimationFrame(detectActivity); + } + }; + + // 开始检测 + detectActivity(); + + console.log('Remote activity detection started'); + + } catch (error) { + console.error('Error starting remote activity detection:', error); + } + } + // 启动网络质量检测 + startNetworkQualityDetection() { + // 每3秒检测一次网络质量 + this.networkQualityInterval = setInterval(() => { + this.detectNetworkQuality(); + //this.simulateNetworkChange(); + }, 3000); + } + + // 停止网络质量检测 + stopNetworkQualityDetection() { + if (this.networkQualityInterval) { + clearInterval(this.networkQualityInterval); + this.networkQualityInterval = null; + } + } // 发送媒体状态到服务器 emitMediaStateChange() { @@ -398,15 +581,104 @@ class CallStateManager { } // 显示统计信息 - showStatsMessage() { + 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; + } + + let statsSummary = { + video: { + packetsLost: 0, + packetsReceived: 0, + bytesReceived: 0, + jitter: 0, + roundTripTime: 0, + fps: 0, + bitrate: 0 + }, + audio: { + packetsLost: 0, + packetsReceived: 0, + bytesReceived: 0, + jitter: 0 + } + }; + + // 分析统计信息 + stats.forEach(report => { + if (report.type === 'inbound-rtp') { + if (report.mediaType === 'video') { + statsSummary.video.packetsLost = report.packetsLost || 0; + statsSummary.video.packetsReceived = report.packetsReceived || 0; + statsSummary.video.bytesReceived = report.bytesReceived || 0; + statsSummary.video.jitter = report.jitter || 0; + statsSummary.video.roundTripTime = report.roundTripTime || 0; + statsSummary.video.fps = report.framesPerSecond || 0; + + // 计算视频比特率 (kbps) + if (report.bytesReceived && report.timestamp) { + const duration = report.timestamp / 1000; // 转换为秒 + statsSummary.video.bitrate = duration > 0 ? Math.round((report.bytesReceived * 8) / (duration * 1000)) : 0; + } + } else if (report.mediaType === 'audio') { + statsSummary.audio.packetsLost = report.packetsLost || 0; + statsSummary.audio.packetsReceived = report.packetsReceived || 0; + statsSummary.audio.bytesReceived = report.bytesReceived || 0; + statsSummary.audio.jitter = report.jitter || 0; + } + } + }); + + // 输出详细统计信息 + console.log('=== WebRTC Statistics ==='); + console.log(`Network Quality: ${this.state.session.remoteUser.networkQuality}`); + console.log('Video Stats:', { + 'Packets Lost': statsSummary.video.packetsLost, + 'Packets Received': statsSummary.video.packetsReceived, + 'Packet Loss Rate': statsSummary.video.packetsReceived > 0 ? + `${((statsSummary.video.packetsLost / (statsSummary.video.packetsLost + statsSummary.video.packetsReceived)) * 100).toFixed(2)}%` : '0%', + 'Jitter': `${(statsSummary.video.jitter * 1000).toFixed(2)}ms`, + 'Round Trip Time': `${(statsSummary.video.roundTripTime * 1000).toFixed(2)}ms`, + 'FPS': statsSummary.video.fps.toFixed(1), + 'Bitrate': `${statsSummary.video.bitrate}kbps` + }); + console.log('Audio Stats:', { + 'Packets Lost': statsSummary.audio.packetsLost, + 'Packets Received': statsSummary.audio.packetsReceived, + 'Packet Loss Rate': statsSummary.audio.packetsReceived > 0 ? + `${((statsSummary.audio.packetsLost / (statsSummary.audio.packetsLost + statsSummary.audio.packetsReceived)) * 100).toFixed(2)}%` : '0%', + 'Jitter': `${(statsSummary.audio.jitter * 1000).toFixed(2)}ms` + }); + console.log('========================'); + + } catch (error) { + console.error('Error showing stats message:', error); + } + }, 5000); // 每5秒更新一次统计信息 } // 清除统计信息 clearStatsMessage() { console.log('Clearing stats message'); - // 这里可以添加清除统计信息的逻辑 + + // 清理统计信息定时器 + if (this.statsInterval) { + clearInterval(this.statsInterval); + this.statsInterval = null; + } } // Getters