From 85c0b0226dbc2a54e88473126234427dc4095914 Mon Sep 17 00:00:00 2001
From: stary <834207172@qq.com>
Date: Mon, 18 May 2026 23:03:28 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=A8=A1=E5=9D=97=E5=BC=80?=
=?UTF-8?q?=E5=8F=91=E5=AE=8C=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
client/public/connectview.js | 16 ++++++-
client/public/main.js | 79 +++++++++++++++++++++++++++++++++--
client/public/store.js | 60 ++++++++++++++++++++++++++
client/src/signaling.js | 30 +++++++++++++
src/class/websockethandler.ts | 10 ++++-
src/websocket.ts | 12 ++++++
6 files changed, 202 insertions(+), 5 deletions(-)
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);