/** * 状态管理 * 使用简单的 Observable 模式,可替换为 Redux/Vuex/Pinia */ import { mockCallSession } from './models.js'; import { Signaling, WebSocketSignaling } from "../../module/signaling.js";// 信令管理 import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连接管理 import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置 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() { // 核心状态 this.state = { id: generateId(), session: { ...mockCallSession, status: 'idle' // 初始状态为空闲 }, localStream: null, // MediaStream 对象 remoteStream: null, // 单路远端流(兼容旧逻辑,participant端使用) remoteStreams: {}, // 多路远端流 Map: { connectionId: MediaStream }(host端使用) participants: {} // 多Participant用户信息 Map: { participantId: { id, name, avatar, mediaState, status } }(host端使用) }; // 监听器数组 this.listeners = []; } // 订阅状态变化 subscribe(callback) { this.listeners.push(callback); return () => { this.listeners = this.listeners.filter(cb => cb !== callback); }; } // 通知所有监听器 notify(changes) { this.listeners.forEach(cb => cb(this.state, changes)); } // 初始化 async init() { // 初始化配置 await this.setupConfig(); // 加载用户设置 this.loadUserSettings(); // 获取本地摄像头视频流 await this.getLocalStream(); } // 加载用户设置 loadUserSettings() { const userSettings = localStorage.getItem('userSettings'); if (userSettings) { try { const settings = JSON.parse(userSettings); // 更新本地用户信息 if (settings.name || settings.avatar) { this.state.session.localUser = { ...this.state.session.localUser, id: settings.userId || this.state.session.localUser.id, name: settings.name || this.state.session.localUser.name, avatar: settings.avatar || this.state.session.localUser.avatar }; // 通知UI更新 this.notify({ type: 'USER_SETTINGS_UPDATED', user: this.state.session.localUser }); } } catch (error) { console.error('Error loading user settings:', error); } } } async setupConfig() { const res = await getServerConfig(); this.useWebSocket = res.useWebSocket; } // 获取本地摄像头视频流 async getLocalStream() { try { console.log('Requesting camera permission...'); // 检查浏览器是否支持getUserMedia if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { console.error('getUserMedia is not supported'); throw new Error('getUserMedia is not supported'); } // 请求摄像头权限并获取媒体流,启用回声消除 const stream = await navigator.mediaDevices.getUserMedia(MEDIA_CONSTRAINTS); console.log('Stream obtained successfully:', stream); console.log('Video tracks:', stream.getVideoTracks()); console.log('Audio tracks:', stream.getAudioTracks()); this.state.localStream = stream; this.state.session.localUser.mediaState.video = true; this.state.session.localUser.mediaState.audio = true; console.log('Local stream stored, notifying UI...'); // 先通知视频流已获取 this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream }); // 再通知媒体状态变化 this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true }); this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: true }); // 发送媒体状态到服务器 this.emitMediaStateChange(); // 启动本地音频活动检测 this.startActivityDetection(this.state.localStream, { isLocal: true }); } catch (error) { console.error('Error getting local stream:', error); // 如果获取视频失败,保持视频关闭状态 this.state.session.localUser.mediaState.video = false; this.state.session.localUser.mediaState.audio = false; // 通知媒体状态变化 this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: false }); this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false }); } } // 更新本地媒体状态 async updateLocalMedia(mediaType, value) { // 如果是开启视频,重新获取摄像头资源 if (mediaType === 'video' && value) { try { // 只获取新的视频轨道,不干扰正在工作的音频 const newVideoStream = await navigator.mediaDevices.getUserMedia(VIDEO_ONLY_CONSTRAINT); const newVideoTrack = newVideoStream.getVideoTracks()[0]; if (!newVideoTrack) { throw new Error('Failed to get video track'); } // 更新本地流中的视频轨道(替换旧的已停止的轨道) if (this.state.localStream) { const oldVideoTracks = this.state.localStream.getVideoTracks(); oldVideoTracks.forEach(track => { track.stop(); this.state.localStream.removeTrack(track); }); this.state.localStream.addTrack(newVideoTrack); } else { // 本地流不存在时(不应该发生),使用新流 this.state.localStream = newVideoStream; } // 更新WebRTC连接中的视频轨道 if (this.renderstreaming) { console.log('Updating video track in WebRTC connection'); if (this.role === 'host') { // Host端:需要遍历所有participant的peer来替换视频轨道 const participantIds = Object.keys(this.state.remoteStreams); for (const participantId of participantIds) { const transceivers = this.renderstreaming.getTransceivers(participantId); if (!transceivers) continue; const videoTransceivers = transceivers.filter(t => t.sender && t.sender.track && t.sender.track.kind === 'video' ); if (videoTransceivers.length > 0) { for (const transceiver of videoTransceivers) { try { await transceiver.sender.replaceTrack(newVideoTrack); console.log(`Replaced video track for participant ${participantId}`); } catch (error) { console.error(`Error replacing video track for ${participantId}:`, error); } } } else { // 没有视频收发器,添加新的 try { this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }, participantId); console.log(`Added new video transceiver for participant ${participantId}`); } catch (error) { console.error(`Error adding video transceiver for ${participantId}:`, error); } } // 设置编解码器偏好 setTimeout(() => { this.setCodecPreferences(participantId); }, 100); } } else { // Participant端:使用单一peer const transceivers = this.renderstreaming.getTransceivers(); if (transceivers) { const videoTransceivers = transceivers.filter(t => t.sender && t.sender.track && t.sender.track.kind === 'video' ); if (videoTransceivers.length > 0) { for (const transceiver of videoTransceivers) { try { await transceiver.sender.replaceTrack(newVideoTrack); console.log('Successfully replaced video track'); } catch (error) { console.error('Error replacing video track:', error); } } } else { try { this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }); console.log('Added new video transceiver'); } catch (error) { console.error('Error adding video transceiver:', error); } } } setTimeout(() => { this.setCodecPreferences(); }, 100); } } // 更新状态和通知UI this.state.session.localUser.mediaState.video = true; this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream: this.state.localStream }); this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true }); this.emitMediaStateChange(); this.startActivityDetection(this.state.localStream, { isLocal: true }); } catch (error) { console.error('Error reopening video:', error); this.state.session.localUser.mediaState.video = false; this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: false }); } } else { // 直接更新媒体状态 this.state.session.localUser.mediaState[mediaType] = value; this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType, value }); // 发送媒体状态到服务器 this.emitMediaStateChange(); } // 如果是关闭视频,释放摄像头资源 if (mediaType === 'video' && !value && this.state.localStream) { this.state.session.localUser.mediaState.video = false; this.state.localStream.getTracks().forEach(track => { if (track.kind === 'video') { track.stop(); } }); // 发送媒体状态到服务器 this.emitMediaStateChange(); } // 如果是音频状态变化,控制本地音频轨道 if (mediaType === 'audio' && this.state.localStream) { this.state.session.localUser.mediaState.audio = value; this.state.localStream.getTracks().forEach(track => { if (track.kind === 'audio') { track.enabled = value; } }); // 发送媒体状态到服务器 this.emitMediaStateChange(); } // 通知UI更新用户列表 this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); } /** * 创建信令和RTC实例 * @async * @param {string} connectionId - 连接ID * @returns {Promise} */ async _createSignalingAndRTC(connectionId) { this.connectionId = connectionId; // 获取连接ID // 设置状态为连接中 this.state.session.status = 'connecting'; this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); // 确保本地流已经初始化 if (!this.state.localStream) { console.log('Local stream not available, waiting for initialization...'); // 等待localStream初始化 await new Promise((resolve) => { const checkStream = () => { if (this.state.localStream) { resolve(); } else { setTimeout(checkStream, 100); } }; checkStream(); }); } // 创建信令实例 const signaling = this.useWebSocket ? new WebSocketSignaling() : new Signaling(); const config = getRTCConfiguration(); // 获取RTC配置 // 优化RTC配置,确保支持高分辨率和良好的音频处理 config.peerConnectionOptions = { optional: [ { googCpuOveruseDetection: false }, // 禁用CPU过度使用检测 { googScreencastMinBitrate: 3000 }, // 设置最小比特率 { googEchoCancellation: true }, // 启用回声消除 { googEchoCancellation2: true }, // 启用高级回声消除 { googNoiseSuppression: true }, // 启用噪声抑制 { googNoiseSuppression2: true }, // 启用高级噪声抑制 { googAutoGainControl: true }, // 启用自动增益控制 { googAutoGainControl2: true }, // 启用高级自动增益控制 { googHighpassFilter: true }, // 启用高通滤波器 { googTypingNoiseDetection: true } // 启用打字噪声检测 ] }; this.renderstreaming = new RenderStreaming(signaling, config); } /** * 设置WebRTC连接 * @async * @returns {Promise} */ 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) { const tracks = this.state.localStream.getTracks(); for (const track of tracks) { this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }, participantId); } this.setCodecPreferences(participantId); } }; // 连接建立回调 this.renderstreaming.onConnect = (connectionId, data) => { // 保存角色信息(host/participant) if (data && data.role) { this.role = data.role; // 更新localUser的isHost标志 this.state.session.localUser.isHost = (this.role === 'host'); // 保存自身的participantId,用于从participants-sync中过滤自身 if (data.participantId) { this.selfParticipantId = data.participantId; } console.log(`Connected as ${this.role}, participantId: ${this.selfParticipantId}`); } // 连接建立后,更新状态为ongoing this.state.session.status = 'ongoing'; this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' }); // 参与者加入时默认静音:禁用音频轨道并更新状态 if (this.role === 'participant') { if (this.state.localStream) { this.state.localStream.getAudioTracks().forEach(track => { track.enabled = false; }); } this.state.session.localUser.mediaState.audio = false; this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false }); console.log('Participant joined with audio muted by default'); } // 连接建立后发送本地用户信息 this.sendMessage('user-info', { id: this.state.session.localUser.id, name: this.state.session.localUser.name, avatar: this.state.session.localUser.avatar }); // 发送当前媒体状态,确保远端收到正确的初始状态 this.emitMediaStateChange(); if (this.state.localStream) { // const tracks = this.state.localStream.getTracks(); // for (const track of tracks) { // this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }); // } // this.setCodecPreferences(); this.showStatsMessage(); } else { console.error('Local stream is not available'); showNotification('本地视频流不可用', 'error'); } }; // 连接断开回调(收到服务器的 disconnect 消息,通常是 host 离开导致房间关闭) this.renderstreaming.onDisconnect = () => { console.log('Received disconnect from server, host left or room closed'); this.hangUp(); // 房间已关闭,挂断连接 }; // participant加入回调(host收到,新participant加入房间) this.renderstreaming.onParticipantJoined = (participantId) => { console.log(`Participant joined: ${participantId}`); if (!this.state.participants[participantId]) { this.state.participants[participantId] = { id: '', name: '参与者', avatar: '/images/p2.png', mediaState: { audio: false, video: true, isSpeaking: false }, status: 'online' }; } this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); this.broadcastParticipantsList(); }; // participant离开回调(host收到,房间仍然存在) this.renderstreaming.onParticipantLeft = (participantId) => { console.log(`Participant left: ${participantId}, room still active`); this.updateRemoteUserStatus('offline'); this.updateRemoteUserNetworkQuality('no_signal'); showNotification('对方已离开通话', 'warning'); // 清理该 participant 的远端流 if (this.state.remoteStreams[participantId]) { this.state.remoteStreams[participantId].getTracks().forEach(track => track.stop()); delete this.state.remoteStreams[participantId]; } if (this.state.remoteStream) { this.state.remoteStream.getTracks().forEach(track => track.stop()); this.state.remoteStream = null; } // 清理该 participant 的用户信息 delete this.state.participants[participantId]; // 通知UI更新,用participantId作为connectionId传给renderer this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId }); this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); this.broadcastParticipantsList(); }; // 轨道事件回调 this.renderstreaming.onTrackEvent = (data) => { const direction = data.transceiver.direction; if (direction == "sendrecv" || direction == "recvonly") { // 使用participantId区分不同participant的流 const trackParticipantId = data.participantId || this.connectionId; const isHost = this.role === 'host'; let targetStream = null; if (isHost) { // Host端: 按 participantId 管理多路远端流 if (!this.state.remoteStreams[trackParticipantId]) { this.state.remoteStreams[trackParticipantId] = new MediaStream(); } targetStream = this.state.remoteStreams[trackParticipantId]; } else { // Participant端: 使用单一远端流 if (this.state.remoteStream == null) { this.state.remoteStream = new MediaStream(); } targetStream = this.state.remoteStream; } // 检查是否已经有相同类型的轨道 const existingTracks = targetStream.getTracks().filter(track => track.kind === data.track.kind); existingTracks.forEach(track => { targetStream.removeTrack(track); console.log('Removed old track:', track.kind); }); targetStream.addTrack(data.track); console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId); // Host端兜底:确保participants中有该participant条目 if (isHost && !this.state.participants[trackParticipantId]) { this.state.participants[trackParticipantId] = { id: '', name: '参与者', avatar: '/images/p2.png', mediaState: { audio: false, video: true, isSpeaking: false }, status: 'online' }; this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); this.broadcastParticipantsList(); } // 通知UI远程流已更新 // 关键优化:如果是音频轨道先到达且流中尚无视频轨道, // 延迟通知UI等待视频轨道到达,避免音频先触发的UI更新导致黑屏 const notifyStreamUpdate = () => { this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: targetStream, connectionId: trackParticipantId, isHost: isHost }); console.log('Notified UI about remote stream update'); }; if (data.track.kind === 'audio' && targetStream.getVideoTracks().length === 0) { // 音频先到,视频尚未到达:延迟200ms通知,给视频轨道到达的机会 console.log('Audio track arrived first, delaying stream notification for video track...'); setTimeout(() => { const nowHasVideo = targetStream.getVideoTracks().length > 0; console.log(`After delay, stream has video: ${nowHasVideo}`); notifyStreamUpdate(); }, 200); } else { // 视频轨道到达,或音频视频同时存在:立即通知 notifyStreamUpdate(); } // 只有当收到远程流时才更新远程用户状态为在线 if (this.state.session.remoteUser.status !== 'online') { this.updateRemoteUserStatus('online'); // 更新远程用户网络质量为好 this.updateRemoteUserNetworkQuality('good'); this.sendMessage('user-info', { id: this.state.session.localUser.id, name: this.state.session.localUser.name, avatar: this.state.session.localUser.avatar }); // 启动通话时长计时器(避免重复启动) 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.startActivityDetection(this.state.remoteStream, { isLocal: false }); } } else if (direction == "sendonly") { // 本地发送轨道,启动本地音频活动检测 if (data.track.kind === 'audio') { this.startActivityDetection(this.state.localStream, { isLocal: true }); } } }; this.renderstreaming.onMessage = (data) => { console.log('收到消息:', data); if (data.type === 'chat-message') { // 处理聊天 // 添加到列表并更新UI chatMessage.handleChatMessage(data.message); // Host端:按participantId更新对应用户信息 if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) { this.state.participants[data.participantId].id = data.message.senderId; if (data.message.senderName) { this.state.participants[data.participantId].name = data.message.senderName; } if (data.message.senderAvatar) { this.state.participants[data.participantId].avatar = data.message.senderAvatar; } this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); this.broadcastParticipantsList(); } // Participant端:根据消息来源更新对应用户信息 if (!this.role || this.role !== 'host') { if (data.participantId && this.state.participants[data.participantId]) { // 来自其他Participant的消息:更新participants中对应条目 if (data.message.senderName) { this.state.participants[data.participantId].name = data.message.senderName; } if (data.message.senderAvatar) { this.state.participants[data.participantId].avatar = data.message.senderAvatar; } this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); } else if (data.message && data.message.senderId !== this.state.session.localUser.id) { // 来自Host的消息:更新remoteUser this.state.session.remoteUser = { ...this.state.session.remoteUser, id: data.message.senderId, name: data.message.senderName, avatar: data.message.senderAvatar }; this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); } } } else if (data.type === 'media-state-changed') { // 处理媒体状态变化 console.log('收到媒体状态变化:', data.data, 'from participant:', data.participantId); if (this.role === 'host') { // Host端:按participantId同步更新participants中对应participant的mediaState if (data.participantId && this.state.participants[data.participantId]) { this.state.participants[data.participantId].mediaState = { ...this.state.participants[data.participantId].mediaState, ...data.data }; } // 更新远端媒体状态 this.updateRemoteMedia(data.data, data.participantId); // 通知UI更新participants this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); // Host端广播最新成员列表(含媒体状态)给所有Participant this.broadcastParticipantsList(); } else { // Participant端:根据消息来源更新对应条目 if (data.participantId && this.state.participants[data.participantId]) { // 来自其他Participant的媒体状态变化:仅更新participants中对应条目 // 不调用updateRemoteMedia,因为Participant端没有其他Participant的视频流 this.state.participants[data.participantId].mediaState = { ...this.state.participants[data.participantId].mediaState, ...data.data }; this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); } else if (!data.participantId) { // 来自Host的媒体状态变化(无participantId): // 更新participants中Host条目 + 更新remoteUser(Host的视频流是本端远端画面) if (this.state.participants['host']) { this.state.participants['host'].mediaState = { ...this.state.participants['host'].mediaState, ...data.data }; } this.updateRemoteMedia(data.data, data.participantId); this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); } } } else if (data.type === 'user-info') { // 处理用户信息更新 console.log('收到用户信息:', data.data, 'from participant:', data.participantId); if (data.data) { if (data.participantId && this.role === 'host') { // Host端:按participantId存储到participants Map if (!this.state.participants[data.participantId]) { this.state.participants[data.participantId] = { id: '', name: '参与者', avatar: '/images/p2.png', mediaState: { audio: false, video: true, isSpeaking: false }, status: 'online' }; } this.state.participants[data.participantId].id = data.data.id || ''; this.state.participants[data.participantId].name = data.data.name || '参与者'; this.state.participants[data.participantId].avatar = data.data.avatar || '/images/p2.png'; this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants }); this.broadcastParticipantsList(); } else { // Participant端:更新单一remoteUser(Host的信息) this.state.session.remoteUser = { ...this.state.session.remoteUser, id: data.data.id || this.state.session.remoteUser.id, name: data.data.name || this.state.session.remoteUser.name, avatar: data.data.avatar || this.state.session.remoteUser.avatar }; this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState }); } } } else if (data.type === 'participants-sync') { // Participant端:接收Host广播的完整成员列表 if (this.role !== 'host' && data.data) { console.log('收到成员列表同步:', data.data); // 过滤掉自身条目,避免在列表中重复显示(自身已作为localUser显示) const filtered = {}; for (const [pid, pInfo] of Object.entries(data.data)) { if (pid !== this.selfParticipantId) { filtered[pid] = pInfo; } } 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} */ async _startConnection(connectionId) { // 启动WebRTC连接 await this.renderstreaming.start(); await this.renderstreaming.createConnection(connectionId); // 启动网络质量检测 this.startNetworkQualityDetection(); // 启动本地音频活动检测 this.startActivityDetection(this.state.localStream, { isLocal: true }); //启动远端音频活动检测 this.startActivityDetection(this.state.remoteStream, { isLocal: false }); } /** * 挂断WebRTC连接 * Host挂断:房间删除,通知所有participants * Participant挂断:仅自己离开,房间保留 * @async * @returns {Promise} */ async hangUp() { this.clearStatsMessage(); // 清除统计信息 this.stopNetworkQualityDetection(); // 停止网络质量检测 // 停止通话时长计时器 if (this.durationInterval) { clearInterval(this.durationInterval); this.durationInterval = null; } // 重置通话时长同步标志 this.durationSynced = false; const isHost = this.role === 'host'; console.log(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`); // 删除连接并停止WebRTC if (this.renderstreaming) { try { // 发送断开连接信令给服务器 // 服务器会根据角色决定: // - host断开:通知所有participants,删除房间 // - participant断开:仅通知host,保留房间 await this.renderstreaming.deleteConnection(); await this.renderstreaming.stop(); } catch (error) { console.error('Error during hangUp:', error); } this.renderstreaming = null; } // 更新远程用户状态为离线 this.updateRemoteUserStatus('offline'); this.updateRemoteUserNetworkQuality('no_signal'); // 清理participants this.state.participants = {}; this.selfParticipantId = null; this.connectionId = null; this.role = null; this.state.session.status = 'ended'; this.notify({ type: 'CALL_ENDED', reason: isHost ? 'host_hangup' : 'participant_hangup' }); } /** * 发送消息 * @param {string} type - 消息类型 * @param {Object} data - 消息数据 */ sendMessage(type, data) { if (this.renderstreaming) { this.renderstreaming.sendMessage({ type: type, data: data }); } } /** * Host端广播完整成员列表给所有Participant * 包含Host自身信息 + 所有Participant信息 * Participant收到后可展示完整通话成员列表 */ broadcastParticipantsList() { if (this.role !== 'host' || !this.renderstreaming) return; const memberList = {}; // 添加Host自身信息 memberList['host'] = { id: this.state.session.localUser.id, name: this.state.session.localUser.name, avatar: this.state.session.localUser.avatar, mediaState: { ...this.state.session.localUser.mediaState }, status: 'online', role: 'host' }; // 添加所有Participant信息 for (const [pid, pInfo] of Object.entries(this.state.participants)) { memberList[pid] = { ...pInfo, role: 'participant' }; } this.renderstreaming.sendMessage({ type: 'participants-sync', data: memberList, callDuration: this.state.session.duration }); console.log('Broadcast participants list:', Object.keys(memberList)); } /** * 设置编解码器偏好 */ setCodecPreferences(participantId) { let selectedCodecs = null; const { codecs } = RTCRtpSender.getCapabilities('video'); if (codecs && codecs.length > 0) { const h264Codec = codecs.find(c => c.mimeType === 'video/H264'); if (h264Codec) { selectedCodecs = [h264Codec]; } } if (selectedCodecs == null) return; if (this.renderstreaming) { const transceivers = this.renderstreaming.getTransceivers(participantId); if (transceivers && transceivers.length > 0) { const videoTransceivers = transceivers.filter(t => t.receiver.track.kind == "video"); if (videoTransceivers && videoTransceivers.length > 0) { videoTransceivers.forEach(t => t.setCodecPreferences(selectedCodecs)); } } } } // 更新远端媒体状态 (由 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 }); } // 更新远端用户状态 updateRemoteUserStatus(status) { this.state.session.remoteUser.status = status; this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); } updateRemoteUserNetworkQuality(networkQuality) { this.state.session.remoteUser.networkQuality = networkQuality; this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); } // 结束通话(用户主动点击挂断按钮) async endCall() { console.log(`endCall called. Role: ${this.role}`); // 调用 hangUp() 正确关闭 WebRTC 连接并发送断开信令 // hangUp 内部会根据角色区分: // - host: 通知所有participants,删除房间 // - participant: 仅自己离开,房间保留 await this.hangUp(); } // 加入通话 async joinCall(connectionId) { this.state.session.status = 'connecting'; this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); showNotification(`正在加入通话 (${connectionId})`); // 初始化 await this.init(); // 保存连接ID this.connectionId = connectionId; } // 创建通话 async createCall() { this.state.session.status = 'connecting'; this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' }); showNotification('正在创建通话...'); // 初始化 await this.init(); } // 真实网络质量检测 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'; } } else { // 没有收到任何RTP包,设置为无信号状态 quality = 'no_signal'; } // 更新网络质量状态 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); } } // 音频活动检测 startActivityDetection(stream, { isLocal = false } = {}) { if (!stream) { return; } 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(stream); const analyser = audioContext.createAnalyser(); analyser.fftSize = fftSize; source.connect(analyser); const dataArray = new Uint8Array(analyser.frequencyBinCount); let isSpeaking = false; let lastActivityTime = 0; const detectActivity = () => { if (!stream || !this.renderstreaming) { 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 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 }); } } } 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 }); } } if (this.state.session.status === 'ongoing') { requestAnimationFrame(detectActivity); } }; detectActivity(); console.log(`${isLocal ? 'Local' : 'Remote'} activity detection started`); } catch (error) { console.error(`Error starting ${isLocal ? 'local' : 'remote'} activity detection:`, error); } } // 启动网络质量检测 startNetworkQualityDetection() { // 每3秒检测一次网络质量 this.networkQualityInterval = setInterval(() => { this.detectNetworkQuality(); }, 3000); } // 停止网络质量检测 stopNetworkQualityDetection() { if (this.networkQualityInterval) { clearInterval(this.networkQualityInterval); this.networkQualityInterval = null; } } // 发送媒体状态到服务器 emitMediaStateChange() { const payload = { userId: this.state.session.localUser.id, ...this.state.session.localUser.mediaState }; console.log('[WebSocket Emit] media-state-changed:', payload); // 使用WebRTC发送媒体状态变化 if (this.renderstreaming) { this.renderstreaming.sendMessage({ type: 'media-state-changed', data: payload }); } } // 显示统计信息 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 getState() { return this.state; } getLocalUser() { return this.state.session.localUser; } getRemoteUser() { return this.state.session.remoteUser; } getConnectionId() { return this.connectionId; } getRenderStreaming() { return this.renderstreaming; } } // 创建单例实例 const store = new CallStateManager(); // 页面卸载前清理 window.addEventListener('beforeunload', async () => { if (!store.renderstreaming) return; await store.renderstreaming.stop(); // 停止WebRTC连接 }, true); export default store;