mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-28 02:44:41 +08:00
feat:传输文件优化
This commit is contained in:
31
chuan-next/src/app/api/room-info/route.ts
Normal file
31
chuan-next/src/app/api/room-info/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const GO_BACKEND_URL = process.env.GO_BACKEND_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const code = searchParams.get('code');
|
||||
|
||||
console.log('API Route: Getting room info, proxying to:', `${GO_BACKEND_URL}/api/room-info?code=${code}`);
|
||||
|
||||
const response = await fetch(`${GO_BACKEND_URL}/api/room-info?code=${code}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('Backend response:', response.status, data);
|
||||
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('API Route Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get room info', details: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { useWebRTCTransfer } from '@/hooks/useWebRTCTransfer';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import { Upload, Download } from 'lucide-react';
|
||||
import { WebRTCFileUpload } from '@/components/webrtc/WebRTCFileUpload';
|
||||
import { WebRTCFileReceive } from '@/components/webrtc/WebRTCFileReceive';
|
||||
@@ -21,11 +21,17 @@ interface FileInfo {
|
||||
export const WebRTCFileTransfer: React.FC = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
|
||||
// 独立的文件状态
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [fileList, setFileList] = useState<FileInfo[]>([]);
|
||||
const [downloadedFiles, setDownloadedFiles] = useState<Map<string, File>>(new Map());
|
||||
const [currentTransferFile, setCurrentTransferFile] = useState<{
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
progress: number;
|
||||
} | null>(null);
|
||||
|
||||
// 房间状态
|
||||
const [pickupCode, setPickupCode] = useState('');
|
||||
@@ -34,9 +40,8 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
isTransferring,
|
||||
isConnecting,
|
||||
isWebSocketConnected,
|
||||
transferProgress,
|
||||
error,
|
||||
connect,
|
||||
disconnect,
|
||||
@@ -45,7 +50,8 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
requestFile: requestFileFromHook,
|
||||
onFileReceived,
|
||||
onFileListReceived,
|
||||
onFileRequested
|
||||
onFileRequested,
|
||||
onFileProgress
|
||||
} = useWebRTCTransfer();
|
||||
|
||||
// 从URL参数中获取初始模式
|
||||
@@ -112,11 +118,7 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
// 创建房间 (发送模式)
|
||||
const generateCode = async () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
toast({
|
||||
title: "请先选择文件",
|
||||
description: "需要选择文件才能创建传输房间",
|
||||
variant: "destructive",
|
||||
});
|
||||
showToast("需要选择文件才能创建传输房间", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,17 +156,10 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
// 连接WebRTC作为发送方
|
||||
connect(code, 'sender');
|
||||
|
||||
toast({
|
||||
title: "房间创建成功",
|
||||
description: `取件码: ${code}`,
|
||||
});
|
||||
showToast(`房间创建成功,取件码: ${code}`, "success");
|
||||
} catch (error) {
|
||||
console.error('创建房间失败:', error);
|
||||
toast({
|
||||
title: "创建房间失败",
|
||||
description: error instanceof Error ? error.message : '网络错误,请重试',
|
||||
variant: "destructive",
|
||||
});
|
||||
showToast(error instanceof Error ? error.message : '网络错误,请重试', "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,10 +171,27 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
setPickupCode(code.trim());
|
||||
connect(code.trim(), 'receiver');
|
||||
|
||||
toast({
|
||||
title: "正在连接...",
|
||||
description: `尝试连接到房间: ${code}`,
|
||||
});
|
||||
showToast(`正在连接到房间: ${code}`, "info");
|
||||
};
|
||||
|
||||
// 重置连接状态 (用于连接失败后重新输入)
|
||||
const resetConnection = () => {
|
||||
console.log('=== 重置连接状态 ===');
|
||||
|
||||
// 断开当前连接
|
||||
disconnect();
|
||||
|
||||
// 清空状态
|
||||
setPickupCode('');
|
||||
setFileList([]);
|
||||
setDownloadedFiles(new Map());
|
||||
|
||||
// 如果是接收模式,更新URL移除code参数
|
||||
if (mode === 'receive') {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete('code');
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文件列表更新
|
||||
@@ -197,6 +209,17 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
return cleanup;
|
||||
}, [onFileListReceived, mode]);
|
||||
|
||||
// 处理连接错误
|
||||
useEffect(() => {
|
||||
if (error && mode === 'receive') {
|
||||
console.log('=== 连接错误处理 ===');
|
||||
console.log('错误信息:', error);
|
||||
|
||||
// 显示错误提示
|
||||
showToast(`连接失败: ${error}`, "error");
|
||||
}
|
||||
}, [error, mode, showToast]);
|
||||
|
||||
// 处理文件接收
|
||||
useEffect(() => {
|
||||
const cleanup = onFileReceived((fileData: { id: string; file: File }) => {
|
||||
@@ -213,31 +236,51 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
: item
|
||||
));
|
||||
|
||||
toast({
|
||||
title: "文件下载完成",
|
||||
description: `${fileData.file.name} 已准备好下载`,
|
||||
});
|
||||
showToast(`${fileData.file.name} 已准备好下载`, "success");
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onFileReceived]);
|
||||
}, [onFileReceived, showToast]);
|
||||
|
||||
// 实时更新传输进度
|
||||
// 监听文件级别的进度更新
|
||||
useEffect(() => {
|
||||
console.log('=== 进度更新 ===');
|
||||
console.log('传输中:', isTransferring, '进度:', transferProgress);
|
||||
|
||||
if (isTransferring && transferProgress > 0) {
|
||||
console.log('更新文件传输进度:', transferProgress);
|
||||
const cleanup = onFileProgress((progressInfo) => {
|
||||
console.log('=== 文件进度更新 ===');
|
||||
console.log('文件:', progressInfo.fileName, 'ID:', progressInfo.fileId, '进度:', progressInfo.progress);
|
||||
|
||||
// 更新当前传输文件信息
|
||||
setCurrentTransferFile({
|
||||
fileId: progressInfo.fileId,
|
||||
fileName: progressInfo.fileName,
|
||||
progress: progressInfo.progress
|
||||
});
|
||||
|
||||
// 更新文件列表中对应文件的进度
|
||||
setFileList(prev => prev.map(item => {
|
||||
if (item.status === 'downloading') {
|
||||
console.log(`更新文件 ${item.name} 进度从 ${item.progress} 到 ${transferProgress}`);
|
||||
return { ...item, progress: transferProgress };
|
||||
if (item.id === progressInfo.fileId || item.name === progressInfo.fileName) {
|
||||
const newProgress = progressInfo.progress;
|
||||
const newStatus = newProgress >= 100 ? 'completed' as const : 'downloading' as const;
|
||||
|
||||
console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${newProgress}`);
|
||||
return { ...item, progress: newProgress, status: newStatus };
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
}
|
||||
}, [isTransferring, transferProgress]);
|
||||
|
||||
// 当传输完成时显示提示
|
||||
if (progressInfo.progress >= 100 && mode === 'send') {
|
||||
showToast(`文件发送完成: ${progressInfo.fileName}`, "success");
|
||||
setCurrentTransferFile(null);
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onFileProgress, mode, showToast]);
|
||||
|
||||
// 实时更新传输进度(旧逻辑 - 删除)
|
||||
// useEffect(() => {
|
||||
// ...已删除的旧代码...
|
||||
// }, [...]);
|
||||
|
||||
// 处理文件请求(发送方监听)
|
||||
useEffect(() => {
|
||||
@@ -254,25 +297,31 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
if (!file) {
|
||||
console.error('找不到匹配的文件:', fileName);
|
||||
console.log('可用文件:', selectedFiles.map(f => `${f.name} (${f.size} bytes)`));
|
||||
toast({
|
||||
title: "文件不存在",
|
||||
description: `无法找到文件: ${fileName}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
showToast(`无法找到文件: ${fileName}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('找到匹配文件,开始发送:', file.name, 'ID:', fileId, '文件大小:', file.size);
|
||||
|
||||
// 更新发送方文件状态为downloading
|
||||
setFileList(prev => prev.map(item =>
|
||||
item.id === fileId || item.name === fileName
|
||||
? { ...item, status: 'downloading' as const, progress: 0 }
|
||||
: item
|
||||
));
|
||||
|
||||
// 发送文件
|
||||
sendFile(file, fileId);
|
||||
|
||||
// 显示开始传输的提示
|
||||
showToast(`开始发送文件: ${fileName}`, "info");
|
||||
} else {
|
||||
console.warn('接收模式下收到文件请求,忽略');
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onFileRequested, mode, selectedFiles, sendFile, toast]);
|
||||
}, [onFileRequested, mode, selectedFiles, sendFile, showToast]);
|
||||
|
||||
// 连接状态变化时同步文件列表
|
||||
useEffect(() => {
|
||||
@@ -328,7 +377,7 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
console.log('=== 开始请求文件 ===');
|
||||
console.log('文件信息:', { name: fileInfo.name, id: fileId, size: fileInfo.size });
|
||||
console.log('当前文件状态:', fileInfo.status);
|
||||
console.log('WebRTC连接状态:', { isConnected, isTransferring });
|
||||
console.log('WebRTC连接状态:', { isConnected, isTransferring: !!currentTransferFile });
|
||||
|
||||
// 更新文件状态为下载中
|
||||
setFileList(prev => {
|
||||
@@ -345,29 +394,20 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
console.log('调用hook的requestFile...');
|
||||
requestFileFromHook(fileId, fileInfo.name);
|
||||
|
||||
toast({
|
||||
title: "请求文件",
|
||||
description: `正在请求文件: ${fileInfo.name}`,
|
||||
});
|
||||
showToast(`正在请求文件: ${fileInfo.name}`, "info");
|
||||
};
|
||||
|
||||
// 复制取件码
|
||||
const copyCode = () => {
|
||||
navigator.clipboard.writeText(pickupCode);
|
||||
toast({
|
||||
title: "取件码已复制",
|
||||
description: "取件码已复制到剪贴板",
|
||||
});
|
||||
showToast("取件码已复制到剪贴板", "success");
|
||||
};
|
||||
|
||||
// 复制链接
|
||||
const copyLink = () => {
|
||||
const link = `${window.location.origin}?type=webrtc&mode=receive&code=${pickupCode}`;
|
||||
navigator.clipboard.writeText(link);
|
||||
toast({
|
||||
title: "取件链接已复制",
|
||||
description: "取件链接已复制到剪贴板",
|
||||
});
|
||||
showToast("取件链接已复制到剪贴板", "success");
|
||||
};
|
||||
|
||||
// 重置状态
|
||||
@@ -409,10 +449,7 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: "文件已保存",
|
||||
description: `${file.name} 已保存到下载文件夹`,
|
||||
});
|
||||
showToast(`${file.name} 已保存到下载文件夹`, "success");
|
||||
};
|
||||
|
||||
// 处理下载请求(接收模式)
|
||||
@@ -432,13 +469,9 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
// 显示错误信息
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toast({
|
||||
title: "连接错误",
|
||||
description: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
showToast(error, "error");
|
||||
}
|
||||
}, [error]);
|
||||
}, [error, showToast]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
@@ -468,6 +501,7 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 sm:p-6 shadow-lg border border-white/20 animate-fade-in-up">
|
||||
<WebRTCFileUpload
|
||||
selectedFiles={selectedFiles}
|
||||
fileList={fileList}
|
||||
onFilesChange={setSelectedFiles}
|
||||
onGenerateCode={generateCode}
|
||||
pickupCode={pickupCode}
|
||||
@@ -478,7 +512,7 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
onRemoveFile={setSelectedFiles}
|
||||
onClearFiles={clearFiles}
|
||||
onReset={resetRoom}
|
||||
disabled={isTransferring}
|
||||
disabled={!!currentTransferFile}
|
||||
isConnected={isConnected}
|
||||
isWebSocketConnected={isWebSocketConnected}
|
||||
/>
|
||||
@@ -489,12 +523,12 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
onJoinRoom={joinRoom}
|
||||
files={fileList}
|
||||
onDownloadFile={handleDownloadRequest}
|
||||
transferProgress={transferProgress}
|
||||
isTransferring={isTransferring}
|
||||
isConnected={isConnected}
|
||||
isConnecting={!!pickupCode && !isConnected}
|
||||
isConnecting={isConnecting}
|
||||
isWebSocketConnected={isWebSocketConnected}
|
||||
downloadedFiles={downloadedFiles}
|
||||
error={error}
|
||||
onReset={resetConnection}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Download, FileText, Image, Video, Music, Archive } from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
@@ -34,31 +35,76 @@ interface WebRTCFileReceiveProps {
|
||||
onJoinRoom: (code: string) => void;
|
||||
files: FileInfo[];
|
||||
onDownloadFile: (fileId: string) => void;
|
||||
transferProgress: number;
|
||||
isTransferring: boolean;
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
isWebSocketConnected?: boolean;
|
||||
downloadedFiles?: Map<string, File>;
|
||||
error?: string | null;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export function WebRTCFileReceive({
|
||||
onJoinRoom,
|
||||
files,
|
||||
onDownloadFile,
|
||||
transferProgress,
|
||||
isTransferring,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
isWebSocketConnected = false,
|
||||
downloadedFiles
|
||||
downloadedFiles,
|
||||
error = null,
|
||||
onReset
|
||||
}: WebRTCFileReceiveProps) {
|
||||
const [pickupCode, setPickupCode] = useState('');
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const { showToast } = useToast();
|
||||
|
||||
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||
// 验证取件码是否存在
|
||||
const validatePickupCode = async (code: string): Promise<boolean> => {
|
||||
try {
|
||||
setIsValidating(true);
|
||||
|
||||
console.log('开始验证取件码:', code);
|
||||
const response = await fetch(`/api/room-info?code=${code}`);
|
||||
const data = await response.json();
|
||||
|
||||
console.log('验证响应:', { status: response.status, data });
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
const errorMessage = data.message || '取件码验证失败';
|
||||
|
||||
// 显示toast错误提示
|
||||
showToast(errorMessage, 'error');
|
||||
|
||||
console.log('验证失败:', errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('取件码验证成功:', data.room);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('验证取件码时发生错误:', error);
|
||||
const errorMessage = '网络错误,请检查连接后重试';
|
||||
|
||||
// 显示toast错误提示
|
||||
showToast(errorMessage, 'error');
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (pickupCode.length === 6) {
|
||||
onJoinRoom(pickupCode.toUpperCase());
|
||||
const code = pickupCode.toUpperCase();
|
||||
|
||||
// 先验证取件码是否存在
|
||||
const isValid = await validatePickupCode(code);
|
||||
if (isValid) {
|
||||
// 验证成功后再进行WebRTC连接
|
||||
onJoinRoom(code);
|
||||
}
|
||||
}
|
||||
}, [pickupCode, onJoinRoom]);
|
||||
|
||||
@@ -69,6 +115,19 @@ export function WebRTCFileReceive({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 当验证失败时重置输入状态
|
||||
React.useEffect(() => {
|
||||
if (error && !isConnecting && !isConnected && !isValidating) {
|
||||
// 延迟重置,确保用户能看到错误信息
|
||||
const timer = setTimeout(() => {
|
||||
console.log('重置取件码输入');
|
||||
setPickupCode('');
|
||||
}, 3000); // 3秒后重置
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [error, isConnecting, isConnected, isValidating]);
|
||||
|
||||
// 如果已经连接但没有文件,显示等待界面
|
||||
if ((isConnected || isConnecting) && files.length === 0) {
|
||||
return (
|
||||
@@ -237,15 +296,13 @@ export function WebRTCFileReceive({
|
||||
const isDownloading = file.status === 'downloading';
|
||||
const isCompleted = file.status === 'completed';
|
||||
const hasDownloadedFile = downloadedFiles?.has(file.id);
|
||||
const currentProgress = isDownloading && isTransferring ? transferProgress : file.progress;
|
||||
const currentProgress = file.progress;
|
||||
|
||||
console.log('文件状态:', {
|
||||
fileName: file.name,
|
||||
status: file.status,
|
||||
progress: file.progress,
|
||||
isDownloading,
|
||||
isTransferring,
|
||||
transferProgress,
|
||||
currentProgress
|
||||
});
|
||||
|
||||
@@ -262,24 +319,24 @@ export function WebRTCFileReceive({
|
||||
{hasDownloadedFile && (
|
||||
<p className="text-xs text-emerald-600 font-medium">✅ 传输完成,点击保存</p>
|
||||
)}
|
||||
{isDownloading && isTransferring && (
|
||||
{isDownloading && (
|
||||
<p className="text-xs text-blue-600 font-medium">⏳ 传输中...{currentProgress.toFixed(1)}%</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onDownloadFile(file.id)}
|
||||
disabled={!isConnected || (isDownloading && isTransferring)}
|
||||
disabled={!isConnected || isDownloading}
|
||||
className={`px-6 py-2 rounded-lg font-medium shadow-lg transition-all duration-200 hover:shadow-xl ${
|
||||
hasDownloadedFile
|
||||
? 'bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 text-white'
|
||||
: (isDownloading && isTransferring)
|
||||
: isDownloading
|
||||
? 'bg-slate-300 text-slate-500 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{(isDownloading && isTransferring) ? '传输中...' : hasDownloadedFile ? '保存文件' : '开始传输'}
|
||||
{isDownloading ? '传输中...' : hasDownloadedFile ? '保存文件' : '开始传输'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -387,7 +444,7 @@ export function WebRTCFileReceive({
|
||||
placeholder="请输入取件码"
|
||||
className="text-center text-2xl sm:text-3xl tracking-[0.3em] sm:tracking-[0.5em] font-mono h-12 sm:h-16 border-2 border-slate-200 rounded-xl focus:border-emerald-500 focus:ring-emerald-500 bg-white/80 backdrop-blur-sm pb-2 sm:pb-4"
|
||||
maxLength={6}
|
||||
disabled={isConnecting}
|
||||
disabled={isValidating || isConnecting}
|
||||
/>
|
||||
<div className="absolute inset-x-0 -bottom-4 sm:-bottom-6 flex justify-center space-x-1 sm:space-x-2">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
@@ -411,9 +468,14 @@ export function WebRTCFileReceive({
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-10 sm:h-12 bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white text-base sm:text-lg font-medium rounded-xl shadow-lg transition-all duration-200 hover:shadow-xl hover:scale-105 disabled:opacity-50 disabled:scale-100"
|
||||
disabled={pickupCode.length !== 6 || isConnecting}
|
||||
disabled={pickupCode.length !== 6 || isValidating || isConnecting}
|
||||
>
|
||||
{isConnecting ? (
|
||||
{isValidating ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>验证中...</span>
|
||||
</div>
|
||||
) : isConnecting ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>连接中...</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import { Upload, FileText, Image, Video, Music, Archive, X } from 'lucide-react';
|
||||
|
||||
interface FileInfo {
|
||||
@@ -32,6 +32,7 @@ const formatFileSize = (bytes: number): string => {
|
||||
|
||||
interface WebRTCFileUploadProps {
|
||||
selectedFiles: File[];
|
||||
fileList?: FileInfo[]; // 添加文件列表信息(包含状态和进度)
|
||||
onFilesChange: (files: File[]) => void;
|
||||
onGenerateCode: () => void;
|
||||
pickupCode?: string;
|
||||
@@ -49,6 +50,7 @@ interface WebRTCFileUploadProps {
|
||||
|
||||
export function WebRTCFileUpload({
|
||||
selectedFiles,
|
||||
fileList = [],
|
||||
onFilesChange,
|
||||
onGenerateCode,
|
||||
pickupCode,
|
||||
@@ -254,31 +256,77 @@ export function WebRTCFileUpload({
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4 sm:mb-6">
|
||||
{selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={`${file.name}-${file.size}-${index}`}
|
||||
className="group flex items-center justify-between p-3 sm:p-4 bg-gradient-to-r from-slate-50 to-blue-50 border border-slate-200 rounded-xl hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center space-x-3 sm:space-x-4 min-w-0 flex-1">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
{getFileIcon(file.type)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-slate-800 truncate text-sm sm:text-base">{file.name}</p>
|
||||
<p className="text-xs sm:text-sm text-slate-500">{formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={disabled}
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all duration-200 flex-shrink-0 ml-2"
|
||||
{selectedFiles.map((file, index) => {
|
||||
// 查找对应的文件信息(包含状态和进度)
|
||||
const fileInfo = fileList.find(f => f.name === file.name && f.size === file.size);
|
||||
const isTransferringThisFile = fileInfo?.status === 'downloading';
|
||||
const currentProgress = fileInfo?.progress || 0;
|
||||
const fileStatus = fileInfo?.status || 'ready';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${file.name}-${file.size}-${index}`}
|
||||
className="group bg-gradient-to-r from-slate-50 to-blue-50 border border-slate-200 rounded-xl hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center justify-between p-3 sm:p-4">
|
||||
<div className="flex items-center space-x-3 sm:space-x-4 min-w-0 flex-1">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
{getFileIcon(file.type)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-slate-800 truncate text-sm sm:text-base">{file.name}</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-xs sm:text-sm text-slate-500">{formatFileSize(file.size)}</p>
|
||||
{fileStatus === 'downloading' && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-1 h-1 bg-orange-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-xs text-orange-600 font-medium">传输中</span>
|
||||
</div>
|
||||
)}
|
||||
{fileStatus === 'completed' && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-1 h-1 bg-emerald-500 rounded-full"></div>
|
||||
<span className="text-xs text-emerald-600 font-medium">已完成</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={disabled || fileStatus === 'downloading'}
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all duration-200 flex-shrink-0 ml-2 disabled:opacity-50"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 传输进度条 */}
|
||||
{(fileStatus === 'downloading' || fileStatus === 'completed') && currentProgress > 0 && (
|
||||
<div className="px-3 sm:px-4 pb-3 sm:pb-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs text-slate-600">
|
||||
<span>{fileStatus === 'downloading' ? '正在发送...' : '发送完成'}</span>
|
||||
<span className="font-medium">{currentProgress.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
fileStatus === 'completed'
|
||||
? 'bg-gradient-to-r from-emerald-500 to-emerald-600'
|
||||
: 'bg-gradient-to-r from-orange-500 to-orange-600'
|
||||
}`}
|
||||
style={{ width: `${currentProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
|
||||
@@ -141,6 +141,7 @@ export function useWebRTCTransfer() {
|
||||
// 回调注册
|
||||
onFileRequested: fileTransfer.onFileRequested,
|
||||
onFileReceived: fileTransfer.onFileReceived,
|
||||
onFileProgress: fileTransfer.onFileProgress,
|
||||
onFileListReceived,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ interface FileTransferState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface FileProgressInfo {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface FileChunk {
|
||||
fileId: string;
|
||||
chunkIndex: number;
|
||||
@@ -21,7 +27,7 @@ interface FileMetadata {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = 16 * 1024; // 16KB chunks
|
||||
const CHUNK_SIZE = 256 * 1024; // 256KB chunks - 平衡传输效率和内存使用
|
||||
|
||||
export function useFileTransfer() {
|
||||
const [state, setState] = useState<FileTransferState>({
|
||||
@@ -39,9 +45,17 @@ export function useFileTransfer() {
|
||||
totalChunks: number;
|
||||
}>>(new Map());
|
||||
|
||||
// 存储当前正在接收的块信息
|
||||
const expectedChunk = useRef<{
|
||||
fileId: string;
|
||||
chunkIndex: number;
|
||||
totalChunks: number;
|
||||
} | null>(null);
|
||||
|
||||
// 文件请求回调
|
||||
const fileRequestCallbacks = useRef<Array<(fileId: string, fileName: string) => void>>([]);
|
||||
const fileReceivedCallbacks = useRef<Array<(fileData: { id: string; file: File }) => void>>([]);
|
||||
const fileProgressCallbacks = useRef<Array<(progressInfo: FileProgressInfo) => void>>([]);
|
||||
|
||||
const updateState = useCallback((updates: Partial<FileTransferState>) => {
|
||||
setState(prev => ({ ...prev, ...updates }));
|
||||
@@ -107,27 +121,54 @@ export function useFileTransfer() {
|
||||
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);
|
||||
// 检查缓冲区状态,避免过度缓冲
|
||||
if (dataChannel.bufferedAmount > 1024 * 1024) { // 1MB 缓冲区限制
|
||||
console.log('数据通道缓冲区满,等待清空...');
|
||||
// 等待缓冲区清空后再发送
|
||||
const waitForBuffer = () => {
|
||||
if (dataChannel.bufferedAmount < 256 * 1024) { // 等到缓冲区低于 256KB
|
||||
sendChunkData();
|
||||
} else {
|
||||
setTimeout(waitForBuffer, 10);
|
||||
}
|
||||
};
|
||||
waitForBuffer();
|
||||
} else {
|
||||
sendChunkData();
|
||||
}
|
||||
|
||||
sentChunks++;
|
||||
const progress = (sentChunks / totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
function sendChunkData() {
|
||||
// 发送分块数据
|
||||
const chunkMessage = JSON.stringify({
|
||||
type: 'file-chunk',
|
||||
payload: {
|
||||
fileId,
|
||||
chunkIndex: sentChunks,
|
||||
totalChunks
|
||||
}
|
||||
});
|
||||
|
||||
dataChannel.send(chunkMessage);
|
||||
dataChannel.send(arrayBuffer);
|
||||
|
||||
console.log(`发送进度: ${progress.toFixed(1)}%, 块: ${sentChunks}/${totalChunks}`);
|
||||
sentChunks++;
|
||||
const progress = (sentChunks / totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
|
||||
// 短暂延迟,避免阻塞
|
||||
setTimeout(sendNextChunk, 10);
|
||||
// 通知文件级别的进度
|
||||
fileProgressCallbacks.current.forEach(callback => {
|
||||
callback({
|
||||
fileId,
|
||||
fileName: file.name,
|
||||
progress
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`发送进度: ${progress.toFixed(1)}%, 块: ${sentChunks}/${totalChunks}, 文件: ${file.name}, 缓冲区: ${dataChannel.bufferedAmount} bytes`);
|
||||
|
||||
// 立即发送下一个块,不等待
|
||||
sendNextChunk();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,7 +220,15 @@ export function useFileTransfer() {
|
||||
|
||||
case 'file-chunk':
|
||||
const chunkInfo = message.payload;
|
||||
console.log(`接收文件块: ${chunkInfo.chunkIndex + 1}/${chunkInfo.totalChunks}`);
|
||||
const { fileId: chunkFileId, chunkIndex, totalChunks } = chunkInfo;
|
||||
console.log(`接收文件块信息: ${chunkIndex + 1}/${totalChunks}, 文件ID: ${chunkFileId}`);
|
||||
|
||||
// 设置期望的下一个块
|
||||
expectedChunk.current = {
|
||||
fileId: chunkFileId,
|
||||
chunkIndex,
|
||||
totalChunks
|
||||
};
|
||||
break;
|
||||
|
||||
case 'file-end':
|
||||
@@ -229,18 +278,37 @@ export function useFileTransfer() {
|
||||
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++;
|
||||
// 检查是否有期望的块信息
|
||||
if (expectedChunk.current) {
|
||||
const { fileId, chunkIndex } = expectedChunk.current;
|
||||
const fileInfo = receivingFiles.current.get(fileId);
|
||||
|
||||
if (fileInfo) {
|
||||
// 确保chunks数组足够大
|
||||
if (!fileInfo.chunks[chunkIndex]) {
|
||||
fileInfo.chunks[chunkIndex] = arrayBuffer;
|
||||
fileInfo.receivedChunks++;
|
||||
|
||||
const progress = (fileInfo.receivedChunks / fileInfo.totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
const progress = (fileInfo.receivedChunks / fileInfo.totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
|
||||
console.log(`文件接收进度: ${progress.toFixed(1)}%, 块: ${fileInfo.receivedChunks}/${fileInfo.totalChunks}`);
|
||||
break;
|
||||
// 通知文件级别的进度
|
||||
fileProgressCallbacks.current.forEach(callback => {
|
||||
callback({
|
||||
fileId,
|
||||
fileName: fileInfo.metadata.name,
|
||||
progress
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`文件接收进度: ${progress.toFixed(1)}%, 块: ${fileInfo.receivedChunks}/${fileInfo.totalChunks}, 文件: ${fileInfo.metadata.name}`);
|
||||
}
|
||||
|
||||
// 清除期望的块信息
|
||||
expectedChunk.current = null;
|
||||
}
|
||||
} else {
|
||||
console.warn('收到块数据但没有对应的块信息');
|
||||
}
|
||||
}
|
||||
}, [updateState]);
|
||||
@@ -288,6 +356,19 @@ export function useFileTransfer() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 注册文件进度回调
|
||||
const onFileProgress = useCallback((callback: (progressInfo: FileProgressInfo) => void) => {
|
||||
fileProgressCallbacks.current.push(callback);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
const index = fileProgressCallbacks.current.indexOf(callback);
|
||||
if (index > -1) {
|
||||
fileProgressCallbacks.current.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
sendFile,
|
||||
@@ -295,5 +376,6 @@ export function useFileTransfer() {
|
||||
handleMessage,
|
||||
onFileRequested,
|
||||
onFileReceived,
|
||||
onFileProgress,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { url } from 'inspector';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
interface WebRTCConnectionState {
|
||||
@@ -22,6 +23,7 @@ export function useWebRTCConnection() {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const pcRef = useRef<RTCPeerConnection | null>(null);
|
||||
const dcRef = useRef<RTCDataChannel | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const STUN_SERVERS = [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
@@ -29,29 +31,78 @@ export function useWebRTCConnection() {
|
||||
{ urls: 'stun:stun2.l.google.com:19302' },
|
||||
];
|
||||
|
||||
// 连接超时时间(30秒)
|
||||
const CONNECTION_TIMEOUT = 30000;
|
||||
|
||||
const updateState = useCallback((updates: Partial<WebRTCConnectionState>) => {
|
||||
setState(prev => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
// 清理超时定时器
|
||||
const clearConnectionTimeout = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 处理连接超时
|
||||
const handleConnectionTimeout = useCallback(() => {
|
||||
console.warn('WebRTC连接超时');
|
||||
updateState({
|
||||
error: '连接超时,请检查取件码是否正确或稍后重试',
|
||||
isConnecting: false
|
||||
});
|
||||
|
||||
// 清理连接
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
if (pcRef.current) {
|
||||
pcRef.current.close();
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
|
||||
console.log('=== 开始WebRTC连接 ===');
|
||||
console.log('房间代码:', roomCode, '角色:', role);
|
||||
|
||||
// 清理之前的超时定时器
|
||||
clearConnectionTimeout();
|
||||
|
||||
updateState({ isConnecting: true, error: null });
|
||||
|
||||
// 设置连接超时
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
handleConnectionTimeout();
|
||||
}, CONNECTION_TIMEOUT);
|
||||
|
||||
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}`);
|
||||
const ws = new WebSocket(`ws://localhost:8080/ws/webrtc?code=${roomCode}&role=${role}`);
|
||||
wsRef.current = ws;
|
||||
|
||||
// WebSocket事件处理
|
||||
ws.onopen = () => {
|
||||
ws.onopen = async () => {
|
||||
console.log('WebSocket连接已建立');
|
||||
updateState({ isWebSocketConnected: true });
|
||||
|
||||
// 如果是发送方,在WebSocket连接建立后创建offer
|
||||
if (role === 'sender') {
|
||||
try {
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
ws.send(JSON.stringify({ type: 'offer', payload: offer }));
|
||||
console.log('已发送offer');
|
||||
} catch (error) {
|
||||
console.error('创建offer失败:', error);
|
||||
updateState({ error: '创建连接失败', isConnecting: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = async (event) => {
|
||||
@@ -61,19 +112,26 @@ export function useWebRTCConnection() {
|
||||
|
||||
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 }));
|
||||
if (message.payload) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'answer', payload: answer }));
|
||||
console.log('已发送answer');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'answer':
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.answer));
|
||||
if (message.payload) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
if (message.candidate) {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(message.candidate));
|
||||
if (message.payload) {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(message.payload));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -104,7 +162,7 @@ export function useWebRTCConnection() {
|
||||
console.log('发送ICE候选:', event.candidate);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ice-candidate',
|
||||
candidate: event.candidate
|
||||
payload: event.candidate
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -113,12 +171,19 @@ export function useWebRTCConnection() {
|
||||
pc.onconnectionstatechange = () => {
|
||||
console.log('连接状态:', pc.connectionState);
|
||||
const isConnected = pc.connectionState === 'connected';
|
||||
|
||||
if (isConnected) {
|
||||
// 连接成功,清理超时定时器
|
||||
clearConnectionTimeout();
|
||||
}
|
||||
|
||||
updateState({
|
||||
isConnected,
|
||||
isConnecting: !isConnected && pc.connectionState !== 'failed'
|
||||
});
|
||||
|
||||
if (pc.connectionState === 'failed') {
|
||||
clearConnectionTimeout();
|
||||
updateState({ error: '连接失败', isConnecting: false });
|
||||
}
|
||||
};
|
||||
@@ -126,19 +191,27 @@ export function useWebRTCConnection() {
|
||||
// 如果是发送方,创建数据通道
|
||||
if (role === 'sender') {
|
||||
const dataChannel = pc.createDataChannel('fileTransfer', {
|
||||
ordered: true
|
||||
ordered: true,
|
||||
maxPacketLifeTime: undefined,
|
||||
maxRetransmits: undefined
|
||||
});
|
||||
dcRef.current = dataChannel;
|
||||
|
||||
// 设置缓冲区管理
|
||||
dataChannel.bufferedAmountLowThreshold = 256 * 1024; // 256KB
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
console.log('数据通道已打开 (发送方)');
|
||||
console.log('数据通道配置:', {
|
||||
id: dataChannel.id,
|
||||
label: dataChannel.label,
|
||||
maxPacketLifeTime: dataChannel.maxPacketLifeTime,
|
||||
maxRetransmits: dataChannel.maxRetransmits,
|
||||
ordered: dataChannel.ordered,
|
||||
bufferedAmountLowThreshold: dataChannel.bufferedAmountLowThreshold
|
||||
});
|
||||
updateState({ localDataChannel: dataChannel });
|
||||
};
|
||||
|
||||
// 创建offer
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
ws.send(JSON.stringify({ type: 'offer', offer }));
|
||||
} else {
|
||||
// 接收方等待数据通道
|
||||
pc.ondatachannel = (event) => {
|
||||
@@ -155,16 +228,20 @@ export function useWebRTCConnection() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接失败:', error);
|
||||
clearConnectionTimeout();
|
||||
updateState({
|
||||
error: error instanceof Error ? error.message : '连接失败',
|
||||
isConnecting: false
|
||||
});
|
||||
}
|
||||
}, [updateState]);
|
||||
}, [updateState, clearConnectionTimeout, handleConnectionTimeout]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
console.log('断开WebRTC连接');
|
||||
|
||||
// 清理超时定时器
|
||||
clearConnectionTimeout();
|
||||
|
||||
if (dcRef.current) {
|
||||
dcRef.current.close();
|
||||
dcRef.current = null;
|
||||
@@ -188,7 +265,7 @@ export function useWebRTCConnection() {
|
||||
localDataChannel: null,
|
||||
remoteDataChannel: null,
|
||||
});
|
||||
}, []);
|
||||
}, [clearConnectionTimeout]);
|
||||
|
||||
const getDataChannel = useCallback(() => {
|
||||
return dcRef.current;
|
||||
|
||||
Reference in New Issue
Block a user