Files
webRtc/WebApp/client/public/onebyone/store.js
2026-03-06 15:15:01 +08:00

872 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 状态管理
* 使用简单的 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) {
// 停止当前的媒体流
if (this.state.localStream) {
this.state.localStream.getTracks().forEach(track => track.stop());
}
this.state.localStream = null;
}
// 请求摄像头权限并获取媒体流
this.state.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
await this.getLocalStream();
// 更新WebRTC连接中的视频轨道
if (this.renderstreaming) {
console.log('Updating video track in WebRTC connection');
// 获取所有收发器
const transceivers = this.renderstreaming.getTransceivers();
console.log('All transceivers:', transceivers);
// 查找现有的视频收发器
const videoTransceivers = transceivers.filter(t => {
return t.sender && t.sender.track && t.sender.track.kind === 'video';
});
console.log('Found video transceivers:', videoTransceivers);
// 获取新的视频轨道
const videoTracks = this.state.localStream.getVideoTracks();
console.log('New video tracks:', videoTracks);
if (videoTracks.length > 0) {
const newVideoTrack = videoTracks[0];
console.log('Using new video track:', newVideoTrack);
if (videoTransceivers.length > 0) {
// 替换现有的视频轨道
for (const transceiver of videoTransceivers) {
try {
console.log('Replacing video track in transceiver:', transceiver);
await transceiver.sender.replaceTrack(newVideoTrack);
console.log('Successfully replaced video track');
} catch (error) {
console.error('Error replacing video track:', error);
}
}
} else {
// 添加新的视频收发器
try {
console.log('Adding new video transceiver');
const transceiver = this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' });
console.log('Added new video transceiver:', transceiver);
} catch (error) {
console.error('Error adding new video transceiver:', error);
}
}
// 延迟设置编解码器偏好,确保收发器已完全创建
setTimeout(() => {
this.setCodecPreferences();
}, 100);
}
}
} 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<void>}
*/
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();
}
// 检查是否已经有相同类型的轨道
const existingTracks = this.state.remoteStream.getTracks().filter(track => track.kind === data.track.kind);
// 移除旧的轨道
existingTracks.forEach(track => {
this.state.remoteStream.removeTrack(track);
console.log('Removed old track:', track.kind);
});
// 添加新的轨道
this.state.remoteStream.addTrack(data.track);
console.log('Added new track:', data.track.kind);
// 通知UI远程流已更新
this.notify({ type: 'REMOTE_STREAM_OBTAINED', stream: this.state.remoteStream });
console.log('Notified UI about remote stream update');
// 如果是音频轨道,启动远程音频活动检测
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<void>}
*/
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;