feat:webrtc支持检测|房间检测|UI状态优化

This commit is contained in:
MatrixSeven
2025-08-26 18:52:29 +08:00
parent 301434fd4c
commit 63e6e956e4
18 changed files with 1184 additions and 167 deletions

View File

@@ -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>
);
}

View File

@@ -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();

View 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>
);
}

View 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>
);
}

View File

@@ -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();

View 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>
);
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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}

View File

@@ -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();

View 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,
};
}

View File

@@ -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> => {

View File

@@ -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,

View File

@@ -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), // 完全重置到初始状态
}));

View File

@@ -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);
}
}
// 导出单例实例

View 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',
},
];
}

View File

@@ -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)

View File

@@ -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 定期清理过期房间