Files
video_socket-server/client/public/store.js

1458 lines
54 KiB
JavaScript
Raw Normal View History

2026-04-29 15:18:30 +08:00
/**
* 状态管理
* 使用简单的 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';
2026-05-24 00:54:58 +08:00
import {
DEFAULT_PARTICIPANT_AVATAR,
DEFAULT_PARTICIPANT_NAME,
buildParticipantsSyncData,
omitParticipant,
removeParticipant,
upsertParticipant
} from './participants.js';
2026-05-24 01:01:28 +08:00
import {
AUDIO_CONFIG,
VAD_CONFIG,
VIDEO_ONLY_CONSTRAINT,
buildVideoConstraints,
getAdaptiveVideoBitrate,
getResolutionLabel,
getTargetResolutionBitrate
} from './media-config.js';
import { getNetworkQualityFromSummary, summarizeInboundStats } from './webrtc-stats.js';
2026-04-29 15:18:30 +08:00
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 = [];
2026-05-18 23:03:28 +08:00
this.socketEventHandlers = {};
this._socketInviteBound = false;
2026-04-29 15:18:30 +08:00
}
// 订阅状态变化
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 });
}
// 恢复保存的分辨率设置
if (settings.resolution) {
this._savedResolution = settings.resolution;
console.log(`已恢复分辨率设置: ${settings.resolution.width}x${settings.resolution.height}`);
}
} 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');
}
// 请求摄像头权限并获取媒体流,启用回声消除
// 使用保存的分辨率(如有),否则使用默认约束
2026-05-24 01:01:28 +08:00
const videoConstraints = buildVideoConstraints(this._savedResolution);
2026-04-29 15:18:30 +08:00
const stream = await navigator.mediaDevices.getUserMedia({
video: videoConstraints,
audio: AUDIO_CONFIG
});
console.log('Stream obtained successfully:', stream);
console.log('Video tracks:', stream.getVideoTracks());
console.log('Audio tracks:', stream.getAudioTracks());
this.state.localStream = stream;
this.state.session.localUser.mediaState.video = true;
this.state.session.localUser.mediaState.audio = true;
console.log('Local stream stored, notifying UI...');
// 先通知视频流已获取
this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream });
// 再通知媒体状态变化
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true });
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: true });
// 发送媒体状态到服务器
this.emitMediaStateChange();
// 启动本地音频活动检测
this.startActivityDetection(this.state.localStream, { isLocal: true });
} catch (error) {
console.error('Error getting local stream:', error);
// 如果获取视频失败,保持视频关闭状态
this.state.session.localUser.mediaState.video = false;
this.state.session.localUser.mediaState.audio = false;
// 通知媒体状态变化
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: false });
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false });
}
}
// 更新本地媒体状态
async updateLocalMedia(mediaType, value) {
2026-05-24 01:18:27 +08:00
await this._updateLocalMediaRefactored(mediaType, value);
return;
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
async _updateLocalMediaRefactored(mediaType, value) {
2026-04-29 15:18:30 +08:00
if (mediaType === 'video' && value) {
2026-05-24 01:18:27 +08:00
await this._enableLocalVideo();
this._notifyUserListUpdate();
return;
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
this.state.session.localUser.mediaState[mediaType] = value;
this._notifyLocalMediaChange(mediaType, value);
this.emitMediaStateChange();
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
if (mediaType === 'video' && !value) {
this._disableLocalVideoTracks();
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
if (mediaType === 'audio') {
this._setLocalAudioTrackEnabled(value);
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
this._notifyUserListUpdate();
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
async _enableLocalVideo() {
try {
const newVideoTrack = await this._requestNewVideoTrack();
this._replaceLocalVideoTrack(newVideoTrack);
await this._updateOutgoingVideoTrack(newVideoTrack);
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
this.state.session.localUser.mediaState.video = true;
this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream: this.state.localStream });
this._notifyLocalMediaChange('video', true);
2026-04-29 15:18:30 +08:00
this.emitMediaStateChange();
2026-05-24 01:18:27 +08:00
this.startActivityDetection(this.state.localStream, { isLocal: true });
} catch (error) {
console.error('Error reopening video:', error);
this.state.session.localUser.mediaState.video = false;
this._notifyLocalMediaChange('video', false);
2026-04-29 15:18:30 +08:00
}
2026-05-24 01:18:27 +08:00
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
async _requestNewVideoTrack() {
const newVideoStream = await navigator.mediaDevices.getUserMedia(VIDEO_ONLY_CONSTRAINT);
const newVideoTrack = newVideoStream.getVideoTracks()[0];
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
if (!newVideoTrack) {
throw new Error('Failed to get video track');
}
return newVideoTrack;
}
_replaceLocalVideoTrack(newVideoTrack) {
if (this.state.localStream) {
const oldVideoTracks = this.state.localStream.getVideoTracks();
oldVideoTracks.forEach(track => {
track.stop();
this.state.localStream.removeTrack(track);
2026-04-29 15:18:30 +08:00
});
2026-05-24 01:18:27 +08:00
this.state.localStream.addTrack(newVideoTrack);
return;
}
this.state.localStream = new MediaStream([newVideoTrack]);
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
async _updateOutgoingVideoTrack(newVideoTrack) {
if (!this.renderstreaming) {
return;
2026-04-29 15:18:30 +08:00
}
2026-05-24 01:18:27 +08:00
console.log('Updating video track in WebRTC connection');
if (this.role === 'host') {
const participantIds = Object.keys(this.state.remoteStreams);
for (const participantId of participantIds) {
await this._updateVideoTrackForPeer(newVideoTrack, participantId);
}
return;
2026-04-29 15:18:30 +08:00
}
2026-05-24 01:18:27 +08:00
await this._updateVideoTrackForPeer(newVideoTrack);
}
async _updateVideoTrackForPeer(newVideoTrack, participantId = undefined) {
const transceivers = this.renderstreaming.getTransceivers(participantId);
if (!transceivers) {
return;
}
const videoTransceivers = transceivers.filter(transceiver =>
transceiver.sender && transceiver.sender.track && transceiver.sender.track.kind === 'video'
);
if (videoTransceivers.length > 0) {
await this._replaceVideoTrackOnTransceivers(videoTransceivers, newVideoTrack, participantId);
} else {
this._addVideoTransceiver(newVideoTrack, participantId);
}
this._scheduleVideoSenderUpdate(participantId);
}
async _replaceVideoTrackOnTransceivers(videoTransceivers, newVideoTrack, participantId) {
for (const transceiver of videoTransceivers) {
try {
await transceiver.sender.replaceTrack(newVideoTrack);
console.log(participantId
? `Replaced video track for participant ${participantId}`
: 'Successfully replaced video track');
} catch (error) {
console.error(
participantId
? `Error replacing video track for ${participantId}:`
: 'Error replacing video track:',
error
);
}
}
}
_addVideoTransceiver(newVideoTrack, participantId) {
try {
if (participantId) {
this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }, participantId);
console.log(`Added new video transceiver for participant ${participantId}`);
return;
}
this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' });
console.log('Added new video transceiver');
} catch (error) {
console.error(
participantId
? `Error adding video transceiver for ${participantId}:`
: 'Error adding video transceiver:',
error
);
}
}
_scheduleVideoSenderUpdate(participantId) {
setTimeout(() => { this.setCodecPreferences(participantId); }, 100);
setTimeout(() => { this.setVideoEncodingParameters(participantId); }, 200);
}
_disableLocalVideoTracks() {
if (!this.state.localStream) {
return;
}
this.state.session.localUser.mediaState.video = false;
this.state.localStream.getTracks().forEach(track => {
if (track.kind === 'video') {
track.stop();
}
});
}
_setLocalAudioTrackEnabled(value) {
if (!this.state.localStream) {
return;
}
this.state.session.localUser.mediaState.audio = value;
this.state.localStream.getTracks().forEach(track => {
if (track.kind === 'audio') {
track.enabled = value;
}
});
}
_notifyLocalMediaChange(mediaType, value) {
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType, value });
}
2026-04-29 15:18:30 +08:00
2026-05-24 01:18:27 +08:00
_notifyUserListUpdate() {
this.notify({
type: 'USER_LIST_UPDATE',
localUser: this.state.session.localUser,
remoteUser: this.state.session.remoteUser
});
2026-04-29 15:18:30 +08:00
}
2026-05-16 21:26:19 +08:00
/**
* 早期连接WebSocket信令在connect视图调用
* 仅建立WebSocket连接不创建/加入房间
* @async
* @returns {Promise<WebSocketSignaling|Signaling>} 信令实例
*/
async connectSignaling() {
// 先获取配置
await this.setupConfig();
if (this._signaling) {
console.log('Signaling already connected, reusing existing instance');
return this._signaling;
}
// 创建信令实例
this._signaling = this.useWebSocket ? new WebSocketSignaling() : new Signaling();
await this._signaling.start();
2026-05-18 23:03:28 +08:00
this._bindSocketInviteEvents(this._signaling);
2026-05-16 21:26:19 +08:00
console.log('Signaling connected (WebSocket only, no room yet)');
return this._signaling;
}
2026-05-18 23:03:28 +08:00
_bindSocketInviteEvents(signaling) {
if (!signaling || this._socketInviteBound || typeof signaling.addEventListener !== 'function') {
return;
}
['invite-call', 'invite-accepted', 'invite-rejected', 'invite-failed'].forEach((eventName) => {
signaling.addEventListener(eventName, (event) => {
const handler = this.socketEventHandlers[eventName];
if (typeof handler === 'function') {
handler(event.detail);
}
});
});
this._socketInviteBound = true;
}
onSocketEvent(eventName, handler) {
this.socketEventHandlers[eventName] = handler;
}
getActiveSignaling() {
if (this._signaling) {
return this._signaling;
}
if (this.renderstreaming && this.renderstreaming._signaling) {
return this.renderstreaming._signaling;
}
return null;
}
sendInviteCall(payload) {
const signaling = this.getActiveSignaling();
if (!signaling || typeof signaling.sendInviteCall !== 'function') {
throw new Error('Invite signaling is not ready');
}
signaling.sendInviteCall(payload);
}
sendInviteAccepted(payload) {
const signaling = this.getActiveSignaling();
if (!signaling || typeof signaling.sendInviteAccepted !== 'function') {
throw new Error('Invite signaling is not ready');
}
signaling.sendInviteAccepted(payload);
}
sendInviteRejected(payload) {
const signaling = this.getActiveSignaling();
if (!signaling || typeof signaling.sendInviteRejected !== 'function') {
throw new Error('Invite signaling is not ready');
}
signaling.sendInviteRejected(payload);
}
2026-05-16 23:07:08 +08:00
/**
* 在仅建立WebSocket连接时同步当前用户信息
* @param {{ id?: string, name?: string, avatar?: string } | null} userInfo - 用户信息
*/
syncSocketUserInfo(userInfo = null) {
const settings = userInfo || (() => {
try {
return JSON.parse(localStorage.getItem('userSettings') || '{}');
} catch (error) {
console.error('Error parsing user settings:', error);
return {};
}
})();
const payload = {
id: settings.id || settings.userId || this.state.session.localUser.id || '',
name: settings.name || this.state.session.localUser.name || '我',
avatar: settings.avatar || this.state.session.localUser.avatar || '/images/p1.png'
};
this.state.session.localUser = {
...this.state.session.localUser,
id: payload.id,
name: payload.name,
avatar: payload.avatar
};
if (this._signaling && typeof this._signaling.sendMessage === 'function') {
this._signaling.sendMessage('', {
type: 'user-info',
data: payload
});
}
}
2026-04-29 15:18:30 +08:00
/**
* 创建信令和RTC实例
* @async
* @param {string} connectionId - 连接ID
* @returns {Promise<void>}
*/
async _createSignalingAndRTC(connectionId) {
this.connectionId = connectionId; // 获取连接ID
// 设置状态为连接中
this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
// 确保本地流已经初始化
if (!this.state.localStream) {
console.log('Local stream not available, waiting for initialization...');
// 等待localStream初始化
await new Promise((resolve) => {
const checkStream = () => {
if (this.state.localStream) {
resolve();
} else {
setTimeout(checkStream, 100);
}
};
checkStream();
});
}
2026-05-16 21:26:19 +08:00
// 复用已有信令实例connectSignaling()已建立WebSocket或创建新实例
const signaling = this._signaling || (this.useWebSocket ? new WebSocketSignaling() : new Signaling());
2026-04-29 15:18:30 +08:00
const config = getRTCConfiguration(); // 获取RTC配置
this.renderstreaming = new RenderStreaming(signaling, config);
2026-05-16 21:26:19 +08:00
this._signaling = null; // RenderStreaming 已接管信令,清除引用
2026-04-29 15:18:30 +08:00
}
/**
* 设置WebRTC连接
* @async
* @returns {Promise<void>}
*/
async setUp(connectionId) {
await this._createSignalingAndRTC(connectionId);
this._registerCallbacks();
await this._startConnection(connectionId);
}
/**
* 注册所有WebRTC回调
*/
_registerCallbacks() {
this.renderstreaming.onNewPeer = (participantId) => {
console.log(`New peer created for ${participantId}, adding local tracks`);
if (this.state.localStream) {
const tracks = this.state.localStream.getTracks();
for (const track of tracks) {
this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }, participantId);
}
this.setCodecPreferences(participantId);
this.setVideoEncodingParameters(participantId);
}
};
// 连接建立回调
this.renderstreaming.onConnect = (connectionId, data) => {
// 保存角色信息host/participant
if (data && data.role) {
this.role = data.role;
// 更新localUser的isHost标志
this.state.session.localUser.isHost = (this.role === 'host');
// 保存自身的participantId用于从participants-sync中过滤自身
if (data.participantId) {
this.selfParticipantId = data.participantId;
}
console.log(`Connected as ${this.role}, participantId: ${this.selfParticipantId}`);
}
// 连接建立后更新状态为ongoing
this.state.session.status = 'ongoing';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'ongoing' });
// 参与者加入时默认静音:禁用音频轨道并更新状态
if (this.role === 'participant') {
if (this.state.localStream) {
this.state.localStream.getAudioTracks().forEach(track => {
track.enabled = false;
});
}
this.state.session.localUser.mediaState.audio = false;
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false });
console.log('Participant joined with audio muted by default');
}
// 连接建立后发送本地用户信息
this.sendMessage('user-info', {
id: this.state.session.localUser.id,
name: this.state.session.localUser.name,
avatar: this.state.session.localUser.avatar
});
// 发送当前媒体状态,确保远端收到正确的初始状态
this.emitMediaStateChange();
if (this.state.localStream) {
// const tracks = this.state.localStream.getTracks();
// for (const track of tracks) {
// this.renderstreaming.addTransceiver(track, { direction: 'sendonly' });
// }
// this.setCodecPreferences();
this.showStatsMessage();
} else {
console.error('Local stream is not available');
showNotification('本地视频流不可用', 'error');
}
};
// 连接断开回调(收到服务器的 disconnect 消息,通常是 host 离开导致房间关闭)
this.renderstreaming.onDisconnect = () => {
console.log('Received disconnect from server, host left or room closed');
this.hangUp(); // 房间已关闭,挂断连接
};
// SDP Answer 接收回调:重新设置编码参数以保障画质
this.renderstreaming.onGotAnswer = (connectionId) => {
console.log('SDP Answer received, resetting encoding parameters for connectionId:', connectionId);
if (this.role === 'host') {
const allParticipantIds = Object.keys(this.state.remoteStreams || {});
for (const pid of allParticipantIds) {
setTimeout(() => { this.setVideoEncodingParameters(pid); }, 50);
}
} else {
setTimeout(() => { this.setVideoEncodingParameters(); }, 50);
}
};
// participant加入回调host收到新participant加入房间
this.renderstreaming.onParticipantJoined = (participantId) => {
console.log(`Participant joined: ${participantId}`);
2026-05-24 00:54:58 +08:00
this._upsertParticipant(participantId);
this._notifyParticipantsUpdate();
2026-04-29 15:18:30 +08:00
this.broadcastParticipantsList();
};
// participant离开回调host收到房间仍然存在
this.renderstreaming.onParticipantLeft = (participantId) => {
console.log(`Participant left: ${participantId}, room still active`);
this.updateRemoteUserStatus('offline');
this.updateRemoteUserNetworkQuality('no_signal');
showNotification('对方已离开通话', 'warning');
// 清理该 participant 的远端流
if (this.state.remoteStreams[participantId]) {
this.state.remoteStreams[participantId].getTracks().forEach(track => track.stop());
delete this.state.remoteStreams[participantId];
}
if (this.state.remoteStream) {
this.state.remoteStream.getTracks().forEach(track => track.stop());
this.state.remoteStream = null;
}
// 清理该 participant 的用户信息
2026-05-24 00:54:58 +08:00
this._removeParticipant(participantId);
2026-04-29 15:18:30 +08:00
// 通知UI更新用participantId作为connectionId传给renderer
this.notify({ type: 'PARTICIPANT_LEFT', connectionId: participantId });
2026-05-24 00:54:58 +08:00
this._notifyParticipantsUpdate();
2026-04-29 15:18:30 +08:00
this.broadcastParticipantsList();
};
// 轨道事件回调
this.renderstreaming.onTrackEvent = (data) => {
2026-05-24 00:54:58 +08:00
this._handleTrackEvent(data);
2026-04-29 15:18:30 +08:00
};
this.renderstreaming.onMessage = (data) => {
2026-05-24 00:54:58 +08:00
this._handleRenderStreamingMessage(data);
2026-04-29 15:18:30 +08:00
};
}
/**
* 启动WebRTC连接和检测
* @async
* @param {string} connectionId - 连接ID
* @returns {Promise<void>}
*/
async _startConnection(connectionId) {
// 启动WebRTC连接
await this.renderstreaming.start();
await this.renderstreaming.createConnection(connectionId);
// 启动网络质量检测
this.startNetworkQualityDetection();
// 启动本地音频活动检测
this.startActivityDetection(this.state.localStream, { isLocal: true });
//启动远端音频活动检测
this.startActivityDetection(this.state.remoteStream, { isLocal: false });
}
/**
* 挂断WebRTC连接
* Host挂断房间删除通知所有participants
* Participant挂断仅自己离开房间保留
* @async
* @returns {Promise<void>}
*/
async hangUp() {
this.clearStatsMessage(); // 清除统计信息
this.stopNetworkQualityDetection(); // 停止网络质量检测
// 停止通话时长计时器
if (this.durationInterval) {
clearInterval(this.durationInterval);
this.durationInterval = null;
}
// 重置通话时长同步标志
this.durationSynced = false;
const isHost = this.role === 'host';
console.log(`Disconnect peer on ${this.connectionId}. Role: ${this.role}`);
// 删除连接并停止WebRTC
if (this.renderstreaming) {
try {
// 发送断开连接信令给服务器
// 服务器会根据角色决定:
// - host断开通知所有participants删除房间
// - participant断开仅通知host保留房间
await this.renderstreaming.deleteConnection();
await this.renderstreaming.stop();
} catch (error) {
console.error('Error during hangUp:', error);
}
this.renderstreaming = null;
}
// 更新远程用户状态为离线
this.updateRemoteUserStatus('offline');
this.updateRemoteUserNetworkQuality('no_signal');
// 清理participants
this.state.participants = {};
this.selfParticipantId = null;
this.connectionId = null;
this.role = null;
this.state.session.status = 'ended';
this.notify({ type: 'CALL_ENDED', reason: isHost ? 'host_hangup' : 'participant_hangup' });
}
2026-05-24 00:54:58 +08:00
_handleTrackEvent(data) {
const direction = data.transceiver.direction;
if (direction === 'sendrecv' || direction === 'recvonly') {
this._handleIncomingTrack(data);
return;
}
if (direction === 'sendonly' && data.track.kind === 'audio') {
this.startActivityDetection(this.state.localStream, { isLocal: true });
}
}
_handleIncomingTrack(data) {
const trackParticipantId = data.participantId || this.connectionId;
const isHost = this.role === 'host';
const targetStream = this._getOrCreateRemoteStream(trackParticipantId, isHost);
this._replaceTrackOfSameKind(targetStream, data.track);
console.log('Added new track:', data.track.kind, 'for participant:', trackParticipantId);
if (isHost && !this.state.participants[trackParticipantId]) {
this._upsertParticipant(trackParticipantId);
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
}
this._notifyRemoteStreamUpdate(targetStream, trackParticipantId, isHost, data.track.kind);
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._startDurationTimer();
}
if (data.track.kind === 'audio') {
this.startActivityDetection(this.state.remoteStream, { isLocal: false });
}
}
_getOrCreateRemoteStream(trackParticipantId, isHost) {
if (isHost) {
if (!this.state.remoteStreams[trackParticipantId]) {
this.state.remoteStreams[trackParticipantId] = new MediaStream();
}
return this.state.remoteStreams[trackParticipantId];
}
if (this.state.remoteStream == null) {
this.state.remoteStream = new MediaStream();
}
return this.state.remoteStream;
}
_replaceTrackOfSameKind(targetStream, track) {
const existingTracks = targetStream.getTracks().filter(existingTrack => existingTrack.kind === track.kind);
existingTracks.forEach(existingTrack => {
targetStream.removeTrack(existingTrack);
console.log('Removed old track:', existingTrack.kind);
});
targetStream.addTrack(track);
}
_notifyRemoteStreamUpdate(targetStream, trackParticipantId, isHost, trackKind) {
const notifyStreamUpdate = () => {
this.notify({
type: 'REMOTE_STREAM_OBTAINED',
stream: targetStream,
connectionId: trackParticipantId,
isHost
});
console.log('Notified UI about remote stream update');
};
if (trackKind === 'audio' && targetStream.getVideoTracks().length === 0) {
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);
return;
}
notifyStreamUpdate();
}
_handleRenderStreamingMessage(data) {
console.log('收到消息:', data);
switch (data.type) {
case 'chat-message':
this._handleChatMessage(data);
break;
case 'media-state-changed':
this._handleMediaStateChangedMessage(data);
break;
case 'user-info':
this._handleUserInfoMessage(data);
break;
case 'participants-sync':
this._handleParticipantsSyncMessage(data);
break;
default:
break;
}
}
_handleChatMessage(data) {
const chatPayload = data.data || data.message;
if (!chatPayload) {
return;
}
chatMessage.handleChatMessage(chatPayload);
if (data.participantId && this.role === 'host' && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
id: chatPayload.senderId,
...(chatPayload.senderName ? { name: chatPayload.senderName } : {}),
...(chatPayload.senderAvatar ? { avatar: chatPayload.senderAvatar } : {})
});
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
return;
}
if (!this.role || this.role !== 'host') {
if (data.participantId && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
...(chatPayload.senderName ? { name: chatPayload.senderName } : {}),
...(chatPayload.senderAvatar ? { avatar: chatPayload.senderAvatar } : {})
});
this._notifyParticipantsUpdate();
} else if (chatPayload.senderId !== this.state.session.localUser.id) {
this._updateRemoteUserProfile({
id: chatPayload.senderId,
name: chatPayload.senderName,
avatar: chatPayload.senderAvatar
});
}
}
}
_handleMediaStateChangedMessage(data) {
console.log('收到媒体状态变化:', data.data, 'from participant:', data.participantId);
if (this.role === 'host') {
if (data.participantId && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
mediaState: data.data
});
}
this.updateRemoteMedia(data.data, data.participantId);
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
return;
}
if (data.participantId && data.participantId !== this.selfParticipantId && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, {
mediaState: data.data
});
this._notifyParticipantsUpdate();
return;
}
if (data.participantId === this.selfParticipantId) {
return;
}
console.log('Received media-state-changed from Host, updating remoteUser:', data.data);
this.updateRemoteMedia(data.data, data.participantId);
this._notifyParticipantsUpdate();
}
_handleUserInfoMessage(data) {
console.log('收到用户信息:', data.data, 'from participant:', data.participantId);
if (!data.data) {
return;
}
if (data.participantId && this.role === 'host') {
this._upsertParticipant(data.participantId, {
id: data.data.id || '',
name: data.data.name || DEFAULT_PARTICIPANT_NAME,
avatar: data.data.avatar || DEFAULT_PARTICIPANT_AVATAR
});
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
return;
}
this._updateRemoteUserProfile({
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
});
}
_handleParticipantsSyncMessage(data) {
if (this.role === 'host' || !data.data) {
return;
}
console.log('收到成员列表同步:', data.data);
this.state.participants = omitParticipant(data.data, this.selfParticipantId);
this._notifyParticipantsUpdate();
this._syncCallDuration(data.callDuration);
}
_updateRemoteUserProfile(profile) {
this.state.session.remoteUser = {
...this.state.session.remoteUser,
...profile
};
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState: this.state.session.remoteUser.mediaState });
}
_syncCallDuration(callDuration) {
if (this.durationSynced || typeof callDuration !== 'number') {
return;
}
this.state.session.duration = callDuration;
this.durationSynced = true;
this._startDurationTimer();
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
console.log(`通话时长已同步,当前时长: ${callDuration}`);
}
_startDurationTimer() {
if (this.durationInterval) {
return;
}
this.durationInterval = setInterval(() => {
this.state.session.duration++;
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
}, 1000);
}
2026-04-29 15:18:30 +08:00
/**
* 发送消息
* @param {string} type - 消息类型
* @param {Object} data - 消息数据
*/
sendMessage(type, data) {
if (this.renderstreaming) {
this.renderstreaming.sendMessage({
type: type,
data: data
});
}
}
2026-05-24 00:54:58 +08:00
/**
* Participant state helpers
*/
_notifyParticipantsUpdate() {
this.notify({ type: 'PARTICIPANTS_UPDATE', participants: this.state.participants });
}
_upsertParticipant(participantId, patch = {}) {
return upsertParticipant(this.state.participants, participantId, patch);
}
_removeParticipant(participantId) {
return removeParticipant(this.state.participants, participantId);
}
2026-04-29 15:18:30 +08:00
/**
* Host端广播完整成员列表给所有Participant
* 包含Host自身信息 + 所有Participant信息
* Participant收到后可展示完整通话成员列表
*/
broadcastParticipantsList() {
if (this.role !== 'host' || !this.renderstreaming) return;
2026-05-24 00:54:58 +08:00
const memberList = buildParticipantsSyncData(this.state.session.localUser, this.state.participants);
2026-04-29 15:18:30 +08:00
this.renderstreaming.sendMessage({
type: 'participants-sync',
data: memberList,
callDuration: this.state.session.duration
});
console.log('Broadcast participants list:', Object.keys(memberList));
}
/**
* 设置编解码器偏好
* 优先选择 VP9/AV1更高效的压缩回退到 H264 High Profile
*/
setCodecPreferences(participantId) {
const capabilities = RTCRtpSender.getCapabilities('video');
if (!capabilities || !capabilities.codecs || capabilities.codecs.length === 0) return;
const { codecs } = capabilities;
// 构建多codec优先级列表而非只选一个
let selectedCodecs = [];
const av1Codec = codecs.find(c => c.mimeType === 'video/AV1');
const vp9Codec = codecs.find(c => c.mimeType === 'video/VP9');
const h264HighCodec = codecs.find(c =>
c.mimeType === 'video/H264' &&
c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=6400')
);
const h264Codec = codecs.find(c => c.mimeType === 'video/H264');
if (av1Codec) selectedCodecs.push(av1Codec);
if (vp9Codec) selectedCodecs.push(vp9Codec);
if (h264HighCodec) selectedCodecs.push(h264HighCodec);
if (h264Codec && (!h264HighCodec || h264Codec !== h264HighCodec)) selectedCodecs.push(h264Codec);
if (selectedCodecs.length === 0) return;
if (this.renderstreaming) {
const transceivers = this.renderstreaming.getTransceivers(participantId);
if (transceivers && transceivers.length > 0) {
const videoTransceivers = transceivers.filter(t => {
if (t.sender && t.sender.track) {
return t.sender.track.kind === 'video';
}
return t.mid !== null && t.receiver && t.receiver.track && t.receiver.track.kind === 'video';
});
if (videoTransceivers && videoTransceivers.length > 0) {
videoTransceivers.forEach(t => {
try {
t.setCodecPreferences(selectedCodecs);
} catch(e) {
console.error('Error setting codec preferences:', e);
}
});
console.log(`Codec preferences set: ${selectedCodecs.map(c => c.mimeType).join(' > ')}`);
}
}
}
}
/**
* 设置视频发送编码参数
* 提升 maxBitrate 以改善远端视频画质
* @param {string} [participantId] - 目标participanthost端使用
*/
setVideoEncodingParameters(participantId) {
if (!this.renderstreaming) return;
const transceivers = this.renderstreaming.getTransceivers(participantId);
if (!transceivers || transceivers.length === 0) return;
const videoTransceivers = transceivers.filter(t =>
t.sender && t.sender.track && t.sender.track.kind === 'video'
);
for (const transceiver of videoTransceivers) {
try {
const sender = transceiver.sender;
const params = sender.getParameters();
if (!params.encodings || params.encodings.length === 0) {
params.encodings = [{}];
}
// 根据实际采集分辨率动态设置maxBitrate
const videoTrack = sender.track;
const settings = videoTrack ? videoTrack.getSettings() : {};
const height = settings.height || 1080;
2026-05-24 01:01:28 +08:00
const maxBitrate = getAdaptiveVideoBitrate(height);
2026-04-29 15:18:30 +08:00
params.encodings[0].maxBitrate = maxBitrate;
params.encodings[0].scaleResolutionDownBy = 1.0;
params.encodings[0].xGoogleMinBitrate = Math.floor(maxBitrate * 0.5);
// 优先保持分辨率,降低帧率来适应带宽
// 'maintain-resolution' 在带宽不足时保持清晰度
if (params.degradationPreference !== undefined) {
params.degradationPreference = 'maintain-resolution';
}
sender.setParameters(params);
console.log(`Set video encoding: maxBitrate=${maxBitrate / 1000000}Mbps, scaleResolutionDownBy=1.0, xGoogleMinBitrate=${Math.floor(maxBitrate * 0.5)}${participantId ? ` for ${participantId}` : ''}`);
} catch (error) {
console.error('Error setting video encoding parameters:', error);
}
}
}
/**
* 动态切换视频分辨率
* 使用 MediaStreamTrack.applyConstraints() 在通话中实时调整
* 同时根据分辨率调整编码比特率
* @param {number} width - 目标宽度
* @param {number} height - 目标高度
*/
async changeResolution(width, height) {
if (!this.state.localStream) {
showNotification('本地视频流不可用', 'error');
return;
}
const videoTracks = this.state.localStream.getVideoTracks();
if (videoTracks.length === 0) {
showNotification('视频轨道不可用', 'error');
return;
}
const track = videoTracks[0];
2026-05-24 01:01:28 +08:00
const label = getResolutionLabel(height);
2026-04-29 15:18:30 +08:00
try {
// 使用 applyConstraints 在不重新获取流的情况下调整分辨率
await track.applyConstraints({
width: { ideal: width, max: width },
height: { ideal: height, max: height },
frameRate: { ideal: 30, max: 30 }
});
console.log(`分辨率已切换为 ${width}x${height}`);
// 根据分辨率调整编码比特率
// 480p: ~1Mbps, 720p: ~2.5Mbps, 1080p: ~4Mbps, 2K: ~6Mbps
2026-05-24 01:01:28 +08:00
const maxBitrate = getTargetResolutionBitrate(height);
2026-04-29 15:18:30 +08:00
this._applyMaxBitrate(maxBitrate);
// 保存当前分辨率设置到本地存储
const userSettings = JSON.parse(localStorage.getItem('userSettings') || '{}');
userSettings.resolution = { width, height };
localStorage.setItem('userSettings', JSON.stringify(userSettings));
// 通知 UI 更新
this.notify({ type: 'RESOLUTION_CHANGED', resolution: { width, height, label } });
showNotification(`已切换为 ${label}`, 'success');
} catch (error) {
console.error('切换分辨率失败:', error);
showNotification('切换分辨率失败,摄像头不支持该分辨率', 'error');
}
}
/**
* 对所有视频 sender 应用 maxBitrate
* @param {number} maxBitrate - 最大比特率bps
*/
_applyMaxBitrate(maxBitrate) {
if (!this.renderstreaming) return;
const isHost = this.role === 'host';
const participantIds = isHost ? Object.keys(this.state.participants) : [null];
for (const pid of participantIds) {
const transceivers = this.renderstreaming.getTransceivers(pid);
if (!transceivers) continue;
const videoTransceivers = transceivers.filter(t =>
t.sender && t.sender.track && t.sender.track.kind === 'video'
);
for (const transceiver of videoTransceivers) {
try {
const sender = transceiver.sender;
const params = sender.getParameters();
if (!params.encodings || params.encodings.length === 0) {
params.encodings = [{}];
}
params.encodings[0].maxBitrate = maxBitrate;
sender.setParameters(params);
console.log(`Updated maxBitrate to ${maxBitrate} for ${pid || 'self'}`);
} catch (error) {
console.error('Error updating maxBitrate:', error);
}
}
}
}
// 更新远端媒体状态 (由 WebSocket 触发)
updateRemoteMedia(mediaState, participantId) {
this.state.session.remoteUser.mediaState = {
...this.state.session.remoteUser.mediaState,
...mediaState
};
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState, participantId });
// 通知UI更新用户列表
this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
}
// 更新远端用户状态
updateRemoteUserStatus(status) {
this.state.session.remoteUser.status = status;
this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
}
updateRemoteUserNetworkQuality(networkQuality) {
this.state.session.remoteUser.networkQuality = networkQuality;
this.notify({ type: 'REMOTE_MEDIA_CHANGE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser });
}
// 结束通话(用户主动点击挂断按钮)
async endCall() {
console.log(`endCall called. Role: ${this.role}`);
// 调用 hangUp() 正确关闭 WebRTC 连接并发送断开信令
// hangUp 内部会根据角色区分:
// - host: 通知所有participants删除房间
// - participant: 仅自己离开,房间保留
await this.hangUp();
}
// 加入通话
async joinCall(connectionId) {
this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
showNotification(`正在加入通话 (${connectionId})`);
// 初始化
await this.init();
// 保存连接ID
this.connectionId = connectionId;
}
// 创建通话
async createCall() {
this.state.session.status = 'connecting';
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
showNotification('正在创建通话...');
// 初始化
await this.init();
}
// 真实网络质量检测
async detectNetworkQuality() {
if (!this.renderstreaming) {
return;
}
try {
const stats = await this.renderstreaming.getStats();
if (!stats) {
return;
}
2026-05-24 01:01:28 +08:00
const summary = summarizeInboundStats(stats);
const quality = getNetworkQualityFromSummary(summary);
2026-04-29 15:18:30 +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);
}
}
// 音频活动检测
startActivityDetection(stream, { isLocal = false } = {}) {
if (!stream) {
return;
}
const audioTracks = stream.getAudioTracks();
if (audioTracks.length === 0) {
return;
}
try {
const { threshold, debounceTime, fftSize } = VAD_CONFIG;
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();
analyser.fftSize = fftSize;
source.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
let isSpeaking = false;
let lastActivityTime = 0;
const detectActivity = () => {
if (!stream || !this.renderstreaming) {
return;
}
analyser.getByteTimeDomainData(dataArray);
let sum = 0;
for (let i = 0; i < dataArray.length; i++) {
const amplitude = dataArray[i] - 128;
sum += amplitude * amplitude;
}
const rms = Math.sqrt(sum / dataArray.length);
const level = rms / 128;
const currentTime = Date.now();
if (level > threshold / 100) {
lastActivityTime = currentTime;
if (!isSpeaking) {
isSpeaking = true;
if (isLocal) {
this.state.session.localUser.mediaState.isSpeaking = true;
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: true });
this.emitMediaStateChange();
} else {
this.updateRemoteMedia({ isSpeaking: true });
}
}
} else if (isSpeaking && currentTime - lastActivityTime > debounceTime) {
isSpeaking = false;
if (isLocal) {
this.state.session.localUser.mediaState.isSpeaking = false;
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'isSpeaking', value: false });
this.emitMediaStateChange();
} else {
this.updateRemoteMedia({ isSpeaking: false });
}
}
if (this.state.session.status === 'ongoing') {
requestAnimationFrame(detectActivity);
}
};
detectActivity();
console.log(`${isLocal ? 'Local' : 'Remote'} activity detection started`);
} catch (error) {
console.error(`Error starting ${isLocal ? 'local' : 'remote'} activity detection:`, error);
}
}
// 启动网络质量检测
startNetworkQualityDetection() {
// 每3秒检测一次网络质量
this.networkQualityInterval = setInterval(() => {
this.detectNetworkQuality();
}, 3000);
}
// 停止网络质量检测
stopNetworkQualityDetection() {
if (this.networkQualityInterval) {
clearInterval(this.networkQualityInterval);
this.networkQualityInterval = null;
}
}
// 发送媒体状态到服务器
emitMediaStateChange() {
const payload = {
userId: this.state.session.localUser.id,
...this.state.session.localUser.mediaState
};
console.log('[WebSocket Emit] media-state-changed:', payload);
// 使用WebRTC发送媒体状态变化
if (this.renderstreaming) {
this.renderstreaming.sendMessage({
type: 'media-state-changed',
data: payload
});
}
}
// 显示统计信息
async showStatsMessage() {
console.log('Showing stats message');
// 立即执行一次网络质量检测
await this.detectNetworkQuality();
// 定期显示详细统计信息
this.statsInterval = setInterval(async () => {
if (!this.renderstreaming) {
return;
}
try {
const stats = await this.renderstreaming.getStats();
if (!stats) {
return;
}
2026-05-24 01:01:28 +08:00
const statsSummary = summarizeInboundStats(stats);
2026-04-29 15:18:30 +08:00
// 输出详细统计信息
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;