/** * 状态管理 * 使用简单的 Observable 模式,可替换为 Redux/Vuex/Pinia */ import { mockCallSession, mockMessages } 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 } from './utils.js'; // 导入通知函数 // 默认视频流尺寸 const defaultStreamWidth = 1280; const defaultStreamHeight = 720; class CallStateManager { constructor() { const renderstreaming = null; // WebRTC连接管理实例 const useWebSocket = null; // 是否使用WebSocket信令 const connectionId = null; // 连接ID // 核心状态 this.state = { session: { ...mockCallSession, status: 'idle' // 初始状态为空闲 }, messages: [...mockMessages], isSidebarOpen: false, unreadCount: 0, localStream: null, // MediaStream 对象 remoteStream: null // MediaStream 对象 }; // 监听器数组 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() { // 启动通话时长计时器 this.durationInterval = setInterval(() => { this.state.session.duration++; this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration }); }, 1000); // 初始化配置 await this.setupConfig(); // 获取本地摄像头视频流 await this.getLocalStream(); } 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({ video: true, audio: true }); 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.startLocalActivityDetection(); } 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) { if (this.state.localStream) { this.state.localStream = null; } //if(this.state.localStream.getVideoTracks().length==0){ // 请求摄像头权限并获取媒体流 this.state.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); // } await this.getLocalStream(); } 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.localStream.getTracks().forEach(track => { if (track.kind === 'video') { track.stop(); } }); } // 如果是音频状态变化,控制本地音频轨道 if (mediaType === 'audio' && this.state.localStream) { this.state.localStream.getTracks().forEach(track => { if (track.kind === 'audio') { track.enabled = value; } }); } } /** * 设置WebRTC连接 * @async * @returns {Promise} */ async setUp(connectionId) { //TODO 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 } // 设置最小比特率 ] }; this.renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例 // 连接建立回调 this.renderstreaming.onConnect = () => { // 连接建立后,更新状态为ongoing this.state.session.status = 'ongoing'; this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' }); 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'); } }; // 连接断开回调 this.renderstreaming.onDisconnect = () => { this.hangUp(); // 挂断连接 }; // 轨道事件回调 this.renderstreaming.onTrackEvent = (data) => { const direction = data.transceiver.direction; if (direction == "sendrecv" || direction == "recvonly") { if (this.state.remoteStream == null) { this.state.remoteStream = new MediaStream(); } this.state.remoteStream.addTrack(data.track); // 通知UI远程流已更新 this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: this.state.remoteStream }); // 如果是音频轨道,启动远程音频活动检测 if (data.track.kind === 'audio') { this.startRemoteActivityDetection(); } } else if (direction == "sendonly") { // 本地发送轨道,启动本地音频活动检测 if (data.track.kind === 'audio') { this.startLocalActivityDetection(); } } }; // 启动WebRTC连接 await this.renderstreaming.start(); await this.renderstreaming.createConnection(connectionId); // 启动网络质量检测 this.startNetworkQualityDetection(); // 启动本地音频活动检测 this.startLocalActivityDetection(); // 启动远端音频活动检测 this.startRemoteActivityDetection(); //模拟远端活动 (开发测试用) //this.simulateRemoteActivity(); } /** * 挂断WebRTC连接 * @async * @returns {Promise} */ async hangUp() { this.clearStatsMessage(); // 清除统计信息 this.stopNetworkQualityDetection(); // 停止网络质量检测 console.log(`Disconnect peer on ${this.connectionId}.`); // 删除连接并停止WebRTC if (this.renderstreaming) { try { await this.renderstreaming.deleteConnection(); await this.renderstreaming.stop(); } catch (error) { console.error('Error during hangUp:', error); } this.renderstreaming = null; } this.connectionId = null; this.state.session.status = 'ended'; this.notify({ type: 'CALL_ENDED' }); } /** * 设置编解码器偏好 */ setCodecPreferences() { /** @type {RTCRtpCodecCapability[] | null} */ let selectedCodecs = null; // 获取视频编解码器能力 const { codecs } = RTCRtpSender.getCapabilities('video'); if (codecs && codecs.length > 0) { // 优先选择H.264编解码器 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().filter(t => t.receiver.track.kind == "video"); if (transceivers && transceivers.length > 0) { transceivers.forEach(t => t.setCodecPreferences(selectedCodecs)); } } } // 更新远端媒体状态 (由 WebSocket 消息触发) updateRemoteMedia(mediaState) { this.state.session.remoteUser.mediaState = { ...this.state.session.remoteUser.mediaState, ...mediaState }; this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState }); } // 添加消息 addMessage(message) { this.state.messages.push(message); // 如果侧边栏关闭且不是自己发的,增加未读 if (!this.state.isSidebarOpen && !message.isSelf) { this.state.unreadCount++; } this.notify({ type: 'NEW_MESSAGE', message, unreadCount: this.state.unreadCount }); } // 切换侧边栏 toggleSidebar() { this.state.isSidebarOpen = !this.state.isSidebarOpen; if (this.state.isSidebarOpen) { this.state.unreadCount = 0; } this.notify({ type: 'SIDEBAR_TOGGLE', isOpen: this.state.isSidebarOpen }); } // 结束通话 endCall() { clearInterval(this.durationInterval); this.state.session.status = 'ended'; this.notify({ type: 'CALL_ENDED' }); // 发送结束通话请求到服务器 // [API_CALL: POST /api/call/:callId/leave] // [WEBSOCKET_EMIT: leave-call] } // 加入通话 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(); } // 模拟远端活动 (开发测试用) simulateRemoteActivity() { setInterval(() => { if (Math.random() > 0.7) { const isSpeaking = Math.random() > 0.5; this.updateRemoteMedia({ isSpeaking }); } }, 800); } simulateNetworkChange() { // 模拟网络质量变化 const qualities = ['good', 'fair', 'excellent', 'poor']; 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) { 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); } } // 真实音频活动检测 - 本地 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 }); // 发送媒体状态到服务器 this.emitMediaStateChange(); } } 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'); } catch (error) { console.error('Error starting local activity detection:', error); } } // 启动网络质量检测 startNetworkQualityDetection() { // 每3秒检测一次网络质量 this.networkQualityInterval = setInterval(() => { this.detectNetworkQuality(); //this.simulateNetworkChange(); }, 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); // socket.emit('media-state-changed', 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; } getMessages() { return this.state.messages; } getConnectionId() { return this.connectionId; } } // 创建单例实例 const store = new CallStateManager(); // 页面卸载前清理 window.addEventListener('beforeunload', async () => { if (!store.renderstreaming) return; await store.renderstreaming.stop(); // 停止WebRTC连接 }, true); export default store;