feat:统一连接层,精简前后端

This commit is contained in:
MatrixSeven
2025-08-06 18:08:02 +08:00
parent 3f3b7d8f18
commit 7cb0d34fb1
42 changed files with 2790 additions and 9426 deletions

View File

@@ -1,62 +0,0 @@
import { useCallback } from 'react';
import { FileInfo, TransferProgress } from '@/types';
import { useToast } from '@/components/ui/toast-simple';
export const useFileReceiver = (
receiverFiles: FileInfo[],
transferProgresses: TransferProgress[],
setTransferProgresses: (progresses: TransferProgress[] | ((prev: TransferProgress[]) => TransferProgress[])) => void,
websocket: WebSocket | null,
sendMessage: (message: any) => void
) => {
const { showToast } = useToast();
// 下载文件
const downloadFile = useCallback((fileId: string) => {
console.log('开始下载文件:', fileId);
if (!websocket || websocket.readyState !== WebSocket.OPEN) {
showToast('连接未建立,请重试', 'error');
return;
}
// 检查是否已有同文件的进行中传输
const existingProgress = transferProgresses.find(p => p.originalFileId === fileId && p.status !== 'completed');
if (existingProgress) {
console.log('文件已在下载中,跳过重复请求:', fileId);
showToast('文件正在下载中...', 'info');
return;
}
const requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
console.log('生成请求ID:', requestId);
sendMessage({
type: 'file-request',
payload: {
file_id: fileId,
request_id: requestId
}
});
// 更新传输状态
const newProgress = {
fileId: requestId, // 传输的唯一标识
originalFileId: fileId, // 原始文件ID用于UI匹配
fileName: receiverFiles.find(f => f.id === fileId)?.name || fileId,
progress: 0,
receivedSize: 0,
totalSize: 0,
status: 'pending' as const
};
console.log('添加传输进度:', newProgress);
setTransferProgresses(prev => [
...prev.filter(p => p.originalFileId !== fileId), // 移除该文件的旧进度记录
newProgress
]);
}, [websocket, sendMessage, receiverFiles, showToast, transferProgresses, setTransferProgresses]);
return {
downloadFile
};
};

View File

@@ -1,78 +0,0 @@
import { useCallback } from 'react';
import { useToast } from '@/components/ui/toast-simple';
export const useFileSender = (selectedFiles: File[], sendMessage: (message: any) => void) => {
const { showToast } = useToast();
// 处理文件请求(发送方)
const handleFileRequest = useCallback(async (payload: any) => {
const fileId = payload.file_id;
const requestId = payload.request_id;
const fileIndex = parseInt(fileId.replace('file_', ''));
const file = selectedFiles[fileIndex];
if (!file) {
console.error('未找到请求的文件:', fileId);
return;
}
console.log('开始发送文件:', file.name);
showToast(`开始发送文件: ${file.name}`);
// 发送文件信息
sendMessage({
type: 'file-info',
payload: {
file_id: requestId,
name: file.name,
size: file.size,
mime_type: file.type,
last_modified: file.lastModified
}
});
// 分块发送文件
const chunkSize = 65536;
let offset = 0;
const sendChunk = () => {
if (offset >= file.size) {
sendMessage({
type: 'file-complete',
payload: { file_id: requestId }
});
showToast(`文件发送完成: ${file.name}`);
return;
}
const slice = file.slice(offset, offset + chunkSize);
const reader = new FileReader();
reader.onload = (e) => {
const chunk = e.target?.result as ArrayBuffer;
sendMessage({
type: 'file-chunk',
payload: {
file_id: requestId,
offset: offset,
data: Array.from(new Uint8Array(chunk)),
is_last: offset + chunk.byteLength >= file.size
}
});
offset += chunk.byteLength;
setTimeout(sendChunk, 10);
};
reader.readAsArrayBuffer(slice);
};
sendChunk();
}, [selectedFiles, sendMessage, showToast]);
return {
handleFileRequest
};
};

View File

@@ -1,203 +0,0 @@
import { useState, useCallback } from 'react';
import { TransferProgress } from '@/types';
import { useToast } from '@/components/ui/toast-simple';
interface FileTransferData {
fileId: string;
chunks: Array<{ offset: number; data: Uint8Array }>;
totalSize: number;
receivedSize: number;
fileName: string;
mimeType: string;
startTime: number;
}
export const useFileTransfer = () => {
const [fileTransfers, setFileTransfers] = useState<Map<string, FileTransferData>>(new Map());
const [completedDownloads, setCompletedDownloads] = useState<Set<string>>(new Set());
const [transferProgresses, setTransferProgresses] = useState<TransferProgress[]>([]);
const { showToast } = useToast();
// 初始化文件传输
const initFileTransfer = useCallback((fileInfo: any) => {
console.log('初始化文件传输:', fileInfo);
const transferKey = fileInfo.file_id;
setFileTransfers(prev => {
const newMap = new Map(prev);
newMap.set(transferKey, {
fileId: fileInfo.file_id,
chunks: [],
totalSize: fileInfo.size,
receivedSize: 0,
fileName: fileInfo.name,
mimeType: fileInfo.mime_type,
startTime: Date.now()
});
console.log('添加文件传输记录:', transferKey);
return newMap;
});
setTransferProgresses(prev => {
const updated = prev.map(p => p.fileId === fileInfo.file_id
? { ...p, status: 'downloading' as const, totalSize: fileInfo.size }
: p
);
console.log('更新传输进度为下载中:', updated);
return updated;
});
}, []);
// 组装并下载文件
const assembleAndDownloadFile = useCallback((transferKey: string, transfer: FileTransferData) => {
// 按偏移量排序数据块
transfer.chunks.sort((a, b) => a.offset - b.offset);
// 合并所有数据块
const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.data.length, 0);
const mergedData = new Uint8Array(totalSize);
let currentOffset = 0;
transfer.chunks.forEach((chunk) => {
mergedData.set(chunk.data, currentOffset);
currentOffset += chunk.data.length;
});
// 创建Blob并触发下载
const blob = new Blob([mergedData], { type: transfer.mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = transfer.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// 清理状态
setFileTransfers(prev => {
const newMap = new Map(prev);
newMap.delete(transferKey);
return newMap;
});
setTransferProgresses(prev =>
prev.filter(p => p.fileId !== transferKey)
);
const transferTime = (Date.now() - transfer.startTime) / 1000;
const speed = (transfer.totalSize / transferTime / 1024 / 1024).toFixed(2);
showToast(`文件 "${transfer.fileName}" 下载完成!传输速度: ${speed} MB/s`);
}, [showToast]);
// 接收文件数据块
const receiveFileChunk = useCallback((chunkData: any) => {
console.log('接收文件数据块:', chunkData);
const transferKey = chunkData.file_id;
setFileTransfers(prev => {
const newMap = new Map(prev);
const transfer = newMap.get(transferKey);
if (transfer) {
// 检查是否已经完成,如果已经完成就不再处理新的数据块
if (transfer.receivedSize >= transfer.totalSize) {
console.log('文件已完成,忽略额外的数据块');
return newMap;
}
const chunkArray = new Uint8Array(chunkData.data);
transfer.chunks.push({
offset: chunkData.offset,
data: chunkArray
});
transfer.receivedSize += chunkArray.length;
// 确保不超过总大小
if (transfer.receivedSize > transfer.totalSize) {
transfer.receivedSize = transfer.totalSize;
}
const progress = (transfer.receivedSize / transfer.totalSize) * 100;
console.log(`文件 ${transferKey} 进度: ${progress.toFixed(2)}%`);
// 更新进度
setTransferProgresses(prev => {
const updated = prev.map(p => p.fileId === transferKey
? {
...p,
progress,
receivedSize: transfer.receivedSize,
totalSize: transfer.totalSize
}
: p
);
console.log('更新进度状态:', updated);
return updated;
});
// 检查是否完成
if (chunkData.is_last || transfer.receivedSize >= transfer.totalSize) {
console.log('文件接收完成,准备下载');
// 标记为完成,等待 file-complete 消息统一处理下载
setTransferProgresses(prev =>
prev.map(p => p.fileId === transferKey
? { ...p, status: 'completed' as const, progress: 100, receivedSize: transfer.totalSize }
: p
)
);
}
} else {
console.warn('未找到对应的文件传输:', transferKey);
}
return newMap;
});
}, []);
// 完成文件下载
const completeFileDownload = useCallback((fileId: string) => {
console.log('文件传输完成,开始下载:', fileId);
// 检查是否已经完成过下载
if (completedDownloads.has(fileId)) {
console.log('文件已经下载过,跳过重复下载:', fileId);
return;
}
// 标记为已完成
setCompletedDownloads(prev => new Set([...prev, fileId]));
// 查找对应的文件传输数据
const transfer = fileTransfers.get(fileId);
if (transfer) {
assembleAndDownloadFile(fileId, transfer);
// 清理传输进度,移除已完成的文件进度显示
setTimeout(() => {
setTransferProgresses(prev =>
prev.filter(p => p.fileId !== fileId)
);
}, 2000); // 2秒后清理让用户看到完成状态
} else {
console.warn('未找到文件传输数据:', fileId);
}
}, [fileTransfers, assembleAndDownloadFile, completedDownloads]);
// 清理传输状态
const clearTransfers = useCallback(() => {
setFileTransfers(new Map());
setCompletedDownloads(new Set());
setTransferProgresses([]);
}, []);
return {
fileTransfers,
transferProgresses,
initFileTransfer,
receiveFileChunk,
completeFileDownload,
clearTransfers,
setTransferProgresses
};
};

View File

@@ -1,145 +0,0 @@
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,
};
}

View File

@@ -1,7 +1,8 @@
import { useCallback, useEffect, useRef } from 'react';
import { useWebRTCConnection } from './webrtc/useWebRTCConnection';
import { useFileTransfer } from './webrtc/useFileTransfer';
import { useCallback } from 'react';
import { useFileTransferBusiness } from './webrtc/useFileTransferBusiness';
import { useTextTransferBusiness } from './webrtc/useTextTransferBusiness';
// 文件信息接口(与现有组件兼容)
interface FileInfo {
id: string;
name: string;
@@ -11,137 +12,147 @@ interface FileInfo {
progress: number;
}
/**
* 统一的 WebRTC 传输 Hook - 新架构版本
* 整合文件传输、文字传输等多种业务功能
*
* 设计原则:
* 1. 独立连接:每个业务功能有自己独立的 WebRTC 连接
* 2. 复用逻辑所有业务功能复用相同的连接建立逻辑useWebRTCCore
* 3. 简单精准:避免过度抽象,每个功能模块职责清晰
* 4. 易于扩展:可以轻松添加新的业务功能(如屏幕共享、语音传输等)
* 5. 向后兼容:与现有的 WebRTCFileTransfer 组件保持接口兼容
*/
export function useWebRTCTransfer() {
const connection = useWebRTCConnection();
const fileTransfer = useFileTransfer();
const fileTransfer = useFileTransferBusiness();
const textTransfer = useTextTransferBusiness();
// 文件传输连接
const connectFileTransfer = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
console.log('连接文件传输通道...');
return fileTransfer.connect(roomCode, role);
}, [fileTransfer.connect]);
// 文字传输连接
const connectTextTransfer = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
console.log('连接文字传输通道...');
return textTransfer.connect(roomCode, role);
}, [textTransfer.connect]);
// 统一连接方法 - 同时连接所有功能
const connectAll = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
console.log('=== 启动 WebRTC 多功能传输 ===');
console.log('房间代码:', roomCode, '角色:', role);
console.log('将建立独立的文件传输和文字传输连接');
// 并行连接所有功能(各自独立的连接)
await Promise.all([
connectFileTransfer(roomCode, role),
connectTextTransfer(roomCode, role),
]);
console.log('所有传输通道连接完成');
}, [connectFileTransfer, connectTextTransfer]);
// 统一断开连接
const disconnectAll = useCallback(() => {
console.log('断开所有 WebRTC 传输连接');
fileTransfer.disconnect();
textTransfer.disconnect();
}, [fileTransfer.disconnect, textTransfer.disconnect]);
// 文件列表回调存储
const fileListCallbacks = useRef<Array<(fileList: FileInfo[]) => void>>([]);
// 设置数据通道消息处理
useEffect(() => {
const dataChannel = connection.localDataChannel || connection.remoteDataChannel;
if (dataChannel && dataChannel.readyState === 'open') {
console.log('设置数据通道消息处理器, 通道类型:', connection.localDataChannel ? '本地' : '远程');
// 扩展消息处理以包含文件列表
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.localDataChannel, connection.remoteDataChannel, 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,
isConnected: fileTransfer.isConnected,
isConnecting: fileTransfer.isConnecting,
isWebSocketConnected: fileTransfer.isWebSocketConnected,
error: fileTransfer.connectionError || fileTransfer.error,
// 传输状态
isTransferring: fileTransfer.isTransferring,
transferProgress: fileTransfer.transferProgress,
transferProgress: fileTransfer.progress,
receivedFiles: fileTransfer.receivedFiles,
// 操作方法
connect: connection.connect,
disconnect: connection.disconnect,
sendFile,
requestFile,
sendFileList,
// 主要方法
connect: fileTransfer.connect,
disconnect: fileTransfer.disconnect,
sendFile: fileTransfer.sendFile,
requestFile: fileTransfer.requestFile,
sendFileList: fileTransfer.sendFileList,
// 回调注册
// 回调方法
onFileRequested: fileTransfer.onFileRequested,
onFileReceived: fileTransfer.onFileReceived,
onFileProgress: fileTransfer.onFileProgress,
onFileListReceived,
onFileListReceived: fileTransfer.onFileListReceived,
// ===== 新的命名空间接口供Demo等组件使用 =====
// 统一操作
connectAll,
disconnectAll,
// 文件传输功能命名空间
file: {
// 连接状态
isConnected: fileTransfer.isConnected,
isConnecting: fileTransfer.isConnecting,
isWebSocketConnected: fileTransfer.isWebSocketConnected,
connectionError: fileTransfer.connectionError,
// 传输状态
isTransferring: fileTransfer.isTransferring,
progress: fileTransfer.progress,
error: fileTransfer.error,
receivedFiles: fileTransfer.receivedFiles,
// 方法
connect: fileTransfer.connect,
disconnect: fileTransfer.disconnect,
sendFile: fileTransfer.sendFile,
sendFileList: fileTransfer.sendFileList,
requestFile: fileTransfer.requestFile,
onFileReceived: fileTransfer.onFileReceived,
onFileRequested: fileTransfer.onFileRequested,
onFileProgress: fileTransfer.onFileProgress,
onFileListReceived: fileTransfer.onFileListReceived,
},
// 文字传输功能命名空间
text: {
// 连接状态
isConnected: textTransfer.isConnected,
isConnecting: textTransfer.isConnecting,
isWebSocketConnected: textTransfer.isWebSocketConnected,
connectionError: textTransfer.connectionError,
// 传输状态
messages: textTransfer.messages,
isTyping: textTransfer.isTyping,
error: textTransfer.error,
// 方法
connect: textTransfer.connect,
disconnect: textTransfer.disconnect,
sendMessage: textTransfer.sendMessage,
sendTypingStatus: textTransfer.sendTypingStatus,
clearMessages: textTransfer.clearMessages,
onMessageReceived: textTransfer.onMessageReceived,
onTypingStatus: textTransfer.onTypingStatus,
},
// 整体状态(用于 UI 显示)
hasAnyConnection: fileTransfer.isConnected || textTransfer.isConnected,
isAnyConnecting: fileTransfer.isConnecting || textTransfer.isConnecting,
hasAnyError: Boolean(fileTransfer.connectionError || textTransfer.connectionError),
// 可以继续添加其他业务功能
// 例如:
// screen: { ... }, // 屏幕共享
// voice: { ... }, // 语音传输
// video: { ... }, // 视频传输
};
}
}

View File

@@ -1,381 +0,0 @@
import { useState, useCallback, useRef } from 'react';
interface FileTransferState {
isTransferring: boolean;
transferProgress: number;
receivedFiles: Array<{ id: string; file: File }>;
error: string | null;
}
interface FileProgressInfo {
fileId: string;
fileName: string;
progress: number;
}
interface FileChunk {
fileId: string;
chunkIndex: number;
totalChunks: number;
data: ArrayBuffer;
}
interface FileMetadata {
id: string;
name: string;
size: number;
type: string;
}
const CHUNK_SIZE = 256 * 1024; // 256KB 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 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 }));
}, []);
// 发送文件
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;
// 检查缓冲区状态,避免过度缓冲
if (dataChannel.bufferedAmount > 1024 * 1024) { // 1MB 缓冲区限制
console.log('数据通道缓冲区满,等待清空...');
// 等待缓冲区清空后再发送
const waitForBuffer = () => {
if (dataChannel.bufferedAmount < 256 * 1024) { // 等到缓冲区低于 256KB
sendChunkData();
} else {
setTimeout(waitForBuffer, 10);
}
};
waitForBuffer();
} else {
sendChunkData();
}
function sendChunkData() {
// 发送分块数据
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 });
// 通知文件级别的进度
fileProgressCallbacks.current.forEach(callback => {
callback({
fileId,
fileName: file.name,
progress
});
});
console.log(`发送进度: ${progress.toFixed(1)}%, 块: ${sentChunks}/${totalChunks}, 文件: ${file.name}, 缓冲区: ${dataChannel.bufferedAmount} bytes`);
// 立即发送下一个块,不等待
sendNextChunk();
}
}
};
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;
const { fileId: chunkFileId, chunkIndex, totalChunks } = chunkInfo;
console.log(`接收文件块信息: ${chunkIndex + 1}/${totalChunks}, 文件ID: ${chunkFileId}`);
// 设置期望的下一个块
expectedChunk.current = {
fileId: chunkFileId,
chunkIndex,
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');
// 检查是否有期望的块信息
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 });
// 通知文件级别的进度
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]);
// 请求文件
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);
}
};
}, []);
// 注册文件进度回调
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,
requestFile,
handleMessage,
onFileRequested,
onFileReceived,
onFileProgress,
};
}

View File

@@ -0,0 +1,353 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useWebRTCCore } from './useWebRTCCore';
// 文件传输状态
interface FileTransferState {
isTransferring: boolean;
progress: number;
error: string | null;
receivedFiles: Array<{ id: string; file: File }>;
}
// 文件信息(用于文件列表)
interface FileInfo {
id: string;
name: string;
size: number;
type: string;
status: 'ready' | 'downloading' | 'completed';
progress: number;
}
// 文件元数据
interface FileMetadata {
id: string;
name: string;
size: number;
type: string;
}
// 文件块信息
interface FileChunk {
fileId: string;
chunkIndex: number;
totalChunks: number;
}
// 回调类型
type FileReceivedCallback = (fileData: { id: string; file: File }) => void;
type FileRequestedCallback = (fileId: string, fileName: string) => void;
type FileProgressCallback = (progressInfo: { fileId: string; fileName: string; progress: number }) => void;
type FileListReceivedCallback = (fileList: FileInfo[]) => void;
const CHUNK_SIZE = 256 * 1024; // 256KB
/**
* 文件传输业务层
* 使用 WebRTC 核心连接逻辑实现文件传输功能
* 每个实例有独立的连接,但复用相同的连接建立逻辑
*
* 支持功能:
* - 文件发送/接收
* - 文件列表同步
* - 文件请求机制
* - 进度跟踪
* - 多文件传输
*/
export function useFileTransferBusiness() {
const webrtcCore = useWebRTCCore('file-transfer');
const [state, setState] = useState<FileTransferState>({
isTransferring: false,
progress: 0,
error: null,
receivedFiles: [],
});
// 接收文件缓存
const receivingFiles = useRef<Map<string, {
metadata: FileMetadata;
chunks: ArrayBuffer[];
receivedChunks: number;
}>>(new Map());
// 当前期望的文件块
const expectedChunk = useRef<FileChunk | null>(null);
// 回调存储
const fileReceivedCallbacks = useRef<Set<FileReceivedCallback>>(new Set());
const fileRequestedCallbacks = useRef<Set<FileRequestedCallback>>(new Set());
const fileProgressCallbacks = useRef<Set<FileProgressCallback>>(new Set());
const fileListCallbacks = useRef<Set<FileListReceivedCallback>>(new Set());
const updateState = useCallback((updates: Partial<FileTransferState>) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
// 处理文件传输消息
const handleMessage = useCallback((message: any) => {
console.log('文件传输处理消息:', message.type);
switch (message.type) {
case 'file-metadata':
const metadata: FileMetadata = message.payload;
console.log('开始接收文件:', metadata.name);
receivingFiles.current.set(metadata.id, {
metadata,
chunks: [],
receivedChunks: 0,
});
updateState({ isTransferring: true, progress: 0 });
break;
case 'file-chunk-info':
expectedChunk.current = message.payload;
console.log('准备接收文件块:', message.payload);
break;
case 'file-complete':
const { 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,
progress: 100
}));
// 触发回调
fileReceivedCallbacks.current.forEach(cb => cb({ id: fileId, file }));
// 清理
receivingFiles.current.delete(fileId);
}
break;
case 'file-list':
console.log('收到文件列表:', message.payload);
fileListCallbacks.current.forEach(cb => cb(message.payload));
break;
case 'file-request':
const { fileId: requestedFileId, fileName } = message.payload;
console.log('收到文件请求:', fileName, requestedFileId);
fileRequestedCallbacks.current.forEach(cb => cb(requestedFileId, fileName));
break;
}
}, [updateState]);
// 处理文件块数据
const handleData = useCallback((data: ArrayBuffer) => {
if (!expectedChunk.current) {
console.warn('收到数据但没有对应的块信息');
return;
}
const { fileId, chunkIndex, totalChunks } = expectedChunk.current;
const fileInfo = receivingFiles.current.get(fileId);
if (fileInfo) {
fileInfo.chunks[chunkIndex] = data;
fileInfo.receivedChunks++;
const progress = (fileInfo.receivedChunks / totalChunks) * 100;
updateState({ progress });
// 触发文件级别的进度回调
fileProgressCallbacks.current.forEach(cb => cb({
fileId: fileId,
fileName: fileInfo.metadata.name,
progress
}));
console.log(`文件 ${fileInfo.metadata.name} 接收进度: ${progress.toFixed(1)}%`);
expectedChunk.current = null;
}
}, [updateState]);
// 设置处理器
useEffect(() => {
webrtcCore.setMessageHandler(handleMessage);
webrtcCore.setDataHandler(handleData);
return () => {
webrtcCore.setMessageHandler(null);
webrtcCore.setDataHandler(null);
};
}, [webrtcCore.setMessageHandler, webrtcCore.setDataHandler, handleMessage, handleData]);
// 连接
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
return webrtcCore.connect(roomCode, role);
}, [webrtcCore.connect]);
// 发送文件
const sendFile = useCallback(async (file: File, fileId?: string) => {
if (webrtcCore.getChannelState() !== 'open') {
updateState({ error: '连接未就绪' });
return;
}
const actualFileId = fileId || `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
console.log('开始发送文件:', file.name, '文件ID:', actualFileId, '总块数:', totalChunks);
updateState({ isTransferring: true, progress: 0, error: null });
try {
// 1. 发送文件元数据
webrtcCore.sendMessage({
type: 'file-metadata',
payload: {
id: actualFileId,
name: file.name,
size: file.size,
type: file.type
}
});
// 2. 分块发送文件
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
// 先发送块信息
webrtcCore.sendMessage({
type: 'file-chunk-info',
payload: {
fileId: actualFileId,
chunkIndex,
totalChunks
}
});
// 再发送块数据
const arrayBuffer = await chunk.arrayBuffer();
webrtcCore.sendData(arrayBuffer);
const progress = ((chunkIndex + 1) / totalChunks) * 100;
updateState({ progress });
// 触发文件级别的进度回调
fileProgressCallbacks.current.forEach(cb => cb({
fileId: actualFileId,
fileName: file.name,
progress
}));
// 简单的流控:等待一小段时间让接收方处理
if (chunkIndex % 10 === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// 3. 发送完成信号
webrtcCore.sendMessage({
type: 'file-complete',
payload: { fileId: actualFileId }
});
updateState({ isTransferring: false, progress: 100 });
console.log('文件发送完成:', file.name);
} catch (error) {
console.error('发送文件失败:', error);
updateState({
error: error instanceof Error ? error.message : '发送失败',
isTransferring: false
});
}
}, [webrtcCore.getChannelState, webrtcCore.sendMessage, webrtcCore.sendData, updateState]);
// 发送文件列表
const sendFileList = useCallback((fileList: FileInfo[]) => {
if (webrtcCore.getChannelState() !== 'open') {
console.error('数据通道未准备就绪,无法发送文件列表');
return;
}
console.log('发送文件列表:', fileList);
webrtcCore.sendMessage({
type: 'file-list',
payload: fileList
});
}, [webrtcCore.getChannelState, webrtcCore.sendMessage]);
// 请求文件
const requestFile = useCallback((fileId: string, fileName: string) => {
if (webrtcCore.getChannelState() !== 'open') {
console.error('数据通道未准备就绪,无法请求文件');
return;
}
console.log('请求文件:', fileName, fileId);
webrtcCore.sendMessage({
type: 'file-request',
payload: { fileId, fileName }
});
}, [webrtcCore.getChannelState, webrtcCore.sendMessage]);
// 注册文件接收回调
const onFileReceived = useCallback((callback: FileReceivedCallback) => {
fileReceivedCallbacks.current.add(callback);
return () => { fileReceivedCallbacks.current.delete(callback); };
}, []);
// 注册文件请求回调
const onFileRequested = useCallback((callback: FileRequestedCallback) => {
fileRequestedCallbacks.current.add(callback);
return () => { fileRequestedCallbacks.current.delete(callback); };
}, []);
// 注册进度回调
const onFileProgress = useCallback((callback: FileProgressCallback) => {
fileProgressCallbacks.current.add(callback);
return () => { fileProgressCallbacks.current.delete(callback); };
}, []);
// 注册文件列表回调
const onFileListReceived = useCallback((callback: FileListReceivedCallback) => {
fileListCallbacks.current.add(callback);
return () => { fileListCallbacks.current.delete(callback); };
}, []);
return {
// 继承基础连接状态
isConnected: webrtcCore.isConnected,
isConnecting: webrtcCore.isConnecting,
isWebSocketConnected: webrtcCore.isWebSocketConnected,
connectionError: webrtcCore.error,
// 文件传输状态
...state,
// 操作方法
connect,
disconnect: webrtcCore.disconnect,
sendFile,
sendFileList,
requestFile,
// 回调注册
onFileReceived,
onFileRequested,
onFileProgress,
onFileListReceived,
};
}

View File

@@ -0,0 +1,229 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useWebRTCCore } from './useWebRTCCore';
// 文字传输状态
interface TextTransferState {
messages: Array<{
id: string;
text: string;
timestamp: Date;
sender: 'self' | 'peer';
}>;
isTyping: boolean;
error: string | null;
}
// 消息类型
interface TextMessage {
id: string;
text: string;
timestamp: string;
}
// 回调类型
type MessageReceivedCallback = (message: TextMessage) => void;
type TypingStatusCallback = (isTyping: boolean) => void;
type RealTimeTextCallback = (text: string) => void;
/**
* 文字传输业务层
* 使用 WebRTC 核心连接逻辑实现实时文字传输功能
* 每个实例有独立的连接,但复用相同的连接建立逻辑
*/
export function useTextTransferBusiness() {
const webrtcCore = useWebRTCCore('text-transfer');
const [state, setState] = useState<TextTransferState>({
messages: [],
isTyping: false,
error: null,
});
// 回调存储
const messageCallbacks = useRef<Set<MessageReceivedCallback>>(new Set());
const typingCallbacks = useRef<Set<TypingStatusCallback>>(new Set());
const realTimeTextCallbacks = useRef<Set<RealTimeTextCallback>>(new Set());
// 打字状态防抖
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const updateState = useCallback((updates: Partial<TextTransferState>) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
// 处理文字传输消息
const handleMessage = useCallback((message: any) => {
switch (message.type) {
case 'text-message':
const textMessage: TextMessage = message.payload;
console.log('收到文字消息:', textMessage.text);
setState(prev => ({
...prev,
messages: [...prev.messages, {
id: textMessage.id,
text: textMessage.text,
timestamp: new Date(textMessage.timestamp),
sender: 'peer'
}]
}));
// 触发回调
messageCallbacks.current.forEach(cb => cb(textMessage));
break;
case 'typing-status':
const { isTyping } = message.payload;
updateState({ isTyping });
typingCallbacks.current.forEach(cb => cb(isTyping));
break;
case 'real-time-text':
const { text } = message.payload;
console.log('收到实时文本:', text);
realTimeTextCallbacks.current.forEach(cb => cb(text));
break;
case 'text-clear':
console.log('收到清空消息指令');
updateState({ messages: [] });
break;
}
}, [updateState]);
// 设置处理器
useEffect(() => {
webrtcCore.setMessageHandler(handleMessage);
// 文字传输不需要数据处理器
return () => {
webrtcCore.setMessageHandler(null);
};
}, [webrtcCore.setMessageHandler, handleMessage]);
// 连接
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
return webrtcCore.connect(roomCode, role);
}, [webrtcCore.connect]);
// 发送文字消息
const sendMessage = useCallback((text: string) => {
if (webrtcCore.getChannelState() !== 'open') {
updateState({ error: '连接未就绪' });
return;
}
const message: TextMessage = {
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
text,
timestamp: new Date().toISOString(),
};
console.log('发送文字消息:', text);
// 发送到对方
const success = webrtcCore.sendMessage({
type: 'text-message',
payload: message
});
if (success) {
// 添加到本地消息列表
setState(prev => ({
...prev,
messages: [...prev.messages, {
id: message.id,
text: message.text,
timestamp: new Date(message.timestamp),
sender: 'self'
}],
error: null
}));
} else {
updateState({ error: '发送消息失败' });
}
}, [webrtcCore.getChannelState, webrtcCore.sendMessage, updateState]);
// 发送打字状态
const sendTypingStatus = useCallback((isTyping: boolean) => {
if (webrtcCore.getChannelState() !== 'open') return;
webrtcCore.sendMessage({
type: 'typing-status',
payload: { isTyping }
});
// 如果开始打字,设置自动停止
if (isTyping) {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
sendTypingStatus(false);
}, 3000); // 3秒后自动停止打字状态
} else {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = null;
}
}
}, [webrtcCore.getChannelState, webrtcCore.sendMessage]);
// 发送实时文本
const sendRealTimeText = useCallback((text: string) => {
if (webrtcCore.getChannelState() !== 'open') return;
webrtcCore.sendMessage({
type: 'real-time-text',
payload: { text }
});
}, [webrtcCore.getChannelState, webrtcCore.sendMessage]);
// 清空消息
const clearMessages = useCallback(() => {
updateState({ messages: [] });
// 通知对方也清空
if (webrtcCore.getChannelState() === 'open') {
webrtcCore.sendMessage({
type: 'text-clear',
payload: {}
});
}
}, [webrtcCore.getChannelState, webrtcCore.sendMessage, updateState]);
// 注册消息接收回调
const onMessageReceived = useCallback((callback: MessageReceivedCallback) => {
messageCallbacks.current.add(callback);
return () => { messageCallbacks.current.delete(callback); };
}, []);
// 注册打字状态回调
const onTypingStatus = useCallback((callback: TypingStatusCallback) => {
typingCallbacks.current.add(callback);
return () => { typingCallbacks.current.delete(callback); };
}, []);
return {
// 继承基础连接状态
isConnected: webrtcCore.isConnected,
isConnecting: webrtcCore.isConnecting,
isWebSocketConnected: webrtcCore.isWebSocketConnected,
connectionError: webrtcCore.error,
// 文字传输状态
...state,
// 操作方法
connect,
disconnect: webrtcCore.disconnect,
sendMessage,
sendTypingStatus,
clearMessages,
// 回调注册
onMessageReceived,
onTypingStatus,
};
}

View File

@@ -1,808 +0,0 @@
import { useState, useRef, useCallback } from 'react';
import { config } from '@/lib/config';
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 detectBrowser = useCallback(() => {
const userAgent = navigator.userAgent;
const isEdge = /Edg/.test(userAgent);
const isChrome = /Chrome/.test(userAgent) && !isEdge;
const isSafari = /Safari/.test(userAgent) && !isChrome && !isEdge;
const isFirefox = /Firefox/.test(userAgent);
const isChromeFamily = isChrome || isEdge; // Chrome内核系列
console.log('浏览器检测结果:', {
userAgent: userAgent.substring(0, 100) + '...',
isEdge,
isChrome,
isSafari,
isFirefox,
isChromeFamily,
webRTCSupport: {
RTCPeerConnection: !!window.RTCPeerConnection,
getUserMedia: !!(navigator.mediaDevices?.getUserMedia),
WebSocket: !!window.WebSocket
}
});
return { isEdge, isChrome, isSafari, isFirefox, isChromeFamily };
}, []);
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 pendingIceCandidates = useRef<RTCIceCandidate[]>([]);
const iceGatheringTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const STUN_SERVERS = [
// Edge 浏览器专用优化配置
{ urls: 'stun:stun.miwifi.com' },
{ urls: 'stun:stun.chat.bilibili.com' },
{ urls: 'stun:turn.cloudflare.com:3478' },
{ urls: 'stun:stun.l.google.com:19302' },
// 备用 STUN 服务器
{ urls: 'stun:stun.nextcloud.com:443' },
{ urls: 'stun:stun.sipgate.net:10000' },
{ urls: 'stun:stun.ekiga.net' },
];
// 获取浏览器特定的 RTCConfiguration
const getBrowserSpecificConfig = useCallback(() => {
const { isSafari, isChromeFamily } = detectBrowser();
const baseConfig = {
iceServers: STUN_SERVERS,
iceCandidatePoolSize: 10,
bundlePolicy: 'max-bundle' as RTCBundlePolicy,
rtcpMuxPolicy: 'require' as RTCRtcpMuxPolicy,
iceTransportPolicy: 'all' as RTCIceTransportPolicy
};
if (isChromeFamily) {
console.log('应用 Chrome 内核浏览器优化配置');
return {
...baseConfig,
// Chrome 内核特定优化
iceCandidatePoolSize: 20, // 增加候选池大小
bundlePolicy: 'max-bundle' as RTCBundlePolicy,
rtcpMuxPolicy: 'require' as RTCRtcpMuxPolicy,
iceTransportPolicy: 'all' as RTCIceTransportPolicy,
// Chrome 内核需要更宽松的配置
sdpSemantics: 'unified-plan' as const, // 明确使用统一计划
};
}
if (isSafari) {
console.log('应用 Safari 浏览器优化配置');
return {
...baseConfig,
// Safari 特定优化
iceCandidatePoolSize: 8,
sdpSemantics: 'unified-plan' as const,
};
}
console.log('应用默认浏览器配置');
return baseConfig;
}, [detectBrowser, STUN_SERVERS]);
// 连接超时时间30秒
const CONNECTION_TIMEOUT = 30000;
const updateState = useCallback((updates: Partial<WebRTCConnectionState>) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
// 处理缓存的ICE候选
const processPendingIceCandidates = useCallback(async () => {
const pc = pcRef.current;
if (!pc || !pc.remoteDescription || pendingIceCandidates.current.length === 0) {
return;
}
console.log('处理缓存的ICE候选数量:', pendingIceCandidates.current.length);
for (const candidate of pendingIceCandidates.current) {
try {
await pc.addIceCandidate(candidate);
console.log('已添加缓存的ICE候选');
} catch (error) {
console.warn('添加缓存ICE候选失败:', error);
}
}
// 清空缓存
pendingIceCandidates.current = [];
}, []);
// 优化的Offer创建和发送
const createAndSendOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
console.log('开始创建offer...');
// 获取浏览器信息
const browserInfo = detectBrowser();
try {
// Chrome内核需要特殊的offer配置
const offerOptions = browserInfo.isChromeFamily ? {
offerToReceiveAudio: false,
offerToReceiveVideo: false,
iceRestart: false,
// Chrome内核特定配置
voiceActivityDetection: false
} : {
offerToReceiveAudio: false,
offerToReceiveVideo: false,
iceRestart: false
};
console.log('使用的 offer 配置:', offerOptions);
// 创建offer
const offer = await pc.createOffer(offerOptions);
await pc.setLocalDescription(offer);
console.log('已设置本地描述等待ICE候选收集...');
// Chrome内核需要更长的ICE收集时间
const iceGatheringTimeout = browserInfo.isChromeFamily ? 5000 : 3000;
// 设置ICE收集超时 - 等待更多ICE候选
const iceTimeout = setTimeout(() => {
console.log('ICE收集超时发送当前offer');
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
console.log('已发送offer (超时发送)');
}
}, iceGatheringTimeout);
iceGatheringTimeoutRef.current = iceTimeout;
// 监听ICE收集完成
const handleIceGatheringComplete = () => {
if (iceGatheringTimeoutRef.current) {
clearTimeout(iceGatheringTimeoutRef.current);
iceGatheringTimeoutRef.current = null;
}
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
const candidateCount = pc.localDescription?.sdp ?
pc.localDescription.sdp.split('a=candidate:').length - 1 : 0;
console.log('已发送offer (ICE收集完成)', '候选数量:', candidateCount);
}
};
// 检查ICE收集状态
if (pc.iceGatheringState === 'complete') {
handleIceGatheringComplete();
} else {
// 监听ICE收集状态变化
const originalHandler = pc.onicegatheringstatechange;
pc.onicegatheringstatechange = (event) => {
console.log('ICE收集状态变化:', pc.iceGatheringState);
if (originalHandler) originalHandler.call(pc, event);
if (pc.iceGatheringState === 'complete') {
handleIceGatheringComplete();
// 恢复原始处理器
pc.onicegatheringstatechange = originalHandler;
}
};
}
} catch (error) {
console.error('创建offer失败:', error);
updateState({ error: '创建连接失败', isConnecting: false });
}
}, [updateState, detectBrowser]);
// 清理超时定时器
const clearConnectionTimeout = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
// 处理连接超时
const handleConnectionTimeout = useCallback(() => {
console.warn('WebRTC连接超时');
// 获取当前连接状态用于调试
const pc = pcRef.current;
const connectionInfo = {
connectionState: pc?.connectionState || 'unknown',
iceConnectionState: pc?.iceConnectionState || 'unknown',
signalingState: pc?.signalingState || 'unknown',
isWebSocketConnected: wsRef.current?.readyState === WebSocket.OPEN
};
console.log('连接超时时的状态:', connectionInfo);
updateState({
error: `连接超时 - WebSocket: ${connectionInfo.isWebSocketConnected ? '已连接' : '未连接'}, 信令状态: ${connectionInfo.signalingState}, 连接状态: ${connectionInfo.connectionState}`,
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);
// 浏览器兼容性检测
const browserInfo = detectBrowser();
console.log('当前浏览器:', browserInfo);
// 清理之前的超时定时器
clearConnectionTimeout();
updateState({ isConnecting: true, error: null });
// Chrome内核浏览器使用更长的超时时间
const timeoutDuration = browserInfo.isChromeFamily ? CONNECTION_TIMEOUT * 2 : CONNECTION_TIMEOUT;
// 只有接收方设置连接超时,发送方无限等待
if (role === 'receiver') {
timeoutRef.current = setTimeout(() => {
handleConnectionTimeout();
}, timeoutDuration);
}
try {
// 获取浏览器特定的配置
const rtcConfig = getBrowserSpecificConfig();
console.log('使用的 RTCConfiguration:', rtcConfig);
// 创建PeerConnection
const pc = new RTCPeerConnection(rtcConfig);
pcRef.current = pc;
// 连接WebSocket信令服务器
const wsUrl = config.api.wsUrl.replace('/ws/p2p', '/ws/webrtc');
const finalWsUrl = `${wsUrl}?code=${roomCode}&role=${role}`;
console.log('WebSocket 连接信息:', {
原始wsUrl: config.api.wsUrl,
替换后wsUrl: wsUrl,
最终URL: finalWsUrl,
当前域名: typeof window !== 'undefined' ? window.location.host : 'unknown',
协议: typeof window !== 'undefined' ? window.location.protocol : 'unknown',
端口: typeof window !== 'undefined' ? window.location.port : 'unknown',
浏览器: browserInfo
});
const ws = new WebSocket(finalWsUrl);
wsRef.current = ws;
// Chrome内核特殊处理增加连接超时检测
let wsConnectTimeout: NodeJS.Timeout | null = null;
if (browserInfo.isChromeFamily) {
wsConnectTimeout = setTimeout(() => {
if (ws.readyState === WebSocket.CONNECTING) {
console.error('Chrome内核 - WebSocket连接超时');
ws.close();
updateState({
error: 'WebSocket连接超时 - Chrome内核可能存在安全策略限制',
isConnecting: false
});
}
}, 10000); // 10秒超时
}
// WebSocket事件处理
ws.onopen = async () => {
console.log('WebSocket连接已建立URL:', finalWsUrl);
// 清理Chrome内核的连接超时
if (wsConnectTimeout) {
clearTimeout(wsConnectTimeout);
wsConnectTimeout = null;
}
updateState({ isWebSocketConnected: true });
// 如果是发送方在WebSocket连接建立后创建offer
if (role === 'sender') {
// 使用优化的offer创建逻辑
createAndSendOffer(pc, ws);
// 发送方发送 offer 后,停止 connecting 状态
updateState({ isConnecting: false });
}
};
ws.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
console.log('收到信令消息:', message, '当前PC状态:', pc.signalingState);
switch (message.type) {
case 'offer':
if (message.payload) {
console.log('处理offer当前状态:', pc.signalingState);
try {
// 根据W3C规范只有在stable状态下才能接收offer
if (pc.signalingState !== 'stable') {
console.warn('跳过offer信令状态不为stable:', pc.signalingState);
return;
}
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
console.log('已设置远程描述,状态变为:', pc.signalingState);
// 创建answer
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
console.log('已创建并设置本地answer状态变为:', pc.signalingState);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'answer', payload: answer }));
console.log('已发送answer');
}
// 接收方发送answer后停止connecting状态
updateState({ isConnecting: false });
// 处理缓存的ICE候选
await processPendingIceCandidates();
} catch (error) {
console.error('处理offer失败:', error);
updateState({ error: '信令交换失败', isConnecting: false });
}
}
break;
case 'answer':
if (message.payload) {
console.log('处理answer当前状态:', pc.signalingState);
try {
// 根据W3C规范只有在have-local-offer状态下才能接收answer
if (pc.signalingState !== 'have-local-offer') {
console.warn('跳过answer信令状态不为have-local-offer:', pc.signalingState);
return;
}
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
console.log('信令交换完成,状态变为:', pc.signalingState);
// 处理缓存的ICE候选
await processPendingIceCandidates();
} catch (error) {
console.error('处理answer失败:', error);
updateState({ error: '信令交换失败', isConnecting: false });
}
}
break;
case 'ice-candidate':
if (message.payload) {
try {
const candidate = new RTCIceCandidate(message.payload);
// 根据W3C规范检查连接状态
if (pc.signalingState === 'closed') {
console.warn('跳过ICE候选连接已关闭');
return;
}
// 如果有远程描述直接添加ICE候选
if (pc.remoteDescription) {
await pc.addIceCandidate(candidate);
console.log('已添加ICE候选:', {
type: candidate.type,
protocol: candidate.protocol,
address: candidate.address?.substring(0, 10) + '...',
port: candidate.port
});
} else {
// 缓存ICE候选等待远程描述设置后处理
pendingIceCandidates.current.push(candidate);
console.log('缓存ICE候选等待远程描述:', {
type: candidate.type,
缓存数量: pendingIceCandidates.current.length
});
}
} catch (error) {
console.warn('处理ICE候选失败:', error);
// 根据W3C规范ICE候选错误不应该终止连接
}
}
break;
case 'disconnection':
console.log('收到断开连接通知:', message.payload);
const disconnectionMessage = message.payload?.message || '对方已停止传输';
updateState({
error: disconnectionMessage,
isConnecting: false,
isConnected: false,
isWebSocketConnected: false
});
// 关闭WebSocket连接
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, '对方已断开连接');
}
// 关闭WebRTC连接
if (pc.connectionState !== 'closed') {
pc.close();
}
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);
console.error('WebSocket连接失败URL:', finalWsUrl);
// 清理Chrome内核的连接超时
if (wsConnectTimeout) {
clearTimeout(wsConnectTimeout);
wsConnectTimeout = null;
}
// Chrome内核特殊错误处理
const errorMessage = browserInfo.isChromeFamily
? 'WebSocket连接失败 - Chrome内核可能阻止了不安全的连接请确保使用HTTPS'
: 'WebSocket连接失败';
updateState({
error: errorMessage,
isConnecting: false,
isWebSocketConnected: false
});
};
ws.onclose = (event) => {
console.log('WebSocket连接已关闭代码:', event.code, '原因:', event.reason, 'URL:', finalWsUrl);
updateState({ isWebSocketConnected: false });
};
// ICE候选事件 - 增强处理Edge浏览器特殊优化
pc.onicecandidate = (event) => {
if (event.candidate) {
const candidateInfo = {
type: event.candidate.type,
protocol: event.candidate.protocol,
address: event.candidate.address,
port: event.candidate.port,
priority: event.candidate.priority,
foundation: event.candidate.foundation
};
console.log('ICE候选信息:', candidateInfo);
// Chrome内核浏览器特殊处理检查候选质量和延迟
if (browserInfo.isChromeFamily && event.candidate.priority !== null) {
// Chrome内核可能生成质量较低的候选添加延迟来等待更好的候选
const isLowQualityCandidate = event.candidate.type === 'host' &&
event.candidate.priority < 1000000;
if (isLowQualityCandidate) {
console.log('Chrome内核 - 延迟发送低质量候选');
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'ice-candidate',
payload: event.candidate
}));
}
}, 800); // Chrome内核需要更长延迟
return;
}
}
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'ice-candidate',
payload: event.candidate
}));
}
} else {
console.log('ICE候选收集完成');
}
};
// 添加ICE收集状态监听
pc.onicegatheringstatechange = () => {
console.log('ICE收集状态变化:', pc.iceGatheringState);
};
// 信令状态变化监听 - 增强版
pc.onsignalingstatechange = () => {
console.log('信令状态变化:', pc.signalingState);
// 根据W3C规范验证状态转换
switch (pc.signalingState) {
case 'stable':
console.log('信令协商完成,连接稳定');
break;
case 'have-local-offer':
console.log('已发送offer等待answer');
break;
case 'have-remote-offer':
console.log('已接收offer需要发送answer');
break;
case 'have-local-pranswer':
console.log('已发送provisional answer');
break;
case 'have-remote-pranswer':
console.log('已接收provisional answer');
break;
case 'closed':
console.log('连接已关闭');
break;
default:
console.warn('未知的信令状态:', pc.signalingState);
}
};
// 连接状态变化 - 根据W3C规范
pc.onconnectionstatechange = () => {
console.log('连接状态变化:', pc.connectionState);
switch (pc.connectionState) {
case 'new':
console.log('连接初始化');
break;
case 'connecting':
console.log('正在建立连接...');
// 只有在当前不是已连接状态时才设置为连接中
if (!state.isConnected) {
updateState({ isConnecting: true });
}
break;
case 'connected':
console.log('连接已建立');
clearConnectionTimeout();
updateState({
isConnected: true,
isConnecting: false
});
break;
case 'disconnected':
console.log('连接已断开');
updateState({
isConnected: false
});
break;
case 'failed':
console.log('连接失败');
clearConnectionTimeout();
updateState({
error: '连接失败',
isConnecting: false,
isConnected: false
});
break;
case 'closed':
console.log('连接已关闭');
updateState({
isConnected: false,
isConnecting: false
});
break;
}
};
// ICE连接状态变化 - 根据W3C规范
pc.oniceconnectionstatechange = () => {
console.log('ICE连接状态变化:', pc.iceConnectionState);
switch (pc.iceConnectionState) {
case 'new':
console.log('ICE连接初始化');
break;
case 'checking':
console.log('ICE正在检查连通性...');
break;
case 'connected':
console.log('ICE连接成功');
clearConnectionTimeout();
break;
case 'completed':
console.log('ICE连接完成');
clearConnectionTimeout();
break;
case 'disconnected':
console.log('ICE连接断开');
updateState({
error: 'ICE连接断开',
isConnected: false
});
break;
case 'failed':
console.log('ICE连接失败');
clearConnectionTimeout();
updateState({
error: 'ICE连接失败',
isConnecting: false,
isConnected: false
});
break;
case 'closed':
console.log('ICE连接已关闭');
break;
}
};
// 如果是发送方,创建数据通道
if (role === 'sender') {
// 根据浏览器优化数据通道配置
const dataChannelConfig = browserInfo.isChromeFamily ? {
ordered: true,
maxPacketLifeTime: undefined,
maxRetransmits: undefined,
// Chrome内核特定配置
negotiated: false,
id: undefined,
protocol: '' // Chrome内核需要明确指定协议
} : {
ordered: true,
maxPacketLifeTime: undefined,
maxRetransmits: undefined
};
console.log('创建数据通道,配置:', dataChannelConfig);
// 根据W3C规范数据通道应该在设置本地描述之前创建
const dataChannel = pc.createDataChannel('fileTransfer', dataChannelConfig);
dcRef.current = dataChannel;
// 设置缓冲区管理
dataChannel.bufferedAmountLowThreshold = 256 * 1024; // 256KB
dataChannel.onopen = () => {
console.log('数据通道已打开 (发送方)');
// 数据通道成功打开,清除超时定时器
clearConnectionTimeout();
console.log('数据通道配置:', {
id: dataChannel.id,
label: dataChannel.label,
maxPacketLifeTime: dataChannel.maxPacketLifeTime,
maxRetransmits: dataChannel.maxRetransmits,
ordered: dataChannel.ordered,
bufferedAmountLowThreshold: dataChannel.bufferedAmountLowThreshold,
readyState: dataChannel.readyState
});
updateState({ localDataChannel: dataChannel });
};
dataChannel.onclose = () => {
console.log('数据通道已关闭 (发送方)');
updateState({ localDataChannel: null });
};
dataChannel.onerror = (error) => {
console.error('数据通道错误 (发送方):', error);
updateState({ error: '数据通道连接失败', isConnecting: false });
};
} else {
// 接收方等待数据通道
pc.ondatachannel = (event) => {
const dataChannel = event.channel;
dcRef.current = dataChannel;
console.log('收到数据通道 (接收方),标签:', dataChannel.label);
dataChannel.onopen = () => {
console.log('数据通道已打开 (接收方)');
// 数据通道成功打开,清除超时定时器
clearConnectionTimeout();
console.log('数据通道配置:', {
id: dataChannel.id,
label: dataChannel.label,
readyState: dataChannel.readyState
});
updateState({ remoteDataChannel: dataChannel });
};
dataChannel.onclose = () => {
console.log('数据通道已关闭 (接收方)');
updateState({ remoteDataChannel: null });
};
dataChannel.onerror = (error) => {
console.error('数据通道错误 (接收方):', error);
updateState({ error: '数据通道连接失败', isConnecting: false });
};
};
}
} catch (error) {
console.error('连接失败:', error);
clearConnectionTimeout();
updateState({
error: error instanceof Error ? error.message : '连接失败',
isConnecting: false
});
}
}, [updateState, clearConnectionTimeout, handleConnectionTimeout, processPendingIceCandidates, createAndSendOffer, detectBrowser, getBrowserSpecificConfig]);
const disconnect = useCallback(() => {
console.log('断开WebRTC连接');
// 清理超时定时器
clearConnectionTimeout();
// 清理ICE收集超时
if (iceGatheringTimeoutRef.current) {
clearTimeout(iceGatheringTimeoutRef.current);
iceGatheringTimeoutRef.current = null;
console.log('已清理ICE收集超时定时器');
}
// 清理缓存的ICE候选
pendingIceCandidates.current = [];
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,
});
}, [clearConnectionTimeout]);
const getDataChannel = useCallback(() => {
return dcRef.current;
}, []);
return {
...state,
connect,
disconnect,
getDataChannel,
};
}

View File

@@ -0,0 +1,402 @@
import { useState, useRef, useCallback } from 'react';
import { config } from '@/lib/config';
// 基础连接状态
interface WebRTCCoreState {
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected: boolean;
error: string | null;
}
// 消息类型
interface WebRTCMessage {
type: string;
payload: any;
}
// 消息处理器类型
type MessageHandler = (message: WebRTCMessage) => void;
type DataHandler = (data: ArrayBuffer) => void;
/**
* WebRTC 核心连接逻辑
* 提供可复用的连接建立逻辑,但每个业务模块独立使用
* 不共享连接实例,只共享连接逻辑
*/
export function useWebRTCCore(channelLabel: string = 'data') {
const [state, setState] = useState<WebRTCCoreState>({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
error: null,
});
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 messageHandlerRef = useRef<MessageHandler | null>(null);
const dataHandlerRef = useRef<DataHandler | null>(null);
// STUN 服务器配置
const STUN_SERVERS = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun.miwifi.com' },
{ urls: 'stun:turn.cloudflare.com:3478' },
];
const updateState = useCallback((updates: Partial<WebRTCCoreState>) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
// 清理连接
const cleanup = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
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;
}
}, []);
// 创建 Offer
const createOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
try {
const offer = await pc.createOffer({
offerToReceiveAudio: false,
offerToReceiveVideo: false,
});
await pc.setLocalDescription(offer);
// 等待 ICE 候选收集完成或超时
const iceTimeout = setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
console.log(`[${channelLabel}] 发送 offer (超时发送)`);
}
}, 3000);
if (pc.iceGatheringState === 'complete') {
clearTimeout(iceTimeout);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
console.log(`[${channelLabel}] 发送 offer (ICE收集完成)`);
}
} else {
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === 'complete') {
clearTimeout(iceTimeout);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
console.log(`[${channelLabel}] 发送 offer (ICE收集完成)`);
}
}
};
}
} catch (error) {
console.error(`[${channelLabel}] 创建 offer 失败:`, error);
updateState({ error: '创建连接失败', isConnecting: false });
}
}, [channelLabel, updateState]);
// 处理数据通道消息
const handleDataChannelMessage = useCallback((event: MessageEvent) => {
if (typeof event.data === 'string') {
try {
const message = JSON.parse(event.data);
console.log(`[${channelLabel}] 收到消息:`, message.type);
if (messageHandlerRef.current) {
messageHandlerRef.current(message);
}
} catch (error) {
console.error(`[${channelLabel}] 解析消息失败:`, error);
}
} else if (event.data instanceof ArrayBuffer) {
console.log(`[${channelLabel}] 收到数据:`, event.data.byteLength, 'bytes');
if (dataHandlerRef.current) {
dataHandlerRef.current(event.data);
}
}
}, [channelLabel]);
// 连接到房间
const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
console.log(`=== [${channelLabel}] WebRTC 连接开始 ===`);
console.log(`[${channelLabel}] 房间代码:`, roomCode, '角色:', role);
// 检查是否已经在连接中或已连接
if (state.isConnecting) {
console.warn(`[${channelLabel}] 正在连接中,跳过重复连接请求`);
return;
}
if (state.isConnected) {
console.warn(`[${channelLabel}] 已经连接,跳过重复连接请求`);
return;
}
// 清理之前的连接(如果存在)
cleanup();
updateState({ isConnecting: true, error: null });
// 设置连接超时
timeoutRef.current = setTimeout(() => {
console.warn(`[${channelLabel}] 连接超时`);
updateState({ error: '连接超时,请检查网络状况或重新尝试', isConnecting: false });
cleanup();
}, 30000);
try {
// 创建 PeerConnection
const pc = new RTCPeerConnection({
iceServers: STUN_SERVERS,
iceCandidatePoolSize: 10,
});
pcRef.current = pc;
// 连接 WebSocket - 使用不同的标识来区分不同的业务连接
const wsUrl = config.api.wsUrl.replace('/ws/p2p', '/ws/webrtc');
const ws = new WebSocket(`${wsUrl}?code=${roomCode}&role=${role}&channel=${channelLabel}`);
wsRef.current = ws;
// WebSocket 事件处理
ws.onopen = () => {
console.log(`[${channelLabel}] WebSocket 连接已建立`);
updateState({ isWebSocketConnected: true });
if (role === 'sender') {
createOffer(pc, ws);
}
};
ws.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
console.log(`[${channelLabel}] 收到信令消息:`, message.type);
switch (message.type) {
case 'offer':
if (pc.signalingState === 'stable') {
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: 'answer', payload: answer }));
console.log(`[${channelLabel}] 发送 answer`);
}
break;
case 'answer':
if (pc.signalingState === 'have-local-offer') {
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
console.log(`[${channelLabel}] 处理 answer 完成`);
}
break;
case 'ice-candidate':
if (message.payload && pc.remoteDescription) {
await pc.addIceCandidate(new RTCIceCandidate(message.payload));
console.log(`[${channelLabel}] 添加 ICE 候选`);
}
break;
case 'error':
console.error(`[${channelLabel}] 信令错误:`, message.error);
updateState({ error: message.error, isConnecting: false });
break;
}
} catch (error) {
console.error(`[${channelLabel}] 处理信令消息失败:`, error);
}
};
ws.onerror = (error) => {
console.error(`[${channelLabel}] WebSocket 错误:`, error);
updateState({ error: 'WebSocket连接失败请检查网络连接', isConnecting: false });
};
ws.onclose = () => {
console.log(`[${channelLabel}] WebSocket 连接已关闭`);
updateState({ isWebSocketConnected: false });
};
// PeerConnection 事件处理
pc.onicecandidate = (event) => {
if (event.candidate && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'ice-candidate',
payload: event.candidate
}));
console.log(`[${channelLabel}] 发送 ICE 候选`);
}
};
pc.onconnectionstatechange = () => {
console.log(`[${channelLabel}] 连接状态变化:`, pc.connectionState);
switch (pc.connectionState) {
case 'connected':
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
updateState({ isConnected: true, isConnecting: false, error: null });
break;
case 'failed':
updateState({ error: 'WebRTC连接失败可能是网络防火墙阻止了连接', isConnecting: false, isConnected: false });
break;
case 'disconnected':
updateState({ isConnected: false });
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
break;
case 'closed':
updateState({ isConnected: false, isConnecting: false });
break;
}
};
// 数据通道处理
if (role === 'sender') {
const dataChannel = pc.createDataChannel(channelLabel, {
ordered: true,
maxRetransmits: 3
});
dcRef.current = dataChannel;
dataChannel.onopen = () => {
console.log(`[${channelLabel}] 数据通道已打开 (发送方)`);
};
dataChannel.onmessage = handleDataChannelMessage;
dataChannel.onerror = (error) => {
console.error(`[${channelLabel}] 数据通道错误:`, error);
updateState({ error: '数据通道连接失败,可能是网络环境受限', isConnecting: false });
};
} else {
pc.ondatachannel = (event) => {
const dataChannel = event.channel;
dcRef.current = dataChannel;
dataChannel.onopen = () => {
console.log(`[${channelLabel}] 数据通道已打开 (接收方)`);
};
dataChannel.onmessage = handleDataChannelMessage;
dataChannel.onerror = (error) => {
console.error(`[${channelLabel}] 数据通道错误:`, error);
updateState({ error: '数据通道连接失败,可能是网络环境受限', isConnecting: false });
};
};
}
} catch (error) {
console.error(`[${channelLabel}] 连接失败:`, error);
updateState({
error: error instanceof Error ? error.message : '连接失败',
isConnecting: false
});
}
}, [channelLabel, updateState, cleanup, createOffer, handleDataChannelMessage, state.isConnecting, state.isConnected]);
// 断开连接
const disconnect = useCallback(() => {
console.log(`[${channelLabel}] 断开 WebRTC 连接`);
cleanup();
setState({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
error: null,
});
}, [channelLabel, cleanup]);
// 发送消息
const sendMessage = useCallback((message: WebRTCMessage) => {
const dataChannel = dcRef.current;
if (!dataChannel || dataChannel.readyState !== 'open') {
console.error(`[${channelLabel}] 数据通道未准备就绪`);
return false;
}
try {
dataChannel.send(JSON.stringify(message));
console.log(`[${channelLabel}] 发送消息:`, message.type);
return true;
} catch (error) {
console.error(`[${channelLabel}] 发送消息失败:`, error);
return false;
}
}, [channelLabel]);
// 发送二进制数据
const sendData = useCallback((data: ArrayBuffer) => {
const dataChannel = dcRef.current;
if (!dataChannel || dataChannel.readyState !== 'open') {
console.error(`[${channelLabel}] 数据通道未准备就绪`);
return false;
}
try {
dataChannel.send(data);
console.log(`[${channelLabel}] 发送数据:`, data.byteLength, 'bytes');
return true;
} catch (error) {
console.error(`[${channelLabel}] 发送数据失败:`, error);
return false;
}
}, [channelLabel]);
// 设置消息处理器
const setMessageHandler = useCallback((handler: MessageHandler | null) => {
messageHandlerRef.current = handler;
}, []);
// 设置数据处理器
const setDataHandler = useCallback((handler: DataHandler | null) => {
dataHandlerRef.current = handler;
}, []);
// 获取数据通道状态
const getChannelState = useCallback(() => {
return dcRef.current?.readyState || 'closed';
}, []);
return {
// 状态
...state,
// 操作方法
connect,
disconnect,
sendMessage,
sendData,
// 处理器设置
setMessageHandler,
setDataHandler,
// 工具方法
getChannelState,
};
}