From ac16fa85e95c8749be63cc164793b36e2dd9ec52 Mon Sep 17 00:00:00 2001 From: stary <834207172@qq.com> Date: Sun, 24 May 2026 01:29:34 +0800 Subject: [PATCH] ++ --- client/public/media-monitoring.js | 62 +++++++++++++++ client/public/store.js | 121 ++++++++++++++---------------- 2 files changed, 117 insertions(+), 66 deletions(-) create mode 100644 client/public/media-monitoring.js diff --git a/client/public/media-monitoring.js b/client/public/media-monitoring.js new file mode 100644 index 0000000..a3c14f8 --- /dev/null +++ b/client/public/media-monitoring.js @@ -0,0 +1,62 @@ +export function createAudioAnalyser(stream, fftSize) { + const AudioContextCtor = window.AudioContext || window.webkitAudioContext; + const audioContext = new AudioContextCtor(); + const source = audioContext.createMediaStreamSource(stream); + const analyser = audioContext.createAnalyser(); + analyser.fftSize = fftSize; + source.connect(analyser); + + return { + audioContext, + analyser, + dataArray: new Uint8Array(analyser.frequencyBinCount) + }; +} + +export function getAudioLevel(analyser, dataArray) { + analyser.getByteTimeDomainData(dataArray); + + let sum = 0; + for (let i = 0; i < dataArray.length; i++) { + const amplitude = dataArray[i] - 128; + sum += amplitude * amplitude; + } + + const rms = Math.sqrt(sum / dataArray.length); + return rms / 128; +} + +function formatPacketLossRate(packetsLost, packetsReceived) { + if (packetsReceived <= 0) { + return '0%'; + } + + return `${((packetsLost / (packetsLost + packetsReceived)) * 100).toFixed(2)}%`; +} + +export function buildStatsLogPayload(networkQuality, statsSummary) { + return { + networkQuality, + video: { + 'Packets Lost': statsSummary.video.packetsLost, + 'Packets Received': statsSummary.video.packetsReceived, + 'Packet Loss Rate': formatPacketLossRate( + statsSummary.video.packetsLost, + statsSummary.video.packetsReceived + ), + '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` + }, + audio: { + 'Packets Lost': statsSummary.audio.packetsLost, + 'Packets Received': statsSummary.audio.packetsReceived, + 'Packet Loss Rate': formatPacketLossRate( + statsSummary.audio.packetsLost, + statsSummary.audio.packetsReceived + ), + 'Jitter': `${(statsSummary.audio.jitter * 1000).toFixed(2)}ms` + } + }; +} diff --git a/client/public/store.js b/client/public/store.js index 67160e9..ba8ed3e 100644 --- a/client/public/store.js +++ b/client/public/store.js @@ -25,6 +25,7 @@ import { getResolutionLabel, getTargetResolutionBitrate } from './media-config.js'; +import { buildStatsLogPayload, createAudioAnalyser, getAudioLevel } from './media-monitoring.js'; import { getNetworkQualityFromSummary, summarizeInboundStats } from './webrtc-stats.js'; class CallStateManager { @@ -923,11 +924,8 @@ class CallStateManager { } _updateRemoteUserProfile(profile) { - this.state.session.remoteUser = { - ...this.state.session.remoteUser, - ...profile - }; - this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); + this._setRemoteUserState(profile); + this._notifyRemoteUserChange({ mediaState: this.state.session.remoteUser.mediaState }); } _syncCallDuration(callDuration) { @@ -953,6 +951,32 @@ class CallStateManager { }, 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(); + } + /** * 发送消息 * @param {string} type - 消息类型 @@ -1188,23 +1212,29 @@ class CallStateManager { // 更新远端媒体状态 (由 WebSocket 触发) updateRemoteMedia(mediaState, participantId) { - this.state.session.remoteUser.mediaState = { - ...this.state.session.remoteUser.mediaState, - ...mediaState - }; - this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState, participantId }); - // 通知UI更新用户列表 - this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); + this._setRemoteUserMediaState(mediaState); + this._notifyRemoteUserChange({ mediaState, participantId }); } // 更新远端用户状态 updateRemoteUserStatus(status) { - this.state.session.remoteUser.status = status; - this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); + this._setRemoteUserState({ status }); + this._notifyRemoteUserChange(); } updateRemoteUserNetworkQuality(networkQuality) { - this.state.session.remoteUser.networkQuality = networkQuality; - this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); + 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() { @@ -1256,7 +1286,7 @@ class CallStateManager { // 更新网络质量状态 if (this.state.session.remoteUser.networkQuality !== quality) { - this.state.session.remoteUser.networkQuality = quality; + this.updateRemoteUserNetworkQuality(quality); this.notify({ type: 'NETWORK_CHANGE', quality }); } @@ -1278,14 +1308,7 @@ class CallStateManager { try { const { threshold, debounceTime, fftSize } = VAD_CONFIG; - const audioContext = new (window.AudioContext || window.webkitAudioContext)(); - const source = audioContext.createMediaStreamSource(stream); - const analyser = audioContext.createAnalyser(); - analyser.fftSize = fftSize; - - source.connect(analyser); - - const dataArray = new Uint8Array(analyser.frequencyBinCount); + const { analyser, dataArray } = createAudioAnalyser(stream, fftSize); let isSpeaking = false; let lastActivityTime = 0; @@ -1294,38 +1317,18 @@ class CallStateManager { return; } - analyser.getByteTimeDomainData(dataArray); - - let sum = 0; - for (let i = 0; i < dataArray.length; i++) { - const amplitude = dataArray[i] - 128; - sum += amplitude * amplitude; - } - const rms = Math.sqrt(sum / dataArray.length); - const level = rms / 128; + const level = getAudioLevel(analyser, dataArray); const currentTime = Date.now(); if (level > threshold / 100) { lastActivityTime = currentTime; if (!isSpeaking) { isSpeaking = true; - if (isLocal) { - this.state.session.localUser.mediaState.isSpeaking = true; - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: true }); - this.emitMediaStateChange(); - } else { - this.updateRemoteMedia({ isSpeaking: true }); - } + this._setSpeakingState(isLocal, true); } } else if (isSpeaking && currentTime - lastActivityTime > debounceTime) { isSpeaking = false; - if (isLocal) { - this.state.session.localUser.mediaState.isSpeaking = false; - this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: false }); - this.emitMediaStateChange(); - } else { - this.updateRemoteMedia({ isSpeaking: false }); - } + this._setSpeakingState(isLocal, false); } if (this.state.session.status === 'ongoing') { @@ -1395,27 +1398,13 @@ class CallStateManager { } const statsSummary = summarizeInboundStats(stats); + const statsLog = buildStatsLogPayload(this.state.session.remoteUser.networkQuality, statsSummary); // 输出详细统计信息 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(`Network Quality: ${statsLog.networkQuality}`); + console.log('Video Stats:', statsLog.video); + console.log('Audio Stats:', statsLog.audio); console.log('========================'); } catch (error) {