mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-04 03:25:03 +08:00
feat:降级
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = () => {
|
||||
// 状态管理
|
||||
|
||||
153
chuan-next/src/components/examples/WebSocketExample.tsx
Normal file
153
chuan-next/src/components/examples/WebSocketExample.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
? "在这里编辑文字内容... 💡 支持实时同步编辑,对方可以看到你的修改 💡 可以直接粘贴图片 (Ctrl+V)"
|
||||
: "等待对方加入P2P网络... 📡 建立连接后即可开始输入文字"
|
||||
}
|
||||
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" />
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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), // 完全重置到初始状态
|
||||
}));
|
||||
123
chuan-next/src/hooks/connection/types.ts
Normal file
123
chuan-next/src/hooks/connection/types.ts
Normal 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;
|
||||
|
||||
}
|
||||
249
chuan-next/src/hooks/connection/useConnectManager.ts
Normal file
249
chuan-next/src/hooks/connection/useConnectManager.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
// 媒体轨道方法
|
||||
@@ -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);
|
||||
|
||||
// 如果正在连接中,避免重复连接
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
2
chuan-next/src/hooks/connection/ws/index.ts
Normal file
2
chuan-next/src/hooks/connection/ws/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// WebSocket 连接相关导出
|
||||
export { useWebSocketConnection } from './useWebSocketConnection';
|
||||
298
chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts
Normal file
298
chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
// 支持的功能类型
|
||||
|
||||
Reference in New Issue
Block a user