diff --git a/chuan-next/src/components/WebRTCFileTransfer.tsx b/chuan-next/src/components/WebRTCFileTransfer.tsx index eb1a9fd..d77ad18 100644 --- a/chuan-next/src/components/WebRTCFileTransfer.tsx +++ b/chuan-next/src/components/WebRTCFileTransfer.tsx @@ -51,7 +51,8 @@ export const WebRTCFileTransfer: React.FC = () => { onFileReceived, onFileListReceived, onFileRequested, - onFileProgress + onFileProgress, + clearSenderData } = useFileTransferBusiness(stableConnection); // 使用自定义 hooks @@ -75,7 +76,8 @@ export const WebRTCFileTransfer: React.FC = () => { clearFiles, resetFiles, updateFileStatus, - updateFileProgress + updateFileProgress, + clearSenderData: clearFileStateData } = useFileStateManager({ mode, pickupCode, @@ -413,20 +415,69 @@ export const WebRTCFileTransfer: React.FC = () => { // 监听P2P连接建立时的状态变化 useEffect(() => { - if (connection.getConnectState().isPeerConnected && mode === 'send' && fileList.length > 0) { - console.log('P2P连接已建立,数据通道首次打开,初始化文件列表'); + const connectState = connection.getConnectState(); + const isPeerConnected = connectState.isPeerConnected; + const isDataChannelConnected = connectState.isDataChannelConnected; + const isChannelOpen = connectState.state === 'open'; + const isConnected = connectState.isConnected; + + // 使用更宽松的条件检查连接状态 + const isReady = isPeerConnected || isDataChannelConnected || isChannelOpen || isConnected; + + if (isReady && mode === 'send' && fileList.length > 0) { + console.log('连接已建立,初始化文件列表:', { + isPeerConnected, + isDataChannelConnected, + isChannelOpen, + isConnected, + fileListLength: fileList.length + }); // 数据通道第一次打开时进行初始化 syncFileListToReceiver(fileList, '数据通道初始化'); } - }, [connection.getConnectState().isPeerConnected, mode, syncFileListToReceiver]); + }, [connection.getConnectState().isPeerConnected, connection.getConnectState().isDataChannelConnected, connection.getConnectState().state, connection.getConnectState().isConnected, mode, fileList.length, syncFileListToReceiver]); // 监听fileList大小变化并同步 useEffect(() => { - if (connection.getConnectState().isPeerConnected && mode === 'send' && pickupCode) { - console.log('fileList大小变化,同步到接收方:', fileList.length); + const connectState = connection.getConnectState(); + const isPeerConnected = connectState.isPeerConnected; + const isDataChannelConnected = connectState.isDataChannelConnected; + const isChannelOpen = connectState.state === 'open'; + const isConnected = connectState.isConnected; + + // 使用更宽松的条件检查连接状态 + const isReady = isPeerConnected || isDataChannelConnected || isChannelOpen || isConnected; + + if (isReady && mode === 'send' && pickupCode) { + console.log('fileList大小变化,同步到接收方:', { + fileListLength: fileList.length, + isPeerConnected, + isDataChannelConnected, + isChannelOpen, + isConnected + }); syncFileListToReceiver(fileList, 'fileList大小变化'); } - }, [fileList.length, connection.getConnectState().isPeerConnected, mode, pickupCode, syncFileListToReceiver]); + }, [fileList.length, connection.getConnectState().isPeerConnected, connection.getConnectState().isDataChannelConnected, connection.getConnectState().state, connection.getConnectState().isConnected, mode, pickupCode, syncFileListToReceiver]); + + // 监听接收方离开房间事件 + useEffect(() => { + const connectState = connection.getConnectState(); + const isPeerConnected = connectState.isPeerConnected; + const isConnected = connectState.isConnected; + + // 当接收方离开房间时(P2P连接断开),清除发送方数据 + if (mode === 'send' && pickupCode && !isPeerConnected && !isConnected) { + console.log('[WebRTCFileTransfer] 检测到接收方离开房间,清除发送方数据'); + + // 清除文件传输业务逻辑中的数据 + clearSenderData(); + + // 清除文件状态管理器中的数据 + clearFileStateData(); + + } + }, [connection.getConnectState().isPeerConnected, connection.getConnectState().isConnected, mode, pickupCode, clearSenderData, clearFileStateData, showToast]); // 监听selectedFiles变化,同步更新fileList并发送给接收方 useEffect(() => { diff --git a/chuan-next/src/hooks/connection/types.ts b/chuan-next/src/hooks/connection/types.ts index 66e1e2f..f7b15ad 100644 --- a/chuan-next/src/hooks/connection/types.ts +++ b/chuan-next/src/hooks/connection/types.ts @@ -39,8 +39,6 @@ export interface IWebConnection extends IRegisterEventHandler, IGetConnectState sendMessage: (message: IWebMessage, channel?: string) => boolean; sendData: (data: ArrayBuffer) => boolean; - - // 工具方法 getConnectState: () => WebConnectState; isConnectedToRoom: (roomCode: string, role: Role) => boolean; @@ -54,6 +52,9 @@ export interface IWebConnection extends IRegisterEventHandler, IGetConnectState onTrack: (callback: (event: RTCTrackEvent) => void) => void; getPeerConnection: () => RTCPeerConnection | null; createOfferNow: () => Promise; + + // 断开连接回调 + setOnDisconnectCallback: (callback: () => void) => void; } diff --git a/chuan-next/src/hooks/connection/useConnectManager.ts b/chuan-next/src/hooks/connection/useConnectManager.ts index a3090f0..249ea85 100644 --- a/chuan-next/src/hooks/connection/useConnectManager.ts +++ b/chuan-next/src/hooks/connection/useConnectManager.ts @@ -163,6 +163,11 @@ export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler return currentConnectionRef.current.createOfferNow(); }, []); + // 设置断开连接回调 + const setOnDisconnectCallback = useCallback((callback: () => void) => { + currentConnectionRef.current.setOnDisconnectCallback(callback); + }, []); + // 扩展方法:切换连接类型 const switchToWebSocket = useCallback(() => { switchConnectionType('websocket'); @@ -236,6 +241,7 @@ export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler onTrack, getPeerConnection, createOfferNow, + setOnDisconnectCallback, // 扩展方法 switchToWebSocket, @@ -246,4 +252,4 @@ export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler switchToWebRTC: () => void; getConnectionStats: () => any; }; -} \ No newline at end of file +} diff --git a/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts b/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts index 22d8b89..60fac21 100644 --- a/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts +++ b/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts @@ -72,6 +72,9 @@ export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHan getPeerConnection: connectionCore.getPeerConnection, createOfferNow, + // 断开连接回调 + setOnDisconnectCallback: connectionCore.setOnDisconnectCallback, + // 当前房间信息 currentRoom: connectionCore.getCurrentRoom(), }; diff --git a/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts b/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts index 6813b0f..53e9bd2 100644 --- a/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts +++ b/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts @@ -12,21 +12,24 @@ import { Role, WebRTCDataChannelManager, WebRTCTrackManager } from '../types'; export interface WebRTCConnectionCore { // 连接到房间 connect: (roomCode: string, role: Role) => Promise; - + // 断开连接 disconnect: (shouldNotifyDisconnect?: boolean) => void; - + // 重试连接 retry: () => Promise; - + // 获取 PeerConnection 实例 getPeerConnection: () => RTCPeerConnection | null; - + // 获取 WebSocket 实例 getWebSocket: () => WebSocket | null; - + // 获取当前房间信息 getCurrentRoom: () => { code: string; role: Role } | null; + + // 设置断开连接回调 + setOnDisconnectCallback: (callback: () => void) => void; } /** @@ -44,14 +47,17 @@ export function useWebRTCConnectionCore( // 当前连接的房间信息 const currentRoom = useRef<{ code: string; role: Role } | null>(null); - + // 用于跟踪是否是用户主动断开连接 const isUserDisconnecting = useRef(false); + // 断开连接回调 + const onDisconnectCallback = useRef<(() => void) | null>(null); + // 清理连接 const cleanup = useCallback((shouldNotifyDisconnect: boolean = false) => { console.log('[ConnectionCore] 清理连接, 是否发送断开通知:', shouldNotifyDisconnect); - + if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; @@ -65,8 +71,8 @@ export function useWebRTCConnectionCore( // 在清理 WebSocket 之前发送断开通知 if (shouldNotifyDisconnect && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { try { - wsRef.current.send(JSON.stringify({ - type: 'disconnection', + wsRef.current.send(JSON.stringify({ + type: 'disconnection', payload: { reason: '用户主动断开' } })); console.log('[ConnectionCore] 📤 清理时已通知对方断开连接'); @@ -87,17 +93,17 @@ export function useWebRTCConnectionCore( // 创建 PeerConnection 和相关设置 const createPeerConnection = useCallback((ws: WebSocket, role: 'sender' | 'receiver', isReconnect: boolean = false) => { console.log('[ConnectionCore] 🔧 创建PeerConnection...', { role, isReconnect }); - + // 如果已经存在PeerConnection,先关闭它 if (pcRef.current) { console.log('[ConnectionCore] 🔧 关闭已存在的PeerConnection'); pcRef.current.close(); } - + // 获取用户配置的ICE服务器 const iceServers = getIceServersConfig(); console.log('[ConnectionCore] 🧊 使用ICE服务器配置:', iceServers); - + // 创建 PeerConnection const pc = new RTCPeerConnection({ iceServers: iceServers, @@ -109,7 +115,7 @@ export function useWebRTCConnectionCore( pc.ontrack = (event) => { console.log('[ConnectionCore] 🎥 PeerConnection收到轨道:', event.track.kind, event.track.id, '状态:', event.track.readyState); console.log('[ConnectionCore] 关联的流数量:', event.streams.length); - + // 这里不处理轨道,让业务逻辑的onTrack处理器处理 // 业务逻辑会在useEffect中设置自己的处理器 // 这样可以确保重新连接时轨道能够被正确处理 @@ -156,7 +162,11 @@ export function useWebRTCConnectionCore( switch (pc.connectionState) { case 'connecting': console.log('[ConnectionCore] 🔄 WebRTC正在连接中...'); - stateManager.updateState({ isPeerConnected: false }); + stateManager.updateState({ + isPeerConnected: false, + isConnecting: true, + isConnected: false + }); break; case 'connected': console.log('[ConnectionCore] 🎉 WebRTC P2P连接已完全建立,可以进行媒体传输'); @@ -165,10 +175,11 @@ export function useWebRTCConnectionCore( isWebSocketConnected: true, isConnected: true, isPeerConnected: true, + isConnecting: false, error: null, canRetry: false }); - + // 如果是重新连接,触发数据同步 if (isReconnect) { console.log('[ConnectionCore] 🔄 检测到重新连接,触发数据同步'); @@ -188,15 +199,29 @@ export function useWebRTCConnectionCore( break; case 'failed': console.error('[ConnectionCore] ❌ WebRTC连接失败'); - stateManager.updateState({ error: 'WebRTC连接失败,请检查网络设置或重试', isPeerConnected: false, canRetry: true }); + stateManager.updateState({ + error: 'WebRTC连接失败,请检查网络设置或重试', + isPeerConnected: false, + isConnecting: false, + isConnected: false, + canRetry: true + }); break; case 'disconnected': console.log('[ConnectionCore] 🔌 WebRTC连接已断开'); - stateManager.updateState({ isPeerConnected: false }); + stateManager.updateState({ + isPeerConnected: false, + isConnecting: false, + isConnected: false + }); break; case 'closed': console.log('[ConnectionCore] 🚫 WebRTC连接已关闭'); - stateManager.updateState({ isPeerConnected: false }); + stateManager.updateState({ + isPeerConnected: false, + isConnecting: false, + isConnected: false + }); break; } }; @@ -230,7 +255,7 @@ export function useWebRTCConnectionCore( currentRoom.current = { code: roomCode, role }; stateManager.setCurrentRoom({ code: roomCode, role }); stateManager.updateState({ isConnecting: true, error: null }); - + // 重置主动断开标志 isUserDisconnecting.current = false; @@ -240,7 +265,7 @@ export function useWebRTCConnectionCore( 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); @@ -258,7 +283,7 @@ export function useWebRTCConnectionCore( isConnecting: false, // WebSocket连接成功即表示初始连接完成 isConnected: true // 可以开始后续操作 }); - + // 如果是重新连接且是发送方,检查是否有接收方在等待 if (reconnectState.isReconnect && reconnectState.role === 'sender') { console.log('[ConnectionCore] 🔄 发送方重新连接,检查是否有接收方在等待'); @@ -283,21 +308,21 @@ export function useWebRTCConnectionCore( isConnected: true, isPeerConnected: true // 标记对方已加入,可以开始P2P }); - + // 如果是重新连接,先清理旧的PeerConnection if (reconnectState.isReconnect && pcRef.current) { console.log('[ConnectionCore] 🔄 重新连接:清理旧的PeerConnection'); pcRef.current.close(); pcRef.current = null; } - + // 对方加入后,创建PeerConnection const pc = createPeerConnection(ws, role, reconnectState.isReconnect); - + // 设置轨道管理器的引用 trackManager.setPeerConnection(pc); trackManager.setWebSocket(ws); - + // 发送方创建offer建立基础P2P连接 try { console.log('[ConnectionCore] 📡 创建基础P2P连接offer'); @@ -313,21 +338,21 @@ export function useWebRTCConnectionCore( 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已准备就绪'); @@ -342,23 +367,23 @@ export function useWebRTCConnectionCore( 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 { @@ -374,15 +399,15 @@ export function useWebRTCConnectionCore( 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 @@ -392,7 +417,7 @@ export function useWebRTCConnectionCore( 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)); @@ -425,11 +450,11 @@ export function useWebRTCConnectionCore( if (!pcIce) { console.log('[ConnectionCore] 🔧 PeerConnection不存在,先创建它'); pcIce = createPeerConnection(ws, role, reconnectState.isReconnect); - + // 等待一小段时间确保PeerConnection完全初始化 await new Promise(resolve => setTimeout(resolve, 100)); } - + if (pcIce && message.payload) { try { // 即使远程描述未设置,也可以先缓存ICE候选 @@ -467,6 +492,11 @@ export function useWebRTCConnectionCore( pcRef.current.close(); pcRef.current = null; } + // 调用断开连接回调,通知上层应用清除数据 + if (onDisconnectCallback.current) { + console.log('[ConnectionCore] 📞 调用断开连接回调'); + onDisconnectCallback.current(); + } break; default: @@ -486,14 +516,14 @@ export function useWebRTCConnectionCore( 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 }); @@ -513,13 +543,13 @@ export function useWebRTCConnectionCore( // 断开连接 const disconnect = useCallback((shouldNotifyDisconnect: boolean = false) => { console.log('[ConnectionCore] 主动断开连接'); - + // 设置主动断开标志 isUserDisconnecting.current = true; - + // 清理连接并发送断开通知 cleanup(shouldNotifyDisconnect); - + // 主动断开时,将状态完全重置为初始状态(没有任何错误或消息) stateManager.resetToInitial(); console.log('[ConnectionCore] ✅ 连接已断开并清理完成'); @@ -533,12 +563,12 @@ export function useWebRTCConnectionCore( stateManager.updateState({ error: '无法重试连接:缺少房间信息', canRetry: false }); return; } - + console.log('[ConnectionCore] 🔄 重试连接到房间:', room.code, room.role); - + // 清理当前连接 cleanup(); - + // 重新连接 await connect(room.code, room.role); }, [cleanup, connect, stateManager]); @@ -558,6 +588,11 @@ export function useWebRTCConnectionCore( return currentRoom.current; }, []); + // 设置断开连接回调 + const setOnDisconnectCallback = useCallback((callback: () => void) => { + onDisconnectCallback.current = callback; + }, []); + return { connect, disconnect, @@ -565,5 +600,6 @@ export function useWebRTCConnectionCore( getPeerConnection, getWebSocket, getCurrentRoom, + setOnDisconnectCallback, }; } \ 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 77f643b..e927b36 100644 --- a/chuan-next/src/hooks/connection/webrtc/useWebRTCDataChannelManager.ts +++ b/chuan-next/src/hooks/connection/webrtc/useWebRTCDataChannelManager.ts @@ -12,6 +12,8 @@ export function useWebRTCDataChannelManager( stateManager: IWebConnectStateManager ): WebRTCDataChannelManager & IRegisterEventHandler { const dcRef = useRef(null); + const stateManagerRef = useRef(stateManager); + stateManagerRef.current = stateManager; // 多通道消息处理器 const messageHandlers = useRef>(new Map()); @@ -41,16 +43,17 @@ export function useWebRTCDataChannelManager( }); dcRef.current = dataChannel; - dataChannel.onopen = () => { + dataChannel.onopen = (event) => { console.log('[DataChannelManager] 数据通道已打开 (发送方)'); // 确保所有连接状态都正确更新 - stateManager.updateState({ + stateManagerRef.current.updateState({ isDataChannelConnected: true, isConnected: true, isPeerConnected: true, error: null, isConnecting: false, - canRetry: false + canRetry: false, + state: 'open' }); // 如果是重新连接,触发数据同步 @@ -113,7 +116,7 @@ export function useWebRTCDataChannelManager( console.error(`[DataChannelManager] 数据通道详细错误 - 状态: ${dataChannel.readyState}, 消息: ${errorMessage}, 建议重试: ${shouldRetry}`); - stateManager.updateState({ + stateManagerRef.current.updateState({ error: errorMessage, isConnecting: false, isPeerConnected: false, // 数据通道出错时,P2P连接肯定不可用 @@ -126,17 +129,18 @@ export function useWebRTCDataChannelManager( const dataChannel = event.channel; dcRef.current = dataChannel; - dataChannel.onopen = () => { + dataChannel.onopen = (event) => { console.log('[DataChannelManager] 数据通道已打开 (接收方)'); // 确保所有连接状态都正确更新 - stateManager.updateState({ + stateManagerRef.current.updateState({ isWebSocketConnected: true, isDataChannelConnected: true, isConnected: true, isPeerConnected: true, error: null, isConnecting: false, - canRetry: false + canRetry: false, + state: 'open' }); // 如果是重新连接,触发数据同步 @@ -198,7 +202,7 @@ export function useWebRTCDataChannelManager( console.error(`[DataChannelManager] 数据通道详细错误 (接收方) - 状态: ${dataChannel.readyState}, 消息: ${errorMessage}, 建议重试: ${shouldRetry}`); - stateManager.updateState({ + stateManagerRef.current.updateState({ error: errorMessage, isConnecting: false, isPeerConnected: false, // 数据通道出错时,P2P连接肯定不可用 @@ -313,12 +317,59 @@ export function useWebRTCDataChannelManager( // 获取数据通道状态 const getChannelState = useCallback(() => { - return stateManager.getState(); + return stateManagerRef.current.getState(); }, []); + // 实时更新数据通道状态 useEffect(() => { - stateManager.updateState({ state: dcRef.current?.readyState || 'closed' }); - }, [dcRef.current?.readyState]); + const updateChannelState = () => { + const readyState = dcRef.current?.readyState || 'closed'; + console.log('[DataChannelManager] 数据通道状态更新:', readyState); + + // 更新状态存储中的数据通道状态 + stateManagerRef.current.updateState({ + state: readyState, + isDataChannelConnected: readyState === 'open' + }); + }; + + // 立即更新一次 + updateChannelState(); + + // 如果数据通道存在,设置状态变化监听 + if (dcRef.current) { + const dc = dcRef.current; + const originalOnOpen = dc.onopen; + const originalOnClose = dc.onclose; + const originalOnError = dc.onerror; + + dc.onopen = (event) => { + console.log('[DataChannelManager] 数据通道打开事件触发'); + updateChannelState(); + if (originalOnOpen) originalOnOpen.call(dc, event); + }; + + dc.onclose = (event) => { + console.log('[DataChannelManager] 数据通道关闭事件触发'); + updateChannelState(); + if (originalOnClose) originalOnClose.call(dc, event); + }; + + dc.onerror = (error) => { + console.log('[DataChannelManager] 数据通道错误事件触发'); + updateChannelState(); + if (originalOnError) originalOnError.call(dc, error); + }; + } + + // 清理函数 + return () => { + if (dcRef.current) { + // 恢复原始事件处理器 + // 注意:在实际应用中,可能需要更复杂的事件处理器管理 + } + }; + }, []); return { createDataChannel, diff --git a/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts b/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts index 40ed39d..8ce1687 100644 --- a/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts +++ b/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts @@ -9,11 +9,14 @@ import { ConnectType, DataHandler, IWebConnection, IWebMessage, MessageHandler, export function useWebSocketConnection(): IWebConnection { const wsRef = useRef(null); const currentRoomRef = useRef<{ code: string; role: Role } | null>(null); - + // 事件处理器存储 const messageHandlers = useRef>(new Map()); const dataHandlers = useRef>(new Map()); - + + // 断开连接回调 + const onDisconnectCallback = useRef<(() => void) | null>(null); + // 连接状态 const connectionState = useRef({ isConnected: false, @@ -52,9 +55,9 @@ export function useWebSocketConnection(): IWebConnection { 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] 连接到:', wsUrl); - + const ws = new WebSocket(wsUrl); wsRef.current = ws; @@ -97,6 +100,12 @@ export function useWebSocketConnection(): IWebConnection { error: event.wasClean ? null : 'WebSocket 连接意外断开', canRetry: !event.wasClean }); + + // 调用断开连接回调 + if (onDisconnectCallback.current) { + console.log('[WebSocket] 调用断开连接回调'); + onDisconnectCallback.current(); + } }; } catch (error) { @@ -132,7 +141,7 @@ export function useWebSocketConnection(): IWebConnection { } else if (event.data instanceof ArrayBuffer) { // 二进制数据 console.log('[WebSocket] 收到二进制数据:', event.data.byteLength, 'bytes'); - + // 优先发给文件传输处理器 const fileHandler = dataHandlers.current.get('file-transfer'); if (fileHandler) { @@ -242,8 +251,8 @@ export function useWebSocketConnection(): IWebConnection { // 检查是否连接到指定房间 const isConnectedToRoom = useCallback((roomCode: string, role: Role) => { return currentRoomRef.current?.code === roomCode && - currentRoomRef.current?.role === role && - connectionState.current.isConnected; + currentRoomRef.current?.role === role && + connectionState.current.isConnected; }, []); // 媒体轨道方法(WebSocket 不支持,返回 null) @@ -270,6 +279,11 @@ export function useWebSocketConnection(): IWebConnection { return false; }, []); + // 设置断开连接回调 + const setOnDisconnectCallback = useCallback((callback: () => void) => { + onDisconnectCallback.current = callback; + }, []); + // 清理连接 useEffect(() => { return () => { @@ -294,5 +308,6 @@ export function useWebSocketConnection(): IWebConnection { onTrack, getPeerConnection, createOfferNow, + setOnDisconnectCallback, }; } \ No newline at end of file diff --git a/chuan-next/src/hooks/file-transfer/useFileListSync.ts b/chuan-next/src/hooks/file-transfer/useFileListSync.ts index 3234cb4..184eb23 100644 --- a/chuan-next/src/hooks/file-transfer/useFileListSync.ts +++ b/chuan-next/src/hooks/file-transfer/useFileListSync.ts @@ -15,7 +15,7 @@ interface UseFileListSyncProps { pickupCode: string; isConnected: boolean; isPeerConnected: boolean; - getChannelState: () => string; + getChannelState: () => any; } export const useFileListSync = ({ @@ -31,11 +31,22 @@ export const useFileListSync = ({ // 统一的文件列表同步函数,带防抖功能 const syncFileListToReceiver = useCallback((fileInfos: FileInfo[], reason: string) => { // 只有在发送模式、连接已建立且有房间时才发送文件列表 - if (mode !== 'send' || !pickupCode || !isConnected || !isPeerConnected) { - console.log('跳过文件列表同步:', { mode, pickupCode: !!pickupCode, isConnected, isPeerConnected }); + if (mode !== 'send' || !pickupCode) { + console.log('跳过文件列表同步: 非发送模式或无房间码', { mode, pickupCode: !!pickupCode }); return; } + // 获取当前通道状态 + const channelState = getChannelState(); + console.log(`文件列表同步检查 (${reason}):`, { + mode, + pickupCode: !!pickupCode, + isConnected, + isPeerConnected, + channelState: channelState.state || channelState, + fileInfosCount: fileInfos.length + }); + // 清除之前的延时发送 if (syncTimeoutRef.current) { clearTimeout(syncTimeoutRef.current); @@ -43,9 +54,27 @@ export const useFileListSync = ({ // 延时发送,避免频繁发送 syncTimeoutRef.current = setTimeout(() => { - if (isPeerConnected && getChannelState() === 'open') { + // 检查数据通道状态 - 使用更宽松的条件 + const currentState = getChannelState(); + const isChannelOpen = typeof currentState === 'object' ? + currentState.state === 'open' || currentState.isDataChannelConnected : + currentState === 'open'; + + // 检查P2P连接状态 + const isP2PConnected = isPeerConnected || (typeof currentState === 'object' && currentState.isPeerConnected); + + console.log(`文件列表同步执行检查 (${reason}):`, { + isChannelOpen, + isP2PConnected, + fileInfosCount: fileInfos.length + }); + + // 如果数据通道已打开或P2P已连接,就可以发送文件列表 + if (isChannelOpen || isP2PConnected) { console.log(`发送文件列表到接收方 (${reason}):`, fileInfos.map(f => f.name)); sendFileList(fileInfos); + } else { + console.log(`跳过文件列表发送: 数据通道未打开或P2P未连接 (${reason})`); } }, 150); }, [mode, pickupCode, isConnected, isPeerConnected, getChannelState, sendFileList]); diff --git a/chuan-next/src/hooks/file-transfer/useFileStateManager.ts b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts index 6572001..9c778da 100644 --- a/chuan-next/src/hooks/file-transfer/useFileStateManager.ts +++ b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; interface FileInfo { id: string; @@ -37,10 +37,10 @@ export const useFileStateManager = ({ const handleFileSelect = useCallback((files: File[]) => { console.log('=== 文件选择 ==='); console.log('新文件:', files.map(f => f.name)); - + // 更新选中的文件 setSelectedFiles(prev => [...prev, ...files]); - + // 创建对应的文件信息 const newFileInfos: FileInfo[] = files.map(file => ({ id: generateFileId(), @@ -50,7 +50,7 @@ export const useFileStateManager = ({ status: 'ready', progress: 0 })); - + setFileList(prev => { const updatedList = [...prev, ...newFileInfos]; console.log('更新后的文件列表:', updatedList); @@ -75,15 +75,15 @@ export const useFileStateManager = ({ // 更新文件状态 const updateFileStatus = useCallback((fileId: string, status: FileInfo['status'], progress?: number, transferSpeed?: number) => { - setFileList(prev => prev.map(item => - item.id === fileId - ? { - ...item, - status, - progress: progress ?? item.progress, - transferSpeed: transferSpeed ?? item.transferSpeed, - startTime: status === 'downloading' && !item.startTime ? Date.now() : item.startTime - } + setFileList(prev => prev.map(item => + item.id === fileId + ? { + ...item, + status, + progress: progress ?? item.progress, + transferSpeed: transferSpeed ?? item.transferSpeed, + startTime: status === 'downloading' && !item.startTime ? Date.now() : item.startTime + } : item )); }, []); @@ -94,9 +94,9 @@ export const useFileStateManager = ({ 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, + return { + ...item, + progress, status: newStatus, transferSpeed: transferSpeed ?? item.transferSpeed, startTime: newStatus === 'downloading' && !item.startTime ? Date.now() : item.startTime @@ -149,9 +149,9 @@ export const useFileStateManager = ({ }); // 检查文件列表是否真正发生变化 - const fileListChanged = + const fileListChanged = newFileInfos.length !== currentFileList.length || - newFileInfos.some(newFile => + newFileInfos.some(newFile => !currentFileList.find(oldFile => oldFile.name === newFile.name && oldFile.size === newFile.size) ); @@ -160,7 +160,7 @@ export const useFileStateManager = ({ before: currentFileList.map(f => f.name), after: newFileInfos.map(f => f.name) }); - + return newFileInfos; } @@ -169,6 +169,20 @@ export const useFileStateManager = ({ }); }, [selectedFiles, mode, pickupCode, generateFileId]); // 移除fileList依赖,避免无限循环 + // 清除发送方数据(当接收方离开房间时) + const clearSenderData = useCallback(() => { + console.log('[FileStateManager] 接收方离开房间,清除发送方数据'); + // 只清除文件列表和传输状态,不清除选中的文件 + // 这样用户可以重新连接后继续发送 + setFileList(prev => prev.map(file => ({ + ...file, + status: 'ready' as const, + progress: 0, + transferSpeed: undefined, + startTime: undefined + }))); + }, []); + return { selectedFiles, setSelectedFiles, @@ -180,6 +194,7 @@ export const useFileStateManager = ({ clearFiles, resetFiles, updateFileStatus, - updateFileProgress + updateFileProgress, + clearSenderData }; }; diff --git a/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts index e7c4687..72ebded 100644 --- a/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts +++ b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts @@ -602,39 +602,86 @@ export function useFileTransferBusiness(connection: IWebConnection) { // 检查连接状态 - 优先检查数据通道状态,因为 P2P 连接可能已经建立但状态未及时更新 const channelState = connection.getConnectState(); const peerConnected = channelState.isPeerConnected; + const dataChannelConnected = channelState.isDataChannelConnected; + const channelReadyState = channelState.state; console.log('发送文件列表检查:', { channelState, peerConnected, + dataChannelConnected, + channelReadyState, fileListLength: fileList.length }); - // 如果数据通道已打开或者 P2P 已连接,就可以发送文件列表 - if (channelState.state === 'open' || peerConnected) { - console.log('发送文件列表:', fileList); + // 使用更宽松的条件检查连接状态 + const isReadyToSend = channelReadyState === 'open' || + dataChannelConnected || + peerConnected || + channelState.isConnected; - connection.sendMessage({ + if (isReadyToSend) { + console.log('发送文件列表:', fileList.map(f => f.name)); + + const sendResult = connection.sendMessage({ type: 'file-list', payload: fileList }, CHANNEL_NAME); + + if (!sendResult) { + console.warn('文件列表发送失败,可能是数据通道未准备好'); + // 不立即重试,让上层逻辑处理重试 + } } else { - console.log('P2P连接未建立,等待连接后再发送文件列表'); + console.log('连接未就绪,等待连接后再发送文件列表:', { + channelReadyState, + dataChannelConnected, + peerConnected, + isConnected: channelState.isConnected + }); } }, [connection]); // 请求文件 const requestFile = useCallback((fileId: string, fileName: string) => { - if (connection.getConnectState().state !== 'open') { - console.error('数据通道未准备就绪,无法请求文件'); + const channelState = connection.getConnectState(); + const isChannelOpen = channelState.state === 'open'; + const isDataChannelConnected = channelState.isDataChannelConnected; + const isPeerConnected = channelState.isPeerConnected; + const isConnected = channelState.isConnected; + + console.log('请求文件前检查连接状态:', { + fileName, + fileId, + isChannelOpen, + isDataChannelConnected, + isPeerConnected, + isConnected + }); + + // 使用更宽松的条件检查连接状态 + const isReadyToRequest = isChannelOpen || isDataChannelConnected || isPeerConnected || isConnected; + + if (!isReadyToRequest) { + console.error('数据通道未准备就绪,无法请求文件:', { + isChannelOpen, + isDataChannelConnected, + isPeerConnected, + isConnected + }); return; } - console.log('请求文件:', fileName, fileId); + console.log('发送文件请求:', fileName, fileId); - connection.sendMessage({ + const sendResult = connection.sendMessage({ type: 'file-request', payload: { fileId, fileName } }, CHANNEL_NAME); + + if (!sendResult) { + console.error('文件请求发送失败,可能是数据通道问题'); + // 不立即重试,让上层逻辑处理重试 + } }, [connection]); // 注册回调函数 @@ -658,6 +705,38 @@ export function useFileTransferBusiness(connection: IWebConnection) { return () => { fileListCallbacks.current.delete(callback); }; }, []); + // 清除发送方数据 + const clearSenderData = useCallback(() => { + console.log('[FileTransferBusiness] 清除发送方数据'); + + // 清除传输状态 + transferStatus.current.clear(); + + // 清除待处理的块 + pendingChunks.current.forEach(timeout => clearTimeout(timeout)); + pendingChunks.current.clear(); + + // 清除块确认回调 + chunkAckCallbacks.current.clear(); + + // 重置状态 + updateState({ + isTransferring: false, + progress: 0, + error: null + }); + }, [updateState]); + + // 设置断开连接回调 + useEffect(() => { + connection.setOnDisconnectCallback(clearSenderData); + + return () => { + // 清理回调 + connection.setOnDisconnectCallback(() => { }); + }; + }, [connection, clearSenderData]); + return { // 文件传输状态(包括连接状态) ...state, @@ -668,6 +747,7 @@ export function useFileTransferBusiness(connection: IWebConnection) { sendFile, sendFileList, requestFile, + clearSenderData, // 回调注册 onFileReceived,