优化完成
This commit is contained in:
@@ -9,6 +9,28 @@ import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务
|
||||
import { showNotification, generateId } from './utils.js'; // 导入通知函数
|
||||
import chatMessage from './chatmessage.js';
|
||||
|
||||
const AUDIO_CONFIG = {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
};
|
||||
|
||||
const VAD_CONFIG = {
|
||||
threshold: 15,
|
||||
debounceTime: 500,
|
||||
fftSize: 256
|
||||
};
|
||||
|
||||
const MEDIA_CONSTRAINTS = {
|
||||
video: true,
|
||||
audio: AUDIO_CONFIG
|
||||
};
|
||||
|
||||
const VIDEO_ONLY_CONSTRAINT = {
|
||||
video: true,
|
||||
audio: false
|
||||
};
|
||||
|
||||
class CallStateManager {
|
||||
constructor() {
|
||||
// 核心状态
|
||||
@@ -91,14 +113,7 @@ class CallStateManager {
|
||||
}
|
||||
|
||||
// 请求摄像头权限并获取媒体流,启用回声消除
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
});
|
||||
const stream = await navigator.mediaDevices.getUserMedia(MEDIA_CONSTRAINTS);
|
||||
|
||||
console.log('Stream obtained successfully:', stream);
|
||||
console.log('Video tracks:', stream.getVideoTracks());
|
||||
@@ -120,7 +135,7 @@ class CallStateManager {
|
||||
this.emitMediaStateChange();
|
||||
|
||||
// 启动本地音频活动检测
|
||||
this.startLocalActivityDetection();
|
||||
this.startActivityDetection(this.state.localStream, { isLocal: true });
|
||||
} catch (error) {
|
||||
console.error('Error getting local stream:', error);
|
||||
// 如果获取视频失败,保持视频关闭状态
|
||||
@@ -140,7 +155,7 @@ class CallStateManager {
|
||||
if (mediaType === 'video' && value) {
|
||||
try {
|
||||
// 只获取新的视频轨道,不干扰正在工作的音频
|
||||
const newVideoStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
||||
const newVideoStream = await navigator.mediaDevices.getUserMedia(VIDEO_ONLY_CONSTRAINT);
|
||||
const newVideoTrack = newVideoStream.getVideoTracks()[0];
|
||||
|
||||
if (!newVideoTrack) {
|
||||
@@ -232,7 +247,7 @@ class CallStateManager {
|
||||
this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream: this.state.localStream });
|
||||
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true });
|
||||
this.emitMediaStateChange();
|
||||
this.startLocalActivityDetection();
|
||||
this.startActivityDetection(this.state.localStream, { isLocal: true });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reopening video:', error);
|
||||
@@ -280,12 +295,12 @@ class CallStateManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebRTC连接
|
||||
* 创建信令和RTC实例
|
||||
* @async
|
||||
* @param {string} connectionId - 连接ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setUp(connectionId) {
|
||||
//TODO
|
||||
async _createSignalingAndRTC(connectionId) {
|
||||
this.connectionId = connectionId; // 获取连接ID
|
||||
// 设置状态为连接中
|
||||
this.state.session.status = 'connecting';
|
||||
@@ -326,6 +341,23 @@ class CallStateManager {
|
||||
]
|
||||
};
|
||||
this.renderstreaming = new RenderStreaming(signaling, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebRTC连接
|
||||
* @async
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setUp(connectionId) {
|
||||
await this._createSignalingAndRTC(connectionId);
|
||||
this._registerCallbacks();
|
||||
await this._startConnection(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册所有WebRTC回调
|
||||
*/
|
||||
_registerCallbacks() {
|
||||
this.renderstreaming.onNewPeer = (participantId) => {
|
||||
console.log(`New peer created for ${participantId}, adding local tracks`);
|
||||
if (this.state.localStream) {
|
||||
@@ -336,6 +368,7 @@ class CallStateManager {
|
||||
this.setCodecPreferences(participantId);
|
||||
}
|
||||
};
|
||||
|
||||
// 连接建立回调
|
||||
this.renderstreaming.onConnect = (connectionId, data) => {
|
||||
// 保存角色信息(host/participant)
|
||||
@@ -515,24 +548,26 @@ class CallStateManager {
|
||||
name: this.state.session.localUser.name,
|
||||
avatar: this.state.session.localUser.avatar
|
||||
});
|
||||
// 启动通话时长计时器
|
||||
this.durationInterval = setInterval(() => {
|
||||
this.state.session.duration++;
|
||||
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
|
||||
}, 1000);
|
||||
// 启动通话时长计时器(避免重复启动)
|
||||
if (!this.durationInterval) {
|
||||
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();
|
||||
this.startActivityDetection(this.state.remoteStream, { isLocal: false });
|
||||
}
|
||||
} else if (direction == "sendonly") {
|
||||
// 本地发送轨道,启动本地音频活动检测
|
||||
if (data.track.kind === 'audio') {
|
||||
this.startLocalActivityDetection();
|
||||
this.startActivityDetection(this.state.localStream, { isLocal: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
// 初始化 RenderStreaming 实例后
|
||||
|
||||
this.renderstreaming.onMessage = (data) => {
|
||||
console.log('收到消息:', data);
|
||||
if (data.type === 'chat-message') {
|
||||
@@ -657,26 +692,44 @@ class CallStateManager {
|
||||
}
|
||||
this.state.participants = filtered;
|
||||
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
|
||||
|
||||
// 同步通话时长:仅首次同步,将Host的时长作为基准
|
||||
if (!this.durationSynced && typeof data.callDuration === 'number') {
|
||||
this.state.session.duration = data.callDuration;
|
||||
this.durationSynced = true;
|
||||
// 如果计时器尚未启动(远程流还未到达),先启动计时器
|
||||
if (!this.durationInterval) {
|
||||
this.durationInterval = setInterval(() => {
|
||||
this.state.session.duration++;
|
||||
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
|
||||
}, 1000);
|
||||
}
|
||||
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
|
||||
console.log(`通话时长已同步,当前时长: ${data.callDuration}秒`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启动WebRTC连接和检测
|
||||
* @async
|
||||
* @param {string} connectionId - 连接ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _startConnection(connectionId) {
|
||||
// 启动WebRTC连接
|
||||
await this.renderstreaming.start();
|
||||
await this.renderstreaming.createConnection(connectionId);
|
||||
|
||||
|
||||
// 启动网络质量检测
|
||||
this.startNetworkQualityDetection();
|
||||
|
||||
// 启动本地音频活动检测
|
||||
this.startLocalActivityDetection();
|
||||
// 启动远端音频活动检测
|
||||
this.startRemoteActivityDetection();
|
||||
//模拟远端活动 (开发测试用)
|
||||
//this.simulateRemoteActivity();
|
||||
this.startActivityDetection(this.state.localStream, { isLocal: true });
|
||||
//启动远端音频活动检测
|
||||
this.startActivityDetection(this.state.remoteStream, { isLocal: false });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,6 +747,8 @@ class CallStateManager {
|
||||
clearInterval(this.durationInterval);
|
||||
this.durationInterval = null;
|
||||
}
|
||||
// 重置通话时长同步标志
|
||||
this.durationSynced = false;
|
||||
|
||||
const isHost = this.role === 'host';
|
||||
console.log(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`);
|
||||
@@ -769,7 +824,8 @@ class CallStateManager {
|
||||
|
||||
this.renderstreaming.sendMessage({
|
||||
type: 'participants-sync',
|
||||
data: memberList
|
||||
data: memberList,
|
||||
callDuration: this.state.session.duration
|
||||
});
|
||||
console.log('Broadcast participants list:', Object.keys(memberList));
|
||||
}
|
||||
@@ -853,27 +909,6 @@ class CallStateManager {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
// 模拟远端活动 (开发测试用)
|
||||
simulateRemoteActivity() {
|
||||
setInterval(() => {
|
||||
if (Math.random() > 0.7) {
|
||||
const isSpeaking = Math.random() > 0.5;
|
||||
this.updateRemoteMedia({ isSpeaking });
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
simulateNetworkChange() {
|
||||
// 模拟网络质量变化
|
||||
|
||||
const qualities = ['good', 'fair', 'excellent', 'poor', 'no_signal'];
|
||||
setInterval(() => {
|
||||
if (Math.random() > 0.8) {
|
||||
const quality = qualities[Math.floor(Math.random() * qualities.length)];
|
||||
this.state.session.remoteUser.networkQuality = quality;
|
||||
this.notify({ type: 'NETWORK_CHANGE', quality });
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
// 真实网络质量检测
|
||||
async detectNetworkQuality() {
|
||||
if (!this.renderstreaming) {
|
||||
@@ -953,181 +988,81 @@ class CallStateManager {
|
||||
console.error('Error detecting network quality:', error);
|
||||
}
|
||||
}
|
||||
// 真实音频活动检测 - 远端
|
||||
startRemoteActivityDetection() {
|
||||
// 检查是否有远端音频流
|
||||
if (!this.state.remoteStream) {
|
||||
// 音频活动检测
|
||||
startActivityDetection(stream, { isLocal = false } = {}) {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取音频轨道
|
||||
const audioTracks = this.state.remoteStream.getAudioTracks();
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
if (audioTracks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建音频上下文
|
||||
const { threshold, debounceTime, fftSize } = VAD_CONFIG;
|
||||
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
// 创建媒体流源
|
||||
const source = audioContext.createMediaStreamSource(this.state.remoteStream);
|
||||
|
||||
// 创建音频分析器
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
analyser.fftSize = fftSize;
|
||||
|
||||
// 连接音频节点
|
||||
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) {
|
||||
if (!stream || !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 level = rms / 128;
|
||||
|
||||
// 检测说话状态
|
||||
const currentTime = Date.now();
|
||||
if (level > threshold / 100) {
|
||||
// 检测到说话
|
||||
lastActivityTime = currentTime;
|
||||
if (!isSpeaking) {
|
||||
isSpeaking = true;
|
||||
this.updateRemoteMedia({ 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 });
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 真实音频活动检测 - 本地
|
||||
startLocalActivityDetection() {
|
||||
// 检查是否有本地音频流
|
||||
if (!this.state.localStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取音频轨道
|
||||
const audioTracks = this.state.localStream.getAudioTracks();
|
||||
if (audioTracks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建音频上下文
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
// 创建媒体流源
|
||||
const source = audioContext.createMediaStreamSource(this.state.localStream);
|
||||
|
||||
// 创建音频分析器
|
||||
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.localStream || !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.state.session.localUser.mediaState.isSpeaking = true;
|
||||
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: true });
|
||||
// 发送媒体状态到服务器
|
||||
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 });
|
||||
}
|
||||
} else if (isSpeaking && currentTime - lastActivityTime > debounceTime) {
|
||||
// 停止说话
|
||||
isSpeaking = false;
|
||||
this.state.session.localUser.mediaState.isSpeaking = false;
|
||||
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: false });
|
||||
// 发送媒体状态到服务器
|
||||
this.emitMediaStateChange();
|
||||
}
|
||||
|
||||
// 继续检测
|
||||
if (this.state.session.status === 'ongoing') {
|
||||
requestAnimationFrame(detectActivity);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始检测
|
||||
detectActivity();
|
||||
|
||||
console.log('Local activity detection started');
|
||||
console.log(`${isLocal ? 'Local' : 'Remote'} activity detection started`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting local activity detection:', error);
|
||||
console.error(`Error starting ${isLocal ? 'local' : 'remote'} activity detection:`, error);
|
||||
}
|
||||
}
|
||||
// 启动网络质量检测
|
||||
@@ -1135,7 +1070,6 @@ class CallStateManager {
|
||||
// 每3秒检测一次网络质量
|
||||
this.networkQualityInterval = setInterval(() => {
|
||||
this.detectNetworkQuality();
|
||||
//this.simulateNetworkChange();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user