mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-04 03:25:03 +08:00
feat: 优化文件传输逻辑,添加断开连接回调和清除发送方数据功能
- 使用更宽松的条件检查连接状态,确保文件列表和文件请求的发送时机 - 添加清除发送方数据的逻辑,当接收方离开房间时重置文件传输状态 - 在文件传输业务中设置断开连接回调,确保在连接断开时清理相关数据 - 更新数据通道和P2P连接状态的处理,增强连接状态的监控和反馈
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHan
|
||||
getPeerConnection: connectionCore.getPeerConnection,
|
||||
createOfferNow,
|
||||
|
||||
// 断开连接回调
|
||||
setOnDisconnectCallback: connectionCore.setOnDisconnectCallback,
|
||||
|
||||
// 当前房间信息
|
||||
currentRoom: connectionCore.getCurrentRoom(),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user