diff --git a/client/public/connectview.js b/client/public/connectview.js index e767445..d5d62e8 100644 --- a/client/public/connectview.js +++ b/client/public/connectview.js @@ -161,6 +161,16 @@ function escapeHtml(value) { .replace(/'/g, '''); } +function getCurrentUserId() { + try { + const settings = JSON.parse(localStorage.getItem('userSettings') || '{}'); + return settings.userId || settings.id || ''; + } catch (error) { + console.error('Error parsing current user settings:', error); + return ''; + } +} + /** * 显示全部在线WebSocket用户 * @param {Array} users - 在线用户列表 @@ -212,6 +222,7 @@ function displayOnlineUsers(users) { const userName = user.name || user.userId || '匿名用户'; const avatar = user.avatar || '/images/p2.png'; const roleLabel = user.role === 'host' ? '房主' : (user.role === 'participant' ? '成员' : '大厅'); + const isSelf = Boolean(user.userId) && user.userId === getCurrentUserId(); const userItem = document.createElement('div'); userItem.className = 'flex items-center justify-between rounded-lg bg-black/20 px-3 py-2'; userItem.innerHTML = ` @@ -222,7 +233,10 @@ function displayOnlineUsers(users) {
${escapeHtml(user.userId || user.socketId || user.participantId || '未设置ID')}
- ${roleLabel} +
+ ${roleLabel} + ${isSelf ? '自己' : ''} +
`; roomList.appendChild(userItem); }); diff --git a/client/public/main.js b/client/public/main.js index 7d9eccd..74ad695 100644 --- a/client/public/main.js +++ b/client/public/main.js @@ -16,6 +16,8 @@ import { let connectionId = ""; // 当前视图状态:'connect' 或 'call'(可用于未来扩展) let currentView = 'connect'; +let pendingIncomingInvite = null; +let inviteHandlersBound = false; function getInvitePayloadFromUrl() { const params = new URLSearchParams(window.location.search); @@ -39,6 +41,7 @@ function showCallRequestDialog(caller = {}) { const callerName = caller.name || '邀请方'; const callerAvatar = caller.avatar || '/images/p2.png'; const targetConnectionId = caller.connectionId || ''; + pendingIncomingInvite = caller; if (document.getElementById('callRequestName')) { document.getElementById('callRequestName').textContent = callerName; @@ -60,6 +63,44 @@ function showCallRequestDialog(caller = {}) { dialog.classList.remove('hidden'); } +function getCurrentUserProfile() { + try { + const settings = JSON.parse(localStorage.getItem('userSettings') || '{}'); + return { + userId: settings.userId || settings.id || '', + name: settings.name || '我', + avatar: settings.avatar || '/images/p1.png' + }; + } catch (error) { + console.error('Error parsing user settings:', error); + return { + userId: '', + name: '我', + avatar: '/images/p1.png' + }; + } +} + +function bindInviteSignalHandlers() { + if (inviteHandlersBound) { + return; + } + + store.onSocketEvent('invite-call', (payload) => { + pendingIncomingInvite = { + connectionId: payload.connectionId, + inviterSocketId: payload.inviterSocketId, + inviterUserId: payload.inviterUserId, + name: payload.inviterName || '邀请方', + avatar: payload.inviterAvatar || '/images/p2.png' + }; + showCallRequestDialog(pendingIncomingInvite); + showNotification(`${pendingIncomingInvite.name} 邀请你加入通话`); + }); + + inviteHandlersBound = true; +} + function bindInviteDialogEvents() { window.showCallRequest = function (caller) { showCallRequestDialog(caller); @@ -70,6 +111,18 @@ function bindInviteDialogEvents() { if (dialog) { dialog.classList.add('hidden'); } + if (pendingIncomingInvite) { + try { + store.sendInviteRejected({ + connectionId: pendingIncomingInvite.connectionId || '', + targetSocketId: pendingIncomingInvite.inviterSocketId || '', + targetUserId: pendingIncomingInvite.inviterUserId || '' + }); + } catch (error) { + console.error('Error rejecting invite:', error); + } + } + pendingIncomingInvite = null; showNotification('已拒绝通话请求'); }; @@ -80,6 +133,7 @@ function bindInviteDialogEvents() { } const targetConnectionId = + (pendingIncomingInvite && pendingIncomingInvite.connectionId) || connectionId || localStorage.getItem('connectionId') || new URLSearchParams(window.location.search).get('connectionId'); @@ -91,7 +145,23 @@ function bindInviteDialogEvents() { connectionId = targetConnectionId; localStorage.setItem('connectionId', targetConnectionId); + + if (pendingIncomingInvite) { + try { + store.sendInviteAccepted({ + connectionId: targetConnectionId, + targetSocketId: pendingIncomingInvite.inviterSocketId || '', + targetUserId: pendingIncomingInvite.inviterUserId || '' + }); + } catch (error) { + console.error('Error accepting invite:', error); + showNotification('接受邀请失败,请稍后重试', 'error'); + return; + } + } + showNotification('已接受通话请求'); + pendingIncomingInvite = null; if (currentView !== 'call') { await switchToCallView(targetConnectionId); @@ -139,9 +209,11 @@ async function switchToCallView(connectionId) { bindCallViewDomEvents(); console.log('Video call app initialized successfully'); + return true; } catch (error) { console.error('Error initializing app:', error); showNotification('初始化失败,请刷新页面重试', 'error'); + return false; } } @@ -177,14 +249,14 @@ function bindCallViewDomEvents() { }; // 切换麦克风 - window.toggleMute = function (button) { + window.toggleMute = function () { const state = store.getState(); const currentState = state.session.localUser.mediaState.audio; store.updateLocalMedia('audio', !currentState); }; // 切换视频 - window.toggleVideo = function (button) { + window.toggleVideo = function () { const state = store.getState(); const currentState = state.session.localUser.mediaState.video; store.updateLocalMedia('video', !currentState); @@ -196,7 +268,7 @@ function bindCallViewDomEvents() { }; // 切换录屏 - window.toggleRecording = function (button) { + window.toggleRecording = function () { const state = store.getState(); const currentState = state.session.localUser.mediaState.recording || false; store.updateLocalMedia('recording', !currentState); @@ -304,6 +376,7 @@ window.addEventListener('DOMContentLoaded', async () => { // 初始化WebSocket连接(在connect视图就建立WebSocket) await initWebSocket(); + bindInviteSignalHandlers(); // 绑定connect视图事件(加入通话、创建通话等) bindConnectViewEvents(handleJoinCall, handleCreateCall); diff --git a/client/public/store.js b/client/public/store.js index acf8af2..4866d20 100644 --- a/client/public/store.js +++ b/client/public/store.js @@ -56,6 +56,8 @@ class CallStateManager { // 监听器数组 this.listeners = []; + this.socketEventHandlers = {}; + this._socketInviteBound = false; } // 订阅状态变化 @@ -339,10 +341,68 @@ class CallStateManager { // 创建信令实例 this._signaling = this.useWebSocket ? new WebSocketSignaling() : new Signaling(); await this._signaling.start(); + this._bindSocketInviteEvents(this._signaling); console.log('Signaling connected (WebSocket only, no room yet)'); return this._signaling; } + _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); + } + /** * 在仅建立WebSocket连接时同步当前用户信息 * @param {{ id?: string, name?: string, avatar?: string } | null} userInfo - 用户信息 diff --git a/client/src/signaling.js b/client/src/signaling.js index af58762..7c2840b 100644 --- a/client/src/signaling.js +++ b/client/src/signaling.js @@ -221,6 +221,18 @@ export class WebSocketSignaling extends EventTarget { case "broadcast": this.dispatchEvent(new CustomEvent('on-message', { detail: msg.message })); break; + case "invite-call": + this.dispatchEvent(new CustomEvent('invite-call', { detail: msg.data })); + break; + case "invite-accepted": + this.dispatchEvent(new CustomEvent('invite-accepted', { detail: msg.data })); + break; + case "invite-rejected": + this.dispatchEvent(new CustomEvent('invite-rejected', { detail: msg.data })); + break; + case "invite-failed": + this.dispatchEvent(new CustomEvent('invite-failed', { detail: msg.data })); + break; default: break; } @@ -288,4 +300,22 @@ export class WebSocketSignaling extends EventTarget { Logger.log(sendJson); this.websocket.send(sendJson); } + + sendInviteCall(payload) { + const sendJson = JSON.stringify({ type: 'invite-call', data: payload }); + Logger.log(sendJson); + this.websocket.send(sendJson); + } + + sendInviteAccepted(payload) { + const sendJson = JSON.stringify({ type: 'invite-accepted', data: payload }); + Logger.log(sendJson); + this.websocket.send(sendJson); + } + + sendInviteRejected(payload) { + const sendJson = JSON.stringify({ type: 'invite-rejected', data: payload }); + Logger.log(sendJson); + this.websocket.send(sendJson); + } } diff --git a/src/class/websockethandler.ts b/src/class/websockethandler.ts index e9ebd2a..135b98a 100644 --- a/src/class/websockethandler.ts +++ b/src/class/websockethandler.ts @@ -378,7 +378,15 @@ function onCallConnectionId(ws: WebSocket, message: any): void { }); } } +function onHostUserInfo(ws: WebSocket, message: any): void { + (ws as any).userInfo = { + id: message.id || '', + name: message.name || '匿名用户', + avatar: message.avatar || '' + }; + log(LogLevel.log, 'Updated current ws userInfo:', (ws as any).userInfo); +} /** * 处理广播消息请求(1对多模式) @@ -555,4 +563,4 @@ function onMessage(ws: WebSocket, message: any): void { /** * 导出WebSocket处理器函数 */ -export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate, onCallConnectionId, onBroadcast, onGetAllConnectionIds, onGetOnlineUsers, AddHeartbeat, RemoveHeartbeat, onMessage, isHost, broadcastToGroup, connectionGroup }; +export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate, onCallConnectionId, onBroadcast, onGetAllConnectionIds, onGetOnlineUsers, AddHeartbeat, RemoveHeartbeat, onMessage, isHost, broadcastToGroup, connectionGroup, onHostUserInfo }; diff --git a/src/websocket.ts b/src/websocket.ts index a88722c..33f7d46 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -105,6 +105,18 @@ export default class WSSignaling { case 'call-request': handler.onCallConnectionId(ws, msg.data); break; + case 'host-userInfo': + handler.onHostUserInfo(ws, msg.data); + break; + // case 'invite-call': + // handler.onInviteCall(ws, msg.data); + // break; + // case 'invite-accepted': + // handler.onInviteAccepted(ws, msg.data); + // break; + // case 'invite-rejected': + // handler.onInviteRejected(ws, msg.data); + // break; case 'on-message': if (msg.from) msg.data.connectionId = msg.from; handler.onMessage(ws, msg.data);