mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-27 02:04:41 +08:00
feat:UI大调整,WEBRTC切换
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { FileInfo, RoomStatus } from '@/types';
|
||||
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import { apiPost, apiGet } from '@/lib/api-utils';
|
||||
|
||||
export const useRoomManager = () => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [pickupCode, setPickupCode] = useState<string>('');
|
||||
const [pickupLink, setPickupLink] = useState<string>('');
|
||||
const [currentRole, setCurrentRole] = useState<'sender' | 'receiver'>('sender');
|
||||
const [receiverFiles, setReceiverFiles] = useState<FileInfo[]>([]);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [roomStatus, setRoomStatus] = useState<RoomStatus | null>(null);
|
||||
|
||||
const { websocket, isConnected, connect, disconnect, sendMessage } = useWebSocket();
|
||||
const { showToast } = useToast();
|
||||
|
||||
// 生成取件码
|
||||
const generateCode = useCallback(async () => {
|
||||
if (selectedFiles.length === 0) return;
|
||||
|
||||
const fileInfos = selectedFiles.map((file, index) => ({
|
||||
id: 'file_' + index,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: file.lastModified
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await apiPost('/api/create-room', { files: fileInfos });
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const code = data.code;
|
||||
setPickupCode(code);
|
||||
setCurrentRole('sender');
|
||||
|
||||
const baseUrl = window.location.origin;
|
||||
const link = `${baseUrl}/?type=file&mode=receive&code=${code}`;
|
||||
setPickupLink(link);
|
||||
|
||||
connect(code, 'sender');
|
||||
showToast('取件码生成成功!');
|
||||
} else {
|
||||
showToast('生成取件码失败: ' + data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成取件码失败:', error);
|
||||
showToast('生成取件码失败,请重试', 'error');
|
||||
}
|
||||
}, [selectedFiles, connect, showToast]);
|
||||
|
||||
// 加入房间
|
||||
const joinRoom = useCallback(async (code: string) => {
|
||||
// 防止重复连接
|
||||
if (isConnecting || (isConnected && pickupCode === code)) {
|
||||
console.log('已在连接中或已连接,跳过重复请求');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConnecting(true);
|
||||
|
||||
try {
|
||||
const response = await apiGet(`/api/room-info?code=${code}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setPickupCode(code);
|
||||
setCurrentRole('receiver');
|
||||
setReceiverFiles(data.files || []);
|
||||
// 开始连接WebSocket
|
||||
connect(code, 'receiver');
|
||||
console.log('房间信息获取成功,开始建立WebSocket连接');
|
||||
// 注意:isConnecting状态会在WebSocket连接建立后自动重置
|
||||
// 不在这里显示成功消息,等WebSocket连接成功后再显示
|
||||
} else {
|
||||
showToast(data.message || '取件码不存在或已过期', 'error');
|
||||
setIsConnecting(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API调用失败:', error);
|
||||
showToast('取件码不存在或已过期', 'error');
|
||||
setIsConnecting(false);
|
||||
}
|
||||
}, [connect, showToast, isConnecting, isConnected, pickupCode]);
|
||||
|
||||
// 通过WebSocket更新文件列表
|
||||
const updateFileList = useCallback((files: File[]) => {
|
||||
if (!pickupCode || !websocket || websocket.readyState !== WebSocket.OPEN) {
|
||||
console.log('无法更新文件列表: pickupCode=', pickupCode, 'websocket状态=', websocket?.readyState);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileInfos = files.map((file, index) => ({
|
||||
id: 'file_' + index,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: file.lastModified
|
||||
}));
|
||||
|
||||
console.log('通过WebSocket发送文件列表更新:', fileInfos);
|
||||
sendMessage({
|
||||
type: 'update-file-list',
|
||||
payload: {
|
||||
files: fileInfos
|
||||
}
|
||||
});
|
||||
|
||||
showToast('文件列表已更新');
|
||||
}, [pickupCode, websocket, sendMessage, showToast]);
|
||||
|
||||
// 处理文件删除后的同步
|
||||
const handleRemoveFile = useCallback((updatedFiles: File[]) => {
|
||||
if (pickupCode) {
|
||||
updateFileList(updatedFiles);
|
||||
}
|
||||
}, [pickupCode, updateFileList]);
|
||||
|
||||
// 清空文件列表但保持房间连接
|
||||
const clearFiles = useCallback(() => {
|
||||
setSelectedFiles([]);
|
||||
if (pickupCode) {
|
||||
updateFileList([]);
|
||||
showToast('文件列表已清空,房间保持连接', 'success');
|
||||
}
|
||||
}, [pickupCode, updateFileList, showToast]);
|
||||
|
||||
// 完全重置状态(关闭房间)
|
||||
const resetRoom = useCallback(() => {
|
||||
setSelectedFiles([]);
|
||||
setPickupCode('');
|
||||
setPickupLink('');
|
||||
setReceiverFiles([]);
|
||||
setRoomStatus(null);
|
||||
disconnect();
|
||||
showToast('已断开连接', 'info');
|
||||
}, [disconnect, showToast]);
|
||||
|
||||
// 重置连接状态
|
||||
const resetConnectingState = useCallback(() => {
|
||||
if (isConnected && isConnecting) {
|
||||
setIsConnecting(false);
|
||||
console.log('WebSocket连接已建立,重置连接状态');
|
||||
}
|
||||
}, [isConnected, isConnecting]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
selectedFiles,
|
||||
pickupCode,
|
||||
pickupLink,
|
||||
currentRole,
|
||||
receiverFiles,
|
||||
isConnecting,
|
||||
roomStatus,
|
||||
isConnected,
|
||||
websocket,
|
||||
|
||||
// 状态更新函数
|
||||
setSelectedFiles,
|
||||
setReceiverFiles,
|
||||
setRoomStatus,
|
||||
setIsConnecting,
|
||||
setCurrentRole,
|
||||
resetConnectingState,
|
||||
|
||||
// 房间操作
|
||||
generateCode,
|
||||
joinRoom,
|
||||
updateFileList,
|
||||
handleRemoveFile,
|
||||
clearFiles,
|
||||
resetRoom,
|
||||
|
||||
// WebSocket 相关
|
||||
sendMessage,
|
||||
disconnect,
|
||||
connect
|
||||
};
|
||||
};
|
||||
145
chuan-next/src/hooks/useWebRTCTransfer.new.ts
Normal file
145
chuan-next/src/hooks/useWebRTCTransfer.new.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useWebRTCConnection } from './webrtc/useWebRTCConnection';
|
||||
import { useFileTransfer } from './webrtc/useFileTransfer';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
status: 'ready' | 'downloading' | 'completed';
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export function useWebRTCTransfer() {
|
||||
const connection = useWebRTCConnection();
|
||||
const fileTransfer = useFileTransfer();
|
||||
|
||||
// 文件列表回调存储
|
||||
const fileListCallbacks = useRef<Array<(fileList: FileInfo[]) => void>>([]);
|
||||
|
||||
// 设置数据通道消息处理
|
||||
useEffect(() => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (dataChannel && dataChannel.readyState === 'open') {
|
||||
console.log('设置数据通道消息处理器');
|
||||
|
||||
// 扩展消息处理以包含文件列表
|
||||
const originalHandler = fileTransfer.handleMessage;
|
||||
|
||||
dataChannel.onmessage = (event) => {
|
||||
console.log('收到数据通道消息:', typeof event.data);
|
||||
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (message.type === 'file-list') {
|
||||
console.log('收到文件列表:', message.payload);
|
||||
fileListCallbacks.current.forEach(callback => {
|
||||
callback(message.payload);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析文件列表消息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他消息类型
|
||||
originalHandler(event);
|
||||
};
|
||||
}
|
||||
}, [connection.isConnected, connection.getDataChannel, fileTransfer.handleMessage]);
|
||||
|
||||
// 发送文件
|
||||
const sendFile = useCallback((file: File, fileId?: string) => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (!dataChannel) {
|
||||
console.error('数据通道未准备就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
const actualFileId = fileId || `file_${Date.now()}`;
|
||||
console.log('=== 发送文件 ===');
|
||||
console.log('文件:', file.name, 'ID:', actualFileId, '大小:', file.size);
|
||||
|
||||
fileTransfer.sendFile(file, actualFileId, dataChannel);
|
||||
}, [connection.getDataChannel, fileTransfer.sendFile]);
|
||||
|
||||
// 请求文件
|
||||
const requestFile = useCallback((fileId: string, fileName: string) => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (!dataChannel) {
|
||||
console.error('数据通道未准备就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== 请求文件 ===');
|
||||
console.log('文件:', fileName, 'ID:', fileId);
|
||||
|
||||
fileTransfer.requestFile(fileId, fileName, dataChannel);
|
||||
}, [connection.getDataChannel, fileTransfer.requestFile]);
|
||||
|
||||
// 发送文件列表
|
||||
const sendFileList = useCallback((fileList: FileInfo[]) => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (!dataChannel || dataChannel.readyState !== 'open') {
|
||||
console.error('数据通道未准备就绪,无法发送文件列表');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== 发送文件列表 ===');
|
||||
console.log('文件列表:', fileList);
|
||||
|
||||
const message = JSON.stringify({
|
||||
type: 'file-list',
|
||||
payload: fileList
|
||||
});
|
||||
|
||||
try {
|
||||
dataChannel.send(message);
|
||||
console.log('文件列表已发送');
|
||||
} catch (error) {
|
||||
console.error('发送文件列表失败:', error);
|
||||
}
|
||||
}, [connection.getDataChannel]);
|
||||
|
||||
// 注册文件列表接收回调
|
||||
const onFileListReceived = useCallback((callback: (fileList: FileInfo[]) => void) => {
|
||||
console.log('注册文件列表回调');
|
||||
fileListCallbacks.current.push(callback);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
const index = fileListCallbacks.current.indexOf(callback);
|
||||
if (index > -1) {
|
||||
fileListCallbacks.current.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 连接状态
|
||||
isConnected: connection.isConnected,
|
||||
isConnecting: connection.isConnecting,
|
||||
error: connection.error || fileTransfer.error,
|
||||
|
||||
// 传输状态
|
||||
isTransferring: fileTransfer.isTransferring,
|
||||
transferProgress: fileTransfer.transferProgress,
|
||||
receivedFiles: fileTransfer.receivedFiles,
|
||||
|
||||
// 操作方法
|
||||
connect: connection.connect,
|
||||
disconnect: connection.disconnect,
|
||||
sendFile,
|
||||
requestFile,
|
||||
sendFileList,
|
||||
|
||||
// 回调注册
|
||||
onFileRequested: fileTransfer.onFileRequested,
|
||||
onFileReceived: fileTransfer.onFileReceived,
|
||||
onFileListReceived,
|
||||
};
|
||||
}
|
||||
146
chuan-next/src/hooks/useWebRTCTransfer.ts
Normal file
146
chuan-next/src/hooks/useWebRTCTransfer.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useWebRTCConnection } from './webrtc/useWebRTCConnection';
|
||||
import { useFileTransfer } from './webrtc/useFileTransfer';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
status: 'ready' | 'downloading' | 'completed';
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export function useWebRTCTransfer() {
|
||||
const connection = useWebRTCConnection();
|
||||
const fileTransfer = useFileTransfer();
|
||||
|
||||
// 文件列表回调存储
|
||||
const fileListCallbacks = useRef<Array<(fileList: FileInfo[]) => void>>([]);
|
||||
|
||||
// 设置数据通道消息处理
|
||||
useEffect(() => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (dataChannel && dataChannel.readyState === 'open') {
|
||||
console.log('设置数据通道消息处理器');
|
||||
|
||||
// 扩展消息处理以包含文件列表
|
||||
const originalHandler = fileTransfer.handleMessage;
|
||||
|
||||
dataChannel.onmessage = (event) => {
|
||||
console.log('收到数据通道消息:', typeof event.data);
|
||||
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (message.type === 'file-list') {
|
||||
console.log('收到文件列表:', message.payload);
|
||||
fileListCallbacks.current.forEach(callback => {
|
||||
callback(message.payload);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析文件列表消息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他消息类型
|
||||
originalHandler(event);
|
||||
};
|
||||
}
|
||||
}, [connection.isConnected, connection.getDataChannel, fileTransfer.handleMessage]);
|
||||
|
||||
// 发送文件
|
||||
const sendFile = useCallback((file: File, fileId?: string) => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (!dataChannel) {
|
||||
console.error('数据通道未准备就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
const actualFileId = fileId || `file_${Date.now()}`;
|
||||
console.log('=== 发送文件 ===');
|
||||
console.log('文件:', file.name, 'ID:', actualFileId, '大小:', file.size);
|
||||
|
||||
fileTransfer.sendFile(file, actualFileId, dataChannel);
|
||||
}, [connection.getDataChannel, fileTransfer.sendFile]);
|
||||
|
||||
// 请求文件
|
||||
const requestFile = useCallback((fileId: string, fileName: string) => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (!dataChannel) {
|
||||
console.error('数据通道未准备就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== 请求文件 ===');
|
||||
console.log('文件:', fileName, 'ID:', fileId);
|
||||
|
||||
fileTransfer.requestFile(fileId, fileName, dataChannel);
|
||||
}, [connection.getDataChannel, fileTransfer.requestFile]);
|
||||
|
||||
// 发送文件列表
|
||||
const sendFileList = useCallback((fileList: FileInfo[]) => {
|
||||
const dataChannel = connection.getDataChannel();
|
||||
if (!dataChannel || dataChannel.readyState !== 'open') {
|
||||
console.error('数据通道未准备就绪,无法发送文件列表');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== 发送文件列表 ===');
|
||||
console.log('文件列表:', fileList);
|
||||
|
||||
const message = JSON.stringify({
|
||||
type: 'file-list',
|
||||
payload: fileList
|
||||
});
|
||||
|
||||
try {
|
||||
dataChannel.send(message);
|
||||
console.log('文件列表已发送');
|
||||
} catch (error) {
|
||||
console.error('发送文件列表失败:', error);
|
||||
}
|
||||
}, [connection.getDataChannel]);
|
||||
|
||||
// 注册文件列表接收回调
|
||||
const onFileListReceived = useCallback((callback: (fileList: FileInfo[]) => void) => {
|
||||
console.log('注册文件列表回调');
|
||||
fileListCallbacks.current.push(callback);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
const index = fileListCallbacks.current.indexOf(callback);
|
||||
if (index > -1) {
|
||||
fileListCallbacks.current.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 连接状态
|
||||
isConnected: connection.isConnected,
|
||||
isConnecting: connection.isConnecting,
|
||||
isWebSocketConnected: connection.isWebSocketConnected,
|
||||
error: connection.error || fileTransfer.error,
|
||||
|
||||
// 传输状态
|
||||
isTransferring: fileTransfer.isTransferring,
|
||||
transferProgress: fileTransfer.transferProgress,
|
||||
receivedFiles: fileTransfer.receivedFiles,
|
||||
|
||||
// 操作方法
|
||||
connect: connection.connect,
|
||||
disconnect: connection.disconnect,
|
||||
sendFile,
|
||||
requestFile,
|
||||
sendFileList,
|
||||
|
||||
// 回调注册
|
||||
onFileRequested: fileTransfer.onFileRequested,
|
||||
onFileReceived: fileTransfer.onFileReceived,
|
||||
onFileListReceived,
|
||||
};
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { UseWebSocketReturn, WebSocketMessage } from '@/types';
|
||||
import { getWebSocketUrl } from '@/lib/api-utils';
|
||||
|
||||
export function useWebSocket(): UseWebSocketReturn {
|
||||
const [websocket, setWebsocket] = useState<WebSocket | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const currentCodeRef = useRef<string>('');
|
||||
const currentRoleRef = useRef<'sender' | 'receiver'>('sender');
|
||||
|
||||
const connect = useCallback((code: string, role: 'sender' | 'receiver') => {
|
||||
// 防止重复连接 - 更严格的检查
|
||||
if (websocket &&
|
||||
(websocket.readyState === WebSocket.OPEN || websocket.readyState === WebSocket.CONNECTING) &&
|
||||
currentCodeRef.current === code &&
|
||||
currentRoleRef.current === role) {
|
||||
console.log('WebSocket已连接或正在连接,跳过重复连接', {
|
||||
readyState: websocket.readyState,
|
||||
currentCode: currentCodeRef.current,
|
||||
newCode: code,
|
||||
currentRole: currentRoleRef.current,
|
||||
newRole: role
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有现有连接,先关闭
|
||||
if (websocket) {
|
||||
console.log('关闭现有WebSocket连接');
|
||||
websocket.close();
|
||||
}
|
||||
|
||||
currentCodeRef.current = code;
|
||||
currentRoleRef.current = role;
|
||||
|
||||
// 连接到Go后端的WebSocket - 使用配置文件中的URL
|
||||
const baseWsUrl = getWebSocketUrl();
|
||||
const wsUrl = `${baseWsUrl}?code=${code}&role=${role}`;
|
||||
|
||||
console.log('连接WebSocket:', wsUrl);
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket连接已建立');
|
||||
setIsConnected(true);
|
||||
setWebsocket(ws);
|
||||
|
||||
// 发送连接建立确认事件
|
||||
const connectEvent = new CustomEvent('websocket-connected', {
|
||||
detail: { code, role }
|
||||
});
|
||||
window.dispatchEvent(connectEvent);
|
||||
|
||||
// 发送初始连接信息
|
||||
const message = {
|
||||
type: 'connect',
|
||||
payload: {
|
||||
code: code,
|
||||
role: role,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
};
|
||||
ws.send(JSON.stringify(message));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
console.log('收到WebSocket消息:', message);
|
||||
|
||||
// 分发事件
|
||||
const customEvent = new CustomEvent('websocket-message', {
|
||||
detail: message
|
||||
});
|
||||
window.dispatchEvent(customEvent);
|
||||
} catch (error) {
|
||||
console.error('解析WebSocket消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket连接关闭:', event.code, event.reason);
|
||||
setIsConnected(false);
|
||||
setWebsocket(null);
|
||||
|
||||
// 发送连接关闭事件
|
||||
const closeEvent = new CustomEvent('websocket-close', {
|
||||
detail: { code: event.code, reason: event.reason }
|
||||
});
|
||||
window.dispatchEvent(closeEvent);
|
||||
|
||||
// 如果不是正常关闭且有房间码,尝试重连
|
||||
if (event.code !== 1000 && currentCodeRef.current) {
|
||||
console.log('尝试重新连接...');
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
connect(currentCodeRef.current, currentRoleRef.current);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error);
|
||||
console.error('WebSocket状态:', ws.readyState);
|
||||
console.error('WebSocket URL:', wsUrl);
|
||||
setIsConnected(false);
|
||||
|
||||
// 发送连接错误事件
|
||||
const errorEvent = new CustomEvent('websocket-error', {
|
||||
detail: { error, url: wsUrl, readyState: ws.readyState }
|
||||
});
|
||||
window.dispatchEvent(errorEvent);
|
||||
};
|
||||
}, [websocket]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
|
||||
currentCodeRef.current = '';
|
||||
|
||||
if (websocket) {
|
||||
websocket.close(1000, 'User disconnected');
|
||||
}
|
||||
}, [websocket]);
|
||||
|
||||
const sendMessage = useCallback((message: WebSocketMessage) => {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn('WebSocket未连接,无法发送消息');
|
||||
}
|
||||
}, [websocket]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
if (websocket) {
|
||||
websocket.close();
|
||||
}
|
||||
};
|
||||
}, [websocket]);
|
||||
|
||||
return {
|
||||
websocket,
|
||||
isConnected,
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage
|
||||
};
|
||||
}
|
||||
299
chuan-next/src/hooks/webrtc/useFileTransfer.ts
Normal file
299
chuan-next/src/hooks/webrtc/useFileTransfer.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
|
||||
interface FileTransferState {
|
||||
isTransferring: boolean;
|
||||
transferProgress: number;
|
||||
receivedFiles: Array<{ id: string; file: File }>;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface FileChunk {
|
||||
fileId: string;
|
||||
chunkIndex: number;
|
||||
totalChunks: number;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
interface FileMetadata {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = 16 * 1024; // 16KB chunks
|
||||
|
||||
export function useFileTransfer() {
|
||||
const [state, setState] = useState<FileTransferState>({
|
||||
isTransferring: false,
|
||||
transferProgress: 0,
|
||||
receivedFiles: [],
|
||||
error: null,
|
||||
});
|
||||
|
||||
// 存储接收中的文件数据
|
||||
const receivingFiles = useRef<Map<string, {
|
||||
metadata: FileMetadata;
|
||||
chunks: ArrayBuffer[];
|
||||
receivedChunks: number;
|
||||
totalChunks: number;
|
||||
}>>(new Map());
|
||||
|
||||
// 文件请求回调
|
||||
const fileRequestCallbacks = useRef<Array<(fileId: string, fileName: string) => void>>([]);
|
||||
const fileReceivedCallbacks = useRef<Array<(fileData: { id: string; file: File }) => void>>([]);
|
||||
|
||||
const updateState = useCallback((updates: Partial<FileTransferState>) => {
|
||||
setState(prev => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
// 发送文件
|
||||
const sendFile = useCallback(async (file: File, fileId: string, dataChannel: RTCDataChannel) => {
|
||||
if (!dataChannel || dataChannel.readyState !== 'open') {
|
||||
console.error('数据通道未准备就绪');
|
||||
updateState({ error: '数据通道未准备就绪' });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== 开始发送文件 ===');
|
||||
console.log('文件:', file.name, '大小:', file.size, 'ID:', fileId);
|
||||
|
||||
updateState({ isTransferring: true, transferProgress: 0, error: null });
|
||||
|
||||
try {
|
||||
// 发送文件元数据
|
||||
const metadata: FileMetadata = {
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
};
|
||||
|
||||
const metadataMessage = JSON.stringify({
|
||||
type: 'file-start',
|
||||
payload: metadata
|
||||
});
|
||||
|
||||
console.log('发送文件元数据:', metadataMessage);
|
||||
dataChannel.send(metadataMessage);
|
||||
|
||||
// 计算分块数量
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
console.log('总分块数:', totalChunks);
|
||||
|
||||
// 分块发送文件
|
||||
let sentChunks = 0;
|
||||
|
||||
const sendNextChunk = () => {
|
||||
if (sentChunks >= totalChunks) {
|
||||
// 发送结束信号
|
||||
const endMessage = JSON.stringify({
|
||||
type: 'file-end',
|
||||
payload: { id: fileId }
|
||||
});
|
||||
dataChannel.send(endMessage);
|
||||
|
||||
updateState({ isTransferring: false, transferProgress: 100 });
|
||||
console.log('文件发送完成:', file.name);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = sentChunks * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (event.target?.result && dataChannel.readyState === 'open') {
|
||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||
|
||||
// 发送分块数据
|
||||
const chunkMessage = JSON.stringify({
|
||||
type: 'file-chunk',
|
||||
payload: {
|
||||
fileId,
|
||||
chunkIndex: sentChunks,
|
||||
totalChunks
|
||||
}
|
||||
});
|
||||
|
||||
dataChannel.send(chunkMessage);
|
||||
dataChannel.send(arrayBuffer);
|
||||
|
||||
sentChunks++;
|
||||
const progress = (sentChunks / totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
|
||||
console.log(`发送进度: ${progress.toFixed(1)}%, 块: ${sentChunks}/${totalChunks}`);
|
||||
|
||||
// 短暂延迟,避免阻塞
|
||||
setTimeout(sendNextChunk, 10);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
console.error('读取文件块失败');
|
||||
updateState({ error: '读取文件失败', isTransferring: false });
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(chunk);
|
||||
};
|
||||
|
||||
sendNextChunk();
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送文件失败:', error);
|
||||
updateState({
|
||||
error: error instanceof Error ? error.message : '发送文件失败',
|
||||
isTransferring: false
|
||||
});
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
// 处理接收到的消息
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
console.log('收到消息:', message.type, message.payload);
|
||||
|
||||
switch (message.type) {
|
||||
case 'file-list':
|
||||
// 文件列表消息由主hook处理
|
||||
console.log('文件列表消息将由主hook处理');
|
||||
return;
|
||||
|
||||
case 'file-start':
|
||||
const metadata = message.payload as FileMetadata;
|
||||
console.log('开始接收文件:', metadata.name, '大小:', metadata.size);
|
||||
|
||||
receivingFiles.current.set(metadata.id, {
|
||||
metadata,
|
||||
chunks: [],
|
||||
receivedChunks: 0,
|
||||
totalChunks: Math.ceil(metadata.size / CHUNK_SIZE)
|
||||
});
|
||||
|
||||
updateState({ isTransferring: true, transferProgress: 0 });
|
||||
break;
|
||||
|
||||
case 'file-chunk':
|
||||
const chunkInfo = message.payload;
|
||||
console.log(`接收文件块: ${chunkInfo.chunkIndex + 1}/${chunkInfo.totalChunks}`);
|
||||
break;
|
||||
|
||||
case 'file-end':
|
||||
const { id: fileId } = message.payload;
|
||||
const fileInfo = receivingFiles.current.get(fileId);
|
||||
|
||||
if (fileInfo) {
|
||||
// 组装文件
|
||||
const blob = new Blob(fileInfo.chunks, { type: fileInfo.metadata.type });
|
||||
const file = new File([blob], fileInfo.metadata.name, { type: fileInfo.metadata.type });
|
||||
|
||||
console.log('文件接收完成:', file.name);
|
||||
|
||||
// 添加到接收文件列表
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
receivedFiles: [...prev.receivedFiles, { id: fileId, file }],
|
||||
isTransferring: false,
|
||||
transferProgress: 100
|
||||
}));
|
||||
|
||||
// 触发回调
|
||||
fileReceivedCallbacks.current.forEach(callback => {
|
||||
callback({ id: fileId, file });
|
||||
});
|
||||
|
||||
// 清理
|
||||
receivingFiles.current.delete(fileId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file-request':
|
||||
const { fileId: requestedFileId, fileName } = message.payload;
|
||||
console.log('收到文件请求:', fileName, 'ID:', requestedFileId);
|
||||
|
||||
// 触发文件请求回调
|
||||
fileRequestCallbacks.current.forEach(callback => {
|
||||
callback(requestedFileId, fileName);
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析消息失败:', error);
|
||||
}
|
||||
} else if (event.data instanceof ArrayBuffer) {
|
||||
// 处理文件块数据
|
||||
const arrayBuffer = event.data;
|
||||
console.log('收到文件块数据:', arrayBuffer.byteLength, 'bytes');
|
||||
|
||||
// 找到最近开始接收的文件(简化逻辑)
|
||||
for (const [fileId, fileInfo] of receivingFiles.current.entries()) {
|
||||
if (fileInfo.receivedChunks < fileInfo.totalChunks) {
|
||||
fileInfo.chunks.push(arrayBuffer);
|
||||
fileInfo.receivedChunks++;
|
||||
|
||||
const progress = (fileInfo.receivedChunks / fileInfo.totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
|
||||
console.log(`文件接收进度: ${progress.toFixed(1)}%, 块: ${fileInfo.receivedChunks}/${fileInfo.totalChunks}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
// 请求文件
|
||||
const requestFile = useCallback((fileId: string, fileName: string, dataChannel: RTCDataChannel) => {
|
||||
if (!dataChannel || dataChannel.readyState !== 'open') {
|
||||
console.error('数据通道未准备就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('请求文件:', fileName, 'ID:', fileId);
|
||||
|
||||
const requestMessage = JSON.stringify({
|
||||
type: 'file-request',
|
||||
payload: { fileId, fileName }
|
||||
});
|
||||
|
||||
dataChannel.send(requestMessage);
|
||||
}, []);
|
||||
|
||||
// 注册文件请求回调
|
||||
const onFileRequested = useCallback((callback: (fileId: string, fileName: string) => void) => {
|
||||
fileRequestCallbacks.current.push(callback);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
const index = fileRequestCallbacks.current.indexOf(callback);
|
||||
if (index > -1) {
|
||||
fileRequestCallbacks.current.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 注册文件接收回调
|
||||
const onFileReceived = useCallback((callback: (fileData: { id: string; file: File }) => void) => {
|
||||
fileReceivedCallbacks.current.push(callback);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
const index = fileReceivedCallbacks.current.indexOf(callback);
|
||||
if (index > -1) {
|
||||
fileReceivedCallbacks.current.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
sendFile,
|
||||
requestFile,
|
||||
handleMessage,
|
||||
onFileRequested,
|
||||
onFileReceived,
|
||||
};
|
||||
}
|
||||
203
chuan-next/src/hooks/webrtc/useWebRTCConnection.ts
Normal file
203
chuan-next/src/hooks/webrtc/useWebRTCConnection.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
interface WebRTCConnectionState {
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
isWebSocketConnected: boolean;
|
||||
error: string | null;
|
||||
localDataChannel: RTCDataChannel | null;
|
||||
remoteDataChannel: RTCDataChannel | null;
|
||||
}
|
||||
|
||||
export function useWebRTCConnection() {
|
||||
const [state, setState] = useState<WebRTCConnectionState>({
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isWebSocketConnected: false,
|
||||
error: null,
|
||||
localDataChannel: null,
|
||||
remoteDataChannel: null,
|
||||
});
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const pcRef = useRef<RTCPeerConnection | null>(null);
|
||||
const dcRef = useRef<RTCDataChannel | null>(null);
|
||||
|
||||
const STUN_SERVERS = [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
{ urls: 'stun:stun2.l.google.com:19302' },
|
||||
];
|
||||
|
||||
const updateState = useCallback((updates: Partial<WebRTCConnectionState>) => {
|
||||
setState(prev => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
|
||||
console.log('=== 开始WebRTC连接 ===');
|
||||
console.log('房间代码:', roomCode, '角色:', role);
|
||||
|
||||
updateState({ isConnecting: true, error: null });
|
||||
|
||||
try {
|
||||
// 创建PeerConnection
|
||||
const pc = new RTCPeerConnection({ iceServers: STUN_SERVERS });
|
||||
pcRef.current = pc;
|
||||
|
||||
// 连接WebSocket信令服务器
|
||||
const ws = new WebSocket(`ws://localhost:8080/ws/webrtc?room=${roomCode}&role=${role}`);
|
||||
wsRef.current = ws;
|
||||
|
||||
// WebSocket事件处理
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket连接已建立');
|
||||
updateState({ isWebSocketConnected: true });
|
||||
};
|
||||
|
||||
ws.onmessage = async (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
console.log('收到信令消息:', message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'offer':
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
ws.send(JSON.stringify({ type: 'answer', answer }));
|
||||
break;
|
||||
|
||||
case 'answer':
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.answer));
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
if (message.candidate) {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(message.candidate));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('信令错误:', message.error);
|
||||
updateState({ error: message.error, isConnecting: false });
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理信令消息失败:', error);
|
||||
updateState({ error: '信令处理失败', isConnecting: false });
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error);
|
||||
updateState({ error: 'WebSocket连接失败', isConnecting: false, isWebSocketConnected: false });
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket连接已关闭');
|
||||
updateState({ isWebSocketConnected: false });
|
||||
};
|
||||
|
||||
// ICE候选事件
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate && ws.readyState === WebSocket.OPEN) {
|
||||
console.log('发送ICE候选:', event.candidate);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ice-candidate',
|
||||
candidate: event.candidate
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 连接状态变化
|
||||
pc.onconnectionstatechange = () => {
|
||||
console.log('连接状态:', pc.connectionState);
|
||||
const isConnected = pc.connectionState === 'connected';
|
||||
updateState({
|
||||
isConnected,
|
||||
isConnecting: !isConnected && pc.connectionState !== 'failed'
|
||||
});
|
||||
|
||||
if (pc.connectionState === 'failed') {
|
||||
updateState({ error: '连接失败', isConnecting: false });
|
||||
}
|
||||
};
|
||||
|
||||
// 如果是发送方,创建数据通道
|
||||
if (role === 'sender') {
|
||||
const dataChannel = pc.createDataChannel('fileTransfer', {
|
||||
ordered: true
|
||||
});
|
||||
dcRef.current = dataChannel;
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
console.log('数据通道已打开 (发送方)');
|
||||
updateState({ localDataChannel: dataChannel });
|
||||
};
|
||||
|
||||
// 创建offer
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
ws.send(JSON.stringify({ type: 'offer', offer }));
|
||||
} else {
|
||||
// 接收方等待数据通道
|
||||
pc.ondatachannel = (event) => {
|
||||
const dataChannel = event.channel;
|
||||
dcRef.current = dataChannel;
|
||||
console.log('收到数据通道 (接收方)');
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
console.log('数据通道已打开 (接收方)');
|
||||
updateState({ remoteDataChannel: dataChannel });
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接失败:', error);
|
||||
updateState({
|
||||
error: error instanceof Error ? error.message : '连接失败',
|
||||
isConnecting: false
|
||||
});
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
console.log('断开WebRTC连接');
|
||||
|
||||
if (dcRef.current) {
|
||||
dcRef.current.close();
|
||||
dcRef.current = null;
|
||||
}
|
||||
|
||||
if (pcRef.current) {
|
||||
pcRef.current.close();
|
||||
pcRef.current = null;
|
||||
}
|
||||
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
setState({
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isWebSocketConnected: false,
|
||||
error: null,
|
||||
localDataChannel: null,
|
||||
remoteDataChannel: null,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getDataChannel = useCallback(() => {
|
||||
return dcRef.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
connect,
|
||||
disconnect,
|
||||
getDataChannel,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user