mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-04 03:25:03 +08:00
feat:webrtc支持检测|房间检测|UI状态优化
This commit is contained in:
@@ -3,17 +3,30 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Upload, MessageSquare, Monitor, TestTube } from 'lucide-react';
|
||||
import { Upload, MessageSquare, Monitor, Users } from 'lucide-react';
|
||||
import Hero from '@/components/Hero';
|
||||
import { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer';
|
||||
import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer';
|
||||
import DesktopShare from '@/components/DesktopShare';
|
||||
import WeChatGroup from '@/components/WeChatGroup';
|
||||
import { WebRTCUnsupportedModal } from '@/components/WebRTCUnsupportedModal';
|
||||
import { useWebRTCSupport } from '@/hooks/useWebRTCSupport';
|
||||
|
||||
export default function HomePage() {
|
||||
const searchParams = useSearchParams();
|
||||
const [activeTab, setActiveTab] = useState('webrtc');
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
|
||||
// WebRTC 支持检测
|
||||
const {
|
||||
webrtcSupport,
|
||||
isSupported,
|
||||
isChecked,
|
||||
showUnsupportedModal,
|
||||
closeUnsupportedModal,
|
||||
showUnsupportedModalManually,
|
||||
} = useWebRTCSupport();
|
||||
|
||||
// 根据URL参数设置初始标签(仅首次加载时)
|
||||
useEffect(() => {
|
||||
if (!hasInitialized) {
|
||||
@@ -50,56 +63,123 @@ export default function HomePage() {
|
||||
<Hero />
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
{/* Tabs Navigation - 横向布局 */}
|
||||
<div className="mb-6">
|
||||
<TabsList className="grid w-full grid-cols-3 max-w-xl mx-auto h-auto bg-white/90 backdrop-blur-sm shadow-lg rounded-xl p-2 border border-slate-200">
|
||||
<TabsTrigger
|
||||
value="webrtc"
|
||||
className="flex items-center justify-center space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-slate-50 data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:shadow-md data-[state=active]:hover:bg-blue-600"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">文件传输</span>
|
||||
<span className="sm:hidden">文件</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="message"
|
||||
className="flex items-center justify-center space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-slate-50 data-[state=active]:bg-emerald-500 data-[state=active]:text-white data-[state=active]:shadow-md data-[state=active]:hover:bg-emerald-600"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">文本消息</span>
|
||||
<span className="sm:hidden">消息</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="desktop"
|
||||
className="flex items-center justify-center space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-slate-50 data-[state=active]:bg-purple-500 data-[state=active]:text-white data-[state=active]:shadow-md data-[state=active]:hover:bg-purple-600"
|
||||
>
|
||||
<Monitor className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">共享桌面</span>
|
||||
<span className="sm:hidden">桌面</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{/* WebRTC 支持检测加载状态 */}
|
||||
{!isChecked && (
|
||||
<div className="max-w-4xl mx-auto text-center py-8">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-48 mx-auto mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-32 mx-auto"></div>
|
||||
</div>
|
||||
<p className="mt-4 text-gray-600">正在检测浏览器支持...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content */}
|
||||
<div>
|
||||
<TabsContent value="webrtc" className="mt-0 animate-fade-in-up">
|
||||
<WebRTCFileTransfer />
|
||||
</TabsContent>
|
||||
{/* 主要内容 - 只有在检测完成后才显示 */}
|
||||
{isChecked && (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* WebRTC 不支持时的警告横幅 */}
|
||||
{!isSupported && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-red-700 font-medium">
|
||||
当前浏览器不支持 WebRTC,功能可能无法正常使用
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={showUnsupportedModalManually}
|
||||
className="text-red-600 hover:text-red-800 text-sm underline"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="message" className="mt-0 animate-fade-in-up">
|
||||
<WebRTCTextImageTransfer />
|
||||
</TabsContent>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
{/* Tabs Navigation - 横向布局 */}
|
||||
<div className="mb-6">
|
||||
<TabsList className="grid w-full grid-cols-4 max-w-2xl mx-auto h-auto bg-white/90 backdrop-blur-sm shadow-lg rounded-xl p-2 border border-slate-200">
|
||||
<TabsTrigger
|
||||
value="webrtc"
|
||||
className="flex items-center justify-center space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-slate-50 data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:shadow-md data-[state=active]:hover:bg-blue-600"
|
||||
disabled={!isSupported}
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">文件传输</span>
|
||||
<span className="sm:hidden">文件</span>
|
||||
{!isSupported && <span className="text-xs opacity-60">*</span>}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="message"
|
||||
className="flex items-center justify-center space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-slate-50 data-[state=active]:bg-emerald-500 data-[state=active]:text-white data-[state=active]:shadow-md data-[state=active]:hover:bg-emerald-600"
|
||||
disabled={!isSupported}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">文本消息</span>
|
||||
<span className="sm:hidden">消息</span>
|
||||
{!isSupported && <span className="text-xs opacity-60">*</span>}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="desktop"
|
||||
className="flex items-center justify-center space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-slate-50 data-[state=active]:bg-purple-500 data-[state=active]:text-white data-[state=active]:shadow-md data-[state=active]:hover:bg-purple-600"
|
||||
disabled={!isSupported}
|
||||
>
|
||||
<Monitor className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">共享桌面</span>
|
||||
<span className="sm:hidden">桌面</span>
|
||||
{!isSupported && <span className="text-xs opacity-60">*</span>}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="wechat"
|
||||
className="flex items-center justify-center space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 hover:bg-slate-50 data-[state=active]:bg-green-500 data-[state=active]:text-white data-[state=active]:shadow-md data-[state=active]:hover:bg-green-600"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">微信群</span>
|
||||
<span className="sm:hidden">微信</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* WebRTC 不支持时的提示 */}
|
||||
{!isSupported && (
|
||||
<p className="text-center text-xs text-gray-500 mt-2">
|
||||
* 需要 WebRTC 支持才能使用
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TabsContent value="desktop" className="mt-0 animate-fade-in-up">
|
||||
<DesktopShare />
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
{/* Tab Content */}
|
||||
<div>
|
||||
<TabsContent value="webrtc" className="mt-0 animate-fade-in-up">
|
||||
<WebRTCFileTransfer />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="message" className="mt-0 animate-fade-in-up">
|
||||
<WebRTCTextImageTransfer />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="desktop" className="mt-0 animate-fade-in-up">
|
||||
<DesktopShare />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="wechat" className="mt-0 animate-fade-in-up">
|
||||
<WeChatGroup />
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* WebRTC 不支持提示模态框 */}
|
||||
{webrtcSupport && (
|
||||
<WebRTCUnsupportedModal
|
||||
isOpen={showUnsupportedModal}
|
||||
onClose={closeUnsupportedModal}
|
||||
webrtcSupport={webrtcSupport}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
console.log('API Route: Creating room, proxying to:', `${GO_BACKEND_URL}/api/create-room`);
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// 不再需要解析和转发请求体,因为后端会忽略它们
|
||||
const response = await fetch(`${GO_BACKEND_URL}/api/create-room`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
// 发送空body即可
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
68
chuan-next/src/components/WeChatGroup.tsx
Normal file
68
chuan-next/src/components/WeChatGroup.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Users } from 'lucide-react';
|
||||
|
||||
export default function WeChatGroup() {
|
||||
return (
|
||||
<div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg p-6 sm:p-8 animate-fade-in-up">
|
||||
<div className="text-center">
|
||||
{/* 标题 */}
|
||||
<div className="mb-6">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-emerald-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<Users className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">加入微信交流群</h2>
|
||||
<p className="text-slate-600 text-lg">
|
||||
佬们有意见/建议/bug反馈或者奇思妙想想来交流,可以扫码加入
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 二维码区域 */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="bg-white rounded-2xl p-6 shadow-lg border border-slate-200">
|
||||
{/* 微信群二维码 - 请将此区域替换为实际的二维码图片 */}
|
||||
<div className="relative">
|
||||
<img
|
||||
src="https://cdn-img.luxika.cc//i/2025/08/25/68abd75c363a6.png"
|
||||
alt="微信群二维码"
|
||||
className="w-64 h-64 rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 说明文字 */}
|
||||
<div className="bg-green-50 rounded-xl p-6 border border-green-200">
|
||||
<div className="text-sm text-green-700 space-y-2">
|
||||
<p className="text-base font-semibold text-green-800 mb-3">🎉 欢迎加入我们的交流群!</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-left">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>💬</span>
|
||||
<span>分享使用心得和建议</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>🐛</span>
|
||||
<span>反馈问题和bug</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>💡</span>
|
||||
<span>提出新功能想法</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>🤝</span>
|
||||
<span>与其他用户交流技术</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 额外信息 */}
|
||||
<div className="mt-4 text-xs text-slate-500">
|
||||
<p>群内禁止广告和无关内容,专注技术交流</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
185
chuan-next/src/components/WebRTCConnectionStatus.tsx
Normal file
185
chuan-next/src/components/WebRTCConnectionStatus.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React from 'react';
|
||||
import { AlertCircle, Wifi, WifiOff, Loader2, RotateCcw } from 'lucide-react';
|
||||
import { WebRTCConnection } from '@/hooks/webrtc/useSharedWebRTCManager';
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCConnection;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebRTC连接状态显示组件
|
||||
* 显示详细的连接状态、错误信息和重试按钮
|
||||
*/
|
||||
export function WebRTCConnectionStatus({ webrtc, className = '' }: Props) {
|
||||
const {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
isWebSocketConnected,
|
||||
isPeerConnected,
|
||||
error,
|
||||
canRetry,
|
||||
retry
|
||||
} = webrtc;
|
||||
|
||||
// 状态图标
|
||||
const getStatusIcon = () => {
|
||||
if (isConnecting) {
|
||||
return <Loader2 className="h-4 w-4 animate-spin text-blue-500" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// 区分信息提示和错误
|
||||
if (error.includes('对方已离开房间') || error.includes('已离开房间')) {
|
||||
return <WifiOff className="h-4 w-4 text-yellow-500" />;
|
||||
}
|
||||
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
||||
}
|
||||
|
||||
if (isPeerConnected) {
|
||||
return <Wifi className="h-4 w-4 text-green-500" />;
|
||||
}
|
||||
|
||||
if (isWebSocketConnected) {
|
||||
return <Wifi className="h-4 w-4 text-yellow-500" />;
|
||||
}
|
||||
|
||||
return <WifiOff className="h-4 w-4 text-gray-400" />;
|
||||
};
|
||||
|
||||
// 状态文本
|
||||
const getStatusText = () => {
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
return '正在连接...';
|
||||
}
|
||||
|
||||
if (isPeerConnected) {
|
||||
return 'P2P连接已建立';
|
||||
}
|
||||
|
||||
if (isWebSocketConnected) {
|
||||
return '信令服务器已连接';
|
||||
}
|
||||
|
||||
return '未连接';
|
||||
};
|
||||
|
||||
// 状态颜色
|
||||
const getStatusColor = () => {
|
||||
if (error) {
|
||||
// 区分信息提示和错误
|
||||
if (error.includes('对方已离开房间') || error.includes('已离开房间')) {
|
||||
return 'text-yellow-600';
|
||||
}
|
||||
return 'text-red-600';
|
||||
}
|
||||
if (isConnecting) return 'text-blue-600';
|
||||
if (isPeerConnected) return 'text-green-600';
|
||||
if (isWebSocketConnected) return 'text-yellow-600';
|
||||
return 'text-gray-600';
|
||||
};
|
||||
|
||||
const handleRetry = async () => {
|
||||
try {
|
||||
await retry();
|
||||
} catch (error) {
|
||||
console.error('重试连接失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center justify-between p-3 bg-white border rounded-lg ${className}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon()}
|
||||
<span className={`text-sm font-medium ${getStatusColor()}`}>
|
||||
{getStatusText()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 连接详细状态指示器 */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* WebSocket状态 */}
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
isWebSocketConnected ? 'bg-green-400' : 'bg-gray-300'
|
||||
}`}
|
||||
title={isWebSocketConnected ? 'WebSocket已连接' : 'WebSocket未连接'}
|
||||
/>
|
||||
|
||||
{/* P2P状态 */}
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
isPeerConnected ? 'bg-green-400' : 'bg-gray-300'
|
||||
}`}
|
||||
title={isPeerConnected ? 'P2P连接已建立' : 'P2P连接未建立'}
|
||||
/>
|
||||
|
||||
{/* 重试按钮 */}
|
||||
{canRetry && (
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
disabled={isConnecting}
|
||||
className="ml-2 p-1 text-gray-500 hover:text-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="重试连接"
|
||||
>
|
||||
<RotateCcw className={`h-3 w-3 ${isConnecting ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的连接状态指示器(用于空间受限的地方)
|
||||
*/
|
||||
export function WebRTCStatusIndicator({ webrtc, className = '' }: Props) {
|
||||
const { isPeerConnected, isConnecting, error } = webrtc;
|
||||
|
||||
if (error) {
|
||||
// 区分信息提示和错误
|
||||
if (error.includes('对方已离开房间') || error.includes('已离开房间')) {
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
<div className="w-2 h-2 bg-yellow-400 rounded-full" />
|
||||
<span className="text-xs text-yellow-600">对方已离开</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
<div className="w-2 h-2 bg-red-400 rounded-full animate-pulse" />
|
||||
<span className="text-xs text-red-600">连接错误</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
<div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse" />
|
||||
<span className="text-xs text-blue-600">连接中</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPeerConnected) {
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full" />
|
||||
<span className="text-xs text-green-600">已连接</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
<div className="w-2 h-2 bg-gray-300 rounded-full" />
|
||||
<span className="text-xs text-gray-600">未连接</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -309,21 +309,14 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
console.log('=== 创建房间 ===');
|
||||
console.log('选中文件数:', selectedFiles.length);
|
||||
|
||||
// 创建后端房间
|
||||
// 创建后端房间 - 简化版本,不发送无用的文件信息
|
||||
const response = await fetch('/api/create-room', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'file',
|
||||
files: selectedFiles.map(file => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: file.lastModified
|
||||
}))
|
||||
}),
|
||||
// 不再发送文件列表,因为后端不使用这些信息
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
186
chuan-next/src/components/WebRTCUnsupportedModal.tsx
Normal file
186
chuan-next/src/components/WebRTCUnsupportedModal.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, Download, X, Chrome, Monitor } from 'lucide-react';
|
||||
import { WebRTCSupport, getBrowserInfo, getRecommendedBrowsers } from '@/lib/webrtc-support';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
webrtcSupport: WebRTCSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebRTC 不支持提示模态框
|
||||
*/
|
||||
export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props) {
|
||||
const browserInfo = getBrowserInfo();
|
||||
const recommendedBrowsers = getRecommendedBrowsers();
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleBrowserDownload = (url: string) => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle className="h-6 w-6 text-red-500" />
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
浏览器不支持 WebRTC
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 当前浏览器信息 */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<h3 className="font-medium text-red-800 mb-2">当前浏览器状态</h3>
|
||||
<div className="space-y-2 text-sm text-red-700">
|
||||
<div>
|
||||
<strong>浏览器:</strong> {browserInfo.name} {browserInfo.version}
|
||||
</div>
|
||||
<div>
|
||||
<strong>WebRTC 支持:</strong>
|
||||
<span className="ml-1 px-2 py-1 bg-red-100 text-red-800 rounded text-xs">
|
||||
不支持
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 缺失的功能 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-gray-900">缺失的功能:</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{webrtcSupport.missing.map((feature, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<div className="w-2 h-2 bg-red-400 rounded-full"></div>
|
||||
{feature}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 功能说明 */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 className="font-medium text-blue-800 mb-2">为什么需要 WebRTC?</h3>
|
||||
<div className="space-y-2 text-sm text-blue-700">
|
||||
<div className="flex items-start gap-2">
|
||||
<Monitor className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<strong>屏幕共享:</strong> 实时共享您的桌面屏幕
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Download className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<strong>文件传输:</strong> 点对点直接传输文件,快速且安全
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Chrome className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<strong>文本传输:</strong> 实时文本和图像传输
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 浏览器推荐 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-gray-900">推荐使用以下浏览器:</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{recommendedBrowsers.map((browser, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-lg p-4 hover:border-blue-300 transition-colors cursor-pointer"
|
||||
onClick={() => handleBrowserDownload(browser.downloadUrl)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">{browser.name}</h4>
|
||||
<p className="text-sm text-gray-600">版本 {browser.minVersion}</p>
|
||||
</div>
|
||||
<Download className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 浏览器特定建议 */}
|
||||
{browserInfo.recommendations && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<h3 className="font-medium text-yellow-800 mb-2">建议</h3>
|
||||
<ul className="space-y-1 text-sm text-yellow-700">
|
||||
{browserInfo.recommendations.map((recommendation, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-yellow-400 rounded-full mt-2 flex-shrink-0"></div>
|
||||
{recommendation}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 技术详情(可折叠) */}
|
||||
<details className="border border-gray-200 rounded-lg">
|
||||
<summary className="p-3 cursor-pointer font-medium text-gray-900 hover:bg-gray-50">
|
||||
技术详情
|
||||
</summary>
|
||||
<div className="p-3 border-t border-gray-200 space-y-2 text-sm">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<strong>RTCPeerConnection:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
webrtcSupport.details.rtcPeerConnection
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{webrtcSupport.details.rtcPeerConnection ? '支持' : '不支持'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>DataChannel:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
webrtcSupport.details.dataChannel
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{webrtcSupport.details.dataChannel ? '支持' : '不支持'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="flex justify-end gap-3 p-6 border-t border-gray-200">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
我知道了
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleBrowserDownload('https://www.google.com/chrome/')}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
||||
>
|
||||
下载 Chrome 浏览器
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ interface WebRTCDesktopReceiverProps {
|
||||
export default function WebRTCDesktopReceiver({ className, initialCode, onConnectionChange }: WebRTCDesktopReceiverProps) {
|
||||
const [inputCode, setInputCode] = useState(initialCode || '');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isJoiningRoom, setIsJoiningRoom] = useState(false); // 添加加入房间状态
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
const hasTriedAutoJoin = React.useRef(false); // 添加 ref 来跟踪是否已尝试自动加入
|
||||
const { showToast } = useToast();
|
||||
@@ -34,27 +35,82 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec
|
||||
|
||||
// 加入观看
|
||||
const handleJoinViewing = useCallback(async () => {
|
||||
if (!inputCode.trim()) {
|
||||
showToast('请输入房间代码', 'error');
|
||||
const trimmedCode = inputCode.trim();
|
||||
|
||||
// 检查房间代码格式
|
||||
if (!trimmedCode || trimmedCode.length !== 6) {
|
||||
showToast('请输入正确的6位房间代码', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复调用 - 检查是否已经在连接或已连接
|
||||
if (desktopShare.isConnecting || desktopShare.isViewing || isJoiningRoom) {
|
||||
console.log('已在连接中或已连接,跳过重复的房间状态检查');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsJoiningRoom(true);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log('[DesktopShareReceiver] 用户加入观看房间:', inputCode);
|
||||
console.log('[DesktopShareReceiver] 开始验证房间状态...');
|
||||
|
||||
await desktopShare.joinSharing(inputCode.trim().toUpperCase());
|
||||
// 先检查房间状态
|
||||
const response = await fetch(`/api/room-info?code=${trimmedCode}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: 无法检查房间状态`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
let errorMessage = result.message || '房间不存在或已过期';
|
||||
if (result.message?.includes('expired')) {
|
||||
errorMessage = '房间已过期,请联系发送方重新创建';
|
||||
} else if (result.message?.includes('not found')) {
|
||||
errorMessage = '房间不存在,请检查房间代码是否正确';
|
||||
}
|
||||
showToast(errorMessage, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查发送方是否在线
|
||||
if (!result.sender_online) {
|
||||
showToast('发送方不在线,请确认房间代码是否正确或联系发送方', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[DesktopShareReceiver] 房间状态检查通过,开始连接...');
|
||||
setIsLoading(true);
|
||||
|
||||
await desktopShare.joinSharing(trimmedCode.toUpperCase());
|
||||
console.log('[DesktopShareReceiver] 加入观看成功');
|
||||
|
||||
showToast('已加入桌面共享', 'success');
|
||||
} catch (error) {
|
||||
console.error('[DesktopShareReceiver] 加入观看失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '加入观看失败';
|
||||
|
||||
let errorMessage = '加入观看失败';
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
errorMessage = '网络连接失败,请检查网络状况';
|
||||
} else if (error.message.includes('timeout')) {
|
||||
errorMessage = '请求超时,请重试';
|
||||
} else if (error.message.includes('HTTP 404')) {
|
||||
errorMessage = '房间不存在,请检查房间代码';
|
||||
} else if (error.message.includes('HTTP 500')) {
|
||||
errorMessage = '服务器错误,请稍后重试';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
showToast(errorMessage, 'error');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsJoiningRoom(false); // 重置加入房间状态
|
||||
}
|
||||
}, [desktopShare, inputCode, showToast]);
|
||||
}, [desktopShare, inputCode, isJoiningRoom, showToast]);
|
||||
|
||||
// 停止观看
|
||||
const handleStopViewing = useCallback(async () => {
|
||||
@@ -77,38 +133,94 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec
|
||||
initialCode,
|
||||
isViewing: desktopShare.isViewing,
|
||||
isConnecting: desktopShare.isConnecting,
|
||||
isJoiningRoom,
|
||||
hasTriedAutoJoin: hasTriedAutoJoin.current
|
||||
});
|
||||
|
||||
const autoJoin = async () => {
|
||||
if (initialCode && !desktopShare.isViewing && !desktopShare.isConnecting && !hasTriedAutoJoin.current) {
|
||||
if (initialCode && !desktopShare.isViewing && !desktopShare.isConnecting && !isJoiningRoom && !hasTriedAutoJoin.current) {
|
||||
hasTriedAutoJoin.current = true;
|
||||
console.log('[WebRTCDesktopReceiver] 检测到初始代码,自动加入观看:', initialCode);
|
||||
const trimmedCode = initialCode.trim();
|
||||
|
||||
// 检查房间代码格式
|
||||
if (!trimmedCode || trimmedCode.length !== 6) {
|
||||
showToast('房间代码格式不正确', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsJoiningRoom(true);
|
||||
console.log('[WebRTCDesktopReceiver] 检测到初始代码,开始验证并自动加入:', trimmedCode);
|
||||
|
||||
try {
|
||||
// 先检查房间状态
|
||||
console.log('[WebRTCDesktopReceiver] 验证房间状态...');
|
||||
const response = await fetch(`/api/room-info?code=${trimmedCode}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: 无法检查房间状态`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
let errorMessage = result.message || '房间不存在或已过期';
|
||||
if (result.message?.includes('expired')) {
|
||||
errorMessage = '房间已过期,请联系发送方重新创建';
|
||||
} else if (result.message?.includes('not found')) {
|
||||
errorMessage = '房间不存在,请检查房间代码是否正确';
|
||||
}
|
||||
showToast(errorMessage, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查发送方是否在线
|
||||
if (!result.sender_online) {
|
||||
showToast('发送方不在线,请确认房间代码是否正确或联系发送方', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[WebRTCDesktopReceiver] 房间验证通过,开始自动连接...');
|
||||
setIsLoading(true);
|
||||
await desktopShare.joinSharing(initialCode.trim().toUpperCase());
|
||||
|
||||
await desktopShare.joinSharing(trimmedCode.toUpperCase());
|
||||
console.log('[WebRTCDesktopReceiver] 自动加入观看成功');
|
||||
showToast('已加入桌面共享', 'success');
|
||||
} catch (error) {
|
||||
console.error('[WebRTCDesktopReceiver] 自动加入观看失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '加入观看失败';
|
||||
|
||||
let errorMessage = '自动加入观看失败';
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
errorMessage = '网络连接失败,请检查网络状况';
|
||||
} else if (error.message.includes('timeout')) {
|
||||
errorMessage = '请求超时,请重试';
|
||||
} else if (error.message.includes('HTTP 404')) {
|
||||
errorMessage = '房间不存在,请检查房间代码';
|
||||
} else if (error.message.includes('HTTP 500')) {
|
||||
errorMessage = '服务器错误,请稍后重试';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
showToast(errorMessage, 'error');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsJoiningRoom(false);
|
||||
}
|
||||
} else {
|
||||
console.log('[WebRTCDesktopReceiver] 不满足自动加入条件:', {
|
||||
hasInitialCode: !!initialCode,
|
||||
notViewing: !desktopShare.isViewing,
|
||||
notConnecting: !desktopShare.isConnecting,
|
||||
notJoiningRoom: !isJoiningRoom,
|
||||
notTriedBefore: !hasTriedAutoJoin.current
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
autoJoin();
|
||||
}, [initialCode, desktopShare.isViewing, desktopShare.isConnecting]); // 移除了 desktopShare.joinSharing 和 showToast
|
||||
}, [initialCode, desktopShare.isViewing, desktopShare.isConnecting, isJoiningRoom]); // 添加isJoiningRoom依赖
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 sm:space-y-6 ${className || ''}`}>
|
||||
@@ -138,11 +250,11 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={inputCode}
|
||||
onChange={(e) => setInputCode(e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase())}
|
||||
onChange={(e) => setInputCode(e.target.value.replace(/[^123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnpqrstuvwxyz]/g, ''))}
|
||||
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-purple-500 focus:ring-purple-500 bg-white/80 backdrop-blur-sm pb-2 sm:pb-4"
|
||||
maxLength={6}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || isJoiningRoom}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-center text-xs sm:text-sm text-slate-500">
|
||||
@@ -153,10 +265,15 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={inputCode.length !== 6 || isLoading}
|
||||
disabled={inputCode.length !== 6 || isLoading || isJoiningRoom}
|
||||
className="w-full h-10 sm:h-12 bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-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"
|
||||
>
|
||||
{isLoading ? (
|
||||
{isJoiningRoom ? (
|
||||
<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>
|
||||
) : isLoading ? (
|
||||
<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>
|
||||
|
||||
@@ -110,7 +110,7 @@ export function WebRTCFileReceive({
|
||||
}, [pickupCode, onJoinRoom]);
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase();
|
||||
const value = e.target.value.replace(/[^123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnpqrstuvwxyz]/g, '');
|
||||
if (value.length <= 6) {
|
||||
setPickupCode(value);
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={inputCode}
|
||||
onChange={(e) => setInputCode(e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase())}
|
||||
onChange={(e) => setInputCode(e.target.value.replace(/[^123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnpqrstuvwxyz]/g, ''))}
|
||||
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}
|
||||
|
||||
@@ -115,21 +115,14 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
||||
console.log('=== 开始创建房间 ===');
|
||||
const currentText = textInput.trim();
|
||||
|
||||
// 创建后端房间 - 简化版本,不发送无用的文本信息
|
||||
const response = await fetch('/api/create-room', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'message',
|
||||
initialText: currentText || '',
|
||||
hasImages: false,
|
||||
maxFileSize: 5 * 1024 * 1024,
|
||||
settings: {
|
||||
enableRealTimeText: true,
|
||||
enableImageTransfer: true
|
||||
}
|
||||
}),
|
||||
// 不再发送文本内容,因为后端不使用这些信息
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
40
chuan-next/src/hooks/useWebRTCSupport.ts
Normal file
40
chuan-next/src/hooks/useWebRTCSupport.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { detectWebRTCSupport, WebRTCSupport } from '@/lib/webrtc-support';
|
||||
|
||||
/**
|
||||
* WebRTC 支持检测 Hook
|
||||
*/
|
||||
export function useWebRTCSupport() {
|
||||
const [webrtcSupport, setWebrtcSupport] = useState<WebRTCSupport | null>(null);
|
||||
const [showUnsupportedModal, setShowUnsupportedModal] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 页面加载时检测WebRTC支持
|
||||
const support = detectWebRTCSupport();
|
||||
setWebrtcSupport(support);
|
||||
setIsChecked(true);
|
||||
|
||||
// 如果不支持,自动显示模态框
|
||||
if (!support.isSupported) {
|
||||
setShowUnsupportedModal(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const closeUnsupportedModal = () => {
|
||||
setShowUnsupportedModal(false);
|
||||
};
|
||||
|
||||
const showUnsupportedModalManually = () => {
|
||||
setShowUnsupportedModal(true);
|
||||
};
|
||||
|
||||
return {
|
||||
webrtcSupport,
|
||||
isSupported: webrtcSupport?.isSupported ?? false,
|
||||
isChecked,
|
||||
showUnsupportedModal,
|
||||
closeUnsupportedModal,
|
||||
showUnsupportedModalManually,
|
||||
};
|
||||
}
|
||||
@@ -60,16 +60,6 @@ export function useDesktopShareBusiness() {
|
||||
});
|
||||
}, [webRTC, handleRemoteStream]);
|
||||
|
||||
// 生成6位房间代码
|
||||
const generateRoomCode = useCallback(() => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
// 获取桌面共享流
|
||||
const getDesktopStream = useCallback(async (): Promise<MediaStream> => {
|
||||
try {
|
||||
@@ -172,13 +162,32 @@ export function useDesktopShareBusiness() {
|
||||
};
|
||||
}, [webRTC]);
|
||||
|
||||
// 创建房间 - 统一使用后端生成房间码
|
||||
const createRoomFromBackend = useCallback(async (): Promise<string> => {
|
||||
const response = await fetch('/api/create-room', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '创建房间失败');
|
||||
}
|
||||
|
||||
return data.code;
|
||||
}, []);
|
||||
|
||||
// 创建房间(只建立连接,等待对方加入)
|
||||
const createRoom = useCallback(async (): Promise<string> => {
|
||||
try {
|
||||
updateState({ error: null, isWaitingForPeer: false });
|
||||
|
||||
// 生成房间代码
|
||||
const roomCode = generateRoomCode();
|
||||
// 从后端获取房间代码
|
||||
const roomCode = await createRoomFromBackend();
|
||||
console.log('[DesktopShare] 🚀 创建桌面共享房间,代码:', roomCode);
|
||||
|
||||
// 建立WebRTC连接(作为发送方)
|
||||
@@ -199,7 +208,7 @@ export function useDesktopShareBusiness() {
|
||||
updateState({ error: errorMessage, connectionCode: '', isWaitingForPeer: false });
|
||||
throw error;
|
||||
}
|
||||
}, [webRTC, generateRoomCode, updateState]);
|
||||
}, [webRTC, createRoomFromBackend, updateState]);
|
||||
|
||||
// 开始桌面共享(在接收方加入后)
|
||||
const startSharing = useCallback(async (): Promise<void> => {
|
||||
|
||||
@@ -9,6 +9,7 @@ interface WebRTCState {
|
||||
isWebSocketConnected: boolean;
|
||||
isPeerConnected: boolean; // 新增:P2P连接状态
|
||||
error: string | null;
|
||||
canRetry: boolean; // 新增:是否可以重试
|
||||
}
|
||||
|
||||
// 消息类型
|
||||
@@ -30,10 +31,12 @@ export interface WebRTCConnection {
|
||||
isWebSocketConnected: boolean;
|
||||
isPeerConnected: boolean; // 新增:P2P连接状态
|
||||
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;
|
||||
|
||||
@@ -71,6 +74,9 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
|
||||
// 当前连接的房间信息
|
||||
const currentRoom = useRef<{ code: string; role: 'sender' | 'receiver' } | null>(null);
|
||||
|
||||
// 用于跟踪是否是用户主动断开连接
|
||||
const isUserDisconnecting = useRef<boolean>(false);
|
||||
|
||||
// 多通道消息处理器
|
||||
const messageHandlers = useRef<Map<string, MessageHandler>>(new Map());
|
||||
@@ -112,6 +118,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
}
|
||||
|
||||
currentRoom.current = null;
|
||||
isUserDisconnecting.current = false; // 重置主动断开标志
|
||||
}, []);
|
||||
|
||||
// 创建 Offer
|
||||
@@ -153,7 +160,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SharedWebRTC] ❌ 创建 offer 失败:', error);
|
||||
updateState({ error: '创建连接失败', isConnecting: false });
|
||||
updateState({ error: '创建连接失败', isConnecting: false, canRetry: true });
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
@@ -209,6 +216,9 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
currentRoom.current = { code: roomCode, role };
|
||||
webrtcStore.setCurrentRoom({ code: roomCode, role });
|
||||
updateState({ isConnecting: true, error: null });
|
||||
|
||||
// 重置主动断开标志
|
||||
isUserDisconnecting.current = false;
|
||||
|
||||
// 注意:不在这里设置超时,因为WebSocket连接很快,
|
||||
// WebRTC连接的建立是在后续添加轨道时进行的
|
||||
@@ -326,7 +336,27 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
|
||||
case 'error':
|
||||
console.error('[SharedWebRTC] ❌ 信令服务器错误:', message.error);
|
||||
updateState({ error: message.error, isConnecting: false });
|
||||
updateState({ error: message.error, isConnecting: false, canRetry: true });
|
||||
break;
|
||||
|
||||
case 'disconnection':
|
||||
console.log('[SharedWebRTC] 🔌 对方主动断开连接');
|
||||
// 对方断开连接的处理
|
||||
updateState({
|
||||
isPeerConnected: false,
|
||||
isConnected: false, // 添加这个状态
|
||||
error: '对方已离开房间',
|
||||
canRetry: true
|
||||
});
|
||||
// 清理P2P连接但保持WebSocket连接,允许重新连接
|
||||
if (pcRef.current) {
|
||||
pcRef.current.close();
|
||||
pcRef.current = null;
|
||||
}
|
||||
if (dcRef.current) {
|
||||
dcRef.current.close();
|
||||
dcRef.current = null;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -334,20 +364,29 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SharedWebRTC] ❌ 处理信令消息失败:', error);
|
||||
updateState({ error: '信令处理失败: ' + error, isConnecting: false });
|
||||
updateState({ error: '信令处理失败: ' + error, isConnecting: false, canRetry: true });
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[SharedWebRTC] ❌ WebSocket 错误:', error);
|
||||
updateState({ error: 'WebSocket连接失败,请检查服务器是否运行在8080端口', isConnecting: false });
|
||||
updateState({ error: 'WebSocket连接失败', isConnecting: false, canRetry: true });
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('[SharedWebRTC] 🔌 WebSocket 连接已关闭, 代码:', event.code, '原因:', event.reason);
|
||||
updateState({ isWebSocketConnected: false });
|
||||
|
||||
// 检查是否是用户主动断开
|
||||
if (isUserDisconnecting.current) {
|
||||
console.log('[SharedWebRTC] ✅ 用户主动断开,正常关闭');
|
||||
// 用户主动断开时不显示错误消息
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有在非正常关闭且不是用户主动断开时才显示错误
|
||||
if (event.code !== 1000 && event.code !== 1001) { // 非正常关闭
|
||||
updateState({ error: `WebSocket异常关闭 (${event.code}): ${event.reason || '未知原因'}`, isConnecting: false });
|
||||
updateState({ error: `WebSocket异常关闭 (${event.code}): ${event.reason || '连接意外断开'}`, isConnecting: false, canRetry: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -376,7 +415,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
break;
|
||||
case 'failed':
|
||||
console.error('[SharedWebRTC] ❌ ICE连接失败');
|
||||
updateState({ error: 'ICE连接失败,可能是网络防火墙阻止了连接', isConnecting: false });
|
||||
updateState({ error: 'ICE连接失败,可能是网络防火墙阻止了连接', isConnecting: false, canRetry: true });
|
||||
break;
|
||||
case 'disconnected':
|
||||
console.log('[SharedWebRTC] 🔌 ICE连接断开');
|
||||
@@ -396,14 +435,14 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
break;
|
||||
case 'connected':
|
||||
console.log('[SharedWebRTC] 🎉 WebRTC P2P连接已完全建立,可以进行媒体传输');
|
||||
updateState({ isPeerConnected: true, error: null });
|
||||
updateState({ isPeerConnected: true, error: null, canRetry: false });
|
||||
break;
|
||||
case 'failed':
|
||||
// 只有在数据通道也未打开的情况下才认为连接真正失败
|
||||
const currentDc = dcRef.current;
|
||||
if (!currentDc || currentDc.readyState !== 'open') {
|
||||
console.error('[SharedWebRTC] ❌ WebRTC连接失败,数据通道未建立');
|
||||
updateState({ error: 'WebRTC连接失败,请检查网络设置或重试', isPeerConnected: false });
|
||||
updateState({ error: 'WebRTC连接失败,请检查网络设置或重试', isPeerConnected: false, canRetry: true });
|
||||
} else {
|
||||
console.log('[SharedWebRTC] ⚠️ WebRTC连接状态为failed,但数据通道正常,忽略此状态');
|
||||
}
|
||||
@@ -429,14 +468,59 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
console.log('[SharedWebRTC] 数据通道已打开 (发送方)');
|
||||
updateState({ isPeerConnected: true, error: null, isConnecting: false });
|
||||
updateState({ isPeerConnected: true, error: null, isConnecting: false, canRetry: false });
|
||||
};
|
||||
|
||||
dataChannel.onmessage = handleDataChannelMessage;
|
||||
|
||||
dataChannel.onerror = (error) => {
|
||||
console.error('[SharedWebRTC] 数据通道错误:', error);
|
||||
updateState({ error: '数据通道连接失败,可能是网络环境受限', isConnecting: false });
|
||||
|
||||
// 获取更详细的错误信息
|
||||
let errorMessage = '数据通道连接失败';
|
||||
let shouldRetry = false;
|
||||
|
||||
// 根据数据通道状态提供更具体的错误信息
|
||||
switch (dataChannel.readyState) {
|
||||
case 'connecting':
|
||||
errorMessage = '数据通道正在连接中,请稍候...';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
case 'closing':
|
||||
errorMessage = '数据通道正在关闭,连接即将断开';
|
||||
break;
|
||||
case 'closed':
|
||||
errorMessage = '数据通道已关闭,P2P连接失败';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
default:
|
||||
// 检查PeerConnection状态
|
||||
const pc = pcRef.current;
|
||||
if (pc) {
|
||||
switch (pc.connectionState) {
|
||||
case 'failed':
|
||||
errorMessage = 'P2P连接失败,可能是网络防火墙阻止了连接,请尝试切换网络或使用VPN';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
case 'disconnected':
|
||||
errorMessage = 'P2P连接已断开,网络可能不稳定';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
default:
|
||||
errorMessage = '数据通道连接失败,可能是网络环境受限';
|
||||
shouldRetry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`[SharedWebRTC] 数据通道详细错误 - 状态: ${dataChannel.readyState}, 消息: ${errorMessage}, 建议重试: ${shouldRetry}`);
|
||||
|
||||
updateState({
|
||||
error: errorMessage,
|
||||
isConnecting: false,
|
||||
isPeerConnected: false, // 数据通道出错时,P2P连接肯定不可用
|
||||
canRetry: shouldRetry // 设置是否可以重试
|
||||
});
|
||||
};
|
||||
} else {
|
||||
pc.ondatachannel = (event) => {
|
||||
@@ -445,14 +529,59 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
console.log('[SharedWebRTC] 数据通道已打开 (接收方)');
|
||||
updateState({ isPeerConnected: true, error: null, isConnecting: false });
|
||||
updateState({ isPeerConnected: true, error: null, isConnecting: false, canRetry: false });
|
||||
};
|
||||
|
||||
dataChannel.onmessage = handleDataChannelMessage;
|
||||
|
||||
dataChannel.onerror = (error) => {
|
||||
console.error('[SharedWebRTC] 数据通道错误:', error);
|
||||
updateState({ error: '数据通道连接失败,可能是网络环境受限', isConnecting: false });
|
||||
console.error('[SharedWebRTC] 数据通道错误 (接收方):', error);
|
||||
|
||||
// 获取更详细的错误信息
|
||||
let errorMessage = '数据通道连接失败';
|
||||
let shouldRetry = false;
|
||||
|
||||
// 根据数据通道状态提供更具体的错误信息
|
||||
switch (dataChannel.readyState) {
|
||||
case 'connecting':
|
||||
errorMessage = '数据通道正在连接中,请稍候...';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
case 'closing':
|
||||
errorMessage = '数据通道正在关闭,连接即将断开';
|
||||
break;
|
||||
case 'closed':
|
||||
errorMessage = '数据通道已关闭,P2P连接失败';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
default:
|
||||
// 检查PeerConnection状态
|
||||
const pc = pcRef.current;
|
||||
if (pc) {
|
||||
switch (pc.connectionState) {
|
||||
case 'failed':
|
||||
errorMessage = 'P2P连接失败,可能是网络防火墙阻止了连接,请尝试切换网络或使用VPN';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
case 'disconnected':
|
||||
errorMessage = 'P2P连接已断开,网络可能不稳定';
|
||||
shouldRetry = true;
|
||||
break;
|
||||
default:
|
||||
errorMessage = '数据通道连接失败,可能是网络环境受限';
|
||||
shouldRetry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`[SharedWebRTC] 数据通道详细错误 (接收方) - 状态: ${dataChannel.readyState}, 消息: ${errorMessage}, 建议重试: ${shouldRetry}`);
|
||||
|
||||
updateState({
|
||||
error: errorMessage,
|
||||
isConnecting: false,
|
||||
isPeerConnected: false, // 数据通道出错时,P2P连接肯定不可用
|
||||
canRetry: shouldRetry // 设置是否可以重试
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -474,18 +603,58 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
console.error('[SharedWebRTC] 连接失败:', error);
|
||||
updateState({
|
||||
error: error instanceof Error ? error.message : '连接失败',
|
||||
isConnecting: false
|
||||
isConnecting: false,
|
||||
canRetry: true
|
||||
});
|
||||
}
|
||||
}, [updateState, cleanup, createOffer, handleDataChannelMessage, webrtcStore.isConnecting, webrtcStore.isConnected]);
|
||||
|
||||
// 断开连接
|
||||
const disconnect = useCallback(() => {
|
||||
console.log('[SharedWebRTC] 断开连接');
|
||||
console.log('[SharedWebRTC] 主动断开连接');
|
||||
|
||||
// 设置主动断开标志
|
||||
isUserDisconnecting.current = true;
|
||||
|
||||
// 在断开之前通知对方
|
||||
const ws = wsRef.current;
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'disconnection',
|
||||
payload: { reason: '用户主动断开' }
|
||||
}));
|
||||
console.log('[SharedWebRTC] 📤 已通知对方断开连接');
|
||||
} catch (error) {
|
||||
console.warn('[SharedWebRTC] 发送断开通知失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理连接
|
||||
cleanup();
|
||||
webrtcStore.reset();
|
||||
|
||||
// 主动断开时,将状态完全重置为初始状态(没有任何错误或消息)
|
||||
webrtcStore.resetToInitial();
|
||||
}, [cleanup, webrtcStore]);
|
||||
|
||||
// 重试连接
|
||||
const retry = useCallback(async () => {
|
||||
const room = currentRoom.current;
|
||||
if (!room) {
|
||||
console.warn('[SharedWebRTC] 没有当前房间信息,无法重试');
|
||||
updateState({ error: '无法重试连接:缺少房间信息', canRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[SharedWebRTC] 🔄 重试连接到房间:', room.code, room.role);
|
||||
|
||||
// 清理当前连接
|
||||
cleanup();
|
||||
|
||||
// 重新连接
|
||||
await connect(room.code, room.role);
|
||||
}, [cleanup, connect, updateState]);
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = useCallback((message: WebRTCMessage, channel?: string) => {
|
||||
const dataChannel = dcRef.current;
|
||||
@@ -642,10 +811,12 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
||||
isWebSocketConnected: webrtcStore.isWebSocketConnected,
|
||||
isPeerConnected: webrtcStore.isPeerConnected,
|
||||
error: webrtcStore.error,
|
||||
canRetry: webrtcStore.canRetry,
|
||||
|
||||
// 操作方法
|
||||
connect,
|
||||
disconnect,
|
||||
retry,
|
||||
sendMessage,
|
||||
sendData,
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ interface WebRTCState {
|
||||
isWebSocketConnected: boolean;
|
||||
isPeerConnected: boolean;
|
||||
error: string | null;
|
||||
canRetry: boolean; // 新增:是否可以重试
|
||||
currentRoom: { code: string; role: 'sender' | 'receiver' } | null;
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ interface WebRTCStore extends WebRTCState {
|
||||
updateState: (updates: Partial<WebRTCState>) => void;
|
||||
setCurrentRoom: (room: { code: string; role: 'sender' | 'receiver' } | null) => void;
|
||||
reset: () => void;
|
||||
resetToInitial: () => void; // 新增:完全重置到初始状态
|
||||
}
|
||||
|
||||
const initialState: WebRTCState = {
|
||||
@@ -21,6 +23,7 @@ const initialState: WebRTCState = {
|
||||
isWebSocketConnected: false,
|
||||
isPeerConnected: false,
|
||||
error: null,
|
||||
canRetry: false, // 初始状态下不需要重试
|
||||
currentRoom: null,
|
||||
};
|
||||
|
||||
@@ -38,4 +41,6 @@ export const useWebRTCStore = create<WebRTCStore>((set) => ({
|
||||
})),
|
||||
|
||||
reset: () => set(initialState),
|
||||
|
||||
resetToInitial: () => set(initialState), // 完全重置到初始状态
|
||||
}));
|
||||
|
||||
@@ -11,23 +11,6 @@ interface ApiResponse {
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
interface CreateRoomData {
|
||||
type?: string;
|
||||
content?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface CreateTextRoomData {
|
||||
type: string;
|
||||
content: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface UpdateFilesData {
|
||||
roomId: string;
|
||||
files: File[];
|
||||
}
|
||||
|
||||
export class ClientAPI {
|
||||
private baseUrl: string;
|
||||
|
||||
@@ -94,20 +77,10 @@ export class ClientAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间(统一接口)
|
||||
* 创建房间(简化版本)- 后端会忽略传入的参数
|
||||
*/
|
||||
async createRoom(data: CreateRoomData): Promise<ApiResponse> {
|
||||
return this.post('/api/create-room', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本房间
|
||||
*/
|
||||
async createTextRoom(content: string): Promise<ApiResponse> {
|
||||
return this.post('/api/create-room', {
|
||||
type: 'text',
|
||||
content: content
|
||||
});
|
||||
async createRoom(): Promise<ApiResponse> {
|
||||
return this.post('/api/create-room', {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,13 +113,6 @@ export class ClientAPI {
|
||||
async getWebRTCRoomStatus(code: string): Promise<ApiResponse> {
|
||||
return this.get(`/api/webrtc-room-status?code=${code}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件
|
||||
*/
|
||||
async updateFiles(data: UpdateFilesData): Promise<ApiResponse> {
|
||||
return this.post('/api/update-files', data);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
|
||||
161
chuan-next/src/lib/webrtc-support.ts
Normal file
161
chuan-next/src/lib/webrtc-support.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* WebRTC 浏览器支持检测工具
|
||||
*/
|
||||
|
||||
export interface WebRTCSupport {
|
||||
isSupported: boolean;
|
||||
missing: string[];
|
||||
details: {
|
||||
rtcPeerConnection: boolean;
|
||||
dataChannel: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测浏览器是否支持WebRTC及其相关功能
|
||||
*/
|
||||
export function detectWebRTCSupport(): WebRTCSupport {
|
||||
const missing: string[] = [];
|
||||
const details = {
|
||||
rtcPeerConnection: false,
|
||||
getUserMedia: false,
|
||||
getDisplayMedia: false,
|
||||
dataChannel: false,
|
||||
};
|
||||
|
||||
// 检测 RTCPeerConnection
|
||||
if (typeof RTCPeerConnection !== 'undefined') {
|
||||
details.rtcPeerConnection = true;
|
||||
} else {
|
||||
missing.push('RTCPeerConnection');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 检测 DataChannel 支持
|
||||
try {
|
||||
if (typeof RTCPeerConnection !== 'undefined') {
|
||||
const pc = new RTCPeerConnection();
|
||||
const dc = pc.createDataChannel('test');
|
||||
if (dc) {
|
||||
details.dataChannel = true;
|
||||
}
|
||||
pc.close();
|
||||
}
|
||||
} catch (error) {
|
||||
missing.push('DataChannel');
|
||||
}
|
||||
|
||||
const isSupported = missing.length === 0;
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
missing,
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
*/
|
||||
export function getBrowserInfo(): {
|
||||
name: string;
|
||||
version: string;
|
||||
isSupported: boolean;
|
||||
recommendations?: string[];
|
||||
} {
|
||||
const userAgent = navigator.userAgent;
|
||||
let browserName = 'Unknown';
|
||||
let version = 'Unknown';
|
||||
let isSupported = true;
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// Chrome
|
||||
if (/Chrome/.test(userAgent) && !/Edg/.test(userAgent)) {
|
||||
browserName = 'Chrome';
|
||||
const match = userAgent.match(/Chrome\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
isSupported = parseInt(version) >= 23;
|
||||
if (!isSupported) {
|
||||
recommendations.push('请升级到 Chrome 23 或更新版本');
|
||||
}
|
||||
}
|
||||
// Firefox
|
||||
else if (/Firefox/.test(userAgent)) {
|
||||
browserName = 'Firefox';
|
||||
const match = userAgent.match(/Firefox\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
isSupported = parseInt(version) >= 22;
|
||||
if (!isSupported) {
|
||||
recommendations.push('请升级到 Firefox 22 或更新版本');
|
||||
}
|
||||
}
|
||||
// Safari
|
||||
else if (/Safari/.test(userAgent) && !/Chrome/.test(userAgent)) {
|
||||
browserName = 'Safari';
|
||||
const match = userAgent.match(/Version\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
isSupported = parseInt(version) >= 11;
|
||||
if (!isSupported) {
|
||||
recommendations.push('请升级到 Safari 11 或更新版本');
|
||||
}
|
||||
}
|
||||
// Edge
|
||||
else if (/Edg/.test(userAgent)) {
|
||||
browserName = 'Edge';
|
||||
const match = userAgent.match(/Edg\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
isSupported = parseInt(version) >= 12;
|
||||
if (!isSupported) {
|
||||
recommendations.push('请升级到 Edge 12 或更新版本');
|
||||
}
|
||||
}
|
||||
// Internet Explorer
|
||||
else if (/MSIE|Trident/.test(userAgent)) {
|
||||
browserName = 'Internet Explorer';
|
||||
isSupported = false;
|
||||
recommendations.push(
|
||||
'请使用现代浏览器,如 Chrome、Firefox、Safari 或 Edge',
|
||||
'Internet Explorer 不支持 WebRTC'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: browserName,
|
||||
version,
|
||||
isSupported,
|
||||
recommendations: recommendations.length > 0 ? recommendations : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐的浏览器列表
|
||||
*/
|
||||
export function getRecommendedBrowsers(): Array<{
|
||||
name: string;
|
||||
minVersion: string;
|
||||
downloadUrl: string;
|
||||
}> {
|
||||
return [
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
minVersion: '23+',
|
||||
downloadUrl: 'https://www.google.com/chrome/',
|
||||
},
|
||||
{
|
||||
name: 'Mozilla Firefox',
|
||||
minVersion: '22+',
|
||||
downloadUrl: 'https://www.mozilla.org/firefox/',
|
||||
},
|
||||
{
|
||||
name: 'Safari',
|
||||
minVersion: '11+',
|
||||
downloadUrl: 'https://www.apple.com/safari/',
|
||||
},
|
||||
{
|
||||
name: 'Microsoft Edge',
|
||||
minVersion: '12+',
|
||||
downloadUrl: 'https://www.microsoft.com/edge',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func (h *Handler) HandleWebRTCWebSocket(w http.ResponseWriter, r *http.Request)
|
||||
h.webrtcService.HandleWebSocket(w, r)
|
||||
}
|
||||
|
||||
// CreateRoomHandler 创建房间API
|
||||
// CreateRoomHandler 创建房间API - 简化版本,不处理无用参数
|
||||
func (h *Handler) CreateRoomHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置响应为JSON格式
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -36,7 +36,7 @@ func (h *Handler) CreateRoomHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建新房间
|
||||
// 创建新房间(忽略请求体中的无用参数)
|
||||
code := h.webrtcService.CreateNewRoom()
|
||||
log.Printf("创建房间成功: %s", code)
|
||||
|
||||
|
||||
@@ -76,6 +76,34 @@ func (ws *WebRTCService) HandleWebSocket(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if code == "" || (role != "sender" && role != "receiver") {
|
||||
log.Printf("WebRTC连接参数无效: code=%s, role=%s", code, role)
|
||||
conn.WriteJSON(map[string]interface{}{
|
||||
"type": "error",
|
||||
"message": "连接参数无效",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证房间是否存在
|
||||
ws.roomsMux.RLock()
|
||||
room := ws.rooms[code]
|
||||
ws.roomsMux.RUnlock()
|
||||
|
||||
if room == nil {
|
||||
log.Printf("房间不存在: %s", code)
|
||||
conn.WriteJSON(map[string]interface{}{
|
||||
"type": "error",
|
||||
"message": "房间不存在或已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查房间是否已过期
|
||||
if time.Now().After(room.ExpiresAt) {
|
||||
log.Printf("房间已过期: %s", code)
|
||||
conn.WriteJSON(map[string]interface{}{
|
||||
"type": "error",
|
||||
"message": "房间已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,13 +155,8 @@ func (ws *WebRTCService) addClientToRoom(code string, client *WebRTCClient) {
|
||||
|
||||
room := ws.rooms[code]
|
||||
if room == nil {
|
||||
room = &WebRTCRoom{
|
||||
Code: code,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Hour), // 1小时后过期
|
||||
}
|
||||
ws.rooms[code] = room
|
||||
log.Printf("自动创建WebRTC房间: %s", code)
|
||||
log.Printf("尝试加入不存在的WebRTC房间: %s", code)
|
||||
return
|
||||
}
|
||||
|
||||
if client.Role == "sender" {
|
||||
@@ -253,19 +276,39 @@ func (ws *WebRTCService) CreateRoom(code string) {
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNewRoom 创建新房间并返回房间码
|
||||
// CreateNewRoom 创建新房间并返回房间码 - 确保不重复
|
||||
func (ws *WebRTCService) CreateNewRoom() string {
|
||||
code := ws.generatePickupCode()
|
||||
var code string
|
||||
|
||||
// 生成唯一房间码,确保不重复
|
||||
for {
|
||||
code = ws.generatePickupCode()
|
||||
ws.roomsMux.RLock()
|
||||
_, exists := ws.rooms[code]
|
||||
ws.roomsMux.RUnlock()
|
||||
|
||||
if !exists {
|
||||
break // 找到了不重复的代码
|
||||
}
|
||||
// 如果重复了,继续生成新的
|
||||
}
|
||||
|
||||
ws.CreateRoom(code)
|
||||
return code
|
||||
}
|
||||
|
||||
// generatePickupCode 生成6位取件码
|
||||
// generatePickupCode 生成6位取件码 - 统一规则:只使用大写字母和数字,排除0和O避免混淆
|
||||
func (ws *WebRTCService) generatePickupCode() string {
|
||||
// 只使用大写字母和数字,排除容易混淆的字符:数字0和字母O
|
||||
chars := "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
rng := rand.New(source)
|
||||
code := rng.Intn(900000) + 100000
|
||||
return fmt.Sprintf("%d", code)
|
||||
|
||||
result := make([]byte, 6)
|
||||
for i := 0; i < 6; i++ {
|
||||
result[i] = chars[rng.Intn(len(chars))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// cleanupExpiredRooms 定期清理过期房间
|
||||
|
||||
Reference in New Issue
Block a user