Files
webRtc/WebApp/client/public/onebyone/store.js
2026-04-25 17:01:18 +08:00

1175 lines
49 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 } 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';
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({
video: true,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: 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) {
try {
// 只获取新的视频轨道,不干扰正在工作的音频
const newVideoStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
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.startLocalActivityDetection();
} 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 });
}
/**
* 设置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 }, // 设置最小比特率
{ googEchoCancellation: true }, // 启用回声消除
{ googEchoCancellation2: true }, // 启用高级回声消除
{ googNoiseSuppression: true }, // 启用噪声抑制
{ googNoiseSuppression2: true }, // 启用高级噪声抑制
{ googAutoGainControl: true }, // 启用自动增益控制
{ googAutoGainControl2: true }, // 启用高级自动增益控制
{ googHighpassFilter: true }, // 启用高通滤波器
{ googTypingNoiseDetection: true } // 启用打字噪声检测
]
};
this.renderstreaming = new RenderStreaming(signaling, config);
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;
console.log(`Connected as ${this.role}`);
}
// 连接建立后更新状态为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 });
};
// 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.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 });
}
// 通知UI远程流已更新
this.notify({
type: 'REMOTE_STREAM_OBTAINED',
stream: targetStream,
connectionId: trackParticipantId,
isHost: isHost
});
console.log('Notified UI about remote stream update');
// 只有当收到远程流时才更新远程用户状态为在线
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
});
// 启动通话时长计时器
this.durationInterval = setInterval(() => {
this.state.session.duration++;
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
}, 1000);
}
// 如果是音频轨道,启动远程音频活动检测
if (data.track.kind === 'audio') {
this.startRemoteActivityDetection();
}
} else if (direction == "sendonly") {
// 本地发送轨道,启动本地音频活动检测
if (data.track.kind === 'audio') {
this.startLocalActivityDetection();
}
}
};
// 初始化 RenderStreaming 实例后
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 });
}
// Participant端从消息中提取Host用户信息并更新remoteUser
if (!this.role || this.role !== 'host') {
if (data.message && data.message.senderId !== this.state.session.localUser.id) {
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);
// Host端同步更新participants中对应participant的mediaState
if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) {
this.state.participants[data.participantId].mediaState = {
...this.state.participants[data.participantId].mediaState,
...data.data
};
}
// 更新远端媒体状态兼容Participant端
this.updateRemoteMedia(data.data, data.participantId);
// 通知UI更新participants
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 });
} 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 });
}
}
}
};
// 启动WebRTC连接
await this.renderstreaming.start();
await this.renderstreaming.createConnection(connectionId);
// 启动网络质量检测
this.startNetworkQualityDetection();
// 启动本地音频活动检测
this.startLocalActivityDetection();
// 启动远端音频活动检测
this.startRemoteActivityDetection();
//模拟远端活动 (开发测试用)
//this.simulateRemoteActivity();
}
/**
* 挂断WebRTC连接
* Host挂断房间删除通知所有participants
* Participant挂断仅自己离开房间保留
* @async
* @returns {Promise<void>}
*/
async hangUp() {
this.clearStatsMessage(); // 清除统计信息
this.stopNetworkQualityDetection(); // 停止网络质量检测
// 停止通话时长计时器
if (this.durationInterval) {
clearInterval(this.durationInterval);
this.durationInterval = null;
}
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.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
});
}
}
/**
* 设置编解码器偏好
*/
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();
}
// 模拟远端活动 (开发测试用)
simulateRemoteActivity() {
setInterval(() => {
if (Math.random() > 0.7) {
const isSpeaking = Math.random() > 0.5;
this.updateRemoteMedia({ isSpeaking });
}
}, 800);
}
simulateNetworkChange() {
// 模拟网络质量变化
const qualities = ['good', 'fair', 'excellent', 'poor', 'no_signal'];
setInterval(() => {
if (Math.random() > 0.8) {
const quality = qualities[Math.floor(Math.random() * qualities.length)];
this.state.session.remoteUser.networkQuality = quality;
this.notify({ type: 'NETWORK_CHANGE', quality });
}
}, 5000);
}
// 真实网络质量检测
async detectNetworkQuality() {
if (!this.renderstreaming) {
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);
}
}
// 真实音频活动检测 - 远端
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);
// 使用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;