From 2fc478e8896df7c64873201489191078756433dd Mon Sep 17 00:00:00 2001 From: MatrixSeven Date: Tue, 16 Sep 2025 16:41:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=20ws=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ConnectionStatus.tsx | 29 +- .../src/components/WebRTCConnectionStatus.tsx | 185 ------- .../src/components/WebRTCFileTransfer.tsx | 8 +- .../components/examples/WebSocketExample.tsx | 153 ----- .../components/webrtc/WebRTCTextReceiver.tsx | 4 +- .../components/webrtc/WebRTCTextSender.tsx | 4 +- chuan-next/src/hooks/connection/index.ts | 2 +- .../hooks/connection/state/webConnectStore.ts | 4 + .../src/hooks/connection/useConnectManager.ts | 111 +++- .../webrtc/useSharedWebRTCManager.ts | 9 +- .../webrtc/useWebRTCConnectionCore.ts | 523 ++++++++++-------- .../webrtc/useWebRTCDataChannelManager.ts | 3 +- .../connection/ws/useWebSocketConnection.ts | 332 +++++++---- .../desktop-share/useDesktopShareBusiness.ts | 50 +- .../file-transfer/useFileStateManager.ts | 1 - .../file-transfer/useFileTransferBusiness.ts | 39 +- chuan-next/src/hooks/ui/useTabNavigation.ts | 36 +- internal/services/webrtc_service.go | 62 ++- 18 files changed, 780 insertions(+), 775 deletions(-) delete mode 100644 chuan-next/src/components/WebRTCConnectionStatus.tsx delete mode 100644 chuan-next/src/components/examples/WebSocketExample.tsx diff --git a/chuan-next/src/components/ConnectionStatus.tsx b/chuan-next/src/components/ConnectionStatus.tsx index 87d8710..de87f7c 100644 --- a/chuan-next/src/components/ConnectionStatus.tsx +++ b/chuan-next/src/components/ConnectionStatus.tsx @@ -23,6 +23,7 @@ const getConnectionStatus = (currentRoom: { code: string; role: Role } | null) = const isPeerConnected = connection?.isPeerConnected || false; const isConnecting = connection?.isConnecting || false; const error = connection?.error || null; + const currentConnectType = connection?.currentConnectType || 'webrtc'; if (error) { return { @@ -58,12 +59,15 @@ const getConnectionStatus = (currentRoom: { code: string; role: Role } | null) = }; } - if (isWebSocketConnected && !isPeerConnected) { - return { - type: 'room-ready' as const, - message: '房间已创建', - detail: '等待对方加入并建立P2P连接...', - }; + if (isWebSocketConnected ) { + // 根据连接类型显示不同信息 + if (currentConnectType === 'websocket') { + return { + type: 'connected' as const, + message: 'P2P链接失败,WS降级中', + detail: 'WebSocket传输模式已就绪', + }; + } } if (isWebSocketConnected && isPeerConnected) { @@ -89,6 +93,8 @@ const getStatusColor = (type: string) => { case 'connecting': case 'room-ready': return 'text-yellow-600'; + case 'websocket-ready': + return 'text-orange-600'; case 'error': return 'text-red-600'; case 'disconnected': @@ -110,6 +116,8 @@ const StatusIcon = ({ type, className = 'w-3 h-3' }: { type: string; className?: return (
); + case 'websocket-ready': + return
; case 'error': return
; case 'disconnected': @@ -120,11 +128,12 @@ const StatusIcon = ({ type, className = 'w-3 h-3' }: { type: string; className?: }; // 获取连接状态文字描述 -const getConnectionStatusText = (connection: { isWebSocketConnected?: boolean; isPeerConnected?: boolean; isConnecting?: boolean; error?: string | null }) => { +const getConnectionStatusText = (connection: { isWebSocketConnected?: boolean; isPeerConnected?: boolean; isConnecting?: boolean; error?: string | null; currentConnectType?: 'webrtc' | 'websocket' }) => { const isWebSocketConnected = connection?.isWebSocketConnected || false; const isPeerConnected = connection?.isPeerConnected || false; const isConnecting = connection?.isConnecting || false; const error = connection?.error || null; + const currentConnectType = connection?.currentConnectType || 'webrtc'; const wsStatus = isWebSocketConnected ? 'WS已连接' : 'WS未连接'; const rtcStatus = isPeerConnected ? 'RTC已连接' : @@ -142,6 +151,11 @@ const getConnectionStatusText = (connection: { isWebSocketConnected?: boolean; i return `${wsStatus} ${rtcStatus} - P2P连接成功`; } + // 如果WebSocket已连接但P2P未连接,且当前连接类型是websocket + if (isWebSocketConnected && !isPeerConnected && currentConnectType === 'websocket') { + return `${wsStatus} ${rtcStatus} - P2P链接失败,将使用WS进行传输`; + } + return `${wsStatus} ${rtcStatus}`; }; @@ -157,6 +171,7 @@ export function ConnectionStatus(props: ConnectionStatusProps) { isPeerConnected: webrtcState.isPeerConnected, isConnecting: webrtcState.isConnecting, error: webrtcState.error, + currentConnectType: webrtcState.currentConnectType, }; const isConnected = webrtcState.isWebSocketConnected && webrtcState.isPeerConnected; diff --git a/chuan-next/src/components/WebRTCConnectionStatus.tsx b/chuan-next/src/components/WebRTCConnectionStatus.tsx deleted file mode 100644 index 1578be4..0000000 --- a/chuan-next/src/components/WebRTCConnectionStatus.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import React from 'react'; -import { AlertCircle, Wifi, WifiOff, Loader2, RotateCcw } from 'lucide-react'; -import { WebRTCConnection } from '@/hooks/connection/useSharedWebRTCManager'; - -interface Props { - webrtc: WebRTCConnection; - className?: string; -} - -/** - * WebRTC连接状态显示组件 - * 显示详细的连接状态、错误信息和重试按钮 - */ -export function WebRTCConnectionStatus({ webrtc, className = '' }: Props) { - const { - isConnected, - isConnecting, - isWebSocketConnected, - isPeerConnected, - error, - canRetry, - retry - } = webrtc; - - // 状态图标 - const getStatusIcon = () => { - if (isConnecting) { - return ; - } - - if (error) { - // 区分信息提示和错误 - if (error.includes('对方已离开房间') || error.includes('已离开房间')) { - return ; - } - return ; - } - - if (isPeerConnected) { - return ; - } - - if (isWebSocketConnected) { - return ; - } - - return ; - }; - - // 状态文本 - const getStatusText = () => { - if (error) { - return error; - } - - if (isConnecting) { - return '正在连接...'; - } - - if (isPeerConnected) { - return 'P2P连接已建立'; - } - - if (isWebSocketConnected) { - return '信令服务器已连接'; - } - - return '未连接'; - }; - - // 状态颜色 - const getStatusColor = () => { - if (error) { - // 区分信息提示和错误 - if (error.includes('对方已离开房间') || error.includes('已离开房间')) { - return 'text-yellow-600'; - } - return 'text-red-600'; - } - if (isConnecting) return 'text-blue-600'; - if (isPeerConnected) return 'text-green-600'; - if (isWebSocketConnected) return 'text-yellow-600'; - return 'text-gray-600'; - }; - - const handleRetry = async () => { - try { - await retry(); - } catch (error) { - console.error('重试连接失败:', error); - } - }; - - return ( -
-
- {getStatusIcon()} - - {getStatusText()} - -
- - {/* 连接详细状态指示器 */} -
- {/* WebSocket状态 */} -
- - {/* P2P状态 */} -
- - {/* 重试按钮 */} - {canRetry && ( - - )} -
-
- ); -} - -/** - * 简单的连接状态指示器(用于空间受限的地方) - */ -export function WebRTCStatusIndicator({ webrtc, className = '' }: Props) { - const { isPeerConnected, isConnecting, error } = webrtc; - - if (error) { - // 区分信息提示和错误 - if (error.includes('对方已离开房间') || error.includes('已离开房间')) { - return ( -
-
- 对方已离开 -
- ); - } - return ( -
-
- 连接错误 -
- ); - } - - if (isConnecting) { - return ( -
-
- 连接中 -
- ); - } - - if (isPeerConnected) { - return ( -
-
- 已连接 -
- ); - } - - return ( -
-
- 未连接 -
- ); -} diff --git a/chuan-next/src/components/WebRTCFileTransfer.tsx b/chuan-next/src/components/WebRTCFileTransfer.tsx index d77ad18..a183e83 100644 --- a/chuan-next/src/components/WebRTCFileTransfer.tsx +++ b/chuan-next/src/components/WebRTCFileTransfer.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/toast-simple'; import { WebRTCFileReceive } from '@/components/webrtc/WebRTCFileReceive'; import { WebRTCFileUpload } from '@/components/webrtc/WebRTCFileUpload'; -import { useConnectionState, useRoomConnection, useSharedWebRTCManager } from '@/hooks/connection'; +import { useConnectionState, useConnectManager, useRoomConnection } from '@/hooks/connection'; import { useFileListSync, useFileStateManager, useFileTransferBusiness } from '@/hooks/file-transfer'; import { useURLHandler } from '@/hooks/ui'; import { Download, Upload } from 'lucide-react'; @@ -34,7 +34,7 @@ export const WebRTCFileTransfer: React.FC = () => { const fileInputRef = useRef(null); // 创建共享连接 - const connection = useSharedWebRTCManager(); + const connection = useConnectManager(); const stableConnection = useMemo(() => connection, [connection.getConnectState().isConnected, connection.getConnectState().isConnecting, connection.getConnectState().isWebSocketConnected, connection.getConnectState().error]); // 使用共享连接创建业务层 @@ -229,10 +229,6 @@ export const WebRTCFileTransfer: React.FC = () => { console.log('连接已断开,忽略进度更新:', progressInfo.fileName); return; } - - console.log('=== 文件进度更新 ==='); - console.log('文件:', progressInfo.fileName, 'ID:', progressInfo.fileId, '进度:', progressInfo.progress); - // 更新当前传输文件信息 setCurrentTransferFile({ fileId: progressInfo.fileId, diff --git a/chuan-next/src/components/examples/WebSocketExample.tsx b/chuan-next/src/components/examples/WebSocketExample.tsx deleted file mode 100644 index bf4ec74..0000000 --- a/chuan-next/src/components/examples/WebSocketExample.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import React, { useState } from 'react'; -import { IWebMessage } from '../../hooks/connection/types'; -import { useWebSocketConnection } from '../../hooks/connection/ws/useWebSocketConnection'; - -/** - * WebSocket 连接示例组件 - * 展示如何使用 WebSocket 实现的 IWebConnection 接口 - */ -export function WebSocketExample() { - const [roomCode, setRoomCode] = useState(''); - const [role, setRole] = useState<'sender' | 'receiver'>('sender'); - const [message, setMessage] = useState(''); - - // 使用 WebSocket 连接 - const connection = useWebSocketConnection(); - const state = connection.getConnectState(); - - // 连接到房间 - const handleConnect = async () => { - if (roomCode.trim()) { - await connection.connect(roomCode.trim(), role); - } - }; - - // 发送消息 - const handleSendMessage = () => { - if (message.trim()) { - connection.sendMessage({ - type: 'text', - payload: { text: message.trim() } - }); - setMessage(''); - } - }; - - // 注册消息处理器 - React.useEffect(() => { - const unsubscribe = connection.registerMessageHandler('text', (msg: IWebMessage) => { - console.log('收到文本消息:', msg.payload.text); - }); - - return unsubscribe; - }, [connection]); - - return ( -
-

WebSocket 连接示例

- - {/* 连接状态 */} -
-

连接状态: {state.isConnected ? '已连接' : '未连接'}

-

传输类型: {connection.connectType}

- {state.error && ( -

错误: {state.error}

- )} - {state.currentRoom && ( -

房间: {state.currentRoom.code} ({state.currentRoom.role})

- )} -
- - {/* 连接控制 */} - {!state.isConnected ? ( -
-
- - setRoomCode(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded" - placeholder="输入房间代码" - /> -
- -
- - -
- - - - {state.canRetry && ( - - )} -
- ) : ( -
- {/* 消息发送 */} -
- -
- setMessage(e.target.value)} - className="flex-1 px-3 py-2 border border-gray-300 rounded" - placeholder="输入消息内容" - onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} - /> - -
-
- - {/* 断开连接 */} - -
- )} - - {/* 功能说明 */} -
-

支持的功能:

-
    -
  • ✅ WebSocket 连接管理
  • -
  • ✅ 消息发送和接收
  • -
  • ✅ 二进制数据传输
  • -
  • ✅ 多通道消息处理
  • -
  • ✅ 自动重连机制
  • -
  • ❌ 媒体轨道(WebSocket 不支持)
  • -
  • ❌ P2P 直连(WebSocket 不支持)
  • -
-
-
- ); -} \ No newline at end of file diff --git a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx index 4a225de..04edf82 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx @@ -4,7 +4,7 @@ import { ConnectionStatus } from '@/components/ConnectionStatus'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useToast } from '@/components/ui/toast-simple'; -import { useSharedWebRTCManager } from '@/hooks/connection'; +import { useConnectManager } from '@/hooks/connection'; import { useFileTransferBusiness } from '@/hooks/file-transfer'; import { useTextTransferBusiness } from '@/hooks/text-transfer'; import { Download, Image, MessageSquare } from 'lucide-react'; @@ -37,7 +37,7 @@ export const WebRTCTextReceiver: React.FC = ({ const hasTriedAutoConnect = useRef(false); // 创建共享连接 - const connection = useSharedWebRTCManager(); + const connection = useConnectManager(); const {getConnectState} = connection; diff --git a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx index feb45f6..c1dcda8 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx @@ -4,7 +4,7 @@ import { ConnectionStatus } from '@/components/ConnectionStatus'; import RoomInfoDisplay from '@/components/RoomInfoDisplay'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/toast-simple'; -import { useSharedWebRTCManager } from '@/hooks/connection'; +import { useConnectManager } from '@/hooks/connection'; import { useFileTransferBusiness } from '@/hooks/file-transfer'; import { useTextTransferBusiness } from '@/hooks/text-transfer'; import { Image, MessageSquare, Send } from 'lucide-react'; @@ -31,7 +31,7 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o const typingTimeoutRef = useRef(null); // 创建共享连接 - const connection = useSharedWebRTCManager(); + const connection = useConnectManager(); const { getConnectState } = connection; diff --git a/chuan-next/src/hooks/connection/index.ts b/chuan-next/src/hooks/connection/index.ts index 14fb6d1..e806e58 100644 --- a/chuan-next/src/hooks/connection/index.ts +++ b/chuan-next/src/hooks/connection/index.ts @@ -1,6 +1,6 @@ // 连接相关的hooks export { useConnectionState } from './useConnectionState'; -export { useSharedWebRTCManager } from './useConnectManager'; +export { useConnectManager } from './useConnectManager'; export { useRoomConnection } from './useRoomConnection'; export { useWebRTCSupport } from './useWebRTCSupport'; diff --git a/chuan-next/src/hooks/connection/state/webConnectStore.ts b/chuan-next/src/hooks/connection/state/webConnectStore.ts index 3cd207a..037c223 100644 --- a/chuan-next/src/hooks/connection/state/webConnectStore.ts +++ b/chuan-next/src/hooks/connection/state/webConnectStore.ts @@ -9,7 +9,9 @@ export interface WebConnectState { isDataChannelConnected: boolean; isMediaStreamConnected: boolean; currentConnectType: 'webrtc' | 'websocket'; + currentIsLocalNetWork: boolean; // 可选,表示当前是否在局域网内 state: RTCDataChannelState; + stateMsg: string | null; error: string | null; canRetry: boolean; // 新增:是否可以重试 currentRoom: { code: string; role: Role } | null; @@ -25,11 +27,13 @@ interface WebRTCStore extends WebConnectState { const initialState: WebConnectState = { isConnected: false, isConnecting: false, + currentIsLocalNetWork: false, isWebSocketConnected: false, isPeerConnected: false, error: null, canRetry: false, // 初始状态下不需要重试 currentRoom: null, + stateMsg: null, isDataChannelConnected: false, isMediaStreamConnected: false, currentConnectType: 'webrtc', diff --git a/chuan-next/src/hooks/connection/useConnectManager.ts b/chuan-next/src/hooks/connection/useConnectManager.ts index 249ea85..0fe46bb 100644 --- a/chuan-next/src/hooks/connection/useConnectManager.ts +++ b/chuan-next/src/hooks/connection/useConnectManager.ts @@ -1,3 +1,4 @@ +import { getWsUrl } from '@/lib/config'; import { useCallback, useRef, useState } from 'react'; import { useReadConnectState } from './state/useWebConnectStateManager'; import { WebConnectState } from "./state/webConnectStore"; @@ -7,14 +8,21 @@ import { useWebSocketConnection } from './ws/useWebSocketConnection'; /** - * 连接管理器 - 代理 WebSocket 和 WebRTC 连接 + * 连接管理器 - 统一管理 WebSocket 和 WebRTC 连接 * 提供统一的连接接口,内部可以在不同传输方式之间切换 + * 统一管理 WebSocket 连接,为 WebRTC 和 WebSocket 传输提供共享的 WebSocket 实例 */ -export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler & IGetConnectState { +export function useConnectManager(): IWebConnection & IRegisterEventHandler & IGetConnectState { // 当前连接类型 const [currentConnectType, setCurrentConnectType] = useState('webrtc'); - // 连接实例 + // 统一的 WebSocket 连接引用 + const wsRef = useRef(null); + + // 当前房间信息 + const currentRoomRef = useRef<{ code: string; role: Role } | null>(null); + + // 连接实例 - 初始化时不传入 WebSocket const wsConnection = useWebSocketConnection(); const webrtcConnection = useSharedWebRTCManagerImpl(); @@ -36,7 +44,9 @@ export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler state: 'closed', error: null, canRetry: false, - currentRoom: null + currentRoom: null, + stateMsg: null, + currentIsLocalNetWork: false }); // 更新连接状态 @@ -47,6 +57,71 @@ export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler }; }, []); + // 创建并管理 WebSocket 连接 + const createWebSocketConnection = useCallback(async (roomCode: string, role: Role) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + console.log('[ConnectManager] 已存在 WebSocket 连接,先断开'); + wsRef.current.close(); + } + + try { + // 构建 WebSocket URL + const baseWsUrl = getWsUrl(); + if (!baseWsUrl) { + throw new Error('WebSocket URL未配置'); + } + + // 构建完整的WebSocket URL + const wsUrl = `${baseWsUrl}/api/ws/webrtc?code=${roomCode}&role=${role}&channel=shared`; + console.log('[ConnectManager] 创建 WebSocket 连接:', wsUrl); + + const ws = new WebSocket(wsUrl); + // 设置二进制数据类型为 ArrayBuffer,避免默认的 Blob 类型 + ws.binaryType = 'arraybuffer'; + wsRef.current = ws; + currentRoomRef.current = { code: roomCode, role }; + + // WebSocket 事件处理 + ws.onopen = () => { + console.log('[ConnectManager] WebSocket 连接成功'); + updateConnectionState({ + isWebSocketConnected: true, + error: null + }); + }; + + ws.onerror = (error) => { + console.error('[ConnectManager] WebSocket 连接错误:', error); + updateConnectionState({ + isWebSocketConnected: false, + error: 'WebSocket 连接失败' + }); + }; + + ws.onclose = (event) => { + console.log('[ConnectManager] WebSocket 连接关闭:', event.code, event.reason); + updateConnectionState({ + isWebSocketConnected: false, + error: event.wasClean ? null : 'WebSocket 连接意外断开' + }); + }; + + return ws; + } catch (error) { + console.error('[ConnectManager] 创建 WebSocket 连接失败:', error); + updateConnectionState({ + isWebSocketConnected: false, + error: '无法建立 WebSocket 连接' + }); + throw error; + } + }, [updateConnectionState]); + + // 获取 WebSocket 连接 + const getWebSocketConnection = useCallback(() => { + return wsRef.current; + }, []); + // 切换连接类型 const switchConnectionType = useCallback((type: ConnectType) => { console.log('[ConnectManager] 切换连接类型:', currentConnectType, '->', type); @@ -62,8 +137,6 @@ export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler updateConnectionState({ currentConnectType: type, - isConnected: false, - isConnecting: false, error: null }); }, [currentConnectType, wsConnection, webrtcConnection, updateConnectionState]); @@ -79,24 +152,42 @@ export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler }); try { + // 首先创建统一的 WebSocket 连接 + const ws = await createWebSocketConnection(roomCode, role); + if (currentConnectType === 'webrtc') { - // 使用当前选择的连接类型进行连接 + // 将 WebSocket 注入到 WebRTC 连接中 + webrtcConnection.injectWebSocket(ws); currentConnectionRef.current = webrtcConnection; await currentConnectionRef.current.connect(roomCode, role); - + } else { + // WebSocket 连接也使用统一的 WebSocket 实例 + wsConnection.injectWebSocket(ws); + currentConnectionRef.current = wsConnection; + await currentConnectionRef.current.connect(roomCode, role); } } catch (error) { console.error('[ConnectManager] 连接失败:', error); - + updateConnectionState({ + isConnecting: false, + error: error instanceof Error ? error.message : '连接失败' + }); } - }, [currentConnectType]); + }, [currentConnectType, createWebSocketConnection, webrtcConnection, wsConnection, updateConnectionState]); // 断开连接 const disconnect = useCallback(() => { console.log('[ConnectManager] 断开连接'); currentConnectionRef.current.disconnect(); + // 断开 WebSocket 连接 + if (wsRef.current) { + wsRef.current.close(1000, '用户主动断开'); + wsRef.current = null; + } + currentRoomRef.current = null; + updateConnectionState({ isConnected: false, isConnecting: false, diff --git a/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts b/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts index 60fac21..458b262 100644 --- a/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts +++ b/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts @@ -11,10 +11,12 @@ import { useWebRTCTrackManager } from './useWebRTCTrackManager'; * 创建单一的 WebRTC 连接实例,供多个业务模块共享使用 * 整合所有模块,提供统一的接口 * - * webrtc 实现 + * webrtc 实现 - 初始化时不需要 WebSocket,通过 injectWebSocket 动态注入 * */ -export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHandler & IGetConnectState { +export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHandler & IGetConnectState & { + injectWebSocket: (ws: WebSocket) => void; +} { // 创建各个管理器实例 const stateManager = useWebConnectStateManager(); const dataChannelManager = useWebRTCDataChannelManager(stateManager); @@ -77,5 +79,8 @@ export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHan // 当前房间信息 currentRoom: connectionCore.getCurrentRoom(), + + // WebSocket 注入方法 + injectWebSocket: connectionCore.injectWebSocket, }; } diff --git a/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts b/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts index 53e9bd2..376d632 100644 --- a/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts +++ b/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts @@ -1,5 +1,4 @@ -import { getWsUrl } from '@/lib/config'; import { useCallback, useRef } from 'react'; import { getIceServersConfig } from '../../settings/useIceServersConfig'; import { IWebConnectStateManager } from '../state/useWebConnectStateManager'; @@ -30,18 +29,24 @@ export interface WebRTCConnectionCore { // 设置断开连接回调 setOnDisconnectCallback: (callback: () => void) => void; + + // 动态注入 WebSocket 连接 + injectWebSocket: (ws: WebSocket) => void; } /** * WebRTC 核心连接管理 Hook * 负责基础的 WebRTC 连接管理,包括 WebSocket 连接、PeerConnection 创建和管理 + * 初始化时不需要 WebSocket,可以通过 injectWebSocket 动态注入 */ export function useWebRTCConnectionCore( stateManager: IWebConnectStateManager, dataChannelManager: WebRTCDataChannelManager, trackManager: WebRTCTrackManager ): WebRTCConnectionCore { + // WebSocket 连接引用,初始为空 const wsRef = useRef(null); + const isExternalWebSocket = useRef(false); const pcRef = useRef(null); const timeoutRef = useRef(null); @@ -81,7 +86,9 @@ export function useWebRTCConnectionCore( } } - if (wsRef.current) { + // 如果是外部 WebSocket,不关闭连接,只是清理引用 + // 外部 WebSocket 的生命周期由外部管理 + if (!isExternalWebSocket.current && wsRef.current) { wsRef.current.close(); wsRef.current = null; } @@ -260,275 +267,309 @@ export function useWebRTCConnectionCore( isUserDisconnecting.current = false; try { - // 连接 WebSocket - 使用动态URL - const baseWsUrl = getWsUrl(); - if (!baseWsUrl) { - throw new Error('WebSocket URL未配置'); - } - - // 构建完整的WebSocket URL - const wsUrl = `${baseWsUrl}/api/ws/webrtc?code=${roomCode}&role=${role}&channel=shared`; - console.log('[ConnectionCore] 🌐 连接WebSocket:', wsUrl); - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - // 保存重新连接状态,供后续使用 const reconnectState = { isReconnect, role }; - // WebSocket 事件处理 - ws.onopen = () => { - console.log('[ConnectionCore] ✅ WebSocket 连接已建立,房间准备就绪'); + // 必须使用注入的 WebSocket 连接 + if (!wsRef.current) { + throw new Error('WebSocket 连接未注入,请先调用 injectWebSocket 方法'); + } + + const ws = wsRef.current; + console.log('[ConnectionCore] 使用注入的 WebSocket 连接,状态:', ws.readyState); + + // 检查 WebSocket 是否已经连接 + if (ws.readyState === WebSocket.OPEN) { + console.log('[ConnectionCore] WebSocket 已连接,房间准备就绪'); stateManager.updateState({ isWebSocketConnected: true, isConnecting: false, // WebSocket连接成功即表示初始连接完成 isConnected: true // 可以开始后续操作 }); + } else if (ws.readyState === WebSocket.CONNECTING) { + // 如果 WebSocket 还在连接中,等待连接成功 + console.log('[ConnectionCore] WebSocket 连接中,等待连接完成'); + stateManager.updateState({ isConnecting: true, error: null }); - // 如果是重新连接且是发送方,检查是否有接收方在等待 - if (reconnectState.isReconnect && reconnectState.role === 'sender') { - console.log('[ConnectionCore] 🔄 发送方重新连接,检查是否有接收方在等待'); - // 这里不需要立即创建PeerConnection,等待接收方加入的通知 - } - }; + // 设置 WebSocket 的事件处理 + const originalOnOpen = ws.onopen; + ws.onopen = (event) => { + console.log('[ConnectionCore] ✅ WebSocket 连接已建立,房间准备就绪'); + stateManager.updateState({ + isWebSocketConnected: true, + isConnecting: false, // WebSocket连接成功即表示初始连接完成 + isConnected: true // 可以开始后续操作 + }); - ws.onmessage = async (event) => { - try { - const message = JSON.parse(event.data); - console.log('[ConnectionCore] 📨 收到信令消息:', message.type); + // 调用原始处理器 + if (originalOnOpen) { + originalOnOpen.call(ws, event); + } + }; + } else { + throw new Error('WebSocket 连接状态异常: ' + ws.readyState); + } - switch (message.type) { - case 'peer-joined': - // 对方加入房间的通知 - console.log('[ConnectionCore] 👥 对方已加入房间,角色:', message.payload?.role); - if (role === 'sender' && message.payload?.role === 'receiver') { - console.log('[ConnectionCore] 🚀 接收方已连接,发送方开始建立P2P连接'); - // 确保WebSocket连接状态正确更新 - stateManager.updateState({ - isWebSocketConnected: true, - isConnected: true, - isPeerConnected: true // 标记对方已加入,可以开始P2P - }); + // 设置 WebSocket 消息处理 + if (ws) { + // 如果是外部 WebSocket,可能已经有事件处理器,我们需要保存它们 + const originalOnMessage = ws.onmessage; + const originalOnError = ws.onerror; + const originalOnClose = ws.onclose; - // 如果是重新连接,先清理旧的PeerConnection - if (reconnectState.isReconnect && pcRef.current) { - console.log('[ConnectionCore] 🔄 重新连接:清理旧的PeerConnection'); - pcRef.current.close(); - pcRef.current = null; - } + ws.onmessage = async (event: MessageEvent) => { + try { + const message = JSON.parse(event.data); + console.log('[ConnectionCore] 📨 收到信令消息:', message.type); - // 对方加入后,创建PeerConnection - const pc = createPeerConnection(ws, role, reconnectState.isReconnect); + switch (message.type) { + case 'peer-joined': + // 对方加入房间的通知 + console.log('[ConnectionCore] 👥 对方已加入房间,角色:', message.payload?.role); + if (role === 'sender' && message.payload?.role === 'receiver') { + console.log('[ConnectionCore] 🚀 接收方已连接,发送方开始建立P2P连接'); + // 确保WebSocket连接状态正确更新 + stateManager.updateState({ + isWebSocketConnected: true, + isConnected: true, + isPeerConnected: true // 标记对方已加入,可以开始P2P + }); - // 设置轨道管理器的引用 - trackManager.setPeerConnection(pc); - trackManager.setWebSocket(ws); + // 如果是重新连接,先清理旧的PeerConnection + if (reconnectState.isReconnect && pcRef.current) { + console.log('[ConnectionCore] 🔄 重新连接:清理旧的PeerConnection'); + pcRef.current.close(); + pcRef.current = null; + } - // 发送方创建offer建立基础P2P连接 - try { - console.log('[ConnectionCore] 📡 创建基础P2P连接offer'); - await trackManager.createOffer(pc, ws); - } catch (error) { - console.error('[ConnectionCore] 创建基础P2P连接失败:', error); - } - } else if (role === 'receiver' && message.payload?.role === 'sender') { - console.log('[ConnectionCore] 🚀 发送方已连接,接收方准备接收P2P连接'); - // 确保WebSocket连接状态正确更新 - stateManager.updateState({ - isWebSocketConnected: true, - isConnected: true, - isPeerConnected: true // 标记对方已加入 - }); - - // 如果是重新连接,先清理旧的PeerConnection - if (reconnectState.isReconnect && pcRef.current) { - console.log('[ConnectionCore] 🔄 重新连接:清理旧的PeerConnection'); - pcRef.current.close(); - pcRef.current = null; - } - - // 对方加入后,立即创建PeerConnection,准备接收offer - const pc = createPeerConnection(ws, role, reconnectState.isReconnect); - - // 设置轨道管理器的引用 - trackManager.setPeerConnection(pc); - trackManager.setWebSocket(ws); - - // 等待一小段时间确保PeerConnection完全初始化 - setTimeout(() => { - console.log('[ConnectionCore] ✅ 接收方PeerConnection已准备就绪'); - }, 100); - } - break; - - case 'offer': - console.log('[ConnectionCore] 📬 处理offer...'); - // 如果PeerConnection不存在,先创建它 - let pcOffer = pcRef.current; - if (!pcOffer) { - console.log('[ConnectionCore] 🔧 PeerConnection不存在,先创建它'); - pcOffer = createPeerConnection(ws, role, reconnectState.isReconnect); - - // 设置轨道管理器的引用 - trackManager.setPeerConnection(pcOffer); - trackManager.setWebSocket(ws); - - // 等待一小段时间确保PeerConnection完全初始化 - await new Promise(resolve => setTimeout(resolve, 100)); - } - - if (pcOffer && pcOffer.signalingState === 'stable') { - await pcOffer.setRemoteDescription(new RTCSessionDescription(message.payload)); - console.log('[ConnectionCore] ✅ 设置远程描述完成'); - - const answer = await pcOffer.createAnswer(); - await pcOffer.setLocalDescription(answer); - console.log('[ConnectionCore] ✅ 创建并设置answer完成'); - - ws.send(JSON.stringify({ type: 'answer', payload: answer })); - console.log('[ConnectionCore] 📤 发送 answer'); - } else { - console.warn('[ConnectionCore] ⚠️ PeerConnection状态不是stable或不存在:', pcOffer?.signalingState); - } - break; - - case 'answer': - console.log('[ConnectionCore] 📬 处理answer...'); - let pcAnswer = pcRef.current; - try { - // 如果PeerConnection不存在,先创建它 - if (!pcAnswer) { - console.log('[ConnectionCore] 🔧 PeerConnection不存在,先创建它'); - pcAnswer = createPeerConnection(ws, role, reconnectState.isReconnect); + // 对方加入后,创建PeerConnection + const pc = createPeerConnection(ws, role, reconnectState.isReconnect); // 设置轨道管理器的引用 - trackManager.setPeerConnection(pcAnswer); + trackManager.setPeerConnection(pc); + trackManager.setWebSocket(ws); + + // 发送方创建offer建立基础P2P连接 + try { + console.log('[ConnectionCore] 📡 创建基础P2P连接offer'); + await trackManager.createOffer(pc, ws); + } catch (error) { + console.error('[ConnectionCore] 创建基础P2P连接失败:', error); + } + } else if (role === 'receiver' && message.payload?.role === 'sender') { + console.log('[ConnectionCore] 🚀 发送方已连接,接收方准备接收P2P连接'); + // 确保WebSocket连接状态正确更新 + stateManager.updateState({ + isWebSocketConnected: true, + isConnected: true, + isPeerConnected: true // 标记对方已加入 + }); + + // 如果是重新连接,先清理旧的PeerConnection + if (reconnectState.isReconnect && pcRef.current) { + console.log('[ConnectionCore] 🔄 重新连接:清理旧的PeerConnection'); + pcRef.current.close(); + pcRef.current = null; + } + + // 对方加入后,立即创建PeerConnection,准备接收offer + const pc = createPeerConnection(ws, role, reconnectState.isReconnect); + + // 设置轨道管理器的引用 + trackManager.setPeerConnection(pc); + trackManager.setWebSocket(ws); + + // 等待一小段时间确保PeerConnection完全初始化 + setTimeout(() => { + console.log('[ConnectionCore] ✅ 接收方PeerConnection已准备就绪'); + }, 100); + } + break; + + case 'offer': + console.log('[ConnectionCore] 📬 处理offer...'); + // 如果PeerConnection不存在,先创建它 + let pcOffer = pcRef.current; + if (!pcOffer) { + console.log('[ConnectionCore] 🔧 PeerConnection不存在,先创建它'); + pcOffer = createPeerConnection(ws, role, reconnectState.isReconnect); + + // 设置轨道管理器的引用 + trackManager.setPeerConnection(pcOffer); trackManager.setWebSocket(ws); // 等待一小段时间确保PeerConnection完全初始化 await new Promise(resolve => setTimeout(resolve, 100)); } - if (pcAnswer) { - const signalingState = pcAnswer.signalingState; - // 如果状态是stable,可能是因为之前的offer已经完成,需要重新创建offer - if (signalingState === 'stable') { - console.log('[ConnectionCore] 🔄 PeerConnection状态为stable,重新创建offer'); - try { - await trackManager.createOffer(pcAnswer, ws); - // 等待一段时间让ICE候选收集完成 - await new Promise(resolve => setTimeout(resolve, 500)); + if (pcOffer && pcOffer.signalingState === 'stable') { + await pcOffer.setRemoteDescription(new RTCSessionDescription(message.payload)); + console.log('[ConnectionCore] ✅ 设置远程描述完成'); - // 现在状态应该是have-local-offer,可以处理answer - if (pcAnswer.signalingState === 'have-local-offer') { - await pcAnswer.setRemoteDescription(new RTCSessionDescription(message.payload)); - console.log('[ConnectionCore] ✅ answer 处理完成'); - } else { - console.warn('[ConnectionCore] ⚠️ 重新创建offer后状态仍然不是have-local-offer:', pcAnswer.signalingState); - } - } catch (error) { - console.error('[ConnectionCore] ❌ 重新创建offer失败:', error); - } - } else if (signalingState === 'have-local-offer') { - await pcAnswer.setRemoteDescription(new RTCSessionDescription(message.payload)); - console.log('[ConnectionCore] ✅ answer 处理完成'); - } else { - console.warn('[ConnectionCore] ⚠️ PeerConnection状态异常:', signalingState); - } + const answer = await pcOffer.createAnswer(); + await pcOffer.setLocalDescription(answer); + console.log('[ConnectionCore] ✅ 创建并设置answer完成'); + + ws.send(JSON.stringify({ type: 'answer', payload: answer })); + console.log('[ConnectionCore] 📤 发送 answer'); + } else { + console.warn('[ConnectionCore] ⚠️ PeerConnection状态不是stable或不存在:', pcOffer?.signalingState); } - } catch (error) { - console.error('[ConnectionCore] ❌ 处理answer失败:', error); - if (error instanceof Error && error.message.includes('Failed to set local answer sdp')) { - console.warn('[ConnectionCore] ⚠️ Answer处理失败,可能是连接状态变化导致的'); - // 清理连接状态,让客户端重新连接 - stateManager.updateState({ error: 'WebRTC连接状态异常,请重新连接', isPeerConnected: false }); - } - } - break; + break; - case 'ice-candidate': - let pcIce = pcRef.current; - if (!pcIce) { - console.log('[ConnectionCore] 🔧 PeerConnection不存在,先创建它'); - pcIce = createPeerConnection(ws, role, reconnectState.isReconnect); - - // 等待一小段时间确保PeerConnection完全初始化 - await new Promise(resolve => setTimeout(resolve, 100)); - } - - if (pcIce && message.payload) { + case 'answer': + console.log('[ConnectionCore] 📬 处理answer...'); + let pcAnswer = pcRef.current; try { - // 即使远程描述未设置,也可以先缓存ICE候选 - if (pcIce.remoteDescription) { - await pcIce.addIceCandidate(new RTCIceCandidate(message.payload)); - console.log('[ConnectionCore] ✅ 添加 ICE 候选成功'); - } else { - console.log('[ConnectionCore] 📝 远程描述未设置,缓存ICE候选'); - // 可以在这里实现ICE候选缓存机制,等远程描述设置后再添加 + // 如果PeerConnection不存在,先创建它 + if (!pcAnswer) { + console.log('[ConnectionCore] 🔧 PeerConnection不存在,先创建它'); + pcAnswer = createPeerConnection(ws, role, reconnectState.isReconnect); + + // 设置轨道管理器的引用 + trackManager.setPeerConnection(pcAnswer); + trackManager.setWebSocket(ws); + + // 等待一小段时间确保PeerConnection完全初始化 + await new Promise(resolve => setTimeout(resolve, 100)); + } + + if (pcAnswer) { + const signalingState = pcAnswer.signalingState; + // 如果状态是stable,可能是因为之前的offer已经完成,需要重新创建offer + if (signalingState === 'stable') { + console.log('[ConnectionCore] 🔄 PeerConnection状态为stable,重新创建offer'); + try { + await trackManager.createOffer(pcAnswer, ws); + // 等待一段时间让ICE候选收集完成 + await new Promise(resolve => setTimeout(resolve, 500)); + + // 现在状态应该是have-local-offer,可以处理answer + if (pcAnswer.signalingState === 'have-local-offer') { + await pcAnswer.setRemoteDescription(new RTCSessionDescription(message.payload)); + console.log('[ConnectionCore] ✅ answer 处理完成'); + } else { + console.warn('[ConnectionCore] ⚠️ 重新创建offer后状态仍然不是have-local-offer:', pcAnswer.signalingState); + } + } catch (error) { + console.error('[ConnectionCore] ❌ 重新创建offer失败:', error); + } + } else if (signalingState === 'have-local-offer') { + await pcAnswer.setRemoteDescription(new RTCSessionDescription(message.payload)); + console.log('[ConnectionCore] ✅ answer 处理完成'); + } else { + console.warn('[ConnectionCore] ⚠️ PeerConnection状态异常:', signalingState); + } + } + } catch (error) { + console.error('[ConnectionCore] ❌ 处理answer失败:', error); + if (error instanceof Error && error.message.includes('Failed to set local answer sdp')) { + console.warn('[ConnectionCore] ⚠️ Answer处理失败,可能是连接状态变化导致的'); + // 清理连接状态,让客户端重新连接 + stateManager.updateState({ error: 'WebRTC连接状态异常,请重新连接', isPeerConnected: false }); } - } catch (err) { - console.warn('[ConnectionCore] ⚠️ 添加 ICE 候选失败:', err); } - } else { - console.warn('[ConnectionCore] ⚠️ ICE候选无效或PeerConnection不存在'); - } - break; + break; - case 'error': - console.error('[ConnectionCore] ❌ 信令服务器错误:', message.error); - stateManager.updateState({ error: message.error, isConnecting: false, canRetry: true }); - break; + case 'ice-candidate': + let pcIce = pcRef.current; + if (!pcIce) { + console.log('[ConnectionCore] 🔧 PeerConnection不存在,先创建它'); + pcIce = createPeerConnection(ws, role, reconnectState.isReconnect); - case 'disconnection': - console.log('[ConnectionCore] 🔌 对方主动断开连接'); - // 对方断开连接的处理 - stateManager.updateState({ - isPeerConnected: false, - isConnected: false, // 添加这个状态 - error: '对方已离开房间', - canRetry: true - }); - // 清理P2P连接但保持WebSocket连接,允许重新连接 - if (pcRef.current) { - pcRef.current.close(); - pcRef.current = null; - } - // 调用断开连接回调,通知上层应用清除数据 - if (onDisconnectCallback.current) { - console.log('[ConnectionCore] 📞 调用断开连接回调'); - onDisconnectCallback.current(); - } - break; + // 等待一小段时间确保PeerConnection完全初始化 + await new Promise(resolve => setTimeout(resolve, 100)); + } - default: - console.warn('[ConnectionCore] ⚠️ 未知消息类型:', message.type); + if (pcIce && message.payload) { + try { + // 即使远程描述未设置,也可以先缓存ICE候选 + if (pcIce.remoteDescription) { + await pcIce.addIceCandidate(new RTCIceCandidate(message.payload)); + console.log('[ConnectionCore] ✅ 添加 ICE 候选成功'); + } else { + console.log('[ConnectionCore] 📝 远程描述未设置,缓存ICE候选'); + // 可以在这里实现ICE候选缓存机制,等远程描述设置后再添加 + } + } catch (err) { + console.warn('[ConnectionCore] ⚠️ 添加 ICE 候选失败:', err); + } + } else { + console.warn('[ConnectionCore] ⚠️ ICE候选无效或PeerConnection不存在'); + } + break; + + case 'error': + const errorMessage = message.error || '信令服务器返回未知错误'; + console.error('[ConnectionCore] ❌ 信令服务器错误:', errorMessage); + stateManager.updateState({ error: errorMessage, isConnecting: false, canRetry: true }); + break; + + case 'disconnection': + console.log('[ConnectionCore] 🔌 对方主动断开连接'); + // 对方断开连接的处理 + stateManager.updateState({ + isPeerConnected: false, + isConnected: false, // 添加这个状态 + error: '对方已离开房间', + canRetry: true + }); + // 清理P2P连接但保持WebSocket连接,允许重新连接 + if (pcRef.current) { + pcRef.current.close(); + pcRef.current = null; + } + // 调用断开连接回调,通知上层应用清除数据 + if (onDisconnectCallback.current) { + console.log('[ConnectionCore] 📞 调用断开连接回调'); + onDisconnectCallback.current(); + } + break; + + default: + console.warn('[ConnectionCore] ⚠️ 未知消息类型:', message.type); + } + } catch (error) { + console.error('[ConnectionCore] ❌ 处理信令消息失败:', error); + stateManager.updateState({ error: '信令处理失败: ' + error, isConnecting: false, canRetry: true }); } - } catch (error) { - console.error('[ConnectionCore] ❌ 处理信令消息失败:', error); - stateManager.updateState({ error: '信令处理失败: ' + error, isConnecting: false, canRetry: true }); + }; + + // 对于外部WebSocket,需要设置错误和关闭事件处理器 + if (isExternalWebSocket.current) { + ws.onerror = (error: Event) => { + console.error('[ConnectionCore] ❌ WebSocket 错误:', error); + stateManager.updateState({ error: 'WebSocket连接失败', isConnecting: false, canRetry: true }); + + // 调用原始错误处理器 + if (originalOnError) { + originalOnError.call(ws, error); + } + }; + + ws.onclose = (event: CloseEvent) => { + console.log('[ConnectionCore] 🔌 WebSocket 连接已关闭, 代码:', event.code, '原因:', event.reason); + stateManager.updateState({ isWebSocketConnected: false }); + + // 检查是否是用户主动断开 + if (isUserDisconnecting.current) { + console.log('[ConnectionCore] ✅ 用户主动断开,正常关闭'); + // 用户主动断开时不显示错误消息 + return; + } + + // 只有在非正常关闭且不是用户主动断开时才显示错误 + if (event.code !== 1000 && event.code !== 1001) { // 非正常关闭 + stateManager.updateState({ error: `WebSocket异常关闭 (${event.code}): ${event.reason || '连接意外断开'}`, isConnecting: false, canRetry: true }); + } + + // 调用原始关闭处理器 + if (originalOnClose) { + originalOnClose.call(ws, event); + } + }; } - }; - - ws.onerror = (error) => { - console.error('[ConnectionCore] ❌ WebSocket 错误:', error); - stateManager.updateState({ error: 'WebSocket连接失败', isConnecting: false, canRetry: true }); - }; - - ws.onclose = (event) => { - console.log('[ConnectionCore] 🔌 WebSocket 连接已关闭, 代码:', event.code, '原因:', event.reason); - stateManager.updateState({ isWebSocketConnected: false }); - - // 检查是否是用户主动断开 - if (isUserDisconnecting.current) { - console.log('[ConnectionCore] ✅ 用户主动断开,正常关闭'); - // 用户主动断开时不显示错误消息 - return; - } - - // 只有在非正常关闭且不是用户主动断开时才显示错误 - if (event.code !== 1000 && event.code !== 1001) { // 非正常关闭 - stateManager.updateState({ error: `WebSocket异常关闭 (${event.code}): ${event.reason || '连接意外断开'}`, isConnecting: false, canRetry: true }); - } - }; + } } catch (error) { console.error('[ConnectionCore] 连接失败:', error); @@ -593,6 +634,13 @@ export function useWebRTCConnectionCore( onDisconnectCallback.current = callback; }, []); + // 动态注入 WebSocket 连接 + const injectWebSocket = useCallback((ws: WebSocket) => { + console.log('[ConnectionCore] 注入外部 WebSocket 连接'); + wsRef.current = ws; + isExternalWebSocket.current = true; + }, []); + return { connect, disconnect, @@ -601,5 +649,6 @@ export function useWebRTCConnectionCore( getWebSocket, getCurrentRoom, setOnDisconnectCallback, + injectWebSocket, }; } \ No newline at end of file diff --git a/chuan-next/src/hooks/connection/webrtc/useWebRTCDataChannelManager.ts b/chuan-next/src/hooks/connection/webrtc/useWebRTCDataChannelManager.ts index e927b36..0d7d838 100644 --- a/chuan-next/src/hooks/connection/webrtc/useWebRTCDataChannelManager.ts +++ b/chuan-next/src/hooks/connection/webrtc/useWebRTCDataChannelManager.ts @@ -53,7 +53,8 @@ export function useWebRTCDataChannelManager( error: null, isConnecting: false, canRetry: false, - state: 'open' + state: 'open', + stateMsg: "数据通道已打开" }); // 如果是重新连接,触发数据同步 diff --git a/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts b/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts index 8ce1687..f2c8dc8 100644 --- a/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts +++ b/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts @@ -1,12 +1,14 @@ import { useCallback, useEffect, useRef } from 'react'; +import { useWebConnectStateManager } from '../state/useWebConnectStateManager'; import { WebConnectState } from '../state/webConnectStore'; import { ConnectType, DataHandler, IWebConnection, IWebMessage, MessageHandler, Role } from '../types'; /** * WebSocket 连接管理器 * 实现 IWebConnection 接口,提供基于 WebSocket 的数据传输 + * 支持注入外部 WebSocket 连接 */ -export function useWebSocketConnection(): IWebConnection { +export function useWebSocketConnection(): IWebConnection & { injectWebSocket: (ws: WebSocket) => void } { const wsRef = useRef(null); const currentRoomRef = useRef<{ code: string; role: Role } | null>(null); @@ -17,96 +19,73 @@ export function useWebSocketConnection(): IWebConnection { // 断开连接回调 const onDisconnectCallback = useRef<(() => void) | null>(null); - // 连接状态 - const connectionState = useRef({ - isConnected: false, - isConnecting: false, - isWebSocketConnected: false, - isPeerConnected: false, - isDataChannelConnected: false, - isMediaStreamConnected: false, - currentConnectType: 'websocket', - state: 'closed', - error: null, - canRetry: false, - currentRoom: null - }); + // 全局状态管理器 + const stateManager = useWebConnectStateManager(); - // 更新连接状态 + // 创建稳定的状态管理器引用,避免无限循环 + const stateManagerRef = useRef(stateManager); + stateManagerRef.current = stateManager; + + // 缓存上次的状态,用于比较是否真正改变 + const lastStateRef = useRef>({}); + + // 智能状态更新 - 只在状态真正改变时才更新,使用稳定引用 const updateState = useCallback((updates: Partial) => { - connectionState.current = { - ...connectionState.current, - ...updates - }; - }, []); + // 检查状态是否真正改变 + const hasChanged = Object.keys(updates).some(key => { + const typedKey = key as keyof WebConnectState; + return lastStateRef.current[typedKey] !== updates[typedKey]; + }); + + if (hasChanged) { + console.log('[WebSocket] 状态更新:', updates); + lastStateRef.current = { ...lastStateRef.current, ...updates }; + stateManagerRef.current.updateState(updates); + } else { + console.log('[WebSocket] 状态未改变,跳过更新:', updates); + } + }, []); // 空依赖数组,使用 ref 访问最新的 stateManager // 连接到房间 const connect = useCallback(async (roomCode: string, role: Role) => { - if (wsRef.current?.readyState === WebSocket.OPEN) { - console.log('[WebSocket] 已存在连接,先断开'); - disconnect(); + // 检查是否已经注入了 WebSocket + if (!wsRef.current) { + throw new Error('[WebSocket] 尚未注入 WebSocket 连接,请先调用 injectWebSocket'); + } + + const ws = wsRef.current; + + // 检查 WebSocket 状态 + if (ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) { + throw new Error('[WebSocket] 注入的 WebSocket 连接已关闭'); } updateState({ isConnecting: true, error: null, canRetry: false }); currentRoomRef.current = { code: roomCode, role }; try { - // 构建 WebSocket URL - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const host = window.location.host; - const wsUrl = `${protocol}//${host}/api/ws/${roomCode}?role=${role}`; + console.log('[WebSocket] 使用注入的 WebSocket 连接到房间:', roomCode, '角色:', role); - console.log('[WebSocket] 连接到:', wsUrl); - - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - // 连接成功 - ws.onopen = () => { - console.log('[WebSocket] 连接成功'); + // 如果 WebSocket 已经连接,直接更新状态 + if (ws.readyState === WebSocket.OPEN) { + console.log('[WebSocket] WebSocket 已连接,直接设置为已连接状态'); updateState({ isConnected: true, isConnecting: false, isWebSocketConnected: true, + isPeerConnected: true, // 欺骗 UI,让 WebRTC 相关功能正常工作 + isDataChannelConnected: true, // 欺骗 UI,WebSocket 也能传输数据 + isMediaStreamConnected: true, // 欺骗 UI,保证所有功能可用 + state: 'open', // RTCDataChannelState.open error: null, canRetry: false }); - }; - - // 接收消息 - ws.onmessage = (event) => { - handleMessage(event); - }; - - // 连接错误 - ws.onerror = (error) => { - console.error('[WebSocket] 连接错误:', error); - updateState({ - isConnected: false, - isConnecting: false, - isWebSocketConnected: false, - error: 'WebSocket 连接失败', - canRetry: true - }); - }; - - // 连接关闭 - ws.onclose = (event) => { - console.log('[WebSocket] 连接关闭:', event.code, event.reason); - updateState({ - isConnected: false, - isConnecting: false, - isWebSocketConnected: false, - error: event.wasClean ? null : 'WebSocket 连接意外断开', - canRetry: !event.wasClean - }); - - // 调用断开连接回调 - if (onDisconnectCallback.current) { - console.log('[WebSocket] 调用断开连接回调'); - onDisconnectCallback.current(); - } - }; + } else if (ws.readyState === WebSocket.CONNECTING) { + console.log('[WebSocket] WebSocket 正在连接中,等待连接完成'); + // WebSocket 正在连接中,等待 onopen 事件 + } else { + throw new Error('[WebSocket] WebSocket 状态异常: ' + ws.readyState); + } } catch (error) { console.error('[WebSocket] 连接异常:', error); @@ -114,33 +93,28 @@ export function useWebSocketConnection(): IWebConnection { isConnected: false, isConnecting: false, isWebSocketConnected: false, - error: '无法建立 WebSocket 连接', + isPeerConnected: false, // 重置所有 WebRTC 相关状态 + isDataChannelConnected: false, + isMediaStreamConnected: false, + state: 'closed', // RTCDataChannelState.closed + error: error instanceof Error ? error.message : '无法使用注入的 WebSocket 连接', canRetry: true }); + throw error; } }, [updateState]); // 处理收到的消息 - const handleMessage = useCallback((event: MessageEvent) => { + const handleMessage = useCallback(async (event: MessageEvent) => { try { - if (typeof event.data === 'string') { - // JSON 消息 - const message = JSON.parse(event.data) as IWebMessage; - console.log('[WebSocket] 收到消息:', message.type, message.channel || 'default'); + console.log('[WebSocket] 收到消息事件:', typeof event.data, event.data.constructor?.name, + event.data instanceof ArrayBuffer ? `ArrayBuffer ${event.data.byteLength} bytes` : + event.data instanceof Blob ? `Blob ${event.data.size} bytes` : 'JSON'); - // 根据通道分发消息 - if (message.channel) { - const handler = messageHandlers.current.get(message.channel); - if (handler) { - handler(message); - } - } else { - // 广播给所有处理器 - messageHandlers.current.forEach(handler => handler(message)); - } - } else if (event.data instanceof ArrayBuffer) { - // 二进制数据 - console.log('[WebSocket] 收到二进制数据:', event.data.byteLength, 'bytes'); + // 处理二进制数据 - 支持 ArrayBuffer 和 Blob + if (event.data instanceof ArrayBuffer) { + // 直接的 ArrayBuffer 数据 + console.log('[WebSocket] 收到 ArrayBuffer 数据:', event.data.byteLength, 'bytes'); // 优先发给文件传输处理器 const fileHandler = dataHandlers.current.get('file-transfer'); @@ -153,6 +127,75 @@ export function useWebSocketConnection(): IWebConnection { firstHandler(event.data); } } + } else if (event.data instanceof Blob) { + // Blob 数据,需要转换为 ArrayBuffer + console.log('[WebSocket] 收到 Blob 数据:', event.data.size, 'bytes,正在转换为 ArrayBuffer'); + + try { + const arrayBuffer = await event.data.arrayBuffer(); + console.log('[WebSocket] Blob 转换完成,ArrayBuffer 大小:', arrayBuffer.byteLength, 'bytes'); + + // 优先发给文件传输处理器 + const fileHandler = dataHandlers.current.get('file-transfer'); + if (fileHandler) { + fileHandler(arrayBuffer); + } else { + // 发给第一个处理器 + const firstHandler = dataHandlers.current.values().next().value; + if (firstHandler) { + firstHandler(arrayBuffer); + } + } + } catch (blobError) { + console.error('[WebSocket] Blob 转换为 ArrayBuffer 失败:', blobError); + } + } else if (typeof event.data === 'string') { + // JSON 消息 + const message = JSON.parse(event.data) as IWebMessage; + + // 特殊处理 disconnection 消息 - 与 WebRTC 保持一致 + if (message.type === 'disconnection') { + console.log('[WebSocket] 🔌 对方主动断开连接'); + // 更新连接状态 + updateState({ + isPeerConnected: false, + isConnected: false, + error: '对方已离开房间', + stateMsg: null, + canRetry: true + }); + + // 调用断开连接回调,通知上层应用清除数据 + if (onDisconnectCallback.current) { + console.log('[WebSocket] 📞 调用断开连接回调'); + onDisconnectCallback.current(); + } + } + if (message.type === 'peer-joined') { + console.log('[WebSocket] 🎉 对方加入房间') + updateState({ + isPeerConnected: true, + isConnected: true, + isWebSocketConnected: true, + currentConnectType: 'websocket', + error: null, + stateMsg: '对方已经加入房间', + canRetry: true + }); + } + + // 根据通道分发消息 + if (message.channel) { + const handler = messageHandlers.current.get(message.channel); + if (handler) { + handler(message); + } + } else { + // 广播给所有处理器 + messageHandlers.current.forEach(handler => handler(message)); + } + } else { + console.warn('[WebSocket] 收到未知数据类型:', typeof event.data, event.data.constructor?.name, event.data); } } catch (error) { console.error('[WebSocket] 处理消息失败:', error); @@ -171,6 +214,10 @@ export function useWebSocketConnection(): IWebConnection { isConnected: false, isConnecting: false, isWebSocketConnected: false, + isPeerConnected: false, // 重置所有 WebRTC 相关状态 + isDataChannelConnected: false, + isMediaStreamConnected: false, + state: 'closed', // RTCDataChannelState.closed error: null, canRetry: false }); @@ -213,7 +260,6 @@ export function useWebSocketConnection(): IWebConnection { try { ws.send(data); - console.log('[WebSocket] 发送二进制数据:', data.byteLength, 'bytes'); return true; } catch (error) { console.error('[WebSocket] 发送数据失败:', error); @@ -245,14 +291,12 @@ export function useWebSocketConnection(): IWebConnection { // 获取连接状态 const getConnectState = useCallback((): WebConnectState => { - return { ...connectionState.current }; + return { ...stateManagerRef.current.getState() }; }, []); // 检查是否连接到指定房间 const isConnectedToRoom = useCallback((roomCode: string, role: Role) => { - return currentRoomRef.current?.code === roomCode && - currentRoomRef.current?.role === role && - connectionState.current.isConnected; + return stateManagerRef.current.isConnectedToRoom(roomCode, role); }, []); // 媒体轨道方法(WebSocket 不支持,返回 null) @@ -279,6 +323,91 @@ export function useWebSocketConnection(): IWebConnection { return false; }, []); + // 注入外部 WebSocket 连接 + const injectWebSocket = useCallback((ws: WebSocket) => { + console.log('[WebSocket] 注入外部 WebSocket 连接'); + + // 如果已有连接,先断开 + if (wsRef.current) { + wsRef.current.close(); + } + + wsRef.current = ws; + + // 设置事件处理器 + ws.onopen = () => { + console.log('[WebSocket] 注入的 WebSocket 连接成功'); + updateState({ + currentConnectType: 'websocket', + isConnected: true, + isConnecting: false, + isWebSocketConnected: true, + isPeerConnected: true, // 欺骗 UI,让 WebRTC 相关功能正常工作 + isDataChannelConnected: true, // 欺骗 UI,WebSocket 也能传输数据 + isMediaStreamConnected: true, // 欺骗 UI,保证所有功能可用 + state: 'open', // RTCDataChannelState.open + error: null, + canRetry: false, + }); + }; + + ws.onmessage = (event) => { + handleMessage(event); + }; + + ws.onerror = (error) => { + console.error('[WebSocket] 注入的 WebSocket 连接错误:', error); + updateState({ + isConnected: false, + isConnecting: false, + isWebSocketConnected: false, + isPeerConnected: false, // 重置所有 WebRTC 相关状态 + isDataChannelConnected: false, + isMediaStreamConnected: false, + state: 'closed', // RTCDataChannelState.closed + error: 'WebSocket 连接失败', + canRetry: true + }); + }; + + ws.onclose = (event) => { + console.log('[WebSocket] 注入的 WebSocket 连接关闭:', event.code, event.reason); + updateState({ + isConnected: false, + isConnecting: false, + isWebSocketConnected: false, + isPeerConnected: false, // 重置所有 WebRTC 相关状态 + isDataChannelConnected: false, + isMediaStreamConnected: false, + state: 'closed', // RTCDataChannelState.closed + error: event.wasClean ? null : 'WebSocket 连接意外断开', + canRetry: !event.wasClean + }); + + // 调用断开连接回调 + if (onDisconnectCallback.current) { + console.log('[WebSocket] 调用断开连接回调'); + onDisconnectCallback.current(); + } + }; + + // 如果 WebSocket 已经连接,立即更新状态 + if (ws.readyState === WebSocket.OPEN) { + console.log('[WebSocket] 注入的 WebSocket 已连接,立即更新状态'); + updateState({ + isConnected: true, + isConnecting: false, + isWebSocketConnected: true, + isPeerConnected: true, // 欺骗 UI,让 WebRTC 相关功能正常工作 + isDataChannelConnected: true, // 欺骗 UI,WebSocket 也能传输数据 + isMediaStreamConnected: true, // 欺骗 UI,保证所有功能可用 + state: 'open', // RTCDataChannelState.open + error: null, + canRetry: false + }); + } + }, [handleMessage, updateState]); + // 设置断开连接回调 const setOnDisconnectCallback = useCallback((callback: () => void) => { onDisconnectCallback.current = callback; @@ -287,9 +416,15 @@ export function useWebSocketConnection(): IWebConnection { // 清理连接 useEffect(() => { return () => { - disconnect(); + // 清理时直接关闭 WebSocket,不调用 disconnect 避免状态更新循环 + if (wsRef.current) { + console.log('[WebSocket] 组件卸载,清理 WebSocket 连接'); + wsRef.current.close(1000, '组件卸载'); + wsRef.current = null; + } + currentRoomRef.current = null; }; - }, [disconnect]); + }, []); // 空依赖数组,只在组件挂载和卸载时执行 return { connectType: 'websocket' as ConnectType, @@ -309,5 +444,6 @@ export function useWebSocketConnection(): IWebConnection { getPeerConnection, createOfferNow, setOnDisconnectCallback, + injectWebSocket, }; } \ No newline at end of file diff --git a/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts b/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts index 109892d..94c9d4c 100644 --- a/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts +++ b/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { useSharedWebRTCManager } from '../connection/useConnectManager'; +import { useConnectManager } from '../connection'; interface DesktopShareState { isSharing: boolean; @@ -11,7 +11,7 @@ interface DesktopShareState { } export function useDesktopShareBusiness() { - const webRTC = useSharedWebRTCManager(); + const webRTC = useConnectManager(); const [state, setState] = useState({ isSharing: false, isViewing: false, @@ -46,14 +46,14 @@ export function useDesktopShareBusiness() { webRTC.onTrack((event: RTCTrackEvent) => { console.log('[DesktopShare] 🎥 收到远程轨道:', event.track.kind, event.track.id, '状态:', event.track.readyState); console.log('[DesktopShare] 远程流数量:', event.streams.length); - + if (event.streams.length > 0) { const remoteStream = event.streams[0]; console.log('[DesktopShare] 🎬 设置远程流,轨道数量:', remoteStream.getTracks().length); remoteStream.getTracks().forEach(track => { console.log('[DesktopShare] 远程轨道:', track.kind, track.id, '启用:', track.enabled, '状态:', track.readyState); }); - + // 确保轨道已启用 remoteStream.getTracks().forEach(track => { if (!track.enabled) { @@ -61,7 +61,7 @@ export function useDesktopShareBusiness() { track.enabled = true; } }); - + handleRemoteStream(remoteStream); } else { console.warn('[DesktopShare] ⚠️ 收到轨道但没有关联的流'); @@ -69,7 +69,7 @@ export function useDesktopShareBusiness() { try { const newStream = new MediaStream([event.track]); console.log('[DesktopShare] 🔄 从轨道创建新流:', newStream.id); - + // 确保轨道已启用 newStream.getTracks().forEach(track => { if (!track.enabled) { @@ -77,7 +77,7 @@ export function useDesktopShareBusiness() { track.enabled = true; } }); - + handleRemoteStream(newStream); } catch (error) { console.error('[DesktopShare] ❌ 从轨道创建流失败:', error); @@ -112,27 +112,27 @@ export function useDesktopShareBusiness() { // 设置视频轨道发送 const setupVideoSending = useCallback(async (stream: MediaStream) => { console.log('[DesktopShare] 🎬 开始设置视频轨道发送...'); - + // 检查P2P连接状态 if (!webRTC.getConnectState().isPeerConnected) { console.warn('[DesktopShare] ⚠️ P2P连接尚未完全建立,等待连接稳定...'); // 等待连接稳定 await new Promise(resolve => setTimeout(resolve, 1000)); - + // 再次检查 if (!webRTC.getConnectState().isPeerConnected) { console.error('[DesktopShare] ❌ P2P连接仍未建立,无法开始媒体传输'); throw new Error('P2P连接尚未建立'); } } - + // 移除之前的轨道(如果存在) if (currentSenderRef.current) { console.log('[DesktopShare] 🗑️ 移除之前的视频轨道'); webRTC.removeTrack(currentSenderRef.current); currentSenderRef.current = null; } - + // 添加新的视频轨道到PeerConnection const videoTrack = stream.getVideoTracks()[0]; const audioTrack = stream.getAudioTracks()[0]; @@ -169,7 +169,7 @@ export function useDesktopShareBusiness() { // 轨道添加完成,现在需要重新协商以包含媒体轨道 console.log('[DesktopShare] ✅ 桌面共享轨道添加完成,开始重新协商'); - + // 获取PeerConnection实例以便调试 const pc = webRTC.getPeerConnection(); if (pc) { @@ -180,20 +180,20 @@ export function useDesktopShareBusiness() { senders: pc.getSenders().length }); } - + // 等待一小段时间确保轨道完全添加 await new Promise(resolve => setTimeout(resolve, 500)); - + // 创建新的offer包含媒体轨道 console.log('[DesktopShare] 📨 创建包含媒体轨道的新offer进行重新协商'); const success = await webRTC.createOfferNow(); if (success) { console.log('[DesktopShare] ✅ 媒体轨道重新协商成功'); - + // 等待重新协商完成 console.log('[DesktopShare] ⏳ 等待重新协商完成...'); await new Promise(resolve => setTimeout(resolve, 2000)); - + // 检查连接状态 if (pc) { console.log('[DesktopShare] 🔍 重新协商后连接状态:', { @@ -233,7 +233,7 @@ export function useDesktopShareBusiness() { }); const data = await response.json(); - + if (!response.ok) { throw new Error(data.error || '创建房间失败'); } @@ -283,12 +283,12 @@ export function useDesktopShareBusiness() { // 获取桌面流 const stream = await getDesktopStream(); - + // 停止之前的流(如果有)- 与switchDesktop保持一致 if (localStreamRef.current) { localStreamRef.current.getTracks().forEach(track => track.stop()); } - + localStreamRef.current = stream; console.log('[DesktopShare] ✅ 桌面流获取成功'); @@ -306,13 +306,13 @@ export function useDesktopShareBusiness() { const errorMessage = error instanceof Error ? error.message : '开始桌面共享失败'; console.error('[DesktopShare] ❌ 开始共享失败:', error); updateState({ error: errorMessage, isSharing: false }); - + // 清理资源 if (localStreamRef.current) { localStreamRef.current.getTracks().forEach(track => track.stop()); localStreamRef.current = null; } - + throw error; } }, [webRTC, getDesktopStream, setupVideoSending, updateState]); @@ -333,12 +333,12 @@ export function useDesktopShareBusiness() { // 获取新的桌面流 const newStream = await getDesktopStream(); - + // 停止之前的流 if (localStreamRef.current) { localStreamRef.current.getTracks().forEach(track => track.stop()); } - + localStreamRef.current = newStream; console.log('[DesktopShare] ✅ 新桌面流获取成功'); @@ -454,7 +454,7 @@ export function useDesktopShareBusiness() { updateState({ isViewing: true }); console.log('[DesktopShare] 👁️ 已进入桌面共享观看模式,等待接收流...'); - + // 设置一个超时检查,如果长时间没有收到流,输出警告 setTimeout(() => { if (!state.remoteStream) { @@ -536,7 +536,7 @@ export function useDesktopShareBusiness() { // WebRTC连接状态 webRTCError: webRTC.getConnectState().error, - + // 暴露WebRTC连接对象 webRTCConnection: webRTC, }; diff --git a/chuan-next/src/hooks/file-transfer/useFileStateManager.ts b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts index 9c778da..a044f7c 100644 --- a/chuan-next/src/hooks/file-transfer/useFileStateManager.ts +++ b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts @@ -93,7 +93,6 @@ export const useFileStateManager = ({ const newStatus = progress >= 100 ? 'completed' as const : 'downloading' as const; setFileList(prev => prev.map(item => { if (item.id === fileId || item.name === fileName) { - console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${progress}${transferSpeed ? `, 速度: ${transferSpeed} B/s` : ''}`); return { ...item, progress, diff --git a/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts index 72ebded..d9b594c 100644 --- a/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts +++ b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts @@ -76,11 +76,20 @@ type FileProgressCallback = (progressInfo: { fileId: string; fileName: string; p type FileListReceivedCallback = (fileList: FileInfo[]) => void; const CHANNEL_NAME = 'file-transfer'; -const CHUNK_SIZE = 256 * 1024; // 256KB +const WEBRTC_CHUNK_SIZE = 256 * 1024; // 256KB for WebRTC +const WEBSOCKET_CHUNK_SIZE = 3 * 1024 * 1024; // 3MB for WebSocket const MAX_RETRIES = 5; // 最大重试次数 const RETRY_DELAY = 1000; // 重试延迟(毫秒) const ACK_TIMEOUT = 5000; // 确认超时(毫秒) +/** + * 根据连接类型获取块大小 + */ +function getChunkSize(connectType: string): number { + return connectType === 'websocket' ? WEBSOCKET_CHUNK_SIZE : WEBRTC_CHUNK_SIZE; +} + + /** * 计算数据的CRC32校验和 */ @@ -98,19 +107,6 @@ function calculateChecksum(data: ArrayBuffer): string { return (crc ^ 0xFFFFFFFF).toString(16).padStart(8, '0'); } -/** - * 生成简单的校验和(备用方案) - */ -function simpleChecksum(data: ArrayBuffer): string { - const buffer = new Uint8Array(data); - let sum = 0; - - for (let i = 0; i < Math.min(buffer.length, 1000); i++) { - sum += buffer[i]; - } - - return sum.toString(16); -} /** * 文件传输业务层 @@ -174,7 +170,7 @@ export function useFileTransferBusiness(connection: IWebConnection) { }); // 初始化接收进度跟踪 - const totalChunks = Math.ceil(metadata.size / CHUNK_SIZE); + const totalChunks = Math.ceil(metadata.size / getChunkSize(connection.connectType)); receiveProgress.current.set(metadata.id, { fileId: metadata.id, fileName: metadata.name, @@ -453,9 +449,10 @@ export function useFileTransferBusiness(connection: IWebConnection) { } const actualFileId = fileId || `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + const chunkSize = getChunkSize(connection.connectType); + const totalChunks = Math.ceil(file.size / chunkSize); - console.log('开始安全发送文件:', file.name, '文件ID:', actualFileId, '总块数:', totalChunks); + console.log('开始安全发送文件:', file.name, '文件ID:', actualFileId, '总块数:', totalChunks, '块大小:', chunkSize); updateState({ isTransferring: true, progress: 0, error: null }); @@ -503,8 +500,8 @@ export function useFileTransferBusiness(connection: IWebConnection) { console.warn(`WebRTC 连接暂时断开,但数据通道正在连接,继续尝试发送文件块 ${chunkIndex}`); } - const start = chunkIndex * CHUNK_SIZE; - const end = Math.min(start + CHUNK_SIZE, file.size); + const start = chunkIndex * chunkSize; + const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const arrayBuffer = await chunk.arrayBuffer(); const checksum = calculateChecksum(arrayBuffer); @@ -555,8 +552,8 @@ export function useFileTransferBusiness(connection: IWebConnection) { // 自适应流控:根据传输速度调整发送间隔 if (status.averageSpeed > 0) { - const chunkSize = Math.min(CHUNK_SIZE, file.size - chunkIndex * CHUNK_SIZE); - const expectedTime = (chunkSize / 1024) / status.averageSpeed; + const currentChunkSize = Math.min(chunkSize, file.size - chunkIndex * chunkSize); + const expectedTime = (currentChunkSize / 1024) / status.averageSpeed; const actualTime = Date.now() - status.lastChunkTime; const delay = Math.max(0, expectedTime - actualTime); diff --git a/chuan-next/src/hooks/ui/useTabNavigation.ts b/chuan-next/src/hooks/ui/useTabNavigation.ts index 45d2cc2..caabcfb 100644 --- a/chuan-next/src/hooks/ui/useTabNavigation.ts +++ b/chuan-next/src/hooks/ui/useTabNavigation.ts @@ -1,6 +1,6 @@ import { useSearchParams } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; -import { useSharedWebRTCManager } from '../connection'; +import { useConnectManager } from '../connection'; import { useWebRTCStore } from '../connection/state/webConnectStore'; import { useConfirmDialog } from './useConfirmDialog'; import { FeatureType, useURLHandler } from './useURLHandler'; @@ -29,33 +29,33 @@ export const useTabNavigation = () => { const [activeTab, setActiveTab] = useState('webrtc'); const [hasInitialized, setHasInitialized] = useState(false); const { showConfirmDialog, dialogState, closeDialog } = useConfirmDialog(); - + // 获取WebRTC全局状态 - const { - isConnected, - isConnecting, + const { + isConnected, + isConnecting, isPeerConnected, currentRoom, } = useWebRTCStore(); // 获取WebRTC连接管理器 - const { disconnect: disconnectWebRTC } = useSharedWebRTCManager(); + const { disconnect: disconnectWebRTC } = useConnectManager(); // 创建一个通用的URL处理器(用于断开连接) const { hasActiveConnection } = useURLHandler({ featureType: 'webrtc', // 默认值,实际使用时会被覆盖 - onModeChange: () => {}, + onModeChange: () => { }, }); // 根据URL参数设置初始标签(仅首次加载时) useEffect(() => { if (!hasInitialized) { const urlType = searchParams.get('type'); - + console.log('=== HomePage URL处理 ==='); console.log('URL type参数:', urlType); console.log('所有搜索参数:', Object.fromEntries(searchParams.entries())); - + // 将旧的text类型重定向到message if (urlType === 'text') { console.log('检测到text类型,重定向到message标签页'); @@ -70,7 +70,7 @@ export const useTabNavigation = () => { console.log('没有有效的type参数,使用默认标签页:webrtc(文件传输)'); // 保持默认的webrtc标签 } - + setHasInitialized(true); } }, [searchParams, hasInitialized]); @@ -79,7 +79,7 @@ export const useTabNavigation = () => { const handleTabChange = useCallback(async (newTab: TabType) => { console.log('=== Tab切换 ==='); console.log('当前tab:', activeTab, '目标tab:', newTab); - + // 对于任何非WebRTC功能的tab(wechat、settings),如果有活跃连接需要确认 if ((newTab === 'wechat' || newTab === 'settings') && hasActiveConnection()) { const currentTabName = TAB_NAMES[activeTab]; @@ -91,15 +91,15 @@ export const useTabNavigation = () => { cancelText: '取消', type: 'warning' }); - + if (!confirmed) { return false; } - + // 断开连接并清除状态 disconnectWebRTC(); console.log(`已清除WebRTC连接状态,切换到${targetTabName}`); - + setActiveTab(newTab); // 清除URL参数 const newUrl = new URL(window.location.href); @@ -122,7 +122,7 @@ export const useTabNavigation = () => { if (hasActiveConnection() && newTab !== activeTab && WEBRTC_FEATURES[newTab]) { const currentTabName = TAB_NAMES[activeTab]; const targetTabName = TAB_NAMES[newTab]; - + const confirmed = await showConfirmDialog({ title: '切换功能确认', message: `切换到${targetTabName}功能需要关闭当前的${currentTabName}连接,是否继续?`, @@ -130,7 +130,7 @@ export const useTabNavigation = () => { cancelText: '取消', type: 'warning' }); - + if (!confirmed) { return false; } @@ -142,7 +142,7 @@ export const useTabNavigation = () => { // 执行tab切换 setActiveTab(newTab); - + // 更新URL(对于WebRTC功能) if (WEBRTC_FEATURES[newTab]) { const params = new URLSearchParams(); @@ -156,7 +156,7 @@ export const useTabNavigation = () => { newUrl.search = ''; window.history.pushState({}, '', newUrl.toString()); } - + return true; }, [activeTab, hasActiveConnection, disconnectWebRTC]); diff --git a/internal/services/webrtc_service.go b/internal/services/webrtc_service.go index 6bd92c7..49ad5e5 100644 --- a/internal/services/webrtc_service.go +++ b/internal/services/webrtc_service.go @@ -1,6 +1,7 @@ package services import ( + "encoding/json" "fmt" "log" "math/rand" @@ -146,18 +147,36 @@ func (ws *WebRTCService) HandleWebSocket(w http.ResponseWriter, r *http.Request) // 处理消息 for { - var msg WebRTCMessage - err := conn.ReadJSON(&msg) + // 首先读取原始消息类型和数据 + messageType, data, err := conn.ReadMessage() if err != nil { log.Printf("读取WebRTC WebSocket消息失败: %v", err) break } - msg.From = clientID - log.Printf("收到WebRTC信令: 类型=%s, 来自=%s, 房间=%s", msg.Type, clientID, code) + if messageType == websocket.TextMessage { + // 文本消息,尝试解析为JSON + var msg WebRTCMessage + if err := json.Unmarshal(data, &msg); err != nil { + log.Printf("解析WebRTC JSON消息失败: %v", err) + continue + } - // 转发信令消息给对方 - ws.forwardMessage(code, clientID, &msg) + msg.From = clientID + log.Printf("收到WebRTC信令: 类型=%s, 来自=%s, 房间=%s", msg.Type, clientID, code) + + // 转发信令消息给对方 + ws.forwardMessage(code, clientID, &msg) + + } else if messageType == websocket.BinaryMessage { + // 二进制消息,直接转发 + log.Printf("收到WebRTC二进制数据: 大小=%d bytes, 来自=%s, 房间=%s", len(data), clientID, code) + + // 转发二进制数据给对方 + ws.forwardBinaryMessage(code, clientID, data) + } else { + log.Printf("收到未知消息类型: %d", messageType) + } } } @@ -259,6 +278,37 @@ func (ws *WebRTCService) forwardMessage(roomCode string, fromClientID string, ms } } +// 转发二进制消息 +func (ws *WebRTCService) forwardBinaryMessage(roomCode string, fromClientID string, data []byte) { + ws.roomsMux.Lock() + defer ws.roomsMux.Unlock() + + room := ws.rooms[roomCode] + if room == nil { + return + } + + var targetClient *WebRTCClient + if room.Sender != nil && room.Sender.ID == fromClientID { + // 消息来自sender,转发给receiver + targetClient = room.Receiver + } else if room.Receiver != nil && room.Receiver.ID == fromClientID { + // 消息来自receiver,转发给sender + targetClient = room.Sender + } + + if targetClient != nil && targetClient.Connection != nil { + err := targetClient.Connection.WriteMessage(websocket.BinaryMessage, data) + if err != nil { + log.Printf("转发WebRTC二进制数据失败: %v", err) + } else { + log.Printf("转发WebRTC二进制数据: 大小=%d bytes, 从=%s到=%s", len(data), fromClientID, targetClient.ID) + } + } else { + log.Printf("目标客户端不在线,无法转发二进制数据") + } +} + // CreateRoom 创建或获取房间 func (ws *WebRTCService) CreateRoom(code string) { ws.roomsMux.Lock()