【m】状态图标更新
This commit is contained in:
@@ -59,12 +59,6 @@ class CallStateManager {
|
|||||||
await this.setupConfig();
|
await this.setupConfig();
|
||||||
// 获取本地摄像头视频流
|
// 获取本地摄像头视频流
|
||||||
await this.getLocalStream();
|
await this.getLocalStream();
|
||||||
|
|
||||||
// 模拟远端音频活动 (实际应由 WebRTC VAD 检测触发)
|
|
||||||
//this.simulateRemoteActivity();
|
|
||||||
|
|
||||||
// 模拟网络质量变化
|
|
||||||
this.simulateNetworkChange();
|
|
||||||
}
|
}
|
||||||
async setupConfig() {
|
async setupConfig() {
|
||||||
const res = await getServerConfig();
|
const res = await getServerConfig();
|
||||||
@@ -237,6 +231,11 @@ class CallStateManager {
|
|||||||
this.state.remoteStream.addTrack(data.track);
|
this.state.remoteStream.addTrack(data.track);
|
||||||
// 通知UI远程流已更新
|
// 通知UI远程流已更新
|
||||||
this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: this.state.remoteStream });
|
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.start();
|
||||||
await this.renderstreaming.createConnection(connectionId);
|
await this.renderstreaming.createConnection(connectionId);
|
||||||
|
|
||||||
|
// 启动网络质量检测
|
||||||
|
this.startNetworkQualityDetection();
|
||||||
|
// 启动远端音频活动检测
|
||||||
|
this.startRemoteActivityDetection();
|
||||||
|
//模拟远端活动 (开发测试用)
|
||||||
|
//this.simulateRemoteActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,6 +258,7 @@ class CallStateManager {
|
|||||||
*/
|
*/
|
||||||
async hangUp() {
|
async hangUp() {
|
||||||
this.clearStatsMessage(); // 清除统计信息
|
this.clearStatsMessage(); // 清除统计信息
|
||||||
|
this.stopNetworkQualityDetection(); // 停止网络质量检测
|
||||||
console.log(`Disconnect peer on ${this.connectionId}.`);
|
console.log(`Disconnect peer on ${this.connectionId}.`);
|
||||||
|
|
||||||
// 删除连接并停止WebRTC
|
// 删除连接并停止WebRTC
|
||||||
@@ -374,18 +380,195 @@ class CallStateManager {
|
|||||||
}
|
}
|
||||||
}, 800);
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟网络质量变化 (开发测试用)
|
|
||||||
simulateNetworkChange() {
|
simulateNetworkChange() {
|
||||||
const qualities = ['excellent', 'good', 'fair', 'poor'];
|
// 模拟网络质量变化
|
||||||
|
|
||||||
|
const qualities = ['good', 'fair', 'excellent', 'poor'];
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (Math.random() > 0.8) {
|
if (Math.random() > 0.8) {
|
||||||
const quality = qualities[Math.floor(Math.random() * qualities.length)];
|
const networkQuality = qualities[Math.floor(Math.random() * qualities.length)];
|
||||||
this.state.session.remoteUser.networkQuality = quality;
|
this.state.session.remoteUser.networkQuality = networkQuality;
|
||||||
this.notify({ type: 'NETWORK_CHANGE', quality });
|
this.notify({ type: 'NETWORK_CHANGE', quality });
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 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() {
|
emitMediaStateChange() {
|
||||||
@@ -398,15 +581,104 @@ class CallStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 显示统计信息
|
// 显示统计信息
|
||||||
showStatsMessage() {
|
async showStatsMessage() {
|
||||||
console.log('Showing stats message');
|
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() {
|
clearStatsMessage() {
|
||||||
console.log('Clearing stats message');
|
console.log('Clearing stats message');
|
||||||
// 这里可以添加清除统计信息的逻辑
|
|
||||||
|
// 清理统计信息定时器
|
||||||
|
if (this.statsInterval) {
|
||||||
|
clearInterval(this.statsInterval);
|
||||||
|
this.statsInterval = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|||||||
Reference in New Issue
Block a user