++
This commit is contained in:
62
client/public/media-monitoring.js
Normal file
62
client/public/media-monitoring.js
Normal file
@@ -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`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
getResolutionLabel,
|
getResolutionLabel,
|
||||||
getTargetResolutionBitrate
|
getTargetResolutionBitrate
|
||||||
} from './media-config.js';
|
} from './media-config.js';
|
||||||
|
import { buildStatsLogPayload, createAudioAnalyser, getAudioLevel } from './media-monitoring.js';
|
||||||
import { getNetworkQualityFromSummary, summarizeInboundStats } from './webrtc-stats.js';
|
import { getNetworkQualityFromSummary, summarizeInboundStats } from './webrtc-stats.js';
|
||||||
|
|
||||||
class CallStateManager {
|
class CallStateManager {
|
||||||
@@ -923,11 +924,8 @@ class CallStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateRemoteUserProfile(profile) {
|
_updateRemoteUserProfile(profile) {
|
||||||
this.state.session.remoteUser = {
|
this._setRemoteUserState(profile);
|
||||||
...this.state.session.remoteUser,
|
this._notifyRemoteUserChange({ mediaState: this.state.session.remoteUser.mediaState });
|
||||||
...profile
|
|
||||||
};
|
|
||||||
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncCallDuration(callDuration) {
|
_syncCallDuration(callDuration) {
|
||||||
@@ -953,6 +951,32 @@ class CallStateManager {
|
|||||||
}, 1000);
|
}, 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 - 消息类型
|
* @param {string} type - 消息类型
|
||||||
@@ -1188,23 +1212,29 @@ class CallStateManager {
|
|||||||
|
|
||||||
// 更新远端媒体状态 (由 WebSocket 触发)
|
// 更新远端媒体状态 (由 WebSocket 触发)
|
||||||
updateRemoteMedia(mediaState, participantId) {
|
updateRemoteMedia(mediaState, participantId) {
|
||||||
this.state.session.remoteUser.mediaState = {
|
this._setRemoteUserMediaState(mediaState);
|
||||||
...this.state.session.remoteUser.mediaState,
|
this._notifyRemoteUserChange({ mediaState, participantId });
|
||||||
...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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新远端用户状态
|
// 更新远端用户状态
|
||||||
updateRemoteUserStatus(status) {
|
updateRemoteUserStatus(status) {
|
||||||
this.state.session.remoteUser.status = status;
|
this._setRemoteUserState({ status });
|
||||||
this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
|
this._notifyRemoteUserChange();
|
||||||
}
|
}
|
||||||
updateRemoteUserNetworkQuality(networkQuality) {
|
updateRemoteUserNetworkQuality(networkQuality) {
|
||||||
this.state.session.remoteUser.networkQuality = networkQuality;
|
this._setRemoteUserState({ networkQuality });
|
||||||
this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
|
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() {
|
async endCall() {
|
||||||
@@ -1256,7 +1286,7 @@ class CallStateManager {
|
|||||||
|
|
||||||
// 更新网络质量状态
|
// 更新网络质量状态
|
||||||
if (this.state.session.remoteUser.networkQuality !== quality) {
|
if (this.state.session.remoteUser.networkQuality !== quality) {
|
||||||
this.state.session.remoteUser.networkQuality = quality;
|
this.updateRemoteUserNetworkQuality(quality);
|
||||||
this.notify({ type: 'NETWORK_CHANGE', quality });
|
this.notify({ type: 'NETWORK_CHANGE', quality });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1278,14 +1308,7 @@ class CallStateManager {
|
|||||||
try {
|
try {
|
||||||
const { threshold, debounceTime, fftSize } = VAD_CONFIG;
|
const { threshold, debounceTime, fftSize } = VAD_CONFIG;
|
||||||
|
|
||||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
const { analyser, dataArray } = createAudioAnalyser(stream, fftSize);
|
||||||
const source = audioContext.createMediaStreamSource(stream);
|
|
||||||
const analyser = audioContext.createAnalyser();
|
|
||||||
analyser.fftSize = fftSize;
|
|
||||||
|
|
||||||
source.connect(analyser);
|
|
||||||
|
|
||||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
||||||
let isSpeaking = false;
|
let isSpeaking = false;
|
||||||
let lastActivityTime = 0;
|
let lastActivityTime = 0;
|
||||||
|
|
||||||
@@ -1294,38 +1317,18 @@ class CallStateManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
analyser.getByteTimeDomainData(dataArray);
|
const level = getAudioLevel(analyser, 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 currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
if (level > threshold / 100) {
|
if (level > threshold / 100) {
|
||||||
lastActivityTime = currentTime;
|
lastActivityTime = currentTime;
|
||||||
if (!isSpeaking) {
|
if (!isSpeaking) {
|
||||||
isSpeaking = true;
|
isSpeaking = true;
|
||||||
if (isLocal) {
|
this._setSpeakingState(isLocal, true);
|
||||||
this.state.session.localUser.mediaState.isSpeaking = true;
|
|
||||||
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: true });
|
|
||||||
this.emitMediaStateChange();
|
|
||||||
} else {
|
|
||||||
this.updateRemoteMedia({ isSpeaking: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (isSpeaking && currentTime - lastActivityTime > debounceTime) {
|
} else if (isSpeaking && currentTime - lastActivityTime > debounceTime) {
|
||||||
isSpeaking = false;
|
isSpeaking = false;
|
||||||
if (isLocal) {
|
this._setSpeakingState(isLocal, false);
|
||||||
this.state.session.localUser.mediaState.isSpeaking = false;
|
|
||||||
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: false });
|
|
||||||
this.emitMediaStateChange();
|
|
||||||
} else {
|
|
||||||
this.updateRemoteMedia({ isSpeaking: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.session.status === 'ongoing') {
|
if (this.state.session.status === 'ongoing') {
|
||||||
@@ -1395,27 +1398,13 @@ class CallStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statsSummary = summarizeInboundStats(stats);
|
const statsSummary = summarizeInboundStats(stats);
|
||||||
|
const statsLog = buildStatsLogPayload(this.state.session.remoteUser.networkQuality, statsSummary);
|
||||||
|
|
||||||
// 输出详细统计信息
|
// 输出详细统计信息
|
||||||
console.log('=== WebRTC Statistics ===');
|
console.log('=== WebRTC Statistics ===');
|
||||||
console.log(`Network Quality: ${this.state.session.remoteUser.networkQuality}`);
|
console.log(`Network Quality: ${statsLog.networkQuality}`);
|
||||||
console.log('Video Stats:', {
|
console.log('Video Stats:', statsLog.video);
|
||||||
'Packets Lost': statsSummary.video.packetsLost,
|
console.log('Audio Stats:', statsLog.audio);
|
||||||
'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('========================');
|
console.log('========================');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user