385 lines
13 KiB
JavaScript
385 lines
13 KiB
JavaScript
/**
|
|
* 状态管理
|
|
* 使用简单的 Observable 模式,可替换为 Redux/Vuex/Pinia
|
|
*/
|
|
import { mockCallSession, mockMessages } from './models.js';
|
|
import { Signaling, WebSocketSignaling } from "../../module/signaling.js";// 信令管理
|
|
import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连接管理
|
|
import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置
|
|
import { showNotification } from './utils.js'; // 导入通知函数
|
|
// 默认视频流尺寸
|
|
const defaultStreamWidth = 1280;
|
|
const defaultStreamHeight = 720;
|
|
|
|
|
|
|
|
class CallStateManager {
|
|
constructor() {
|
|
let renderstreaming; // WebRTC连接管理实例
|
|
let useWebSocket; // 是否使用WebSocket信令
|
|
let connectionId; // 连接ID
|
|
// 核心状态
|
|
this.state = {
|
|
session: {
|
|
...mockCallSession,
|
|
status: 'idle' // 初始状态为空闲
|
|
},
|
|
messages: [...mockMessages],
|
|
isSidebarOpen: false,
|
|
unreadCount: 0,
|
|
localStream: null, // MediaStream 对象
|
|
remoteStream: null // MediaStream 对象
|
|
};
|
|
|
|
// 监听器数组
|
|
this.listeners = [];
|
|
|
|
// 初始化
|
|
//this.init();
|
|
}
|
|
|
|
// 订阅状态变化
|
|
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));
|
|
}
|
|
|
|
// 初始化
|
|
init() {
|
|
// 启动通话时长计时器
|
|
this.durationInterval = setInterval(() => {
|
|
this.state.session.duration++;
|
|
this.notify({ type: 'DURATION_UPDATE', duration: this.state.session.duration });
|
|
}, 1000);
|
|
// 初始化配置
|
|
this.setupConfig();
|
|
// 获取本地摄像头视频流
|
|
this.getLocalStream();
|
|
|
|
|
|
// 模拟远端音频活动 (实际应由 WebRTC VAD 检测触发)
|
|
this.simulateRemoteActivity();
|
|
|
|
// 模拟网络质量变化
|
|
this.simulateNetworkChange();
|
|
}
|
|
async setupConfig() {
|
|
const res = await getServerConfig();
|
|
this.useWebSocket = res.useWebSocket;
|
|
}
|
|
// 获取本地摄像头视频流
|
|
async getLocalStream() {
|
|
try {
|
|
console.log('Requesting camera permission...');
|
|
|
|
// 检查浏览器是否支持getUserMedia
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
console.error('getUserMedia is not supported');
|
|
throw new Error('getUserMedia is not supported');
|
|
}
|
|
|
|
// 请求摄像头权限并获取媒体流
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
audio: true
|
|
});
|
|
|
|
console.log('Stream obtained successfully:', stream);
|
|
console.log('Video tracks:', stream.getVideoTracks());
|
|
console.log('Audio tracks:', stream.getAudioTracks());
|
|
|
|
this.state.localStream = stream;
|
|
this.state.session.localUser.mediaState.video = true;
|
|
this.state.session.localUser.mediaState.audio = true;
|
|
|
|
console.log('Local stream stored, notifying UI...');
|
|
|
|
// 先通知视频流已获取
|
|
this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream });
|
|
// 再通知媒体状态变化
|
|
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: true });
|
|
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: true });
|
|
|
|
// 发送媒体状态到服务器
|
|
this.emitMediaStateChange();
|
|
} catch (error) {
|
|
console.error('Error getting local stream:', error);
|
|
// 如果获取视频失败,保持视频关闭状态
|
|
this.state.session.localUser.mediaState.video = false;
|
|
this.state.session.localUser.mediaState.audio = false;
|
|
|
|
// 通知媒体状态变化
|
|
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'video', value: false });
|
|
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType: 'audio', value: false });
|
|
}
|
|
}
|
|
|
|
// 更新本地媒体状态
|
|
async updateLocalMedia(mediaType, value) {
|
|
|
|
// 如果是开启视频,重新获取摄像头资源
|
|
if (mediaType === 'video' && value) {
|
|
if (this.state.localStream) {
|
|
this.state.localStream = null;
|
|
}
|
|
//if(this.state.localStream.getVideoTracks().length==0){
|
|
// 请求摄像头权限并获取媒体流
|
|
this.state.localStream = await navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
audio: true
|
|
});
|
|
// }
|
|
await this.getLocalStream();
|
|
} else {
|
|
// 直接更新媒体状态
|
|
this.state.session.localUser.mediaState[mediaType] = value;
|
|
this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType, value });
|
|
|
|
// 发送媒体状态到服务器
|
|
this.emitMediaStateChange();
|
|
}
|
|
|
|
|
|
// 如果是关闭视频,释放摄像头资源
|
|
if (mediaType === 'video' && !value && this.state.localStream) {
|
|
this.state.localStream.getTracks().forEach(track => {
|
|
if (track.kind === 'video') {
|
|
track.stop();
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
// 如果是音频状态变化,控制本地音频轨道
|
|
if (mediaType === 'audio' && this.state.localStream) {
|
|
this.state.localStream.getTracks().forEach(track => {
|
|
if (track.kind === 'audio') {
|
|
track.enabled = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
}
|
|
/**
|
|
* 设置WebRTC连接
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async setUp(connectionId) {
|
|
//TODO
|
|
this.connectionId = connectionId; // 获取连接ID
|
|
codecPreferences.disabled = true; // 禁用编解码器选择
|
|
|
|
// 创建信令实例
|
|
const signaling = useWebSocket ? new WebSocketSignaling() : new Signaling();
|
|
const config = getRTCConfiguration(); // 获取RTC配置
|
|
this.renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例
|
|
|
|
// 连接建立回调
|
|
this.renderstreaming.onConnect = () => {
|
|
const tracks = this.state.localStream.getTracks(); // 获取本地媒体轨道
|
|
for (const track of tracks) {
|
|
this.renderstreaming.addTransceiver(track, { direction: 'sendonly' }); // 添加发送轨道
|
|
}
|
|
setCodecPreferences(); // 设置编解码器偏好
|
|
showStatsMessage(); // 显示统计信息
|
|
};
|
|
|
|
// 连接断开回调
|
|
this.renderstreaming.onDisconnect = () => {
|
|
hangUp(); // 挂断连接
|
|
};
|
|
|
|
// 轨道事件回调
|
|
this.renderstreaming.onTrackEvent = (data) => {
|
|
const direction = data.transceiver.direction;
|
|
if (direction == "sendrecv" || direction == "recvonly") {
|
|
if (this.state.remoteStream == null) {
|
|
this.state.remoteStream = new MediaStream();
|
|
}
|
|
this.state.remoteStream.addTrack(data.track);
|
|
}
|
|
};
|
|
|
|
// 启动WebRTC连接
|
|
await this.renderstreaming.start();
|
|
await this.renderstreaming.createConnection(connectionId);
|
|
|
|
}
|
|
|
|
/**
|
|
* 挂断WebRTC连接
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async hangUp() {
|
|
clearStatsMessage(); // 清除统计信息
|
|
messageDiv.style.display = 'block';
|
|
messageDiv.innerText = `Disconnect peer on ${connectionId}.`;
|
|
|
|
// 删除连接并停止WebRTC
|
|
await renderstreaming.deleteConnection();
|
|
await renderstreaming.stop();
|
|
renderstreaming = null;
|
|
remoteVideo.srcObject = null; // 清除远程视频源
|
|
|
|
connectionId = null;
|
|
|
|
// 启用编解码器选择
|
|
if (supportsSetCodecPreferences) {
|
|
codecPreferences.disabled = false;
|
|
}
|
|
}
|
|
/**
|
|
* 设置编解码器偏好
|
|
*/
|
|
setCodecPreferences() {
|
|
/** @type {RTCRtpCodecCapability[] | null} */
|
|
let selectedCodecs = null;
|
|
|
|
if (supportsSetCodecPreferences) {
|
|
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
|
|
if (preferredCodec.value !== '') {
|
|
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
|
|
const { codecs } = RTCRtpSender.getCapabilities('video');
|
|
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
|
|
const selectCodec = codecs[selectedCodecIndex];
|
|
selectedCodecs = [selectCodec];
|
|
}
|
|
}
|
|
|
|
if (selectedCodecs == null) {
|
|
return;
|
|
}
|
|
|
|
// 获取视频收发器并设置编解码器偏好
|
|
const transceivers = renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video");
|
|
if (transceivers && transceivers.length > 0) {
|
|
transceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
|
|
}
|
|
}
|
|
|
|
|
|
// 更新远端媒体状态 (由 WebSocket 消息触发)
|
|
updateRemoteMedia(mediaState) {
|
|
this.state.session.remoteUser.mediaState = {
|
|
...this.state.session.remoteUser.mediaState,
|
|
...mediaState
|
|
};
|
|
this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState });
|
|
}
|
|
|
|
// 添加消息
|
|
addMessage(message) {
|
|
this.state.messages.push(message);
|
|
|
|
// 如果侧边栏关闭且不是自己发的,增加未读
|
|
if (!this.state.isSidebarOpen && !message.isSelf) {
|
|
this.state.unreadCount++;
|
|
}
|
|
|
|
this.notify({ type: 'NEW_MESSAGE', message, unreadCount: this.state.unreadCount });
|
|
}
|
|
|
|
// 切换侧边栏
|
|
toggleSidebar() {
|
|
this.state.isSidebarOpen = !this.state.isSidebarOpen;
|
|
if (this.state.isSidebarOpen) {
|
|
this.state.unreadCount = 0;
|
|
}
|
|
this.notify({ type: 'SIDEBAR_TOGGLE', isOpen: this.state.isSidebarOpen });
|
|
}
|
|
|
|
// 结束通话
|
|
endCall() {
|
|
clearInterval(this.durationInterval);
|
|
this.state.session.status = 'ended';
|
|
this.notify({ type: 'CALL_ENDED' });
|
|
|
|
// 发送结束通话请求到服务器
|
|
// [API_CALL: POST /api/call/:callId/leave]
|
|
// [WEBSOCKET_EMIT: leave-call]
|
|
}
|
|
|
|
// 加入通话
|
|
joinCall(connectionId) {
|
|
this.state.session.status = 'connecting';
|
|
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
|
|
showNotification(`正在加入通话 (${connectionId})`);
|
|
|
|
// 初始化
|
|
this.init();
|
|
|
|
// 保存连接ID
|
|
this.connectionId = connectionId;
|
|
}
|
|
|
|
// 创建通话
|
|
createCall() {
|
|
this.state.session.status = 'connecting';
|
|
this.notify({ type: 'CALL_STATUS_CHANGE', status: 'connecting' });
|
|
showNotification('正在创建通话...');
|
|
|
|
// 初始化
|
|
this.init();
|
|
}
|
|
|
|
// 模拟远端活动 (开发测试用)
|
|
simulateRemoteActivity() {
|
|
setInterval(() => {
|
|
if (Math.random() > 0.7) {
|
|
const isSpeaking = Math.random() > 0.5;
|
|
this.updateRemoteMedia({ isSpeaking });
|
|
}
|
|
}, 800);
|
|
}
|
|
|
|
// 模拟网络质量变化 (开发测试用)
|
|
simulateNetworkChange() {
|
|
const qualities = ['excellent', 'good', 'fair', 'poor'];
|
|
setInterval(() => {
|
|
if (Math.random() > 0.8) {
|
|
const quality = qualities[Math.floor(Math.random() * qualities.length)];
|
|
this.state.session.remoteUser.networkQuality = quality;
|
|
this.notify({ type: 'NETWORK_CHANGE', quality });
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
// 发送媒体状态到服务器
|
|
emitMediaStateChange() {
|
|
const payload = {
|
|
userId: this.state.session.localUser.id,
|
|
...this.state.session.localUser.mediaState
|
|
};
|
|
console.log('[WebSocket Emit] media-state-changed:', payload);
|
|
// socket.emit('media-state-changed', payload);
|
|
}
|
|
|
|
// Getters
|
|
getState() { return this.state; }
|
|
getLocalUser() { return this.state.session.localUser; }
|
|
getRemoteUser() { return this.state.session.remoteUser; }
|
|
getMessages() { return this.state.messages; }
|
|
getConnectionId() { return this.connectionId; }
|
|
}
|
|
|
|
// 创建单例实例
|
|
const store = new CallStateManager();
|
|
|
|
// 页面卸载前清理
|
|
window.addEventListener('beforeunload', async () => {
|
|
if (!store.renderstreaming)
|
|
return;
|
|
await store.renderstreaming.stop(); // 停止WebRTC连接
|
|
}, true);
|
|
export default store;
|