Files
webRtc/WebApp/client/public/onebyone/store.js

1289 lines
55 KiB
JavaScript
Raw Normal View History

/**
* 状态管理
* 使用简单的 Observable 模式可替换为 Redux/Vuex/Pinia
*/
2026-03-12 14:41:00 +08:00
import { mockCallSession } from './models.js';
2026-03-04 17:55:55 +08:00
import { Signaling, WebSocketSignaling } from "../../module/signaling.js";// 信令管理
import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连接管理
import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置
2026-03-12 12:10:40 +08:00
import { showNotification, generateId } from './utils.js'; // 导入通知函数
2026-03-12 14:41:00 +08:00
import chatMessage from './chatmessage.js';
2026-03-04 17:55:55 +08:00
class CallStateManager {
constructor() {
// 核心状态
this.state = {
2026-03-12 12:10:40 +08:00
id: generateId(),
2026-03-04 18:40:19 +08:00
session: {
...mockCallSession,
status: 'idle' // 初始状态为空闲
},
localStream: null, // MediaStream 对象
2026-04-23 16:08:08 +08:00
remoteStream: null, // 单路远端流兼容旧逻辑participant端使用
2026-04-25 16:53:50 +08:00
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));
}
// 初始化
2026-03-04 22:29:10 +08:00
async init() {
2026-03-04 17:55:55 +08:00
// 初始化配置
2026-03-04 22:29:10 +08:00
await this.setupConfig();
2026-04-10 17:14:31 +08:00
// 加载用户设置
this.loadUserSettings();
2026-03-04 11:19:50 +08:00
// 获取本地摄像头视频流
2026-03-04 22:29:10 +08:00
await this.getLocalStream();
}
2026-04-10 17:14:31 +08:00
// 加载用户设置
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);
}
}
}
2026-03-04 17:55:55 +08:00
async setupConfig() {
const res = await getServerConfig();
this.useWebSocket = res.useWebSocket;
}
2026-03-04 11:19:50 +08:00
// 获取本地摄像头视频流
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');
}
2026-03-09 20:44:37 +08:00
// 请求摄像头权限并获取媒体流,启用回声消除
2026-03-04 11:19:50 +08:00
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
2026-03-09 20:44:37 +08:00
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
2026-03-04 11:19:50 +08:00
});
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();
2026-03-06 13:59:38 +08:00
// 启动本地音频活动检测
this.startLocalActivityDetection();
2026-03-04 11:19:50 +08:00
} 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 });
}
}
// 更新本地媒体状态
2026-03-04 11:19:50 +08:00
async updateLocalMedia(mediaType, value) {
2026-03-04 17:55:55 +08:00
// 如果是开启视频,重新获取摄像头资源
if (mediaType === 'video' && value) {
2026-04-25 13:29:35 +08:00
try {
// 只获取新的视频轨道,不干扰正在工作的音频
const newVideoStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
const newVideoTrack = newVideoStream.getVideoTracks()[0];
2026-03-06 15:15:01 +08:00
2026-04-25 13:29:35 +08:00
if (!newVideoTrack) {
throw new Error('Failed to get video track');
}
2026-03-06 15:15:01 +08:00
2026-04-25 13:29:35 +08:00
// 更新本地流中的视频轨道(替换旧的已停止的轨道)
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;
}
2026-03-06 15:15:01 +08:00
2026-04-25 13:29:35 +08:00
// 更新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);
}
2026-04-11 19:05:19 +08:00
}
2026-04-25 13:29:35 +08:00
// 设置编解码器偏好
setTimeout(() => { this.setCodecPreferences(participantId); }, 100);
2026-04-11 19:05:19 +08:00
}
} else {
2026-04-25 13:29:35 +08:00
// 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);
}
2026-03-06 15:15:01 +08:00
}
}
2026-04-25 13:29:35 +08:00
setTimeout(() => { this.setCodecPreferences(); }, 100);
2026-03-06 15:15:01 +08:00
}
2026-03-09 20:44:37 +08:00
}
2026-03-06 15:15:01 +08:00
2026-04-25 13:29:35 +08:00
// 更新状态和通知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.startLocalActivityDetection();
2026-03-09 20:44:37 +08:00
2026-04-25 13:29:35 +08:00
} 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 });
2026-03-06 15:15:01 +08:00
}
2026-03-04 17:55:55 +08:00
} else {
// 直接更新媒体状态
this.state.session.localUser.mediaState[mediaType] = value;
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType, value });
// 发送媒体状态到服务器
this.emitMediaStateChange();
}
2026-03-04 11:19:50 +08:00
// 如果是关闭视频,释放摄像头资源
if (mediaType === 'video' && !value && this.state.localStream) {
2026-03-12 15:33:34 +08:00
this.state.session.localUser.mediaState.video = false;
2026-03-04 11:19:50 +08:00
this.state.localStream.getTracks().forEach(track => {
if (track.kind === 'video') {
track.stop();
}
});
2026-03-12 15:33:34 +08:00
// 发送媒体状态到服务器
this.emitMediaStateChange();
2026-03-04 11:19:50 +08:00
}
2026-03-04 11:19:50 +08:00
// 如果是音频状态变化,控制本地音频轨道
if (mediaType === 'audio' && this.state.localStream) {
2026-03-12 15:33:34 +08:00
this.state.session.localUser.mediaState.audio = value;
2026-03-04 11:19:50 +08:00
this.state.localStream.getTracks().forEach(track => {
if (track.kind === 'audio') {
track.enabled = value;
}
});
2026-03-12 15:33:34 +08:00
// 发送媒体状态到服务器
this.emitMediaStateChange();
2026-03-04 11:19:50 +08:00
}
2026-03-12 15:33:34 +08:00
// 通知UI更新用户列表
this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
2026-03-04 17:55:55 +08:00
}
2026-03-12 12:10:40 +08:00
2026-03-04 17:55:55 +08:00
/**
* 设置WebRTC连接
* @async
* @returns {Promise<void>}
*/
async setUp(connectionId) {
//TODO
this.connectionId = connectionId; // 获取连接ID
2026-03-05 11:06:08 +08:00
// 设置状态为连接中
this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
2026-04-11 19:05:19 +08:00
2026-03-04 22:29:10 +08:00
// 确保本地流已经初始化
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();
});
}
2026-03-04 17:55:55 +08:00
// 创建信令实例
2026-03-04 22:29:10 +08:00
const signaling = this.useWebSocket ? new WebSocketSignaling() : new Signaling();
2026-03-04 17:55:55 +08:00
const config = getRTCConfiguration(); // 获取RTC配置
2026-03-09 20:44:37 +08:00
// 优化RTC配置确保支持高分辨率和良好的音频处理
2026-03-05 11:30:27 +08:00
config.peerConnectionOptions = {
optional: [
{ googCpuOveruseDetection: false }, // 禁用CPU过度使用检测
2026-03-09 20:44:37 +08:00
{ googScreencastMinBitrate: 3000 }, // 设置最小比特率
{ googEchoCancellation: true }, // 启用回声消除
{ googEchoCancellation2: true }, // 启用高级回声消除
{ googNoiseSuppression: true }, // 启用噪声抑制
{ googNoiseSuppression2: true }, // 启用高级噪声抑制
{ googAutoGainControl: true }, // 启用自动增益控制
{ googAutoGainControl2: true }, // 启用高级自动增益控制
{ googHighpassFilter: true }, // 启用高通滤波器
{ googTypingNoiseDetection: true } // 启用打字噪声检测
2026-03-05 11:30:27 +08:00
]
};
2026-04-24 23:03:41 +08:00
this.renderstreaming = new RenderStreaming(signaling, config);
this.renderstreaming.onNewPeer = (participantId) => {
console.log(`New peer created for ${participantId}, adding local tracks`);
2026-04-23 15:22:24 +08:00
if (this.state.localStream) {
const tracks = this.state.localStream.getTracks();
for (const track of tracks) {
2026-04-24 23:03:41 +08:00
this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }, participantId);
2026-04-23 15:22:24 +08:00
}
2026-04-24 23:03:41 +08:00
this.setCodecPreferences(participantId);
2026-04-23 15:22:24 +08:00
}
};
2026-03-04 17:55:55 +08:00
// 连接建立回调
2026-04-23 15:22:24 +08:00
this.renderstreaming.onConnect = (connectionId, data) => {
// 保存角色信息host/participant
if (data && data.role) {
this.role = data.role;
2026-04-25 17:43:53 +08:00
// 更新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}`);
2026-04-23 15:22:24 +08:00
}
2026-03-05 11:06:08 +08:00
// 连接建立后更新状态为ongoing
2026-03-05 11:30:27 +08:00
this.state.session.status = 'ongoing';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' });
2026-04-25 17:01:18 +08:00
// 参与者加入时默认静音:禁用音频轨道并更新状态
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');
}
2026-04-11 00:03:30 +08:00
// 连接建立后发送本地用户信息
this.sendMessage('user-info', {
id: this.state.session.localUser.id,
name: this.state.session.localUser.name,
avatar: this.state.session.localUser.avatar
});
2026-04-25 17:01:18 +08:00
// 发送当前媒体状态,确保远端收到正确的初始状态
this.emitMediaStateChange();
2026-03-04 22:29:10 +08:00
if (this.state.localStream) {
2026-04-23 15:22:24 +08:00
// const tracks = this.state.localStream.getTracks();
// for (const track of tracks) {
// this.renderstreaming.addTransceiver(track, { direction: 'sendonly' });
// }
// this.setCodecPreferences();
this.showStatsMessage();
2026-03-04 22:29:10 +08:00
} else {
console.error('Local stream is not available');
showNotification('本地视频流不可用', 'error');
2026-03-04 11:19:50 +08:00
}
2026-03-04 17:55:55 +08:00
};
2026-03-04 11:19:50 +08:00
2026-04-23 15:22:24 +08:00
// 连接断开回调(收到服务器的 disconnect 消息,通常是 host 离开导致房间关闭)
2026-03-04 17:55:55 +08:00
this.renderstreaming.onDisconnect = () => {
2026-04-23 15:22:24 +08:00
console.log('Received disconnect from server, host left or room closed');
this.hangUp(); // 房间已关闭,挂断连接
};
2026-04-25 16:53:50 +08:00
// 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',
2026-04-25 17:01:18 +08:00
mediaState: { audio: false, video: true, isSpeaking: false },
2026-04-25 16:53:50 +08:00
status: 'online'
};
}
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
2026-04-25 17:43:53 +08:00
this.broadcastParticipantsList();
2026-04-25 16:53:50 +08:00
};
2026-04-23 15:22:24 +08:00
// participant离开回调host收到房间仍然存在
2026-04-24 23:03:41 +08:00
this.renderstreaming.onParticipantLeft = (participantId) => {
console.log(`Participant left: ${participantId}, room still active`);
2026-04-23 15:22:24 +08:00
this.updateRemoteUserStatus('offline');
this.updateRemoteUserNetworkQuality('no_signal');
showNotification('对方已离开通话', 'warning');
2026-04-23 16:08:08 +08:00
// 清理该 participant 的远端流
2026-04-24 23:03:41 +08:00
if (this.state.remoteStreams[participantId]) {
this.state.remoteStreams[participantId].getTracks().forEach(track => track.stop());
delete this.state.remoteStreams[participantId];
2026-04-23 16:08:08 +08:00
}
2026-04-23 15:22:24 +08:00
if (this.state.remoteStream) {
this.state.remoteStream.getTracks().forEach(track => track.stop());
this.state.remoteStream = null;
}
2026-04-25 16:53:50 +08:00
// 清理该 participant 的用户信息
delete this.state.participants[participantId];
2026-04-24 23:03:41 +08:00
// 通知UI更新用participantId作为connectionId传给renderer
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
2026-04-25 16:53:50 +08:00
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
2026-04-25 17:43:53 +08:00
this.broadcastParticipantsList();
2026-03-04 17:55:55 +08:00
};
// 轨道事件回调
this.renderstreaming.onTrackEvent = (data) => {
const direction = data.transceiver.direction;
if (direction == "sendrecv" || direction == "recvonly") {
2026-04-24 23:03:41 +08:00
// 使用participantId区分不同participant的流
const trackParticipantId = data.participantId || this.connectionId;
2026-04-23 16:08:08 +08:00
const isHost = this.role === 'host';
let targetStream = null;
if (isHost) {
2026-04-24 23:03:41 +08:00
// Host端: 按 participantId 管理多路远端流
if (!this.state.remoteStreams[trackParticipantId]) {
this.state.remoteStreams[trackParticipantId] = new MediaStream();
2026-04-23 16:08:08 +08:00
}
2026-04-24 23:03:41 +08:00
targetStream = this.state.remoteStreams[trackParticipantId];
2026-04-23 16:08:08 +08:00
} else {
// Participant端: 使用单一远端流
if (this.state.remoteStream == null) {
this.state.remoteStream = new MediaStream();
}
targetStream = this.state.remoteStream;
2026-03-04 17:55:55 +08:00
}
2026-03-06 15:15:01 +08:00
// 检查是否已经有相同类型的轨道
2026-04-23 16:08:08 +08:00
const existingTracks = targetStream.getTracks().filter(track => track.kind === data.track.kind);
2026-03-06 15:15:01 +08:00
existingTracks.forEach(track => {
2026-04-23 16:08:08 +08:00
targetStream.removeTrack(track);
2026-03-06 15:15:01 +08:00
console.log('Removed old track:', track.kind);
});
2026-04-23 16:08:08 +08:00
targetStream.addTrack(data.track);
2026-04-24 23:03:41 +08:00
console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId);
2026-03-06 15:15:01 +08:00
2026-04-25 16:53:50 +08:00
// Host端兜底确保participants中有该participant条目
if (isHost && !this.state.participants[trackParticipantId]) {
this.state.participants[trackParticipantId] = {
id: '',
name: '参与者',
avatar: '/images/p2.png',
2026-04-25 17:01:18 +08:00
mediaState: { audio: false, video: true, isSpeaking: false },
2026-04-25 16:53:50 +08:00
status: 'online'
};
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
2026-04-25 17:43:53 +08:00
this.broadcastParticipantsList();
2026-04-25 16:53:50 +08:00
}
2026-03-04 22:29:10 +08:00
// 通知UI远程流已更新
2026-04-25 19:26:39 +08:00
// 关键优化:如果是音频轨道先到达且流中尚无视频轨道,
// 延迟通知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();
}
2026-03-12 17:53:34 +08:00
// 只有当收到远程流时才更新远程用户状态为在线
if (this.state.session.remoteUser.status !== 'online') {
this.updateRemoteUserStatus('online');
// 更新远程用户网络质量为好
this.updateRemoteUserNetworkQuality('good');
2026-04-11 00:03:30 +08:00
this.sendMessage('user-info', {
id: this.state.session.localUser.id,
name: this.state.session.localUser.name,
avatar: this.state.session.localUser.avatar
});
2026-03-12 17:53:34 +08:00
// 启动通话时长计时器
this.durationInterval = setInterval(() => {
this.state.session.duration++;
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
}, 1000);
}
2026-03-06 13:59:38 +08:00
// 如果是音频轨道,启动远程音频活动检测
2026-03-05 16:35:32 +08:00
if (data.track.kind === 'audio') {
this.startRemoteActivityDetection();
}
2026-03-06 13:59:38 +08:00
} else if (direction == "sendonly") {
// 本地发送轨道,启动本地音频活动检测
if (data.track.kind === 'audio') {
this.startLocalActivityDetection();
}
2026-03-04 17:55:55 +08:00
}
};
2026-03-12 12:10:40 +08:00
// 初始化 RenderStreaming 实例后
this.renderstreaming.onMessage = (data) => {
2026-03-12 15:33:34 +08:00
console.log('收到消息:', data);
2026-03-12 12:10:40 +08:00
if (data.type === 'chat-message') {
2026-03-12 12:46:22 +08:00
// 处理聊天
// 添加到列表并更新UI
2026-03-12 14:41:00 +08:00
chatMessage.handleChatMessage(data.message);
2026-04-25 16:53:50 +08:00
// 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 });
2026-04-25 17:43:53 +08:00
this.broadcastParticipantsList();
2026-04-25 16:53:50 +08:00
}
2026-04-25 17:43:53 +08:00
// Participant端根据消息来源更新对应用户信息
2026-04-25 16:53:50 +08:00
if (!this.role || this.role !== 'host') {
2026-04-25 17:43:53 +08:00
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
2026-04-25 16:53:50 +08:00
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 });
}
2026-04-11 00:03:30 +08:00
}
2026-03-12 15:33:34 +08:00
} else if (data.type === 'media-state-changed') {
// 处理媒体状态变化
2026-04-25 13:29:35 +08:00
console.log('收到媒体状态变化:', data.data, 'from participant:', data.participantId);
2026-04-25 18:34:54 +08:00
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条目 + 更新remoteUserHost的视频流是本端远端画面
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 });
}
2026-04-25 16:53:50 +08:00
}
2026-04-11 00:03:30 +08:00
} else if (data.type === 'user-info') {
// 处理用户信息更新
2026-04-25 16:53:50 +08:00
console.log('收到用户信息:', data.data, 'from participant:', data.participantId);
2026-04-11 00:03:30 +08:00
if (data.data) {
2026-04-25 16:53:50 +08:00
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',
2026-04-25 17:01:18 +08:00
mediaState: { audio: false, video: true, isSpeaking: false },
2026-04-25 16:53:50 +08:00
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 });
2026-04-25 17:43:53 +08:00
this.broadcastParticipantsList();
2026-04-25 16:53:50 +08:00
} else {
// Participant端更新单一remoteUserHost的信息
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 });
}
2026-04-11 00:03:30 +08:00
}
2026-04-25 17:43:53 +08:00
} 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 });
}
2026-03-12 12:10:40 +08:00
}
2026-03-04 17:55:55 +08:00
2026-03-12 12:10:40 +08:00
};
2026-04-11 00:03:30 +08:00
2026-03-04 17:55:55 +08:00
// 启动WebRTC连接
await this.renderstreaming.start();
await this.renderstreaming.createConnection(connectionId);
2026-03-12 17:53:34 +08:00
2026-03-05 16:35:32 +08:00
// 启动网络质量检测
this.startNetworkQualityDetection();
2026-03-06 13:59:38 +08:00
// 启动本地音频活动检测
this.startLocalActivityDetection();
2026-03-05 16:35:32 +08:00
// 启动远端音频活动检测
this.startRemoteActivityDetection();
//模拟远端活动 (开发测试用)
//this.simulateRemoteActivity();
2026-03-04 17:55:55 +08:00
}
/**
* 挂断WebRTC连接
2026-04-23 15:22:24 +08:00
* Host挂断房间删除通知所有participants
* Participant挂断仅自己离开房间保留
2026-03-04 17:55:55 +08:00
* @async
* @returns {Promise<void>}
*/
async hangUp() {
2026-03-04 22:29:10 +08:00
this.clearStatsMessage(); // 清除统计信息
2026-03-05 16:35:32 +08:00
this.stopNetworkQualityDetection(); // 停止网络质量检测
2026-03-12 17:53:34 +08:00
// 停止通话时长计时器
if (this.durationInterval) {
clearInterval(this.durationInterval);
this.durationInterval = null;
}
2026-04-23 15:22:24 +08:00
const isHost = this.role === 'host';
console.log(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`);
2026-03-04 17:55:55 +08:00
// 删除连接并停止WebRTC
if (this.renderstreaming) {
try {
2026-04-23 15:22:24 +08:00
// 发送断开连接信令给服务器
// 服务器会根据角色决定:
// - host断开通知所有participants删除房间
// - participant断开仅通知host保留房间
await this.renderstreaming.deleteConnection();
await this.renderstreaming.stop();
} catch (error) {
console.error('Error during hangUp:', error);
}
this.renderstreaming = null;
}
2026-03-04 17:55:55 +08:00
2026-03-12 17:53:34 +08:00
// 更新远程用户状态为离线
this.updateRemoteUserStatus('offline');
this.updateRemoteUserNetworkQuality('no_signal');
2026-04-25 16:53:50 +08:00
// 清理participants
this.state.participants = {};
2026-04-25 17:43:53 +08:00
this.selfParticipantId = null;
2026-03-04 22:29:10 +08:00
this.connectionId = null;
2026-04-23 15:22:24 +08:00
this.role = null;
this.state.session.status = 'ended';
2026-04-23 15:22:24 +08:00
this.notify({ type: 'CALL_ENDED', reason: isHost ? 'host_hangup' : 'participant_hangup' });
2026-03-04 17:55:55 +08:00
}
2026-04-11 00:03:30 +08:00
/**
* 发送消息
* @param {string} type - 消息类型
* @param {Object} data - 消息数据
*/
sendMessage(type, data) {
if (this.renderstreaming) {
this.renderstreaming.sendMessage({
type: type,
data: data
});
}
}
2026-03-04 17:55:55 +08:00
/**
2026-04-25 17:43:53 +08:00
* 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
});
console.log('Broadcast participants list:', Object.keys(memberList));
}
/**
2026-03-04 17:55:55 +08:00
* 设置编解码器偏好
*/
2026-04-24 23:03:41 +08:00
setCodecPreferences(participantId) {
2026-03-04 17:55:55 +08:00
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];
2026-03-04 17:55:55 +08:00
}
}
2026-04-24 23:03:41 +08:00
if (selectedCodecs == null) return;
2026-03-04 17:55:55 +08:00
if (this.renderstreaming) {
2026-04-24 23:03:41 +08:00
const transceivers = this.renderstreaming.getTransceivers(participantId);
if (transceivers && transceivers.length > 0) {
2026-04-24 23:03:41 +08:00
const videoTransceivers = transceivers.filter(t => t.receiver.track.kind == "video");
if (videoTransceivers && videoTransceivers.length > 0) {
videoTransceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
}
}
2026-03-04 11:19:50 +08:00
}
}
2026-03-04 17:55:55 +08:00
2026-03-12 12:46:22 +08:00
// 更新远端媒体状态 (由 WebSocket 触发)
2026-04-25 13:29:35 +08:00
updateRemoteMedia(mediaState, participantId) {
this.state.session.remoteUser.mediaState = {
...this.state.session.remoteUser.mediaState,
...mediaState
};
2026-04-25 13:29:35 +08:00
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState, participantId });
2026-03-12 15:33:34 +08:00
// 通知UI更新用户列表
this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
}
2026-03-12 17:53:34 +08:00
// 更新远端用户状态
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 });
}
2026-04-23 15:22:24 +08:00
// 结束通话(用户主动点击挂断按钮)
async endCall() {
console.log(`endCall called. Role: ${this.role}`);
// 调用 hangUp() 正确关闭 WebRTC 连接并发送断开信令
// hangUp 内部会根据角色区分:
// - host: 通知所有participants删除房间
// - participant: 仅自己离开,房间保留
await this.hangUp();
}
2026-03-04 18:40:19 +08:00
// 加入通话
2026-03-04 22:29:10 +08:00
async joinCall(connectionId) {
2026-03-04 18:40:19 +08:00
this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
showNotification(`正在加入通话 (${connectionId})`);
// 初始化
2026-03-04 22:29:10 +08:00
await this.init();
2026-03-04 18:40:19 +08:00
// 保存连接ID
this.connectionId = connectionId;
}
// 创建通话
2026-03-04 22:29:10 +08:00
async createCall() {
2026-03-04 18:40:19 +08:00
this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
showNotification('正在创建通话...');
// 初始化
2026-03-04 22:29:10 +08:00
await this.init();
2026-03-04 18:40:19 +08:00
}
// 模拟远端活动 (开发测试用)
simulateRemoteActivity() {
setInterval(() => {
if (Math.random() > 0.7) {
const isSpeaking = Math.random() > 0.5;
this.updateRemoteMedia({ isSpeaking });
}
}, 800);
}
simulateNetworkChange() {
2026-03-05 16:35:32 +08:00
// 模拟网络质量变化
2026-03-12 17:53:34 +08:00
const qualities = ['good', 'fair', 'excellent', 'poor', 'no_signal'];
setInterval(() => {
if (Math.random() > 0.8) {
2026-03-05 16:39:19 +08:00
const quality = qualities[Math.floor(Math.random() * qualities.length)];
this.state.session.remoteUser.networkQuality = quality;
this.notify({ type: 'NETWORK_CHANGE', quality });
}
}, 5000);
}
2026-03-05 16:35:32 +08:00
// 真实网络质量检测
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';
}
2026-03-12 17:53:34 +08:00
} else {
// 没有收到任何RTP包设置为无信号状态
quality = 'no_signal';
2026-03-05 16:35:32 +08:00
}
// 更新网络质量状态
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);
}
}
2026-03-12 12:10:40 +08:00
// 真实音频活动检测 - 远端
2026-03-05 16:35:32 +08:00
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);
}
}
2026-03-06 13:59:38 +08:00
// 真实音频活动检测 - 本地
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);
}
}
2026-03-05 16:35:32 +08:00
// 启动网络质量检测
startNetworkQualityDetection() {
// 每3秒检测一次网络质量
this.networkQualityInterval = setInterval(() => {
this.detectNetworkQuality();
//this.simulateNetworkChange();
}, 3000);
}
// 停止网络质量检测
stopNetworkQualityDetection() {
if (this.networkQualityInterval) {
clearInterval(this.networkQualityInterval);
this.networkQualityInterval = null;
}
}
2026-03-12 12:46:22 +08:00
// 发送媒体状态到服务器
emitMediaStateChange() {
const payload = {
userId: this.state.session.localUser.id,
...this.state.session.localUser.mediaState
};
console.log('[WebSocket Emit] media-state-changed:', payload);
2026-03-12 15:33:34 +08:00
// 使用WebRTC发送媒体状态变化
if (this.renderstreaming) {
this.renderstreaming.sendMessage({
type: 'media-state-changed',
data: payload
});
}
}
2026-03-04 22:29:10 +08:00
// 显示统计信息
2026-03-05 16:35:32 +08:00
async showStatsMessage() {
2026-03-04 22:29:10 +08:00
console.log('Showing stats message');
2026-03-05 16:35:32 +08:00
// 立即执行一次网络质量检测
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秒更新一次统计信息
2026-03-04 22:29:10 +08:00
}
// 清除统计信息
clearStatsMessage() {
console.log('Clearing stats message');
2026-03-05 16:35:32 +08:00
// 清理统计信息定时器
if (this.statsInterval) {
clearInterval(this.statsInterval);
this.statsInterval = null;
}
2026-03-04 22:29:10 +08:00
}
// Getters
getState() { return this.state; }
getLocalUser() { return this.state.session.localUser; }
getRemoteUser() { return this.state.session.remoteUser; }
2026-03-12 14:41:00 +08:00
2026-03-04 18:40:19 +08:00
getConnectionId() { return this.connectionId; }
2026-03-12 14:41:00 +08:00
getRenderStreaming() { return this.renderstreaming; }
}
// 创建单例实例
const store = new CallStateManager();
2026-03-04 17:55:55 +08:00
// 页面卸载前清理
window.addEventListener('beforeunload', async () => {
if (!store.renderstreaming)
return;
await store.renderstreaming.stop(); // 停止WebRTC连接
}, true);
export default store;