feat: 优化文件传输逻辑,添加断开连接回调和清除发送方数据功能

- 使用更宽松的条件检查连接状态,确保文件列表和文件请求的发送时机
- 添加清除发送方数据的逻辑,当接收方离开房间时重置文件传输状态
- 在文件传输业务中设置断开连接回调,确保在连接断开时清理相关数据
- 更新数据通道和P2P连接状态的处理,增强连接状态的监控和反馈
This commit is contained in:
MatrixSeven
2025-09-15 19:39:57 +08:00
parent 550be8bcc6
commit 15d23de5a7
10 changed files with 397 additions and 110 deletions

View File

@@ -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(() => {

View File

@@ -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<boolean>;
// 断开连接回调
setOnDisconnectCallback: (callback: () => void) => void;
}

View File

@@ -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;
};
}
}

View File

@@ -72,6 +72,9 @@ export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHan
getPeerConnection: connectionCore.getPeerConnection,
createOfferNow,
// 断开连接回调
setOnDisconnectCallback: connectionCore.setOnDisconnectCallback,
// 当前房间信息
currentRoom: connectionCore.getCurrentRoom(),
};

View File

@@ -12,21 +12,24 @@ import { Role, WebRTCDataChannelManager, WebRTCTrackManager } from '../types';
export interface WebRTCConnectionCore {
// 连接到房间
connect: (roomCode: string, role: Role) => Promise<void>;
// 断开连接
disconnect: (shouldNotifyDisconnect?: boolean) => void;
// 重试连接
retry: () => Promise<void>;
// 获取 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<boolean>(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,
};
}

View File

@@ -12,6 +12,8 @@ export function useWebRTCDataChannelManager(
stateManager: IWebConnectStateManager
): WebRTCDataChannelManager & IRegisterEventHandler {
const dcRef = useRef<RTCDataChannel | null>(null);
const stateManagerRef = useRef(stateManager);
stateManagerRef.current = stateManager;
// 多通道消息处理器
const messageHandlers = useRef<Map<string, MessageHandler>>(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,

View File

@@ -9,11 +9,14 @@ import { ConnectType, DataHandler, IWebConnection, IWebMessage, MessageHandler,
export function useWebSocketConnection(): IWebConnection {
const wsRef = useRef<WebSocket | null>(null);
const currentRoomRef = useRef<{ code: string; role: Role } | null>(null);
// 事件处理器存储
const messageHandlers = useRef<Map<string, MessageHandler>>(new Map());
const dataHandlers = useRef<Map<string, DataHandler>>(new Map());
// 断开连接回调
const onDisconnectCallback = useRef<(() => void) | null>(null);
// 连接状态
const connectionState = useRef<WebConnectState>({
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,
};
}

View File

@@ -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]);

View File

@@ -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
};
};

View File

@@ -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,