feat:降级

This commit is contained in:
MatrixSeven
2025-09-15 18:28:16 +08:00
parent 50d30f23bf
commit 550be8bcc6
29 changed files with 1189 additions and 431 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { cn } from '@/lib/utils';
import { useReadConnectState } from '@/hooks/connection/state/useWebConnectStateManager';
import { Role } from '@/hooks/connection/types';
import { useWebRTCStore } from '@/hooks/index';
import { cn } from '@/lib/utils';
interface ConnectionStatusProps {
// 房间信息 - 只需要这个基本信息
@@ -14,7 +15,10 @@ interface ConnectionStatusProps {
}
// 连接状态枚举
const getConnectionStatus = (connection: { isWebSocketConnected?: boolean; isPeerConnected?: boolean; isConnecting?: boolean; error?: string | null }, currentRoom: { code: string; role: 'sender' | 'receiver' } | null) => {
const getConnectionStatus = (currentRoom: { code: string; role: Role } | null) => {
const { getConnectState } = useReadConnectState(); // 确保状态管理器被初始化
const connection = getConnectState();
const isWebSocketConnected = connection?.isWebSocketConnected || false;
const isPeerConnected = connection?.isPeerConnected || false;
const isConnecting = connection?.isConnecting || false;
@@ -162,7 +166,7 @@ export function ConnectionStatus(props: ConnectionStatusProps) {
return <span className={cn('text-sm text-slate-600', className)}>{getConnectionStatusText(connection)}</span>;
}
const status = getConnectionStatus(connection, currentRoom ?? null);
const status = getConnectionStatus(currentRoom ?? null);
if (compact) {
return (

View File

@@ -1,14 +1,14 @@
"use client";
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useSharedWebRTCManager, useConnectionState, useRoomConnection } from '@/hooks/connection';
import { useFileTransferBusiness, useFileListSync, useFileStateManager } from '@/hooks/file-transfer';
import { useURLHandler } from '@/hooks/ui';
import { Button } from '@/components/ui/button';
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';
import { WebRTCFileUpload } from '@/components/webrtc/WebRTCFileUpload';
import { useConnectionState, useRoomConnection, useSharedWebRTCManager } from '@/hooks/connection';
import { useFileListSync, useFileStateManager, useFileTransferBusiness } from '@/hooks/file-transfer';
import { useURLHandler } from '@/hooks/ui';
import { Download, Upload } from 'lucide-react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
interface FileInfo {
id: string;
@@ -35,7 +35,7 @@ export const WebRTCFileTransfer: React.FC = () => {
// 创建共享连接
const connection = useSharedWebRTCManager();
const stableConnection = useMemo(() => connection, [connection.isConnected, connection.isConnecting, connection.isWebSocketConnected, connection.error]);
const stableConnection = useMemo(() => connection, [connection.getConnectState().isConnected, connection.getConnectState().isConnecting, connection.getConnectState().isWebSocketConnected, connection.getConnectState().error]);
// 使用共享连接创建业务层
const {
@@ -60,8 +60,8 @@ export const WebRTCFileTransfer: React.FC = () => {
mode,
pickupCode,
isConnected,
isPeerConnected: connection.isPeerConnected,
getChannelState: connection.getChannelState
isPeerConnected: connection.getConnectState().isPeerConnected,
getChannelState: () => connection.getConnectState().state
});
const {
@@ -80,7 +80,7 @@ export const WebRTCFileTransfer: React.FC = () => {
mode,
pickupCode,
syncFileListToReceiver,
isPeerConnected: connection.isPeerConnected
isPeerConnected: connection.getConnectState().isPeerConnected
});
const { joinRoom: originalJoinRoom } = useRoomConnection({
@@ -409,24 +409,24 @@ export const WebRTCFileTransfer: React.FC = () => {
selectedFilesCount: selectedFiles.length,
fileListCount: fileList.length
});
}, [isConnected, connection.isPeerConnected, isConnecting, isWebSocketConnected, pickupCode, mode, selectedFiles.length, fileList.length]);
}, [isConnected, connection.getConnectState().isPeerConnected, isConnecting, isWebSocketConnected, pickupCode, mode, selectedFiles.length, fileList.length]);
// 监听P2P连接建立时的状态变化
useEffect(() => {
if (connection.isPeerConnected && mode === 'send' && fileList.length > 0) {
if (connection.getConnectState().isPeerConnected && mode === 'send' && fileList.length > 0) {
console.log('P2P连接已建立数据通道首次打开初始化文件列表');
// 数据通道第一次打开时进行初始化
syncFileListToReceiver(fileList, '数据通道初始化');
}
}, [connection.isPeerConnected, mode, syncFileListToReceiver]);
}, [connection.getConnectState().isPeerConnected, mode, syncFileListToReceiver]);
// 监听fileList大小变化并同步
useEffect(() => {
if (connection.isPeerConnected && mode === 'send' && pickupCode) {
if (connection.getConnectState().isPeerConnected && mode === 'send' && pickupCode) {
console.log('fileList大小变化同步到接收方:', fileList.length);
syncFileListToReceiver(fileList, 'fileList大小变化');
}
}, [fileList.length, connection.isPeerConnected, mode, pickupCode, syncFileListToReceiver]);
}, [fileList.length, connection.getConnectState().isPeerConnected, mode, pickupCode, syncFileListToReceiver]);
// 监听selectedFiles变化同步更新fileList并发送给接收方
useEffect(() => {
@@ -635,9 +635,7 @@ export const WebRTCFileTransfer: React.FC = () => {
onJoinRoom={joinRoom}
files={fileList}
onDownloadFile={handleDownloadRequest}
isConnected={isConnected}
isConnecting={isConnecting}
isWebSocketConnected={isWebSocketConnected}
downloadedFiles={downloadedFiles}
error={error}
onReset={resetConnection}

View File

@@ -1,28 +1,28 @@
"use client";
import React, { useState } from 'react';
import {
Settings,
Plus,
Trash2,
RotateCcw,
Save,
Info,
Server,
import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/toast-simple';
import { useWebRTCStore } from '@/hooks/connection/state/webConnectStore';
import { IceServerConfig, useIceServersConfig } from '@/hooks/settings/useIceServersConfig';
import {
AlertTriangle,
Database,
Eye,
EyeOff,
AlertTriangle,
Info,
Plus,
RotateCcw,
Save,
Server,
Settings,
Shield,
Database,
X,
Wifi
Trash2,
Wifi,
X
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useIceServersConfig, IceServerConfig } from '@/hooks/settings/useIceServersConfig';
import { useToast } from '@/components/ui/toast-simple';
import { useWebRTCStore } from '@/hooks/ui/webRTCStore';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import React, { useState } from 'react';
interface AddServerModalProps {
isOpen: boolean;

View File

@@ -1,12 +1,12 @@
"use client";
import React, { useState, useCallback } from 'react';
import { useURLHandler } from '@/hooks/ui';
import { useWebRTCStore } from '@/hooks/ui/webRTCStore';
import { WebRTCTextSender } from '@/components/webrtc/WebRTCTextSender';
import { WebRTCTextReceiver } from '@/components/webrtc/WebRTCTextReceiver';
import { Button } from '@/components/ui/button';
import { MessageSquare, Send, Download, X } from 'lucide-react';
import { WebRTCTextReceiver } from '@/components/webrtc/WebRTCTextReceiver';
import { WebRTCTextSender } from '@/components/webrtc/WebRTCTextSender';
import { useWebRTCStore } from '@/hooks/connection/state/webConnectStore';
import { useURLHandler } from '@/hooks/ui';
import { Download, Send, X } from 'lucide-react';
import React, { useCallback, useState } from 'react';
export const WebRTCTextImageTransfer: React.FC = () => {
// 状态管理

View File

@@ -0,0 +1,153 @@
import React, { useState } from 'react';
import { IWebMessage } from '../../hooks/connection/types';
import { useWebSocketConnection } from '../../hooks/connection/ws/useWebSocketConnection';
/**
* WebSocket 连接示例组件
* 展示如何使用 WebSocket 实现的 IWebConnection 接口
*/
export function WebSocketExample() {
const [roomCode, setRoomCode] = useState('');
const [role, setRole] = useState<'sender' | 'receiver'>('sender');
const [message, setMessage] = useState('');
// 使用 WebSocket 连接
const connection = useWebSocketConnection();
const state = connection.getConnectState();
// 连接到房间
const handleConnect = async () => {
if (roomCode.trim()) {
await connection.connect(roomCode.trim(), role);
}
};
// 发送消息
const handleSendMessage = () => {
if (message.trim()) {
connection.sendMessage({
type: 'text',
payload: { text: message.trim() }
});
setMessage('');
}
};
// 注册消息处理器
React.useEffect(() => {
const unsubscribe = connection.registerMessageHandler('text', (msg: IWebMessage) => {
console.log('收到文本消息:', msg.payload.text);
});
return unsubscribe;
}, [connection]);
return (
<div className="p-4 max-w-md mx-auto bg-white rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">WebSocket </h2>
{/* 连接状态 */}
<div className="mb-4 p-2 bg-gray-100 rounded">
<p><strong>:</strong> {state.isConnected ? '已连接' : '未连接'}</p>
<p><strong>:</strong> {connection.connectType}</p>
{state.error && (
<p className="text-red-600"><strong>:</strong> {state.error}</p>
)}
{state.currentRoom && (
<p><strong>:</strong> {state.currentRoom.code} ({state.currentRoom.role})</p>
)}
</div>
{/* 连接控制 */}
{!state.isConnected ? (
<div className="space-y-3">
<div>
<label className="block text-sm font-medium mb-1">:</label>
<input
type="text"
value={roomCode}
onChange={(e) => setRoomCode(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded"
placeholder="输入房间代码"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">:</label>
<select
value={role}
onChange={(e) => setRole(e.target.value as 'sender' | 'receiver')}
className="w-full px-3 py-2 border border-gray-300 rounded"
>
<option value="sender"></option>
<option value="receiver"></option>
</select>
</div>
<button
onClick={handleConnect}
disabled={state.isConnecting || !roomCode.trim()}
className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{state.isConnecting ? '连接中...' : '连接'}
</button>
{state.canRetry && (
<button
onClick={connection.retry}
className="w-full px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600"
>
</button>
)}
</div>
) : (
<div className="space-y-3">
{/* 消息发送 */}
<div>
<label className="block text-sm font-medium mb-1">:</label>
<div className="flex space-x-2">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded"
placeholder="输入消息内容"
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
/>
<button
onClick={handleSendMessage}
disabled={!message.trim()}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
>
</button>
</div>
</div>
{/* 断开连接 */}
<button
onClick={connection.disconnect}
className="w-full px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
</button>
</div>
)}
{/* 功能说明 */}
<div className="mt-4 p-2 bg-blue-50 rounded text-sm">
<p><strong>:</strong></p>
<ul className="list-disc list-inside space-y-1">
<li> WebSocket </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> WebSocket </li>
<li> P2P WebSocket </li>
</ul>
</div>
</div>
);
}

View File

@@ -1,12 +1,13 @@
"use client";
import React, { useState, useCallback, useRef } from 'react';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Download, FileText, Image, Video, Music, Archive, Clock, Zap } from 'lucide-react';
import { useToast } from '@/components/ui/toast-simple';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { TransferProgressTracker, formatTransferSpeed, formatTime } from '@/lib/transfer-utils';
import { useReadConnectState } from '@/hooks/connection/state/useWebConnectStateManager';
import { TransferProgressTracker } from '@/lib/transfer-utils';
import { Archive, Clock, Download, FileText, Image, Music, Video, Zap } from 'lucide-react';
import React, { useCallback, useRef, useState } from 'react';
interface FileInfo {
id: string;
@@ -39,9 +40,6 @@ interface WebRTCFileReceiveProps {
onJoinRoom: (code: string) => void;
files: FileInfo[];
onDownloadFile: (fileId: string) => void;
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected?: boolean;
downloadedFiles?: Map<string, File>;
error?: string | null;
onReset?: () => void;
@@ -52,9 +50,6 @@ export function WebRTCFileReceive({
onJoinRoom,
files,
onDownloadFile,
isConnected,
isConnecting,
isWebSocketConnected = false,
downloadedFiles,
error = null,
onReset,
@@ -64,12 +59,16 @@ export function WebRTCFileReceive({
const [isValidating, setIsValidating] = useState(false);
const { showToast } = useToast();
// 用于跟踪传输进度的trackers
const transferTrackers = useRef<Map<string, TransferProgressTracker>>(new Map());
// 使用传入的取件码或本地状态的取件码
const displayPickupCode = propPickupCode || pickupCode;
const { getConnectState } = useReadConnectState();
// 验证取件码是否存在
const validatePickupCode = async (code: string): Promise<boolean> => {
try {
@@ -146,7 +145,7 @@ export function WebRTCFileReceive({
// 当验证失败时重置输入状态
React.useEffect(() => {
if (error && !isConnecting && !isConnected && !isValidating) {
if (error && !getConnectState().isConnecting && !getConnectState().isConnected && !isValidating) {
// 延迟重置,确保用户能看到错误信息
const timer = setTimeout(() => {
console.log('重置取件码输入');
@@ -155,10 +154,10 @@ export function WebRTCFileReceive({
return () => clearTimeout(timer);
}
}, [error, isConnecting, isConnected, isValidating]);
}, [error, getConnectState, isValidating]);
// 如果已经连接但没有文件,显示等待界面
if ((isConnected || isConnecting) && files.length === 0) {
if ((getConnectState().isConnected || getConnectState().isConnecting) && files.length === 0) {
return (
<div>
{/* 功能标题和状态 */}
@@ -168,6 +167,7 @@ export function WebRTCFileReceive({
<Download className="w-5 h-5 text-white" />
</div>
<div>
{getConnectState().isWebSocketConnected}
<h3 className="text-lg font-semibold text-slate-800"></h3>
<p className="text-sm text-slate-600">: {displayPickupCode}</p>
</div>
@@ -190,9 +190,9 @@ export function WebRTCFileReceive({
{/* 连接状态指示器 */}
<div className="flex items-center justify-center space-x-4 mb-6">
<div className="flex items-center">
<div className={`w-3 h-3 rounded-full mr-2 ${isConnected ? 'bg-emerald-500 animate-pulse' : 'bg-orange-500 animate-spin'}`}></div>
<span className={`text-sm font-medium ${isConnected ? 'text-emerald-600' : 'text-orange-600'}`}>
{isConnected ? '连接已建立' : '连接中...'}
<div className={`w-3 h-3 rounded-full mr-2 ${getConnectState().isConnected ? 'bg-emerald-500 animate-pulse' : 'bg-orange-500 animate-spin'}`}></div>
<span className={`text-sm font-medium ${getConnectState().isConnected ? 'text-emerald-600' : 'text-orange-600'}`}>
{getConnectState().isConnected ? '连接已建立' : '连接中...'}
</span>
</div>
</div>
@@ -326,7 +326,7 @@ export function WebRTCFileReceive({
</div>
<Button
onClick={() => onDownloadFile(file.id)}
disabled={!isConnected || isDownloading}
disabled={!getConnectState().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'
@@ -399,7 +399,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={isValidating || isConnecting}
disabled={isValidating || getConnectState().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) => (
@@ -423,14 +423,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 || isValidating || isConnecting}
disabled={pickupCode.length !== 6 || isValidating || getConnectState().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 ? (
) : getConnectState().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>

View File

@@ -1,14 +1,14 @@
"use client";
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useSharedWebRTCManager } from '@/hooks/connection';
import { useTextTransferBusiness } from '@/hooks/text-transfer';
import { useFileTransferBusiness } from '@/hooks/file-transfer';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/toast-simple';
import { MessageSquare, Image, Download } from 'lucide-react';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { useSharedWebRTCManager } from '@/hooks/connection';
import { useFileTransferBusiness } from '@/hooks/file-transfer';
import { useTextTransferBusiness } from '@/hooks/text-transfer';
import { Download, Image, MessageSquare } from 'lucide-react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
interface WebRTCTextReceiverProps {
initialCode?: string;
@@ -38,6 +38,8 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
// 创建共享连接
const connection = useSharedWebRTCManager();
const {getConnectState} = connection;
// 使用共享连接创建业务层
const textTransfer = useTextTransferBusiness(connection);
@@ -61,7 +63,7 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
if (onConnectionChange) {
onConnectionChange(connection);
}
}, [onConnectionChange, connection.isConnected, connection.isConnecting, connection.isPeerConnected]);
}, [onConnectionChange, getConnectState().isConnected, getConnectState().isConnecting, getConnectState().isPeerConnected]);
// 是否有任何错误
const hasAnyError = textTransfer.connectionError || fileTransfer.connectionError;
@@ -336,7 +338,7 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
<div className="flex flex-col items-center justify-center h-full text-slate-400 space-y-3">
<MessageSquare className="w-12 h-12 text-slate-300" />
<p className="text-center">
{connection.isPeerConnected ?
{getConnectState().isPeerConnected ?
'等待对方发送文字内容...' :
'等待连接建立...'}
</p>

View File

@@ -1,14 +1,14 @@
"use client";
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useSharedWebRTCManager } from '@/hooks/connection';
import { useTextTransferBusiness } from '@/hooks/text-transfer';
import { useFileTransferBusiness } from '@/hooks/file-transfer';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import RoomInfoDisplay from '@/components/RoomInfoDisplay';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast-simple';
import { MessageSquare, Image, Send, Copy } from 'lucide-react';
import RoomInfoDisplay from '@/components/RoomInfoDisplay';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { useSharedWebRTCManager } from '@/hooks/connection';
import { useFileTransferBusiness } from '@/hooks/file-transfer';
import { useTextTransferBusiness } from '@/hooks/text-transfer';
import { Image, MessageSquare, Send } from 'lucide-react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
interface WebRTCTextSenderProps {
onRestart?: () => void;
@@ -32,7 +32,9 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
// 创建共享连接
const connection = useSharedWebRTCManager();
const { getConnectState } = connection;
// 使用共享连接创建业务层
const textTransfer = useTextTransferBusiness(connection);
const fileTransfer = useFileTransferBusiness(connection);
@@ -43,9 +45,6 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
// 只需要连接一次,因为使用的是共享连接
await connection.connect(code, role);
}, [connection]);
// 是否有任何连接
const hasAnyConnection = textTransfer.isConnected || fileTransfer.isConnected;
// 是否正在连接
const isAnyConnecting = textTransfer.isConnecting || fileTransfer.isConnecting;
@@ -55,10 +54,8 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
if (onConnectionChange) {
onConnectionChange(connection);
}
}, [onConnectionChange, connection.isConnected, connection.isConnecting, connection.isPeerConnected]);
}, [onConnectionChange, getConnectState().isConnected, getConnectState().isConnecting, getConnectState().isPeerConnected]);
// 是否有任何错误
const hasAnyError = textTransfer.connectionError || fileTransfer.connectionError;
// 重新开始
const restart = () => {
@@ -140,7 +137,7 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
// 如果有初始文本,发送它
if (currentText) {
setTimeout(() => {
if (connection.isPeerConnected && textTransfer.isConnected) {
if (getConnectState().isPeerConnected && textTransfer.isConnected) {
// 发送实时文本同步
textTransfer.sendTextSync(currentText);
@@ -171,7 +168,7 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
textarea.style.height = `${newHeight}px`;
// 实时同步文本内容如果P2P连接已建立
if (connection.isPeerConnected && textTransfer.isConnected) {
if (getConnectState().isPeerConnected && textTransfer.isConnected) {
// 发送实时文本同步
textTransfer.sendTextSync(value);
@@ -214,10 +211,10 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
}]);
// 发送文件
if (connection.isPeerConnected && fileTransfer.isConnected) {
if (getConnectState().isPeerConnected && fileTransfer.isConnected) {
fileTransfer.sendFile(file);
showToast('图片发送中...', "success");
} else if (!connection.isPeerConnected) {
} else if (!getConnectState().isPeerConnected) {
showToast('等待对方加入P2P网络...', "error");
} else {
showToast('请先连接到房间', "error");
@@ -362,19 +359,18 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
)}
</div>
</div>
<textarea
ref={textareaRef}
value={textInput}
onChange={handleTextInputChange}
onPaste={handlePaste}
disabled={!connection.isPeerConnected}
placeholder={connection.isPeerConnected
disabled={!getConnectState().isPeerConnected}
placeholder={getConnectState().isPeerConnected
? "在这里编辑文字内容...&#10;&#10;💡 支持实时同步编辑,对方可以看到你的修改&#10;💡 可以直接粘贴图片 (Ctrl+V)"
: "等待对方加入P2P网络...&#10;&#10;📡 建立连接后即可开始输入文字"
}
className={`w-full h-40 px-4 py-3 border rounded-lg resize-none text-slate-700 ${
connection.isPeerConnected
getConnectState().isPeerConnected
? "border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-slate-400"
: "border-slate-200 bg-slate-50 cursor-not-allowed placeholder-slate-300"
}`}
@@ -386,9 +382,9 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
onClick={() => fileInputRef.current?.click()}
variant="outline"
size="sm"
disabled={!connection.isPeerConnected}
disabled={!getConnectState().isPeerConnected}
className={`flex items-center space-x-1 ${
!connection.isPeerConnected ? 'cursor-not-allowed opacity-50' : ''
!getConnectState().isPeerConnected ? 'cursor-not-allowed opacity-50' : ''
}`}
>
<Image className="w-4 h-4" />

View File

@@ -1,5 +1,8 @@
// 连接相关的hooks
export { useConnectionState } from './useConnectionState';
export { useSharedWebRTCManager } from './useConnectManager';
export { useRoomConnection } from './useRoomConnection';
export { useSharedWebRTCManager } from './useSharedWebRTCManager';
export { useWebRTCSupport } from './useWebRTCSupport';

View File

@@ -0,0 +1,80 @@
import { useCallback } from 'react';
import { Role } from '../types';
import { useWebRTCStore, type WebConnectState } from './webConnectStore';
/**
* WebRTC 状态管理器
* 负责连接状态的统一管理
*/
export interface IWebConnectStateManager {
// 获取当前状态
getState: () => Readonly<WebConnectState>;
// 更新状态
updateState: (updates: Partial<WebConnectState>) => void;
// 设置当前房间
setCurrentRoom: (room: { code: string; role: Role } | null) => void;
// 重置到初始状态
resetToInitial: () => void;
// 检查是否已连接到指定房间
isConnectedToRoom: (roomCode: string, role: Role) => boolean;
}
export interface IUseReadConnectState {
getConnectState: () => Readonly<WebConnectState>;
}
export function useReadConnectState(): IUseReadConnectState {
const webrtcStore = useWebRTCStore();
const getConnectState = useCallback((): Readonly<WebConnectState> => {
return webrtcStore;
}, [webrtcStore]);
return {
getConnectState
};
}
/**
* WebRTC 状态管理 Hook
* 封装对 webRTCStore 的操作,提供状态更新和查询的统一接口
*/
export function useWebConnectStateManager(): IWebConnectStateManager {
const webrtcStore = useWebRTCStore();
const getState = useCallback((): WebConnectState => {
return webrtcStore;
}, [webrtcStore]);
const updateState = useCallback((updates: Partial<WebConnectState>) => {
webrtcStore.updateState(updates);
}, [webrtcStore]);
const setCurrentRoom = useCallback((room: { code: string; role: Role } | null) => {
webrtcStore.setCurrentRoom(room);
}, [webrtcStore]);
const resetToInitial = useCallback(() => {
webrtcStore.resetToInitial();
}, [webrtcStore]);
const isConnectedToRoom = useCallback((roomCode: string, role: Role) => {
return webrtcStore.currentRoom?.code === roomCode &&
webrtcStore.currentRoom?.role === role &&
webrtcStore.isConnected;
}, [webrtcStore]);
return {
getState,
updateState,
setCurrentRoom,
resetToInitial,
isConnectedToRoom,
};
}

View File

@@ -1,46 +1,55 @@
import { create } from 'zustand';
import { Role } from '../types';
interface WebRTCState {
export interface WebConnectState {
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected: boolean;
isPeerConnected: boolean;
isDataChannelConnected: boolean;
isMediaStreamConnected: boolean;
currentConnectType: 'webrtc' | 'websocket';
state: RTCDataChannelState;
error: string | null;
canRetry: boolean; // 新增:是否可以重试
currentRoom: { code: string; role: 'sender' | 'receiver' } | null;
currentRoom: { code: string; role: Role } | null;
}
interface WebRTCStore extends WebRTCState {
updateState: (updates: Partial<WebRTCState>) => void;
setCurrentRoom: (room: { code: string; role: 'sender' | 'receiver' } | null) => void;
interface WebRTCStore extends WebConnectState {
updateState: (updates: Partial<WebConnectState>) => void;
setCurrentRoom: (room: { code: string; role: Role } | null) => void;
reset: () => void;
resetToInitial: () => void; // 新增:完全重置到初始状态
}
const initialState: WebRTCState = {
const initialState: WebConnectState = {
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
isPeerConnected: false,
error: null,
canRetry: false, // 初始状态下不需要重试
canRetry: false, // 初始状态下不需要重试
currentRoom: null,
isDataChannelConnected: false,
isMediaStreamConnected: false,
currentConnectType: 'webrtc',
state: 'closed'
};
export const useWebRTCStore = create<WebRTCStore>((set) => ({
...initialState,
updateState: (updates) => set((state) => ({
...state,
...updates,
})),
setCurrentRoom: (room) => set((state) => ({
...state,
currentRoom: room,
})),
reset: () => set(initialState),
resetToInitial: () => set(initialState), // 完全重置到初始状态
}));

View File

@@ -0,0 +1,123 @@
import { WebConnectState } from "./state/webConnectStore";
// 消息和数据处理器类型
export type MessageHandler = (message: IWebMessage) => void;
export type DataHandler = (data: ArrayBuffer) => void;
// 角色类型
export type Role = 'sender' | 'receiver';
export type ConnectType = 'webrtc' | 'websocket';
// 对外包装类型 暴露接口
export interface IRegisterEventHandler {
registerMessageHandler: (channel: string, handler: MessageHandler) => () => void;
registerDataHandler: (channel: string, handler: DataHandler) => () => void;
}
export interface IGetConnectState {
getConnectState: () => WebConnectState;
}
/***
*
* 对外包装类型 暴露接口
*
*/
// WebRTC 连接接口
export interface IWebConnection extends IRegisterEventHandler, IGetConnectState {
connectType: ConnectType;
// 操作方法
connect: (roomCode: string, role: Role) => Promise<void>;
disconnect: () => void;
retry: () => Promise<void>;
sendMessage: (message: IWebMessage, channel?: string) => boolean;
sendData: (data: ArrayBuffer) => boolean;
// 工具方法
getConnectState: () => WebConnectState;
isConnectedToRoom: (roomCode: string, role: Role) => boolean;
// 当前房间信息
currentRoom: { code: string; role: Role } | null;
// 媒体轨道方法
addTrack: (track: MediaStreamTrack, stream: MediaStream) => RTCRtpSender | null;
removeTrack: (sender: RTCRtpSender) => void;
onTrack: (callback: (event: RTCTrackEvent) => void) => void;
getPeerConnection: () => RTCPeerConnection | null;
createOfferNow: () => Promise<boolean>;
}
// 消息类型
export interface IWebMessage {
type: string;
payload: any;
channel?: string;
}
/***
*
* 数据通道类型
* WebRTC 数据通道管理器
* 负责数据通道的创建和管理
*/
export interface WebRTCDataChannelManager extends IGetConnectState {
// 创建数据通道
createDataChannel: (pc: RTCPeerConnection, role: Role, isReconnect?: boolean) => void;
// 发送消息
sendMessage: (message: IWebMessage, channel?: string) => boolean;
// 发送二进制数据
sendData: (data: ArrayBuffer) => boolean;
// 处理数据通道消息
handleDataChannelMessage: (event: MessageEvent) => void;
}
/**
* WebRTC 媒体轨道管理器
* 负责媒体轨道的添加和移除
*/
export interface WebRTCTrackManager {
// 添加媒体轨道
addTrack: (track: MediaStreamTrack, stream: MediaStream) => RTCRtpSender | null;
// 移除媒体轨道
removeTrack: (sender: RTCRtpSender) => void;
// 设置轨道处理器
onTrack: (handler: (event: RTCTrackEvent) => void) => void;
// 创建 Offer
createOffer: (pc: RTCPeerConnection, ws: WebSocket) => Promise<void>;
// 立即创建offer用于媒体轨道添加后的重新协商
createOfferNow: (pc: RTCPeerConnection, ws: WebSocket) => Promise<boolean>;
// 内部方法,供核心连接管理器调用
setPeerConnection: (pc: RTCPeerConnection | null) => void;
setWebSocket: (ws: WebSocket | null) => void;
}

View File

@@ -0,0 +1,249 @@
import { useCallback, useRef, useState } from 'react';
import { useReadConnectState } from './state/useWebConnectStateManager';
import { WebConnectState } from "./state/webConnectStore";
import { ConnectType, DataHandler, IGetConnectState, IRegisterEventHandler, IWebConnection, IWebMessage, MessageHandler, Role } from "./types";
import { useSharedWebRTCManagerImpl } from './webrtc/useSharedWebRTCManager';
import { useWebSocketConnection } from './ws/useWebSocketConnection';
/**
* 连接管理器 - 代理 WebSocket 和 WebRTC 连接
* 提供统一的连接接口,内部可以在不同传输方式之间切换
*/
export function useSharedWebRTCManager(): IWebConnection & IRegisterEventHandler & IGetConnectState {
// 当前连接类型
const [currentConnectType, setCurrentConnectType] = useState<ConnectType>('webrtc');
// 连接实例
const wsConnection = useWebSocketConnection();
const webrtcConnection = useSharedWebRTCManagerImpl();
// 当前活跃连接的引用
const currentConnectionRef = useRef<IWebConnection>(wsConnection);
const { getConnectState: innerState } = useReadConnectState();
// 连接状态管理
const connectionStateRef = useRef<WebConnectState>({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
isPeerConnected: false,
isDataChannelConnected: false,
isMediaStreamConnected: false,
currentConnectType: 'webrtc',
state: 'closed',
error: null,
canRetry: false,
currentRoom: null
});
// 更新连接状态
const updateConnectionState = useCallback((updates: Partial<WebConnectState>) => {
connectionStateRef.current = {
...connectionStateRef.current,
...updates
};
}, []);
// 切换连接类型
const switchConnectionType = useCallback((type: ConnectType) => {
console.log('[ConnectManager] 切换连接类型:', currentConnectType, '->', type);
// 如果当前有连接,先断开
if (connectionStateRef.current.isConnected) {
currentConnectionRef.current.disconnect();
}
// 切换到新的连接类型
setCurrentConnectType(type);
currentConnectionRef.current = type === 'websocket' ? wsConnection : webrtcConnection;
updateConnectionState({
currentConnectType: type,
isConnected: false,
isConnecting: false,
error: null
});
}, [currentConnectType, wsConnection, webrtcConnection, updateConnectionState]);
// 连接到房间
const connect = useCallback(async (roomCode: string, role: Role) => {
console.log('[ConnectManager] 连接到房间:', roomCode, '角色:', role, '类型:', currentConnectType);
updateConnectionState({
isConnecting: true,
error: null,
currentRoom: { code: roomCode, role }
});
try {
if (currentConnectType === 'webrtc') {
// 使用当前选择的连接类型进行连接
currentConnectionRef.current = webrtcConnection;
await currentConnectionRef.current.connect(roomCode, role);
}
} catch (error) {
console.error('[ConnectManager] 连接失败:', error);
}
}, [currentConnectType]);
// 断开连接
const disconnect = useCallback(() => {
console.log('[ConnectManager] 断开连接');
currentConnectionRef.current.disconnect();
updateConnectionState({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
isPeerConnected: false,
isDataChannelConnected: false,
isMediaStreamConnected: false,
error: null,
canRetry: false,
currentRoom: null
});
}, [updateConnectionState]);
// 重试连接
const retry = useCallback(async () => {
console.log('[ConnectManager] 重试连接');
if (connectionStateRef.current.currentRoom) {
const { code, role } = connectionStateRef.current.currentRoom;
await connect(code, role);
}
}, [connect]);
// 发送消息
const sendMessage = useCallback((message: IWebMessage, channel?: string) => {
return currentConnectionRef.current.sendMessage(message, channel);
}, []);
// 发送数据
const sendData = useCallback((data: ArrayBuffer) => {
return currentConnectionRef.current.sendData(data);
}, []);
// 获取连接状态
const getConnectState = useCallback((): WebConnectState => {
// 合并当前连接的状态和管理器的状态
return innerState();
}, [innerState]);
// 检查是否连接到指定房间
const isConnectedToRoom = useCallback((roomCode: string, role: Role) => {
return currentConnectionRef.current.isConnectedToRoom(roomCode, role);
}, []);
// 媒体轨道方法(代理到当前连接)
const addTrack = useCallback((track: MediaStreamTrack, stream: MediaStream) => {
return currentConnectionRef.current.addTrack(track, stream);
}, []);
const removeTrack = useCallback((sender: RTCRtpSender) => {
currentConnectionRef.current.removeTrack(sender);
}, []);
const onTrack = useCallback((callback: (event: RTCTrackEvent) => void) => {
currentConnectionRef.current.onTrack(callback);
}, []);
const getPeerConnection = useCallback(() => {
return currentConnectionRef.current.getPeerConnection();
}, []);
const createOfferNow = useCallback(async () => {
return currentConnectionRef.current.createOfferNow();
}, []);
// 扩展方法:切换连接类型
const switchToWebSocket = useCallback(() => {
switchConnectionType('websocket');
}, [switchConnectionType]);
const switchToWebRTC = useCallback(() => {
switchConnectionType('webrtc');
}, [switchConnectionType]);
// 获取连接统计信息
const getConnectionStats = useCallback(() => {
const state = getConnectState();
return {
currentType: currentConnectType,
isConnected: state.isConnected,
hasWebSocket: state.isWebSocketConnected,
hasWebRTC: state.isPeerConnected,
hasDataChannel: state.isDataChannelConnected,
hasMediaStream: state.isMediaStreamConnected,
room: state.currentRoom,
error: state.error,
canRetry: state.canRetry
};
}, [currentConnectType, innerState]);
// 注册消息处理器
const registerMessageHandler = useCallback((channel: string, handler: MessageHandler) => {
console.log('[DataChannelManager] 注册消息处理器:', channel);
const webrtcConnectionUninstall = webrtcConnection.registerMessageHandler(channel, handler);
const wsConnectionUninstall = wsConnection.registerMessageHandler(channel, handler);
return () => {
console.log('[DataChannelManager] 取消注册消息处理器:', channel);
webrtcConnectionUninstall();
wsConnectionUninstall();
};
}, []);
// 注册数据处理器
const registerDataHandler = useCallback((channel: string, handler: DataHandler) => {
console.log('[DataChannelManager] 注册数据处理器:', channel);
const webrtcConnectionUninstall = webrtcConnection.registerDataHandler(channel, handler);
const wsConnectionUninstall = wsConnection.registerDataHandler(channel, handler);
return () => {
console.log('[DataChannelManager] 取消注册数据处理器:', channel);
webrtcConnectionUninstall();
wsConnectionUninstall();
};
}, []);
return {
connectType: currentConnectType,
connect,
disconnect,
retry,
sendMessage,
sendData,
registerMessageHandler,
registerDataHandler,
getConnectState,
isConnectedToRoom,
currentRoom: connectionStateRef.current.currentRoom,
addTrack,
removeTrack,
onTrack,
getPeerConnection,
createOfferNow,
// 扩展方法
switchToWebSocket,
switchToWebRTC,
getConnectionStats,
} as IWebConnection & {
switchToWebSocket: () => void;
switchToWebRTC: () => void;
getConnectionStats: () => any;
};
}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useToast } from '@/components/ui/toast-simple';
import { useEffect, useState } from 'react';
interface UseConnectionStateProps {
isWebSocketConnected: boolean;

View File

@@ -1,8 +1,9 @@
import { useState, useCallback } from 'react';
import { useToast } from '@/components/ui/toast-simple';
import { useCallback, useState } from 'react';
import { Role } from './types';
interface UseRoomConnectionProps {
connect: (code: string, role: 'sender' | 'receiver') => void;
connect: (code: string, role: Role) => void;
isConnecting: boolean;
isConnected: boolean;
}

View File

@@ -1,78 +0,0 @@
import { useCallback } from 'react';
import { useWebRTCStore } from '../ui/webRTCStore';
// 基础连接状态
export interface WebRTCState {
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected: boolean;
isPeerConnected: boolean;
error: string | null;
canRetry: boolean;
}
/**
* WebRTC 状态管理器
* 负责连接状态的统一管理
*/
export interface WebRTCStateManager {
// 获取当前状态
getState: () => WebRTCState;
// 更新状态
updateState: (updates: Partial<WebRTCState>) => void;
// 设置当前房间
setCurrentRoom: (room: { code: string; role: 'sender' | 'receiver' } | null) => void;
// 重置到初始状态
resetToInitial: () => void;
// 检查是否已连接到指定房间
isConnectedToRoom: (roomCode: string, role: 'sender' | 'receiver') => boolean;
}
/**
* WebRTC 状态管理 Hook
* 封装对 webRTCStore 的操作,提供状态更新和查询的统一接口
*/
export function useWebRTCStateManager(): WebRTCStateManager {
const webrtcStore = useWebRTCStore();
const getState = useCallback((): WebRTCState => {
return {
isConnected: webrtcStore.isConnected,
isConnecting: webrtcStore.isConnecting,
isWebSocketConnected: webrtcStore.isWebSocketConnected,
isPeerConnected: webrtcStore.isPeerConnected,
error: webrtcStore.error,
canRetry: webrtcStore.canRetry,
};
}, [webrtcStore]);
const updateState = useCallback((updates: Partial<WebRTCState>) => {
webrtcStore.updateState(updates);
}, [webrtcStore]);
const setCurrentRoom = useCallback((room: { code: string; role: 'sender' | 'receiver' } | null) => {
webrtcStore.setCurrentRoom(room);
}, [webrtcStore]);
const resetToInitial = useCallback(() => {
webrtcStore.resetToInitial();
}, [webrtcStore]);
const isConnectedToRoom = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
return webrtcStore.currentRoom?.code === roomCode &&
webrtcStore.currentRoom?.role === role &&
webrtcStore.isConnected;
}, [webrtcStore]);
return {
getState,
updateState,
setCurrentRoom,
resetToInitial,
isConnectedToRoom,
};
}

View File

@@ -1,57 +1,22 @@
import { useCallback } from 'react';
import { useWebRTCStateManager } from './useWebRTCStateManager';
import { useWebRTCDataChannelManager, WebRTCMessage } from './useWebRTCDataChannelManager';
import { useWebRTCTrackManager } from './useWebRTCTrackManager';
import { useWebConnectStateManager } from '../state/useWebConnectStateManager';
import { IGetConnectState, IRegisterEventHandler, IWebConnection } from '../types';
import { useWebRTCConnectionCore } from './useWebRTCConnectionCore';
import { useWebRTCDataChannelManager } from './useWebRTCDataChannelManager';
import { useWebRTCTrackManager } from './useWebRTCTrackManager';
// 消息和数据处理器类型
export type MessageHandler = (message: WebRTCMessage) => void;
export type DataHandler = (data: ArrayBuffer) => void;
// WebRTC 连接接口
export interface WebRTCConnection {
// 状态
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected: boolean;
isPeerConnected: boolean;
error: string | null;
canRetry: boolean;
// 操作方法
connect: (roomCode: string, role: 'sender' | 'receiver') => Promise<void>;
disconnect: () => void;
retry: () => Promise<void>;
sendMessage: (message: WebRTCMessage, channel?: string) => boolean;
sendData: (data: ArrayBuffer) => boolean;
// 处理器注册
registerMessageHandler: (channel: string, handler: MessageHandler) => () => void;
registerDataHandler: (channel: string, handler: DataHandler) => () => void;
// 工具方法
getChannelState: () => RTCDataChannelState;
isConnectedToRoom: (roomCode: string, role: 'sender' | 'receiver') => boolean;
// 当前房间信息
currentRoom: { code: string; role: 'sender' | 'receiver' } | null;
// 媒体轨道方法
addTrack: (track: MediaStreamTrack, stream: MediaStream) => RTCRtpSender | null;
removeTrack: (sender: RTCRtpSender) => void;
onTrack: (callback: (event: RTCTrackEvent) => void) => void;
getPeerConnection: () => RTCPeerConnection | null;
createOfferNow: () => Promise<boolean>;
}
/**
* WebRTC
* WebRTC 使
*
*
* webrtc
*
*/
export function useSharedWebRTCManager(): WebRTCConnection {
export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHandler & IGetConnectState {
// 创建各个管理器实例
const stateManager = useWebRTCStateManager();
const stateManager = useWebConnectStateManager();
const dataChannelManager = useWebRTCDataChannelManager(stateManager);
const trackManager = useWebRTCTrackManager(stateManager);
const connectionCore = useWebRTCConnectionCore(
@@ -71,7 +36,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
console.error('[SharedWebRTC] PeerConnection 或 WebSocket 不可用');
return false;
}
try {
return await trackManager.createOfferNow(pc, ws);
} catch (error) {
@@ -83,12 +48,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
// 返回统一的接口,保持与当前 API 一致
return {
// 状态
isConnected: state.isConnected,
isConnecting: state.isConnecting,
isWebSocketConnected: state.isWebSocketConnected,
isPeerConnected: state.isPeerConnected,
error: state.error,
canRetry: state.canRetry,
connectType: 'webrtc',
// 操作方法
connect: connectionCore.connect,
@@ -102,7 +62,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
registerDataHandler: dataChannelManager.registerDataHandler,
// 工具方法
getChannelState: dataChannelManager.getChannelState,
getConnectState: stateManager.getState,
isConnectedToRoom: stateManager.isConnectedToRoom,
// 媒体轨道方法

View File

@@ -1,10 +1,9 @@
import { useRef, useCallback } from 'react';
import { getWsUrl } from '@/lib/config';
import { getIceServersConfig } from '../settings/useIceServersConfig';
import { WebRTCStateManager } from './useWebRTCStateManager';
import { WebRTCDataChannelManager, WebRTCMessage } from './useWebRTCDataChannelManager';
import { WebRTCTrackManager } from './useWebRTCTrackManager';
import { useCallback, useRef } from 'react';
import { getIceServersConfig } from '../../settings/useIceServersConfig';
import { IWebConnectStateManager } from '../state/useWebConnectStateManager';
import { Role, WebRTCDataChannelManager, WebRTCTrackManager } from '../types';
/**
* WebRTC
@@ -12,7 +11,7 @@ import { WebRTCTrackManager } from './useWebRTCTrackManager';
*/
export interface WebRTCConnectionCore {
// 连接到房间
connect: (roomCode: string, role: 'sender' | 'receiver') => Promise<void>;
connect: (roomCode: string, role: Role) => Promise<void>;
// 断开连接
disconnect: (shouldNotifyDisconnect?: boolean) => void;
@@ -27,7 +26,7 @@ export interface WebRTCConnectionCore {
getWebSocket: () => WebSocket | null;
// 获取当前房间信息
getCurrentRoom: () => { code: string; role: 'sender' | 'receiver' } | null;
getCurrentRoom: () => { code: string; role: Role } | null;
}
/**
@@ -35,7 +34,7 @@ export interface WebRTCConnectionCore {
* WebRTC WebSocket PeerConnection
*/
export function useWebRTCConnectionCore(
stateManager: WebRTCStateManager,
stateManager: IWebConnectStateManager,
dataChannelManager: WebRTCDataChannelManager,
trackManager: WebRTCTrackManager
): WebRTCConnectionCore {
@@ -44,7 +43,7 @@ export function useWebRTCConnectionCore(
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// 当前连接的房间信息
const currentRoom = useRef<{ code: string; role: 'sender' | 'receiver' } | null>(null);
const currentRoom = useRef<{ code: string; role: Role } | null>(null);
// 用于跟踪是否是用户主动断开连接
const isUserDisconnecting = useRef<boolean>(false);
@@ -210,7 +209,7 @@ export function useWebRTCConnectionCore(
}, [stateManager, dataChannelManager]);
// 连接到房间
const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
const connect = useCallback(async (roomCode: string, role: Role) => {
console.log('[ConnectionCore] 🚀 开始连接到房间:', roomCode, role);
// 如果正在连接中,避免重复连接

View File

@@ -1,65 +1,31 @@
import { useRef, useCallback } from 'react';
import { WebRTCStateManager } from './useWebRTCStateManager';
import { useCallback, useEffect, useRef } from 'react';
import { IWebConnectStateManager } from '../state/useWebConnectStateManager';
import { DataHandler, IRegisterEventHandler, IWebMessage, MessageHandler, Role, WebRTCDataChannelManager } from '../types';
// 消息类型
export interface WebRTCMessage {
type: string;
payload: any;
channel?: string;
}
// 消息和数据处理器类型
export type MessageHandler = (message: WebRTCMessage) => void;
export type DataHandler = (data: ArrayBuffer) => void;
/**
* WebRTC
*
*/
export interface WebRTCDataChannelManager {
// 创建数据通道
createDataChannel: (pc: RTCPeerConnection, role: 'sender' | 'receiver', isReconnect?: boolean) => void;
// 发送消息
sendMessage: (message: WebRTCMessage, channel?: string) => boolean;
// 发送二进制数据
sendData: (data: ArrayBuffer) => boolean;
// 注册消息处理器
registerMessageHandler: (channel: string, handler: MessageHandler) => () => void;
// 注册数据处理器
registerDataHandler: (channel: string, handler: DataHandler) => () => void;
// 获取数据通道状态
getChannelState: () => RTCDataChannelState;
// 处理数据通道消息
handleDataChannelMessage: (event: MessageEvent) => void;
}
/**
* WebRTC Hook
*
*/
export function useWebRTCDataChannelManager(
stateManager: WebRTCStateManager
): WebRTCDataChannelManager {
stateManager: IWebConnectStateManager
): WebRTCDataChannelManager & IRegisterEventHandler {
const dcRef = useRef<RTCDataChannel | null>(null);
// 多通道消息处理器
const messageHandlers = useRef<Map<string, MessageHandler>>(new Map());
const dataHandlers = useRef<Map<string, DataHandler>>(new Map());
// 创建数据通道
const createDataChannel = useCallback((
pc: RTCPeerConnection,
role: 'sender' | 'receiver',
pc: RTCPeerConnection,
role: Role,
isReconnect: boolean = false
) => {
console.log('[DataChannelManager] 创建数据通道...', { role, isReconnect });
// 如果已经存在数据通道,先关闭它
if (dcRef.current) {
console.log('[DataChannelManager] 关闭已存在的数据通道');
@@ -79,14 +45,14 @@ export function useWebRTCDataChannelManager(
console.log('[DataChannelManager] 数据通道已打开 (发送方)');
// 确保所有连接状态都正确更新
stateManager.updateState({
isWebSocketConnected: true,
isDataChannelConnected: true,
isConnected: true,
isPeerConnected: true,
error: null,
isConnecting: false,
canRetry: false
});
// 如果是重新连接,触发数据同步
if (isReconnect) {
console.log('[DataChannelManager] 发送方重新连接,数据通道已打开,准备同步数据');
@@ -102,17 +68,17 @@ export function useWebRTCDataChannelManager(
}, 300); // 等待数据通道完全稳定
}
};
dataChannel.onmessage = handleDataChannelMessage;
dataChannel.onerror = (error) => {
console.error('[DataChannelManager] 数据通道错误:', error);
// 获取更详细的错误信息
let errorMessage = '数据通道连接失败';
let shouldRetry = false;
// 根据数据通道状态提供更具体的错误信息
switch (dataChannel.readyState) {
case 'connecting':
@@ -144,13 +110,14 @@ export function useWebRTCDataChannelManager(
}
}
}
console.error(`[DataChannelManager] 数据通道详细错误 - 状态: ${dataChannel.readyState}, 消息: ${errorMessage}, 建议重试: ${shouldRetry}`);
stateManager.updateState({
error: errorMessage,
isConnecting: false,
isPeerConnected: false, // 数据通道出错时P2P连接肯定不可用
isDataChannelConnected: false,
canRetry: shouldRetry // 设置是否可以重试
});
};
@@ -164,13 +131,14 @@ export function useWebRTCDataChannelManager(
// 确保所有连接状态都正确更新
stateManager.updateState({
isWebSocketConnected: true,
isDataChannelConnected: true,
isConnected: true,
isPeerConnected: true,
error: null,
isConnecting: false,
canRetry: false
});
// 如果是重新连接,触发数据同步
if (isReconnect) {
console.log('[DataChannelManager] 接收方重新连接,数据通道已打开,准备同步数据');
@@ -191,11 +159,11 @@ export function useWebRTCDataChannelManager(
dataChannel.onerror = (error) => {
console.error('[DataChannelManager] 数据通道错误 (接收方):', error);
// 获取更详细的错误信息
let errorMessage = '数据通道连接失败';
let shouldRetry = false;
// 根据数据通道状态提供更具体的错误信息
switch (dataChannel.readyState) {
case 'connecting':
@@ -227,13 +195,14 @@ export function useWebRTCDataChannelManager(
}
}
}
console.error(`[DataChannelManager] 数据通道详细错误 (接收方) - 状态: ${dataChannel.readyState}, 消息: ${errorMessage}, 建议重试: ${shouldRetry}`);
stateManager.updateState({
error: errorMessage,
isConnecting: false,
isPeerConnected: false, // 数据通道出错时P2P连接肯定不可用
isDataChannelConnected: false,
canRetry: shouldRetry // 设置是否可以重试
});
};
@@ -245,9 +214,11 @@ export function useWebRTCDataChannelManager(
// 处理数据通道消息
const handleDataChannelMessage = useCallback((event: MessageEvent) => {
console.log('[DataChannelManager] 收到数据通道消息,类型:', typeof event.data);
console.log('[DataChannelManager] 数据通道当前状态:', messageHandlers.current);
if (typeof event.data === 'string') {
try {
const message = JSON.parse(event.data) as WebRTCMessage;
const message = JSON.parse(event.data) as IWebMessage;
console.log('[DataChannelManager] 收到消息:', message.type, message.channel || 'default');
// 根据通道分发消息
@@ -280,8 +251,29 @@ export function useWebRTCDataChannelManager(
}
}, []);
// 注册消息处理器
const registerMessageHandler = useCallback((channel: string, handler: MessageHandler) => {
console.log('[DataChannelManager] 注册消息处理器:', channel);
messageHandlers.current.set(channel, handler);
return () => {
console.log('[DataChannelManager] 取消注册消息处理器:', channel);
messageHandlers.current.delete(channel);
};
}, []);
// 注册数据处理器
const registerDataHandler = useCallback((channel: string, handler: DataHandler) => {
console.log('[DataChannelManager] 注册数据处理器:', channel);
dataHandlers.current.set(channel, handler);
return () => {
console.log('[DataChannelManager] 取消注册数据处理器:', channel);
dataHandlers.current.delete(channel);
};
}, []);
// 发送消息
const sendMessage = useCallback((message: WebRTCMessage, channel?: string) => {
const sendMessage = useCallback((message: IWebMessage, channel?: string) => {
const dataChannel = dcRef.current;
if (!dataChannel || dataChannel.readyState !== 'open') {
console.error('[DataChannelManager] 数据通道未准备就绪');
@@ -317,40 +309,24 @@ export function useWebRTCDataChannelManager(
}
}, []);
// 注册消息处理器
const registerMessageHandler = useCallback((channel: string, handler: MessageHandler) => {
console.log('[DataChannelManager] 注册消息处理器:', channel);
messageHandlers.current.set(channel, handler);
return () => {
console.log('[DataChannelManager] 取消注册消息处理器:', channel);
messageHandlers.current.delete(channel);
};
}, []);
// 注册数据处理器
const registerDataHandler = useCallback((channel: string, handler: DataHandler) => {
console.log('[DataChannelManager] 注册数据处理器:', channel);
dataHandlers.current.set(channel, handler);
return () => {
console.log('[DataChannelManager] 取消注册数据处理器:', channel);
dataHandlers.current.delete(channel);
};
}, []);
// 获取数据通道状态
const getChannelState = useCallback(() => {
return dcRef.current?.readyState || 'closed';
return stateManager.getState();
}, []);
useEffect(() => {
stateManager.updateState({ state: dcRef.current?.readyState || 'closed' });
}, [dcRef.current?.readyState]);
return {
createDataChannel,
sendMessage,
sendData,
registerMessageHandler,
getConnectState: getChannelState,
registerDataHandler,
getChannelState,
registerMessageHandler,
handleDataChannelMessage,
};
}

View File

@@ -1,37 +1,14 @@
import { useCallback, useRef } from 'react';
import { WebRTCStateManager } from './useWebRTCStateManager';
import { IWebConnectStateManager } from '../state/useWebConnectStateManager';
import { WebRTCTrackManager } from '../types';
/**
* WebRTC
*
*/
export interface WebRTCTrackManager {
// 添加媒体轨道
addTrack: (track: MediaStreamTrack, stream: MediaStream) => RTCRtpSender | null;
// 移除媒体轨道
removeTrack: (sender: RTCRtpSender) => void;
// 设置轨道处理器
onTrack: (handler: (event: RTCTrackEvent) => void) => void;
// 创建 Offer
createOffer: (pc: RTCPeerConnection, ws: WebSocket) => Promise<void>;
// 立即创建offer用于媒体轨道添加后的重新协商
createOfferNow: (pc: RTCPeerConnection, ws: WebSocket) => Promise<boolean>;
// 内部方法,供核心连接管理器调用
setPeerConnection: (pc: RTCPeerConnection | null) => void;
setWebSocket: (ws: WebSocket | null) => void;
}
/**
* WebRTC Hook
* createOffer
*/
export function useWebRTCTrackManager(
stateManager: WebRTCStateManager
stateManager: IWebConnectStateManager
): WebRTCTrackManager {
const pcRef = useRef<RTCPeerConnection | null>(null);
const wsRef = useRef<WebSocket | null>(null);
@@ -40,12 +17,12 @@ export function useWebRTCTrackManager(
const createOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
try {
console.log('[TrackManager] 🎬 开始创建offer当前轨道数量:', pc.getSenders().length);
// 确保连接状态稳定
if (pc.connectionState !== 'connecting' && pc.connectionState !== 'new') {
console.warn('[TrackManager] ⚠️ PeerConnection状态异常:', pc.connectionState);
}
const offer = await pc.createOffer({
offerToReceiveAudio: true, // 改为true以支持音频接收
offerToReceiveVideo: true, // 改为true以支持视频接收
@@ -84,7 +61,7 @@ export function useWebRTCTrackManager(
}
}
};
// 同时监听ICE候选事件用于调试
pc.onicecandidate = (event) => {
if (event.candidate) {
@@ -107,7 +84,7 @@ export function useWebRTCTrackManager(
console.error('[TrackManager] PeerConnection 不可用');
return null;
}
try {
return pc.addTrack(track, stream);
} catch (error) {
@@ -123,7 +100,7 @@ export function useWebRTCTrackManager(
console.error('[TrackManager] PeerConnection 不可用');
return;
}
try {
pc.removeTrack(sender);
} catch (error) {
@@ -142,17 +119,17 @@ export function useWebRTCTrackManager(
console.log('[TrackManager] WebSocket未连接等待连接建立...');
return;
}
// 延迟设置等待PeerConnection准备就绪
let retryCount = 0;
const maxRetries = 50; // 增加重试次数到50次即5秒
const checkAndSetTrackHandler = () => {
const currentPc = pcRef.current;
if (currentPc) {
console.log('[TrackManager] ✅ PeerConnection 已准备就绪设置onTrack处理器');
currentPc.ontrack = handler;
// 如果已经有远程轨道,立即触发处理
const receivers = currentPc.getReceivers();
console.log(`[TrackManager] 📡 当前有 ${receivers.length} 个接收器`);
@@ -177,10 +154,10 @@ export function useWebRTCTrackManager(
checkAndSetTrackHandler();
return;
}
console.log('[TrackManager] ✅ 立即设置onTrack处理器');
pc.ontrack = handler;
// 检查是否已有轨道
const receivers = pc.getReceivers();
console.log(`[TrackManager] 📡 当前有 ${receivers.length} 个接收器`);
@@ -197,7 +174,7 @@ export function useWebRTCTrackManager(
console.error('[TrackManager] PeerConnection 或 WebSocket 不可用');
return false;
}
try {
await createOffer(pc, ws);
return true;

View File

@@ -0,0 +1,2 @@
// WebSocket 连接相关导出
export { useWebSocketConnection } from './useWebSocketConnection';

View File

@@ -0,0 +1,298 @@
import { useCallback, useEffect, useRef } from 'react';
import { WebConnectState } from '../state/webConnectStore';
import { ConnectType, DataHandler, IWebConnection, IWebMessage, MessageHandler, Role } from '../types';
/**
* WebSocket 连接管理器
* 实现 IWebConnection 接口,提供基于 WebSocket 的数据传输
*/
export function useWebSocketConnection(): IWebConnection {
const wsRef = useRef<WebSocket | null>(null);
const currentRoomRef = useRef<{ code: string; role: Role } | null>(null);
// 事件处理器存储
const messageHandlers = useRef<Map<string, MessageHandler>>(new Map());
const dataHandlers = useRef<Map<string, DataHandler>>(new Map());
// 连接状态
const connectionState = useRef<WebConnectState>({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
isPeerConnected: false,
isDataChannelConnected: false,
isMediaStreamConnected: false,
currentConnectType: 'websocket',
state: 'closed',
error: null,
canRetry: false,
currentRoom: null
});
// 更新连接状态
const updateState = useCallback((updates: Partial<WebConnectState>) => {
connectionState.current = {
...connectionState.current,
...updates
};
}, []);
// 连接到房间
const connect = useCallback(async (roomCode: string, role: Role) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
console.log('[WebSocket] 已存在连接,先断开');
disconnect();
}
updateState({ isConnecting: true, error: null, canRetry: false });
currentRoomRef.current = { code: roomCode, role };
try {
// 构建 WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const wsUrl = `${protocol}//${host}/api/ws/${roomCode}?role=${role}`;
console.log('[WebSocket] 连接到:', wsUrl);
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
// 连接成功
ws.onopen = () => {
console.log('[WebSocket] 连接成功');
updateState({
isConnected: true,
isConnecting: false,
isWebSocketConnected: true,
error: null,
canRetry: false
});
};
// 接收消息
ws.onmessage = (event) => {
handleMessage(event);
};
// 连接错误
ws.onerror = (error) => {
console.error('[WebSocket] 连接错误:', error);
updateState({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
error: 'WebSocket 连接失败',
canRetry: true
});
};
// 连接关闭
ws.onclose = (event) => {
console.log('[WebSocket] 连接关闭:', event.code, event.reason);
updateState({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
error: event.wasClean ? null : 'WebSocket 连接意外断开',
canRetry: !event.wasClean
});
};
} catch (error) {
console.error('[WebSocket] 连接异常:', error);
updateState({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
error: '无法建立 WebSocket 连接',
canRetry: true
});
}
}, [updateState]);
// 处理收到的消息
const handleMessage = useCallback((event: MessageEvent) => {
try {
if (typeof event.data === 'string') {
// JSON 消息
const message = JSON.parse(event.data) as IWebMessage;
console.log('[WebSocket] 收到消息:', message.type, message.channel || 'default');
// 根据通道分发消息
if (message.channel) {
const handler = messageHandlers.current.get(message.channel);
if (handler) {
handler(message);
}
} else {
// 广播给所有处理器
messageHandlers.current.forEach(handler => handler(message));
}
} else if (event.data instanceof ArrayBuffer) {
// 二进制数据
console.log('[WebSocket] 收到二进制数据:', event.data.byteLength, 'bytes');
// 优先发给文件传输处理器
const fileHandler = dataHandlers.current.get('file-transfer');
if (fileHandler) {
fileHandler(event.data);
} else {
// 发给第一个处理器
const firstHandler = dataHandlers.current.values().next().value;
if (firstHandler) {
firstHandler(event.data);
}
}
}
} catch (error) {
console.error('[WebSocket] 处理消息失败:', error);
}
}, []);
// 断开连接
const disconnect = useCallback(() => {
if (wsRef.current) {
console.log('[WebSocket] 主动断开连接');
wsRef.current.close(1000, '用户主动断开');
wsRef.current = null;
}
currentRoomRef.current = null;
updateState({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
error: null,
canRetry: false
});
}, [updateState]);
// 重试连接
const retry = useCallback(async () => {
if (currentRoomRef.current) {
console.log('[WebSocket] 重试连接');
await connect(currentRoomRef.current.code, currentRoomRef.current.role);
}
}, [connect]);
// 发送消息
const sendMessage = useCallback((message: IWebMessage, channel?: string) => {
const ws = wsRef.current;
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.error('[WebSocket] 连接未就绪,无法发送消息');
return false;
}
try {
const messageWithChannel = channel ? { ...message, channel } : message;
ws.send(JSON.stringify(messageWithChannel));
console.log('[WebSocket] 发送消息:', message.type, channel || 'default');
return true;
} catch (error) {
console.error('[WebSocket] 发送消息失败:', error);
return false;
}
}, []);
// 发送二进制数据
const sendData = useCallback((data: ArrayBuffer) => {
const ws = wsRef.current;
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.error('[WebSocket] 连接未就绪,无法发送数据');
return false;
}
try {
ws.send(data);
console.log('[WebSocket] 发送二进制数据:', data.byteLength, 'bytes');
return true;
} catch (error) {
console.error('[WebSocket] 发送数据失败:', error);
return false;
}
}, []);
// 注册消息处理器
const registerMessageHandler = useCallback((channel: string, handler: MessageHandler) => {
console.log('[WebSocket] 注册消息处理器:', channel);
messageHandlers.current.set(channel, handler);
return () => {
console.log('[WebSocket] 取消注册消息处理器:', channel);
messageHandlers.current.delete(channel);
};
}, []);
// 注册数据处理器
const registerDataHandler = useCallback((channel: string, handler: DataHandler) => {
console.log('[WebSocket] 注册数据处理器:', channel);
dataHandlers.current.set(channel, handler);
return () => {
console.log('[WebSocket] 取消注册数据处理器:', channel);
dataHandlers.current.delete(channel);
};
}, []);
// 获取连接状态
const getConnectState = useCallback((): WebConnectState => {
return { ...connectionState.current };
}, []);
// 检查是否连接到指定房间
const isConnectedToRoom = useCallback((roomCode: string, role: Role) => {
return currentRoomRef.current?.code === roomCode &&
currentRoomRef.current?.role === role &&
connectionState.current.isConnected;
}, []);
// 媒体轨道方法WebSocket 不支持,返回 null
const addTrack = useCallback(() => {
console.warn('[WebSocket] WebSocket 不支持媒体轨道');
return null;
}, []);
const removeTrack = useCallback(() => {
console.warn('[WebSocket] WebSocket 不支持媒体轨道');
}, []);
const onTrack = useCallback(() => {
console.warn('[WebSocket] WebSocket 不支持媒体轨道');
}, []);
const getPeerConnection = useCallback(() => {
console.warn('[WebSocket] WebSocket 不支持 PeerConnection');
return null;
}, []);
const createOfferNow = useCallback(async () => {
console.warn('[WebSocket] WebSocket 不支持创建 Offer');
return false;
}, []);
// 清理连接
useEffect(() => {
return () => {
disconnect();
};
}, [disconnect]);
return {
connectType: 'websocket' as ConnectType,
connect,
disconnect,
retry,
sendMessage,
sendData,
registerMessageHandler,
registerDataHandler,
getConnectState,
isConnectedToRoom,
currentRoom: currentRoomRef.current,
addTrack,
removeTrack,
onTrack,
getPeerConnection,
createOfferNow,
};
}

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useCallback, useEffect } from 'react';
import { useSharedWebRTCManager } from '../connection/useSharedWebRTCManager';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSharedWebRTCManager } from '../connection/useConnectManager';
interface DesktopShareState {
isSharing: boolean;
@@ -114,13 +114,13 @@ export function useDesktopShareBusiness() {
console.log('[DesktopShare] 🎬 开始设置视频轨道发送...');
// 检查P2P连接状态
if (!webRTC.isPeerConnected) {
if (!webRTC.getConnectState().isPeerConnected) {
console.warn('[DesktopShare] ⚠️ P2P连接尚未完全建立等待连接稳定...');
// 等待连接稳定
await new Promise(resolve => setTimeout(resolve, 1000));
// 再次检查
if (!webRTC.isPeerConnected) {
if (!webRTC.getConnectState().isPeerConnected) {
console.error('[DesktopShare] ❌ P2P连接仍未建立无法开始媒体传输');
throw new Error('P2P连接尚未建立');
}
@@ -274,7 +274,7 @@ export function useDesktopShareBusiness() {
const startSharing = useCallback(async (): Promise<void> => {
try {
// 检查P2P连接状态与switchDesktop保持一致
if (!webRTC.isPeerConnected) {
if (!webRTC.getConnectState().isPeerConnected) {
throw new Error('P2P连接未建立');
}
@@ -320,7 +320,7 @@ export function useDesktopShareBusiness() {
// 切换桌面共享(重新选择屏幕)
const switchDesktop = useCallback(async (): Promise<void> => {
try {
if (!webRTC.isPeerConnected) {
if (!webRTC.getConnectState().isPeerConnected) {
throw new Error('P2P连接未建立');
}
@@ -517,12 +517,12 @@ export function useDesktopShareBusiness() {
remoteStream: state.remoteStream,
error: state.error,
isWaitingForPeer: state.isWaitingForPeer,
isConnected: webRTC.isConnected,
isConnecting: webRTC.isConnecting,
isWebSocketConnected: webRTC.isWebSocketConnected,
isPeerConnected: webRTC.isPeerConnected,
isConnected: webRTC.getConnectState().isConnected,
isConnecting: webRTC.getConnectState().isConnecting,
isWebSocketConnected: webRTC.getConnectState().isWebSocketConnected,
isPeerConnected: webRTC.getConnectState().isPeerConnected,
// 新增表示是否可以开始共享WebSocket已连接且有房间代码
canStartSharing: webRTC.isWebSocketConnected && !!state.connectionCode,
canStartSharing: webRTC.getConnectState().isWebSocketConnected && !!state.connectionCode,
// 方法
createRoom, // 创建房间
@@ -535,7 +535,7 @@ export function useDesktopShareBusiness() {
setRemoteVideoRef,
// WebRTC连接状态
webRTCError: webRTC.error,
webRTCError: webRTC.getConnectState().error,
// 暴露WebRTC连接对象
webRTCConnection: webRTC,

View File

@@ -1,4 +1,4 @@
import { useRef, useCallback, useEffect } from 'react';
import { useCallback, useEffect, useRef } from 'react';
interface FileInfo {
id: string;

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import type { WebRTCConnection } from '../connection/useSharedWebRTCManager';
import { useCallback, useEffect, useRef, useState } from 'react';
import { type IWebConnection } from '../connection/types';
// 文件传输状态
interface FileTransferState {
@@ -115,7 +116,7 @@ function simpleChecksum(data: ArrayBuffer): string {
* 文件传输业务层
* 必须传入共享的 WebRTC 连接
*/
export function useFileTransferBusiness(connection: WebRTCConnection) {
export function useFileTransferBusiness(connection: IWebConnection) {
const [state, setState] = useState<FileTransferState>({
isConnecting: false,
@@ -301,10 +302,10 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
// 检查是否已经接收过这个块,避免重复计数
const alreadyReceived = fileInfo.chunks[chunkIndex] !== undefined;
// 数据有效,保存到缓存
fileInfo.chunks[chunkIndex] = data;
// 只有在首次接收时才增加计数
if (!alreadyReceived) {
fileInfo.receivedChunks++;
@@ -347,6 +348,7 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
}, [updateState, connection]);
const connectionRef = useRef(connection);
useEffect(() => {
connectionRef.current = connection;
}, [connection]);
@@ -366,12 +368,12 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
useEffect(() => {
// 同步连接状态
updateState({
isConnecting: connection.isConnecting,
isConnected: connection.isConnected,
isWebSocketConnected: connection.isWebSocketConnected,
connectionError: connection.error
isConnecting: connection.getConnectState().isConnecting,
isConnected: connection.getConnectState().isConnected,
isWebSocketConnected: connection.getConnectState().isWebSocketConnected,
connectionError: connection.getConnectState().error
});
}, [connection.isConnecting, connection.isConnected, connection.isWebSocketConnected, connection.error, updateState]);
}, [connection.getConnectState, updateState]);
// 连接
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
@@ -388,15 +390,15 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
): Promise<boolean> => {
return new Promise((resolve) => {
// 主要检查数据通道状态,因为数据通道是文件传输的实际通道
const channelState = connection.getChannelState();
if (channelState === 'closed') {
const channelState = connection.getConnectState();
if (channelState.state === 'closed') {
console.warn(`数据通道已关闭,停止发送文件块 ${chunkIndex}`);
resolve(false);
return;
}
// 如果连接暂时断开但数据通道可用,仍然可以尝试发送
if (!connection.isConnected && channelState === 'connecting') {
if (!channelState.isConnected && channelState.state === 'connecting') {
console.warn(`WebRTC 连接暂时断开,但数据通道正在连接,继续尝试发送文件块 ${chunkIndex}`);
}
@@ -445,7 +447,7 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
// 安全发送文件
const sendFileSecure = useCallback(async (file: File, fileId?: string) => {
if (connection.getChannelState() !== 'open') {
if (connection.getConnectState().state !== 'open') {
updateState({ error: '连接未就绪' });
return;
}
@@ -490,14 +492,14 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
while (!success && retryCount <= MAX_RETRIES) {
// 检查数据通道状态,这是文件传输的实际通道
const channelState = connection.getChannelState();
if (channelState === 'closed') {
const channelState = connection.getConnectState();
if (channelState.state === 'closed') {
console.warn(`数据通道已关闭,停止文件传输`);
throw new Error('数据通道已关闭');
}
// 如果连接暂时断开但数据通道可用,仍然可以尝试发送
if (!connection.isConnected && channelState === 'connecting') {
if (!connection.getConnectState().isConnected && channelState.state === 'connecting') {
console.warn(`WebRTC 连接暂时断开,但数据通道正在连接,继续尝试发送文件块 ${chunkIndex}`);
}
@@ -598,8 +600,8 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
// 发送文件列表
const sendFileList = useCallback((fileList: FileInfo[]) => {
// 检查连接状态 - 优先检查数据通道状态,因为 P2P 连接可能已经建立但状态未及时更新
const channelState = connection.getChannelState();
const peerConnected = connection.isPeerConnected;
const channelState = connection.getConnectState();
const peerConnected = channelState.isPeerConnected;
console.log('发送文件列表检查:', {
channelState,
@@ -608,7 +610,7 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
});
// 如果数据通道已打开或者 P2P 已连接,就可以发送文件列表
if (channelState === 'open' || peerConnected) {
if (channelState.state === 'open' || peerConnected) {
console.log('发送文件列表:', fileList);
connection.sendMessage({
@@ -622,7 +624,7 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
// 请求文件
const requestFile = useCallback((fileId: string, fileName: string) => {
if (connection.getChannelState() !== 'open') {
if (connection.getConnectState().state !== 'open') {
console.error('数据通道未准备就绪,无法请求文件');
return;
}

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import type { WebRTCConnection } from '../connection/useSharedWebRTCManager';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useWebConnectStateManager } from '../connection/state/useWebConnectStateManager';
import { Role, type IWebConnection } from '../connection/types';
// 文本传输状态
interface TextTransferState {
@@ -21,7 +22,7 @@ const CHANNEL_NAME = 'text-transfer';
* 文本传输业务层
* 必须传入共享的 WebRTC 连接
*/
export function useTextTransferBusiness(connection: WebRTCConnection) {
export function useTextTransferBusiness(connection: IWebConnection) {
const [state, setState] = useState<TextTransferState>({
isConnecting: false,
isConnected: false,
@@ -31,6 +32,8 @@ export function useTextTransferBusiness(connection: WebRTCConnection) {
isTyping: false
});
const connectState = useWebConnectStateManager(); // 确保状态管理器被初始化
// 回调引用
const textSyncCallbackRef = useRef<TextSyncCallback | null>(null);
const typingCallbackRef = useRef<TypingStatusCallback | null>(null);
@@ -86,15 +89,15 @@ export function useTextTransferBusiness(connection: WebRTCConnection) {
useEffect(() => {
// 同步连接状态
updateState({
isConnecting: connection.isConnecting,
isConnected: connection.isConnected,
isWebSocketConnected: connection.isWebSocketConnected,
connectionError: connection.error
isConnecting: connectState.getState().isConnecting,
isConnected: connectState.getState().isConnected,
isWebSocketConnected: connectState.getState().isWebSocketConnected,
connectionError: connectState.getState().error
});
}, [connection.isConnecting, connection.isConnected, connection.isWebSocketConnected, connection.error, updateState]);
}, [connectState.getState, updateState]);
// 连接
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
const connect = useCallback((roomCode: string, role: Role) => {
return connection.connect(roomCode, role);
}, [connection]);
@@ -105,7 +108,7 @@ export function useTextTransferBusiness(connection: WebRTCConnection) {
// 发送实时文本同步 (替代原来的 sendMessage)
const sendTextSync = useCallback((text: string) => {
if (!connection || !connection.isPeerConnected) return;
if (!connectState.getState().isConnected || !connection.getConnectState().isPeerConnected) return;
const message = {
type: 'text-sync',
@@ -116,11 +119,11 @@ export function useTextTransferBusiness(connection: WebRTCConnection) {
if (success) {
console.log('发送实时文本同步:', text.length, '字符');
}
}, [connection]);
}, [connectState.getState]);
// 发送打字状态
const sendTypingStatus = useCallback((isTyping: boolean) => {
if (!connection || !connection.isPeerConnected) return;
if (!connection || !connection.getConnectState().isPeerConnected) return;
const message = {
type: 'text-typing',
@@ -155,10 +158,10 @@ export function useTextTransferBusiness(connection: WebRTCConnection) {
return {
// 状态 - 直接从 connection 获取
isConnecting: connection.isConnecting,
isConnected: connection.isConnected,
isWebSocketConnected: connection.isWebSocketConnected,
connectionError: connection.error,
isConnecting: connection.getConnectState().isConnecting,
isConnected: connection.getConnectState().isConnected,
isWebSocketConnected: connection.getConnectState().isWebSocketConnected,
connectionError: connection.getConnectState().error,
currentText: state.currentText,
isTyping: state.isTyping,

View File

@@ -1,5 +1,6 @@
// UI状态管理相关的hooks
export { useURLHandler } from './useURLHandler';
export { useWebRTCStore } from './webRTCStore';
export { useWebRTCStore } from '../connection/state/webConnectStore';
export { useTabNavigation } from './useTabNavigation';
export type { TabType } from './useTabNavigation';
export { useURLHandler } from './useURLHandler';

View File

@@ -1,9 +1,9 @@
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'next/navigation';
import { useURLHandler, FeatureType } from './useURLHandler';
import { useWebRTCStore } from './webRTCStore';
import { useCallback, useEffect, useState } from 'react';
import { useSharedWebRTCManager } from '../connection';
import { useWebRTCStore } from '../connection/state/webConnectStore';
import { useConfirmDialog } from './useConfirmDialog';
import { useSharedWebRTCManager } from '../connection/useSharedWebRTCManager';
import { FeatureType, useURLHandler } from './useURLHandler';
// Tab类型定义包括非WebRTC功能
export type TabType = 'webrtc' | 'message' | 'desktop' | 'wechat' | 'settings';

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { useToast } from '@/components/ui/toast-simple';
import { useWebRTCStore } from './webRTCStore';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useWebRTCStore } from '../connection/state/webConnectStore';
import { useConfirmDialog } from './useConfirmDialog';
// 支持的功能类型